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:
|
Legend:
|
||||||
* Orange = External event.
|
* Orange = External event.
|
||||||
* Purple = Deterministic flow.
|
* Purple = Deterministic flow.
|
||||||
* Green = Algorithm definition.
|
* Green = Algorithm definition.
|
||||||
* Red = Exit condition/point.
|
* Red = Exit condition/point.
|
||||||
* Blue = Process definition.
|
* 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
|
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,
|
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.
|
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.
|
Each category then gets sorted by the appropriate tag sorting algorithm.
|
||||||
|
|
||||||
### Tag sorting algorithm: Alphabetical
|
### Tag sorting algorithm: Alphabetical
|
||||||
|
@ -36,7 +36,7 @@ useful.
|
||||||
|
|
||||||
### Tag sorting algorithm: Manual
|
### 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
|
[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.
|
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'.
|
set to 'All Messages'.
|
||||||
* **Bold**: The room has unread messages waiting for the user. Essentially this is a grey room without
|
* **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').
|
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.
|
last read it.
|
||||||
|
|
||||||
Conveniently, each tag gets ordered by those categories as presented: red rooms appear above grey, grey
|
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
|
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)
|
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.
|
collectively the tag will be sorted into categories with red being at the top.
|
||||||
|
|
||||||
## Sticky rooms
|
## 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
|
above the sticky room will move underneath to allow for the new room to take the top slot, maintaining
|
||||||
the sticky room's position.
|
the sticky room's position.
|
||||||
|
|
||||||
Though only applicable to the importance algorithm, the sticky room is not aware of category boundaries
|
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
|
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
|
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
|
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.
|
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
|
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
|
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 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.
|
put the sticky room in a position where it's had to decrease N will not increase N.
|
||||||
|
|
||||||
## Responsibilities of the store
|
## Responsibilities of the store
|
||||||
|
|
||||||
The store is responsible for the ordering, upkeep, and tracking of all rooms. The room list component simply gets
|
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
|
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
|
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
|
||||||
all kinds of filtering.
|
all kinds of filtering.
|
||||||
|
|
||||||
## Filtering
|
## Filtering
|
||||||
|
|
||||||
Filters are provided to the store as condition classes, which are then passed along to the algorithm
|
Filters are provided to the store as condition classes and have two major kinds: Prefilters and Runtime.
|
||||||
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.
|
|
||||||
|
|
||||||
The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms,
|
Prefilters flush out rooms which shouldn't appear to the algorithm implementations. Typically this is
|
||||||
as the old room list store does. When a filter condition changes, it emits an update which (in this
|
due to some higher order room list filtering (such as spaces or tags) deliberately exposing a subset of
|
||||||
case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
|
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.
|
minor subset where possible to avoid over-iterating rooms.
|
||||||
|
|
||||||
All filter conditions are considered "stable" by the consumers, meaning that the consumer does not
|
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
|
expect a change in the condition unless the condition says it has changed. This is intentional to
|
||||||
maintain the caching behaviour described above.
|
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
|
## Class breakdowns
|
||||||
|
|
||||||
The `RoomListStore` is the major coordinator of various algorithm implementations, which take care
|
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
|
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
|
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
|
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
|
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
|
more general when needed. For example, the `membership` utilities could easily be moved elsewhere
|
||||||
as needed.
|
as needed.
|
||||||
|
|
||||||
The various bits throughout the room list store should also have jsdoc of some kind to help describe
|
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 2018-2021 The Matrix.org Foundation C.I.C.
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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.
|
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 SettingsStore from "../../settings/SettingsStore";
|
||||||
import { DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
|
import {DefaultTagID, isCustomTag, OrderedDefaultTagIDs, RoomUpdateCause, TagID} from "./models";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
import {Room} from "matrix-js-sdk/src/models/room";
|
||||||
import { IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm } from "./algorithms/models";
|
import {IListOrderingMap, ITagMap, ITagSortingMap, ListAlgorithm, SortAlgorithm} from "./algorithms/models";
|
||||||
import { ActionPayload } from "../../dispatcher/payloads";
|
import {ActionPayload} from "../../dispatcher/payloads";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
import {readReceiptChangeIsFor} from "../../utils/read-receipts";
|
||||||
import { FILTER_CHANGED, IFilterCondition } from "./filters/IFilterCondition";
|
import {FILTER_CHANGED, FilterKind, IFilterCondition} from "./filters/IFilterCondition";
|
||||||
import { TagWatcher } from "./TagWatcher";
|
import {TagWatcher} from "./TagWatcher";
|
||||||
import RoomViewStore from "../RoomViewStore";
|
import RoomViewStore from "../RoomViewStore";
|
||||||
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/Algorithm";
|
import {Algorithm, LIST_UPDATED_EVENT} from "./algorithms/Algorithm";
|
||||||
import { EffectiveMembership, getEffectiveMembership } from "../../utils/membership";
|
import {EffectiveMembership, getEffectiveMembership} from "../../utils/membership";
|
||||||
import { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
import {isNullOrUndefined} from "matrix-js-sdk/src/utils";
|
||||||
import RoomListLayoutStore from "./RoomListLayoutStore";
|
import RoomListLayoutStore from "./RoomListLayoutStore";
|
||||||
import { MarkedExecution } from "../../utils/MarkedExecution";
|
import {MarkedExecution} from "../../utils/MarkedExecution";
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
import {AsyncStoreWithClient} from "../AsyncStoreWithClient";
|
||||||
import { NameFilterCondition } from "./filters/NameFilterCondition";
|
import {NameFilterCondition} from "./filters/NameFilterCondition";
|
||||||
import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore";
|
import {RoomNotificationStateStore} from "../notifications/RoomNotificationStateStore";
|
||||||
import { VisibilityProvider } from "./filters/VisibilityProvider";
|
import {VisibilityProvider} from "./filters/VisibilityProvider";
|
||||||
import { SpaceWatcher } from "./SpaceWatcher";
|
import {SpaceWatcher} from "./SpaceWatcher";
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
tagsEnabled?: boolean;
|
tagsEnabled?: boolean;
|
||||||
|
@ -57,6 +56,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
private initialListsGenerated = false;
|
private initialListsGenerated = false;
|
||||||
private algorithm = new Algorithm();
|
private algorithm = new Algorithm();
|
||||||
private filterConditions: IFilterCondition[] = [];
|
private filterConditions: IFilterCondition[] = [];
|
||||||
|
private prefilterConditions: IFilterCondition[] = [];
|
||||||
private tagWatcher: TagWatcher;
|
private tagWatcher: TagWatcher;
|
||||||
private spaceWatcher: SpaceWatcher;
|
private spaceWatcher: SpaceWatcher;
|
||||||
private updateFn = new MarkedExecution(() => {
|
private updateFn = new MarkedExecution(() => {
|
||||||
|
@ -104,6 +104,7 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
public async resetStore() {
|
public async resetStore() {
|
||||||
await this.reset();
|
await this.reset();
|
||||||
this.filterConditions = [];
|
this.filterConditions = [];
|
||||||
|
this.prefilterConditions = [];
|
||||||
this.initialListsGenerated = false;
|
this.initialListsGenerated = false;
|
||||||
this.setupWatchers();
|
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) {
|
public async setTagSorting(tagId: TagID, sort: SortAlgorithm) {
|
||||||
await this.setAndPersistTagSorting(tagId, sort);
|
await this.setAndPersistTagSorting(tagId, sort);
|
||||||
this.updateFn.trigger();
|
this.updateFn.trigger();
|
||||||
|
@ -557,6 +591,34 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.updateFn.trigger();
|
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.
|
* 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}) {
|
public async regenerateAllLists({trigger = true}) {
|
||||||
console.warn("Regenerating all room lists");
|
console.warn("Regenerating all room lists");
|
||||||
|
|
||||||
const rooms = [
|
const rooms = this.getPlausibleRooms();
|
||||||
...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 customTags = new Set<TagID>();
|
const customTags = new Set<TagID>();
|
||||||
if (this.state.tagsEnabled) {
|
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
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.log("Adding filter condition:", filter);
|
console.log("Adding filter condition:", filter);
|
||||||
}
|
}
|
||||||
this.filterConditions.push(filter);
|
let promise = Promise.resolve(); // use a promise to maintain sync API contract
|
||||||
if (this.algorithm) {
|
if (filter.kind === FilterKind.Prefilter) {
|
||||||
this.algorithm.addFilterCondition(filter);
|
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 {
|
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
|
// TODO: Remove debug: https://github.com/vector-im/element-web/issues/14602
|
||||||
console.log("Removing filter condition:", filter);
|
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) {
|
if (idx >= 0) {
|
||||||
this.filterConditions.splice(idx, 1);
|
this.filterConditions.splice(idx, 1);
|
||||||
|
|
||||||
|
@ -626,7 +692,13 @@ export class RoomListStoreClass extends AsyncStoreWithClient<IState> {
|
||||||
this.algorithm.removeFilterCondition(filter);
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 { isNullOrUndefined } from "matrix-js-sdk/src/utils";
|
||||||
import DMRoomMap from "../../../utils/DMRoomMap";
|
import DMRoomMap from "../../../utils/DMRoomMap";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { arrayDiff, arrayHasDiff, ArrayUtil } from "../../../utils/arrays";
|
import { arrayDiff, arrayHasDiff } from "../../../utils/arrays";
|
||||||
import { getEnumValues } from "../../../utils/enums";
|
|
||||||
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
import { DefaultTagID, RoomUpdateCause, TagID } from "../models";
|
||||||
import {
|
import {
|
||||||
IListOrderingMap,
|
IListOrderingMap,
|
||||||
|
@ -29,7 +28,7 @@ import {
|
||||||
ListAlgorithm,
|
ListAlgorithm,
|
||||||
SortAlgorithm,
|
SortAlgorithm,
|
||||||
} from "./models";
|
} 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 { EffectiveMembership, getEffectiveMembership, splitRoomsByMembership } from "../../../utils/membership";
|
||||||
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
import { OrderingAlgorithm } from "./list-ordering/OrderingAlgorithm";
|
||||||
import { getListAlgorithmInstance } from "./list-ordering";
|
import { getListAlgorithmInstance } from "./list-ordering";
|
||||||
|
@ -79,6 +78,11 @@ export class Algorithm extends EventEmitter {
|
||||||
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
private allowedByFilter: Map<IFilterCondition, Room[]> = new Map<IFilterCondition, Room[]>();
|
||||||
private allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
private allowedRoomsByFilters: Set<Room> = new Set<Room>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set to true to suspend emissions of algorithm updates.
|
||||||
|
*/
|
||||||
|
public updatesInhibited = false;
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
@ -87,6 +91,14 @@ export class Algorithm extends EventEmitter {
|
||||||
return this._stickyRoom ? this._stickyRoom.room : null;
|
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 {
|
protected get hasFilters(): boolean {
|
||||||
return this.allowedByFilter.size > 0;
|
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
|
// 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.
|
// view. This will trick them into getting the complete room list.
|
||||||
if (!this.hasFilters) {
|
if (!this.hasFilters && !this.updatesInhibited) {
|
||||||
this.emit(LIST_UPDATED_EVENT);
|
this.emit(LIST_UPDATED_EVENT);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -174,6 +186,7 @@ export class Algorithm extends EventEmitter {
|
||||||
await this.recalculateFilteredRooms();
|
await this.recalculateFilteredRooms();
|
||||||
|
|
||||||
// re-emit the update so the list store can fire an off-cycle update if needed
|
// re-emit the update so the list store can fire an off-cycle update if needed
|
||||||
|
if (this.updatesInhibited) return;
|
||||||
this.emit(FILTER_CHANGED);
|
this.emit(FILTER_CHANGED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -299,6 +312,7 @@ export class Algorithm extends EventEmitter {
|
||||||
this.recalculateStickyRoom();
|
this.recalculateStickyRoom();
|
||||||
|
|
||||||
// Finally, trigger an update
|
// Finally, trigger an update
|
||||||
|
if (this.updatesInhibited) return;
|
||||||
this.emit(LIST_UPDATED_EVENT);
|
this.emit(LIST_UPDATED_EVENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -309,10 +323,6 @@ export class Algorithm extends EventEmitter {
|
||||||
|
|
||||||
console.warn("Recalculating filtered room list");
|
console.warn("Recalculating filtered room list");
|
||||||
const filters = Array.from(this.allowedByFilter.keys());
|
const filters = Array.from(this.allowedByFilter.keys());
|
||||||
const orderedFilters = new ArrayUtil(filters)
|
|
||||||
.groupBy(f => f.relativePriority)
|
|
||||||
.orderBy(getEnumValues(FilterPriority))
|
|
||||||
.value;
|
|
||||||
const newMap: ITagMap = {};
|
const newMap: ITagMap = {};
|
||||||
for (const tagId of Object.keys(this.cachedRooms)) {
|
for (const tagId of Object.keys(this.cachedRooms)) {
|
||||||
// Cheaply clone the rooms so we can more easily do operations on the list.
|
// 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);
|
this.tryInsertStickyRoomToFilterSet(rooms, tagId);
|
||||||
let remainingRooms = rooms.map(r => r);
|
let remainingRooms = rooms.map(r => r);
|
||||||
let allowedRoomsInThisTag = [];
|
let allowedRoomsInThisTag = [];
|
||||||
let lastFilterPriority = orderedFilters[0].relativePriority;
|
for (const filter of filters) {
|
||||||
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 filteredRooms = remainingRooms.filter(r => filter.isVisible(r));
|
const filteredRooms = remainingRooms.filter(r => filter.isVisible(r));
|
||||||
for (const room of filteredRooms) {
|
for (const room of filteredRooms) {
|
||||||
const idx = remainingRooms.indexOf(room);
|
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[]>[]);
|
const allowedRooms = Object.values(newMap).reduce((rv, v) => { rv.push(...v); return rv; }, <Room[]>[]);
|
||||||
this.allowedRoomsByFilters = new Set(allowedRooms);
|
this.allowedRoomsByFilters = new Set(allowedRooms);
|
||||||
this.filteredRooms = newMap;
|
this.filteredRooms = newMap;
|
||||||
|
if (this.updatesInhibited) return;
|
||||||
this.emit(LIST_UPDATED_EVENT);
|
this.emit(LIST_UPDATED_EVENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -404,6 +406,7 @@ export class Algorithm extends EventEmitter {
|
||||||
if (!!this._cachedStickyRooms) {
|
if (!!this._cachedStickyRooms) {
|
||||||
// Clear the cache if we won't be needing it
|
// Clear the cache if we won't be needing it
|
||||||
this._cachedStickyRooms = null;
|
this._cachedStickyRooms = null;
|
||||||
|
if (this.updatesInhibited) return;
|
||||||
this.emit(LIST_UPDATED_EVENT);
|
this.emit(LIST_UPDATED_EVENT);
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
|
@ -446,6 +449,7 @@ export class Algorithm extends EventEmitter {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, trigger an update
|
// Finally, trigger an update
|
||||||
|
if (this.updatesInhibited) return;
|
||||||
this.emit(LIST_UPDATED_EVENT);
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 { 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 { Group } from "matrix-js-sdk/src/models/group";
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import GroupStore from "../../GroupStore";
|
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
|
this.onStoreUpdate(); // trigger a false update to seed the store
|
||||||
}
|
}
|
||||||
|
|
||||||
public get relativePriority(): FilterPriority {
|
public get kind(): FilterKind {
|
||||||
// Lowest priority so we can coarsely find rooms.
|
return FilterKind.Prefilter;
|
||||||
return FilterPriority.Lowest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public isVisible(room: Room): boolean {
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 const FILTER_CHANGED = "filter_changed";
|
||||||
|
|
||||||
export enum FilterPriority {
|
export enum FilterKind {
|
||||||
Lowest,
|
/**
|
||||||
// in the middle would be Low, Normal, and High if we had a need
|
* A prefilter is one which coarsely determines which rooms are
|
||||||
Highest,
|
* 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 {
|
export interface IFilterCondition extends EventEmitter {
|
||||||
/**
|
/**
|
||||||
* The relative priority that this filter should be applied with.
|
* The kind of filter this presents.
|
||||||
* Lower priorities get applied first.
|
|
||||||
*/
|
*/
|
||||||
relativePriority: FilterPriority;
|
kind: FilterKind;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if a given room should be visible under this
|
* 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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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 { 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 { EventEmitter } from "events";
|
||||||
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
|
import { removeHiddenChars } from "matrix-js-sdk/src/utils";
|
||||||
import { throttle } from "lodash";
|
import { throttle } from "lodash";
|
||||||
|
@ -31,9 +31,8 @@ export class NameFilterCondition extends EventEmitter implements IFilterConditio
|
||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get relativePriority(): FilterPriority {
|
public get kind(): FilterKind {
|
||||||
// We want this one to be at the highest priority so it can search within other filters.
|
return FilterKind.Runtime;
|
||||||
return FilterPriority.Highest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public get search(): string {
|
public get search(): string {
|
||||||
|
|
|
@ -17,7 +17,7 @@ limitations under the License.
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from "events";
|
||||||
import { Room } from "matrix-js-sdk/src/models/room";
|
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 { IDestroyable } from "../../../utils/IDestroyable";
|
||||||
import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
|
import SpaceStore, {HOME_SPACE} from "../../SpaceStore";
|
||||||
import { setHasDiff } from "../../../utils/sets";
|
import { setHasDiff } from "../../../utils/sets";
|
||||||
|
@ -32,9 +32,8 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
|
||||||
private roomIds = new Set<Room>();
|
private roomIds = new Set<Room>();
|
||||||
private space: Room = null;
|
private space: Room = null;
|
||||||
|
|
||||||
public get relativePriority(): FilterPriority {
|
public get kind(): FilterKind {
|
||||||
// Lowest priority so we can coarsely find rooms.
|
return FilterKind.Prefilter;
|
||||||
return FilterPriority.Lowest;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public isVisible(room: Room): boolean {
|
public isVisible(room: Room): boolean {
|
||||||
|
@ -46,12 +45,7 @@ export class SpaceFilterCondition extends EventEmitter implements IFilterConditi
|
||||||
this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space);
|
this.roomIds = SpaceStore.instance.getSpaceFilteredRoomIds(this.space);
|
||||||
|
|
||||||
if (setHasDiff(beforeRoomIds, this.roomIds)) {
|
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);
|
this.emit(FILTER_CHANGED);
|
||||||
setTimeout(() => {
|
|
||||||
this.emit(FILTER_CHANGED);
|
|
||||||
}, 500);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue