Restabilize room list ordering with prefiltering on spaces/communities
Fixes https://github.com/vector-im/element-web/issues/16799 This change replaces the "relative priority" system for filters with a kind model. The kind is used to differentiate and optimize when/where a filter condition is applied, resulting in a more stable ordering of the room list. The included documentation describes what this means in detail. This also introduces a way to inhibit updates being emitted from the Algorithm class given what we're doing to the poor thing will cause it to do a bunch of recalculation. Inhibiting the update and implicitly applying it (as part of our updateFn.mark()/trigger steps) results in much better performance. This has been tested on my own account with both communities and spaces of varying complexity: it feels faster, though the measurements appear to be within an error tolerance of each other (read: there's no performance impact of this).
This commit is contained in:
parent
2ab304189d
commit
70db749430
7 changed files with 192 additions and 102 deletions
|
@ -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
|
||||
subtly: 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
|
||||
|
|
|
@ -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) {
|
||||
|
@ -606,11 +664,18 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// 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(); // use a promise to maintain sync API contract
|
||||
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());
|
||||
}
|
||||
|
||||
public removeFilter(filter: IFilterCondition): void {
|
||||
|
@ -618,7 +683,8 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
|||
// 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(); // use a promise to maintain sync API contract
|
||||
let idx = this.filterConditions.indexOf(filter);
|
||||
if (idx >= 0) {
|
||||
this.filterConditions.splice(idx, 1);
|
||||
|
||||
|
@ -626,7 +692,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());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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, FilterKind, 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.
|
||||
|
@ -322,16 +332,7 @@ export class Algorithm extends EventEmitter {
|
|||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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…
Reference in a new issue