diff --git a/docs/room-list-store.md b/docs/room-list-store.md index fa849e2505..6fc5f71124 100644 --- a/docs/room-list-store.md +++ b/docs/room-list-store.md @@ -6,7 +6,7 @@ It's so complicated it needs its own README. Legend: * Orange = External event. -* Purple = Deterministic flow. +* Purple = Deterministic flow. * Green = Algorithm definition. * Red = Exit condition/point. * Blue = Process definition. @@ -24,8 +24,8 @@ algorithm to call, instead of having all the logic in the room list store itself Tag sorting is effectively the comparator supplied to the list algorithm. This gives the list algorithm -the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm, -later described in this document, heavily uses the list ordering behaviour to break the tag into categories. +the power to decide when and how to apply the tag sorting, if at all. For example, the importance algorithm, +later described in this document, heavily uses the list ordering behaviour to break the tag into categories. Each category then gets sorted by the appropriate tag sorting algorithm. ### Tag sorting algorithm: Alphabetical @@ -36,7 +36,7 @@ useful. ### Tag sorting algorithm: Manual -Manual sorting makes use of the `order` property present on all tags for a room, per the +Manual sorting makes use of the `order` property present on all tags for a room, per the [Matrix specification](https://matrix.org/docs/spec/client_server/r0.6.0#room-tagging). Smaller values of `order` cause rooms to appear closer to the top of the list. @@ -74,7 +74,7 @@ relative (perceived) importance to the user: set to 'All Messages'. * **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without a badge/notification count (or 'Mentions Only'/'Muted'). -* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user +* **Idle**: No useful (see definition of useful above) activity has occurred in the room since the user last read it. Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey @@ -82,7 +82,7 @@ above bold, etc. Once the algorithm has determined which rooms belong in which categories, the tag sorting algorithm gets applied to each category in a sub-list fashion. This should result in the red rooms (for example) -being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but +being sorted alphabetically amongst each other as well as the grey rooms sorted amongst each other, but collectively the tag will be sorted into categories with red being at the top. ## Sticky rooms @@ -103,48 +103,62 @@ receive another notification which causes the room to move into the topmost posi above the sticky room will move underneath to allow for the new room to take the top slot, maintaining the sticky room's position. -Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries -and thus the user can see a shift in what kinds of rooms move around their selection. An example would -be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having -the rooms above it read on another device. This would result in 1 red room and 1 other kind of room +Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries +and thus the user can see a shift in what kinds of rooms move around their selection. An example would +be the user having 4 red rooms, the user selecting the third room (leaving 2 above it), and then having +the rooms above it read on another device. This would result in 1 red room and 1 other kind of room above the sticky room as it will try to maintain 2 rooms above the sticky room. An exception for the sticky room placement is when there's suddenly not enough rooms to maintain the placement exactly. This typically happens if the user selects a room and leaves enough rooms where it cannot maintain the N required rooms above the sticky room. In this case, the sticky room will simply decrease N as needed. -The N value will never increase while selection remains unchanged: adding a bunch of rooms after having +The N value will never increase while selection remains unchanged: adding a bunch of rooms after having put the sticky room in a position where it's had to decrease N will not increase N. ## Responsibilities of the store -The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets -an object containing the tags it needs to worry about and the rooms within. The room list component will -decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with +The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets +an object containing the tags it needs to worry about and the rooms within. The room list component will +decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with all kinds of filtering. ## Filtering -Filters are provided to the store as condition classes, which are then passed along to the algorithm -implementations. The implementations then get to decide how to actually filter the rooms, however in -practice the base `Algorithm` class deals with the filtering in a more optimized/generic way. +Filters are provided to the store as condition classes and have two major kinds: Prefilters and Runtime. -The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms, -as the old room list store does. When a filter condition changes, it emits an update which (in this -case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a +Prefilters flush out rooms which shouldn't appear to the algorithm implementations. Typically this is +due to some higher order room list filtering (such as spaces or tags) deliberately exposing a subset of +rooms to the user. The algorithm implementations will not see a room being prefiltered out. + +Runtime filters are used for more dynamic filtering, such as the user filtering by room name. These +filters are passed along to the algorithm implementations where those implementations decide how and +when to apply the filter. In practice, the base `Algorithm` class ends up doing the heavy lifting for +optimization reasons. + +The results of runtime filters get cached to avoid needlessly iterating over potentially thousands of +rooms, as the old room list store does. When a filter condition changes, it emits an update which (in this +case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a minor subset where possible to avoid over-iterating rooms. All filter conditions are considered "stable" by the consumers, meaning that the consumer does not expect a change in the condition unless the condition says it has changed. This is intentional to maintain the caching behaviour described above. +One might ask why we don't just use prefilter conditions for everything, and the answer is one of slight +subtlety: in the cases of prefilters we are knowingly exposing the user to a workspace-style UX where +room notifications are self-contained within that workspace. Runtime filters tend to not want to affect +visible notification counts (as it doesn't want the room header to suddenly be confusing to the user as +they type), and occasionally UX like "found 2/12 rooms" is desirable. If prefiltering were used instead, +the notification counts would vary while the user was typing and "found 2/12" UX would not be possible. + ## Class breakdowns -The `RoomListStore` is the major coordinator of various algorithm implementations, which take care -of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible -for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get -defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the -user). Various list-specific utilities are also included, though they are expected to move somewhere -more general when needed. For example, the `membership` utilities could easily be moved elsewhere +The `RoomListStore` is the major coordinator of various algorithm implementations, which take care +of the various `ListAlgorithm` and `SortingAlgorithm` options. The `Algorithm` class is responsible +for figuring out which tags get which rooms, as Matrix specifies them as a reverse map: tags get +defined on rooms and are not defined as a collection of rooms (unlike how they are presented to the +user). Various list-specific utilities are also included, though they are expected to move somewhere +more general when needed. For example, the `membership` utilities could easily be moved elsewhere as needed. The various bits throughout the room list store should also have jsdoc of some kind to help describe diff --git a/src/stores/room-list/RoomListStore.ts b/src/stores/room-list/RoomListStore.ts index 074c2e569d..88df05b5d0 100644 --- a/src/stores/room-list/RoomListStore.ts +++ b/src/stores/room-list/RoomListStore.ts @@ -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 { 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 { public async resetStore() { await this.reset(); this.filterConditions = []; + this.prefilterConditions = []; this.initialListsGenerated = false; this.setupWatchers(); @@ -435,6 +436,39 @@ export class RoomListStoreClass extends AsyncStoreWithClient { } } + 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 { 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 { 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(); if (this.state.tagsEnabled) { @@ -601,24 +659,44 @@ export class RoomListStoreClass extends AsyncStoreWithClient { 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 { 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()); } /** diff --git a/src/stores/room-list/algorithms/Algorithm.ts b/src/stores/room-list/algorithms/Algorithm.ts index fed3099325..83ee803115 100644 --- a/src/stores/room-list/algorithms/Algorithm.ts +++ b/src/stores/room-list/algorithms/Algorithm.ts @@ -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 = new Map(); private allowedRoomsByFilters: Set = new Set(); + /** + * 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; }, []); 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. diff --git a/src/stores/room-list/filters/CommunityFilterCondition.ts b/src/stores/room-list/filters/CommunityFilterCondition.ts index fbdfefb983..a66bc01bce 100644 --- a/src/stores/room-list/filters/CommunityFilterCondition.ts +++ b/src/stores/room-list/filters/CommunityFilterCondition.ts @@ -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 { diff --git a/src/stores/room-list/filters/IFilterCondition.ts b/src/stores/room-list/filters/IFilterCondition.ts index 3b054eaece..cb9841a3c9 100644 --- a/src/stores/room-list/filters/IFilterCondition.ts +++ b/src/stores/room-list/filters/IFilterCondition.ts @@ -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 diff --git a/src/stores/room-list/filters/NameFilterCondition.ts b/src/stores/room-list/filters/NameFilterCondition.ts index 88edaecfb6..68c5a9bd6d 100644 --- a/src/stores/room-list/filters/NameFilterCondition.ts +++ b/src/stores/room-list/filters/NameFilterCondition.ts @@ -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 { diff --git a/src/stores/room-list/filters/SpaceFilterCondition.ts b/src/stores/room-list/filters/SpaceFilterCondition.ts index 49c58c9d1d..ad0ab88868 100644 --- a/src/stores/room-list/filters/SpaceFilterCondition.ts +++ b/src/stores/room-list/filters/SpaceFilterCondition.ts @@ -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(); 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); } }; diff --git a/test/components/views/rooms/RoomList-test.js b/test/components/views/rooms/RoomList-test.js index 1c2a1c9992..fcdd71629e 100644 --- a/test/components/views/rooms/RoomList-test.js +++ b/test/components/views/rooms/RoomList-test.js @@ -296,6 +296,11 @@ describe('RoomList', () => { GroupStore._notifyListeners(); await waitForRoomListStoreUpdate(); + + // XXX: Even though the store updated, it can take a bit before the update makes + // it to the components. This gives it plenty of time to figure out what to do. + await (new Promise(resolve => setTimeout(resolve, 500))); + expectRoomInSubList(otherRoom, (s) => s.props.tagId === DefaultTagID.Untagged); });