Merge branch 'develop' into spaces/context-switching
This commit is contained in:
commit
a3d0ccf306
215 changed files with 7653 additions and 2562 deletions
|
@ -121,21 +121,16 @@ export class SetupEncryptionStore extends EventEmitter {
|
|||
// on the first trust check, and the key backup restore will happen
|
||||
// in the background.
|
||||
await new Promise((resolve, reject) => {
|
||||
try {
|
||||
accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
resolve();
|
||||
if (backupInfo) {
|
||||
// A complete restore can take many minutes for large
|
||||
// accounts / slow servers, so we allow the dialog
|
||||
// to advance before this.
|
||||
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||
}
|
||||
}).catch(reject);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
reject(e);
|
||||
}
|
||||
accessSecretStorage(async () => {
|
||||
await cli.checkOwnCrossSigningTrust();
|
||||
resolve();
|
||||
if (backupInfo) {
|
||||
// A complete restore can take many minutes for large
|
||||
// accounts / slow servers, so we allow the dialog
|
||||
// to advance before this.
|
||||
await cli.restoreKeyBackupWithSecretStorage(backupInfo);
|
||||
}
|
||||
}).catch(reject);
|
||||
});
|
||||
|
||||
if (cli.getCrossSigningId()) {
|
||||
|
|
|
@ -145,7 +145,8 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const data = await this.fetchSuggestedRooms(space);
|
||||
if (this._activeSpace === space) {
|
||||
this._suggestedRooms = data.rooms.filter(roomInfo => {
|
||||
return roomInfo.room_type !== RoomType.Space && !this.matrixClient.getRoom(roomInfo.room_id);
|
||||
return roomInfo.room_type !== RoomType.Space
|
||||
&& this.matrixClient.getRoom(roomInfo.room_id)?.getMyMembership() !== "join";
|
||||
});
|
||||
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
|
||||
}
|
||||
|
@ -317,6 +318,12 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
};
|
||||
|
||||
private onSpaceMembersChange = (ev: MatrixEvent) => {
|
||||
// skip this update if we do not have a DM with this user
|
||||
if (DMRoomMap.shared().getDMRoomsForUserId(ev.getStateKey()).length < 1) return;
|
||||
this.onRoomsUpdate();
|
||||
};
|
||||
|
||||
private onRoomsUpdate = throttle(() => {
|
||||
// TODO resolve some updates as deltas
|
||||
const visibleRooms = this.matrixClient.getVisibleRooms();
|
||||
|
@ -392,15 +399,17 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.onRoomsUpdate();
|
||||
}
|
||||
|
||||
// if the user was looking at the room and then joined select that space
|
||||
if (room.getMyMembership() === "join" && room.roomId === RoomViewStore.getRoomId()) {
|
||||
this.setActiveSpace(room);
|
||||
}
|
||||
|
||||
const numSuggestedRooms = this._suggestedRooms.length;
|
||||
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
|
||||
if (numSuggestedRooms !== this._suggestedRooms.length) {
|
||||
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
|
||||
if (room.getMyMembership() === "join") {
|
||||
if (!room.isSpaceRoom()) {
|
||||
const numSuggestedRooms = this._suggestedRooms.length;
|
||||
this._suggestedRooms = this._suggestedRooms.filter(r => r.room_id !== room.roomId);
|
||||
if (numSuggestedRooms !== this._suggestedRooms.length) {
|
||||
this.emit(SUGGESTED_ROOMS, this._suggestedRooms);
|
||||
}
|
||||
} else if (room.roomId === RoomViewStore.getRoomId()) {
|
||||
// if the user was looking at the space and then joined: select that space
|
||||
this.setActiveSpace(room);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -408,18 +417,30 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
const room = this.matrixClient.getRoom(ev.getRoomId());
|
||||
if (!room) return;
|
||||
|
||||
if (ev.getType() === EventType.SpaceChild && room.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
this.emit(room.roomId);
|
||||
} else if (ev.getType() === EventType.SpaceParent) {
|
||||
// TODO rebuild the space parent and not the room - check permissions?
|
||||
// TODO confirm this after implementing parenting behaviour
|
||||
if (room.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
} else {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
this.emit(room.roomId);
|
||||
switch (ev.getType()) {
|
||||
case EventType.SpaceChild:
|
||||
if (room.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
this.emit(room.roomId);
|
||||
}
|
||||
break;
|
||||
|
||||
case EventType.SpaceParent:
|
||||
// TODO rebuild the space parent and not the room - check permissions?
|
||||
// TODO confirm this after implementing parenting behaviour
|
||||
if (room.isSpaceRoom()) {
|
||||
this.onSpaceUpdate();
|
||||
} else {
|
||||
this.onRoomUpdate(room);
|
||||
}
|
||||
this.emit(room.roomId);
|
||||
break;
|
||||
|
||||
case EventType.RoomMember:
|
||||
if (room.isSpaceRoom()) {
|
||||
this.onSpaceMembersChange(ev);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -536,6 +557,26 @@ export class SpaceStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.notificationStateMap.set(key, state);
|
||||
return state;
|
||||
}
|
||||
|
||||
// traverse space tree with DFS calling fn on each space including the given root one
|
||||
public traverseSpace(
|
||||
spaceId: string,
|
||||
fn: (roomId: string) => void,
|
||||
includeRooms = false,
|
||||
parentPath?: Set<string>,
|
||||
) {
|
||||
if (parentPath && parentPath.has(spaceId)) return; // prevent cycles
|
||||
|
||||
fn(spaceId);
|
||||
|
||||
const newPath = new Set(parentPath).add(spaceId);
|
||||
const [childSpaces, childRooms] = partitionSpacesAndRooms(this.getChildren(spaceId));
|
||||
|
||||
if (includeRooms) {
|
||||
childRooms.forEach(r => fn(r.roomId));
|
||||
}
|
||||
childSpaces.forEach(s => this.traverseSpace(s.roomId, fn, includeRooms, newPath));
|
||||
}
|
||||
}
|
||||
|
||||
export default class SpaceStore {
|
||||
|
|
80
src/stores/VoiceRecordingStore.ts
Normal file
80
src/stores/VoiceRecordingStore.ts
Normal file
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
import {AsyncStoreWithClient} from "./AsyncStoreWithClient";
|
||||
import defaultDispatcher from "../dispatcher/dispatcher";
|
||||
import {ActionPayload} from "../dispatcher/payloads";
|
||||
import {VoiceRecording} from "../voice/VoiceRecording";
|
||||
|
||||
interface IState {
|
||||
recording?: VoiceRecording;
|
||||
}
|
||||
|
||||
export class VoiceRecordingStore extends AsyncStoreWithClient<IState> {
|
||||
private static internalInstance: VoiceRecordingStore;
|
||||
|
||||
public constructor() {
|
||||
super(defaultDispatcher, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the active recording instance, if any.
|
||||
*/
|
||||
public get activeRecording(): VoiceRecording | null {
|
||||
return this.state.recording;
|
||||
}
|
||||
|
||||
public static get instance(): VoiceRecordingStore {
|
||||
if (!VoiceRecordingStore.internalInstance) {
|
||||
VoiceRecordingStore.internalInstance = new VoiceRecordingStore();
|
||||
}
|
||||
return VoiceRecordingStore.internalInstance;
|
||||
}
|
||||
|
||||
protected async onAction(payload: ActionPayload): Promise<void> {
|
||||
// Nothing to do, but we're required to override the function
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new recording if one isn't already in progress. Note that this simply
|
||||
* creates a recording instance - whether or not recording is actively in progress
|
||||
* can be seen via the VoiceRecording class.
|
||||
* @returns {VoiceRecording} The recording.
|
||||
*/
|
||||
public startRecording(): VoiceRecording {
|
||||
if (!this.matrixClient) throw new Error("Cannot start a recording without a MatrixClient");
|
||||
if (this.state.recording) throw new Error("A recording is already in progress");
|
||||
|
||||
const recording = new VoiceRecording(this.matrixClient);
|
||||
|
||||
// noinspection JSIgnoredPromiseFromCall - we can safely run this async
|
||||
this.updateState({recording});
|
||||
|
||||
return recording;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes of the current recording, no matter the state of it.
|
||||
* @returns {Promise<void>} Resolves when complete.
|
||||
*/
|
||||
public disposeRecording(): Promise<void> {
|
||||
if (this.state.recording) {
|
||||
this.state.recording.destroy(); // stops internally
|
||||
}
|
||||
return this.updateState({recording: null});
|
||||
}
|
||||
}
|
|
@ -22,6 +22,7 @@ import { FetchRoomFn, ListNotificationState } from "./ListNotificationState";
|
|||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomNotificationState } from "./RoomNotificationState";
|
||||
import { SummarizedNotificationState } from "./SummarizedNotificationState";
|
||||
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
|
||||
|
||||
interface IState {}
|
||||
|
||||
|
@ -47,7 +48,9 @@ export class RoomNotificationStateStore extends AsyncStoreWithClient<IState> {
|
|||
// This will include highlights from the previous version of the room internally
|
||||
const globalState = new SummarizedNotificationState();
|
||||
for (const room of this.matrixClient.getVisibleRooms()) {
|
||||
globalState.add(this.getRoomState(room));
|
||||
if (VisibilityProvider.instance.isRoomVisible(room)) {
|
||||
globalState.add(this.getRoomState(room));
|
||||
}
|
||||
}
|
||||
return globalState;
|
||||
}
|
||||
|
|
|
@ -82,7 +82,7 @@ export class ListLayout {
|
|||
|
||||
public get defaultVisibleTiles(): number {
|
||||
// This number is what "feels right", and mostly subject to design's opinion.
|
||||
return 5;
|
||||
return 8;
|
||||
}
|
||||
|
||||
public tilesWithPadding(n: number, paddingPx: number): number {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/*
|
||||
Copyright 2018, 2019 New Vector Ltd
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2018-2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,27 +14,27 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import {MatrixClient} from "matrix-js-sdk/src/client";
|
||||
import SettingsStore from "../../settings/SettingsStore";
|
||||
import { DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
||||
import { ActionPayload } from "../../dispatcher/payloads";
|
||||
import {DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID} from "./models";
|
||||
import {Room} from "matrix-js-sdk/src/models/room";
|
||||
import {IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm} from "./algorithms/models";
|
||||
import {ActionPayload} from "../../dispatcher/payloads";
|
||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||
import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition";
|
||||
import { TagWatcher } from "./TagWatcher";
|
||||
import {readReceiptChangeIsFor} from "../../utils/read-receipts";
|
||||
import {FILTER_CHANGED, FilterKind, IFilterCondition} from "./filters/IFilterCondition";
|
||||
import {TagWatcher} from "./TagWatcher";
|
||||
import RoomViewStore from "../RoomViewStore";
|
||||
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
|
||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import {Algorithm, LIST_UPDATED_EVENT} from "./algorithms/Algorithm";
|
||||
import {EffectiveMembership, getEffectiveMembership} from "../../utils/membership";
|
||||
import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
|
||||
import RoomListLayoutStore from "./RoomListLayoutStore";
|
||||
import { MarkedExecution } from "../../utils/MarkedExecution";
|
||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
||||
import { VisibilityProvider } from "./filters/VisibilityProvider";
|
||||
import { SpaceWatcher } from "./SpaceWatcher";
|
||||
import {MarkedExecution} from "../../utils/MarkedExecution";
|
||||
import {AsyncStoreWithClient} from "../AsyncStoreWithClient";
|
||||
import {NameFilterCondition} from "./filters/NameFilterCondition";
|
||||
import {RoomNotificationStateStore} from "../notifications/RoomNotificationStateStore";
|
||||
import {VisibilityProvider} from "./filters/VisibilityProvider";
|
||||
import {SpaceWatcher} from "./SpaceWatcher";
|
||||
|
||||
interface IState {
|
||||
tagsEnabled?: boolean;
|
||||
|
@ -57,6 +56,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
private initialListsGenerated = false;
|
||||
private algorithm = new Algorithm();
|
||||
private filterConditions: IFilterCondition[] = [];
|
||||
private prefilterConditions: IFilterCondition[] = [];
|
||||
private tagWatcher: TagWatcher;
|
||||
private spaceWatcher: SpaceWatcher;
|
||||
private updateFn = new MarkedExecution(() => {
|
||||
|
@ -104,6 +104,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
public async resetStore() {
|
||||
await this.reset();
|
||||
this.filterConditions = [];
|
||||
this.prefilterConditions = [];
|
||||
this.initialListsGenerated = false;
|
||||
this.setupWatchers();
|
||||
|
||||
|
@ -435,6 +436,39 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
}
|
||||
}
|
||||
|
||||
private async recalculatePrefiltering() {
|
||||
if (!this.algorithm) return;
|
||||
if (!this.algorithm.hasTagSortingMap) return; // we're still loading
|
||||
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log("Calculating new prefiltered room list");
|
||||
}
|
||||
|
||||
// Inhibit updates because we're about to lie heavily to the algorithm
|
||||
this.algorithm.updatesInhibited = true;
|
||||
|
||||
// Figure out which rooms are about to be valid, and the state of affairs
|
||||
const rooms = this.getPlausibleRooms();
|
||||
const currentSticky = this.algorithm.stickyRoom;
|
||||
const stickyIsStillPresent = currentSticky && rooms.includes(currentSticky);
|
||||
|
||||
// Reset the sticky room before resetting the known rooms so the algorithm
|
||||
// doesn't freak out.
|
||||
await this.algorithm.setStickyRoom(null);
|
||||
await this.algorithm.setKnownRooms(rooms);
|
||||
|
||||
// Set the sticky room back, if needed, now that we have updated the store.
|
||||
// This will use relative stickyness to the new room set.
|
||||
if (stickyIsStillPresent) {
|
||||
await this.algorithm.setStickyRoom(currentSticky);
|
||||
}
|
||||
|
||||
// Finally, mark an update and resume updates from the algorithm
|
||||
this.updateFn.mark();
|
||||
this.algorithm.updatesInhibited = false;
|
||||
}
|
||||
|
||||
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||
await this.setAndPersistTagSorting(tagId, sort);
|
||||
this.updateFn.trigger();
|
||||
|
@ -557,6 +591,34 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.updateFn.trigger();
|
||||
};
|
||||
|
||||
private onPrefilterUpdated = async () => {
|
||||
await this.recalculatePrefiltering();
|
||||
this.updateFn.trigger();
|
||||
};
|
||||
|
||||
private getPlausibleRooms(): Room[] {
|
||||
if (!this.matrixClient) return [];
|
||||
|
||||
let rooms = [
|
||||
...this.matrixClient.getVisibleRooms(),
|
||||
// also show space invites in the room list
|
||||
...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"),
|
||||
].filter(r => VisibilityProvider.instance.isRoomVisible(r));
|
||||
|
||||
if (this.prefilterConditions.length > 0) {
|
||||
rooms = rooms.filter(r => {
|
||||
for (const filter of this.prefilterConditions) {
|
||||
if (!filter.isVisible(r)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
return rooms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Regenerates the room whole room list, discarding any previous results.
|
||||
*
|
||||
|
@ -568,11 +630,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
public async regenerateAllLists({trigger = true}) {
|
||||
console.warn("Regenerating all room lists");
|
||||
|
||||
const rooms = [
|
||||
...this.matrixClient.getVisibleRooms(),
|
||||
// also show space invites in the room list
|
||||
...this.matrixClient.getRooms().filter(r => r.isSpaceRoom() && r.getMyMembership() === "invite"),
|
||||
].filter(r => VisibilityProvider.instance.isRoomVisible(r));
|
||||
const rooms = this.getPlausibleRooms();
|
||||
|
||||
const customTags = new Set<TagID>();
|
||||
if (this.state.tagsEnabled) {
|
||||
|
@ -601,24 +659,44 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
if (trigger) this.updateFn.trigger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a filter condition to the room list store. Filters may be applied async,
|
||||
* and thus might not cause an update to the store immediately.
|
||||
* @param {IFilterCondition} filter The filter condition to add.
|
||||
*/
|
||||
public addFilter(filter: IFilterCondition): void {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log("Adding filter condition:", filter);
|
||||
}
|
||||
this.filterConditions.push(filter);
|
||||
if (this.algorithm) {
|
||||
this.algorithm.addFilterCondition(filter);
|
||||
let promise = Promise.resolve();
|
||||
if (filter.kind === FilterKind.Prefilter) {
|
||||
filter.on(FILTER_CHANGED, this.onPrefilterUpdated);
|
||||
this.prefilterConditions.push(filter);
|
||||
promise = this.recalculatePrefiltering();
|
||||
} else {
|
||||
this.filterConditions.push(filter);
|
||||
if (this.algorithm) {
|
||||
this.algorithm.addFilterCondition(filter);
|
||||
}
|
||||
}
|
||||
this.updateFn.trigger();
|
||||
promise.then(() => this.updateFn.trigger());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a filter condition from the room list store. If the filter was
|
||||
* not previously added to the room list store, this will no-op. The effects
|
||||
* of removing a filter may be applied async and therefore might not cause
|
||||
* an update right away.
|
||||
* @param {IFilterCondition} filter The filter condition to remove.
|
||||
*/
|
||||
public removeFilter(filter: IFilterCondition): void {
|
||||
if (SettingsStore.getValue("advancedRoomListLogging")) {
|
||||
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||
console.log("Removing filter condition:", filter);
|
||||
}
|
||||
const idx = this.filterConditions.indexOf(filter);
|
||||
let promise = Promise.resolve();
|
||||
let idx = this.filterConditions.indexOf(filter);
|
||||
if (idx >= 0) {
|
||||
this.filterConditions.splice(idx, 1);
|
||||
|
||||
|
@ -626,7 +704,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
this.algorithm.removeFilterCondition(filter);
|
||||
}
|
||||
}
|
||||
this.updateFn.trigger();
|
||||
idx = this.prefilterConditions.indexOf(filter);
|
||||
if (idx >= 0) {
|
||||
filter.off(FILTER_CHANGED, this.onPrefilterUpdated);
|
||||
this.prefilterConditions.splice(idx, 1);
|
||||
promise = this.recalculatePrefiltering();
|
||||
}
|
||||
promise.then(() => this.updateFn.trigger());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -28,12 +28,22 @@ export class SpaceWatcher {
|
|||
private activeSpace: Room = SpaceStore.instance.activeSpace;
|
||||
|
||||
constructor(private store: RoomListStoreClass) {
|
||||
this.filter.updateSpace(this.activeSpace); // get the filter into a consistent state
|
||||
this.updateFilter(); // get the filter into a consistent state
|
||||
store.addFilter(this.filter);
|
||||
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.onSelectedSpaceUpdated);
|
||||
}
|
||||
|
||||
private onSelectedSpaceUpdated = (activeSpace) => {
|
||||
this.filter.updateSpace(this.activeSpace = activeSpace);
|
||||
private onSelectedSpaceUpdated = (activeSpace: Room) => {
|
||||
this.activeSpace = activeSpace;
|
||||
this.updateFilter();
|
||||
};
|
||||
|
||||
private updateFilter = () => {
|
||||
if (this.activeSpace) {
|
||||
SpaceStore.instance.traverseSpace(this.activeSpace.roomId, roomId => {
|
||||
this.store.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded();
|
||||
});
|
||||
}
|
||||
this.filter.updateSpace(this.activeSpace);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -18,8 +18,7 @@ import { Room } from "matrix-js-sdk/src/models/room";
|
|||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||
import { EventEmitter } from "events";
|
||||
import { arrayDiff, arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
|
||||
import { getEnumValues } from "../../../utils/enums";
|
||||
import { arrayDiff, arrayHasDiff } from "../../../utils/arrays";
|
||||
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
||||
import {
|
||||
IListOrderingMap,
|
||||
|
@ -29,7 +28,7 @@ import {
|
|||
ListAlgorithm,
|
||||
SortAlgorithm,
|
||||
} from "./models";
|
||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "../filters/IFilterCondition";
|
||||
import { FILTER_CHANGED, IFilterCondition } from "../filters/IFilterCondition";
|
||||
import { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership";
|
||||
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
||||
import { getListAlgorithmInstance } from "./list-ordering";
|
||||
|
@ -79,6 +78,11 @@ export class Algorithm extends EventEmitter {
|
|||
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
||||
private allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
||||
|
||||
/**
|
||||
* Set to true to suspend emissions of algorithm updates.
|
||||
*/
|
||||
public updatesInhibited = false;
|
||||
|
||||
public constructor() {
|
||||
super();
|
||||
}
|
||||
|
@ -87,6 +91,14 @@ export class Algorithm extends EventEmitter {
|
|||
return this._stickyRoom ? this._stickyRoom.room : null;
|
||||
}
|
||||
|
||||
public get knownRooms(): Room[] {
|
||||
return this.rooms;
|
||||
}
|
||||
|
||||
public get hasTagSortingMap(): boolean {
|
||||
return !!this.sortAlgorithms;
|
||||
}
|
||||
|
||||
protected get hasFilters(): boolean {
|
||||
return this.allowedByFilter.size > 0;
|
||||
}
|
||||
|
@ -164,7 +176,7 @@ export class Algorithm extends EventEmitter {
|
|||
|
||||
// If we removed the last filter, tell consumers that we've "updated" our filtered
|
||||
// view. This will trick them into getting the complete room list.
|
||||
if (!this.hasFilters) {
|
||||
if (!this.hasFilters && !this.updatesInhibited) {
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
}
|
||||
|
@ -174,6 +186,7 @@ export class Algorithm extends EventEmitter {
|
|||
await this.recalculateFilteredRooms();
|
||||
|
||||
// re-emit the update so the list store can fire an off-cycle update if needed
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(FILTER_CHANGED);
|
||||
}
|
||||
|
||||
|
@ -299,6 +312,7 @@ export class Algorithm extends EventEmitter {
|
|||
this.recalculateStickyRoom();
|
||||
|
||||
// Finally, trigger an update
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
|
||||
|
@ -309,10 +323,6 @@ export class Algorithm extends EventEmitter {
|
|||
|
||||
console.warn("Recalculating filtered room list");
|
||||
const filters = Array.from(this.allowedByFilter.keys());
|
||||
const orderedFilters = new ArrayUtil(filters)
|
||||
.groupBy(f => f.relativePriority)
|
||||
.orderBy(getEnumValues(FilterPriority))
|
||||
.value;
|
||||
const newMap: ITagMap = {};
|
||||
for (const tagId of Object.keys(this.cachedRooms)) {
|
||||
// Cheaply clone the rooms so we can more easily do operations on the list.
|
||||
|
@ -320,18 +330,9 @@ export class Algorithm extends EventEmitter {
|
|||
// to the rooms we know will be deduped by the Set.
|
||||
const rooms = this.cachedRooms[tagId].map(r => r); // cheap clone
|
||||
this.tryInsertStickyRoomToFilterSet(rooms, tagId);
|
||||
let remainingRooms = rooms.map(r => r);
|
||||
let allowedRoomsInThisTag = [];
|
||||
let lastFilterPriority = orderedFilters[0].relativePriority;
|
||||
for (const filter of orderedFilters) {
|
||||
if (filter.relativePriority !== lastFilterPriority) {
|
||||
// Every time the filter changes priority, we want more specific filtering.
|
||||
// To accomplish that, reset the variables to make it look like the process
|
||||
// has started over, but using the filtered rooms as the seed.
|
||||
remainingRooms = allowedRoomsInThisTag;
|
||||
allowedRoomsInThisTag = [];
|
||||
lastFilterPriority = filter.relativePriority;
|
||||
}
|
||||
const remainingRooms = rooms.map(r => r);
|
||||
const allowedRoomsInThisTag = [];
|
||||
for (const filter of filters) {
|
||||
const filteredRooms = remainingRooms.filter(r => filter.isVisible(r));
|
||||
for (const room of filteredRooms) {
|
||||
const idx = remainingRooms.indexOf(room);
|
||||
|
@ -350,6 +351,7 @@ export class Algorithm extends EventEmitter {
|
|||
const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
|
||||
this.allowedRoomsByFilters = new Set(allowedRooms);
|
||||
this.filteredRooms = newMap;
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
|
||||
|
@ -404,6 +406,7 @@ export class Algorithm extends EventEmitter {
|
|||
if (!!this._cachedStickyRooms) {
|
||||
// Clear the cache if we won't be needing it
|
||||
this._cachedStickyRooms = null;
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
return;
|
||||
|
@ -446,6 +449,7 @@ export class Algorithm extends EventEmitter {
|
|||
}
|
||||
|
||||
// Finally, trigger an update
|
||||
if (this.updatesInhibited) return;
|
||||
this.emit(LIST_UPDATED_EVENT);
|
||||
}
|
||||
|
||||
|
@ -512,7 +516,12 @@ export class Algorithm extends EventEmitter {
|
|||
if (isNullOrUndefined(rooms)) throw new Error(`Array of rooms cannot be null`);
|
||||
if (!this.sortAlgorithms) throw new Error(`Cannot set known rooms without a tag sorting map`);
|
||||
|
||||
console.warn("Resetting known rooms, initiating regeneration");
|
||||
if (!this.updatesInhibited) {
|
||||
// We only log this if we're expecting to be publishing updates, which means that
|
||||
// this could be an unexpected invocation. If we're inhibited, then this is probably
|
||||
// an intentional invocation.
|
||||
console.warn("Resetting known rooms, initiating regeneration");
|
||||
}
|
||||
|
||||
// Before we go any further we need to clear (but remember) the sticky room to
|
||||
// avoid accidentally duplicating it in the list.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
|
||||
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
|
||||
import { Group } from "matrix-js-sdk/src/models/group";
|
||||
import { EventEmitter } from "events";
|
||||
import GroupStore from "../../GroupStore";
|
||||
|
@ -39,9 +39,8 @@ export class CommunityFilterCondition extends EventEmitter implements IFilterCon
|
|||
this.onStoreUpdate(); // trigger a false update to seed the store
|
||||
}
|
||||
|
||||
public get relativePriority(): FilterPriority {
|
||||
// Lowest priority so we can coarsely find rooms.
|
||||
return FilterPriority.Lowest;
|
||||
public get kind(): FilterKind {
|
||||
return FilterKind.Prefilter;
|
||||
}
|
||||
|
||||
public isVisible(room: Room): boolean {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -19,10 +19,19 @@ import { EventEmitter } from "events";
|
|||
|
||||
export const FILTER_CHANGED = "filter_changed";
|
||||
|
||||
export enum FilterPriority {
|
||||
Lowest,
|
||||
// in the middle would be Low, Normal, and High if we had a need
|
||||
Highest,
|
||||
export enum FilterKind {
|
||||
/**
|
||||
* A prefilter is one which coarsely determines which rooms are
|
||||
* available for runtime filtering/rendering. Typically this will
|
||||
* be things like Space selection.
|
||||
*/
|
||||
Prefilter,
|
||||
|
||||
/**
|
||||
* Runtime filters operate on the data set exposed by prefilters.
|
||||
* Typically these are dynamic values like room name searching.
|
||||
*/
|
||||
Runtime,
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -39,10 +48,9 @@ export enum FilterPriority {
|
|||
*/
|
||||
export interface IFilterCondition extends EventEmitter {
|
||||
/**
|
||||
* The relative priority that this filter should be applied with.
|
||||
* Lower priorities get applied first.
|
||||
* The kind of filter this presents.
|
||||
*/
|
||||
relativePriority: FilterPriority;
|
||||
kind: FilterKind;
|
||||
|
||||
/**
|
||||
* Determines if a given room should be visible under this
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
||||
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -15,7 +15,7 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
|
||||
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
|
||||
import { EventEmitter } from "events";
|
||||
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
|
||||
import { throttle } from "lodash";
|
||||
|
@ -31,9 +31,8 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
|||
super();
|
||||
}
|
||||
|
||||
public get relativePriority(): FilterPriority {
|
||||
// We want this one to be at the highest priority so it can search within other filters.
|
||||
return FilterPriority.Highest;
|
||||
public get kind(): FilterKind {
|
||||
return FilterKind.Runtime;
|
||||
}
|
||||
|
||||
public get search(): string {
|
||||
|
@ -66,12 +65,17 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
|||
return this.matches(room.name);
|
||||
}
|
||||
|
||||
public matches(val: string): boolean {
|
||||
private normalize(val: string): string {
|
||||
// Note: we have to match the filter with the removeHiddenChars() room name because the
|
||||
// function strips spaces and other characters (M becomes RN for example, in lowercase).
|
||||
// We also doubly convert to lowercase to work around oddities of the library.
|
||||
const noSecretsFilter = removeHiddenChars(this.search.toLowerCase()).toLowerCase();
|
||||
const noSecretsName = removeHiddenChars(val.toLowerCase()).toLowerCase();
|
||||
return noSecretsName.includes(noSecretsFilter);
|
||||
return removeHiddenChars(val.toLowerCase())
|
||||
// Strip all punctuation
|
||||
.replace(/[\\'!"#$%&()*+,\-./:;<=>?@[\]^_`{|}~\u2000-\u206f\u2e00-\u2e7f]/g, "")
|
||||
// We also doubly convert to lowercase to work around oddities of the library.
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
public matches(val: string): boolean {
|
||||
return this.normalize(val).includes(this.normalize(this.search));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
|||
import { EventEmitter } from "events";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
|
||||
import { FILTER_CHANGED, FilterPriority, IFilterCondition } from "./IFilterCondition";
|
||||
import { FILTER_CHANGED, FilterKind, IFilterCondition } from "./IFilterCondition";
|
||||
import { IDestroyable } from "../../../utils/IDestroyable";
|
||||
import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
|
||||
import { setHasDiff } from "../../../utils/sets";
|
||||
|
@ -32,9 +32,8 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
|
|||
private roomIds = new Set<Room>();
|
||||
private space: Room = null;
|
||||
|
||||
public get relativePriority(): FilterPriority {
|
||||
// Lowest priority so we can coarsely find rooms.
|
||||
return FilterPriority.Lowest;
|
||||
public get kind(): FilterKind {
|
||||
return FilterKind.Prefilter;
|
||||
}
|
||||
|
||||
public isVisible(room: Room): boolean {
|
||||
|
@ -46,12 +45,7 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
|
|||
this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space);
|
||||
|
||||
if (setHasDiff(beforeRoomIds, this.roomIds)) {
|
||||
// XXX: Room List Store has a bug where rooms which are synced after the filter is set
|
||||
// are excluded from the filter, this is a workaround for it.
|
||||
this.emit(FILTER_CHANGED);
|
||||
setTimeout(() => {
|
||||
this.emit(FILTER_CHANGED);
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue