Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 73a8e77

Browse files
committed
Add initial filtering support to new room list
For element-hq/element-web#13635 This is an incomplete implementation and is mostly dumped in this state for review purposes. The remainder of the features/bugs are expected to be in more bite-sized chunks. This exposes the RoomListStore on the window for easy access to things like the new filter functions (used in debugging). This also adds initial handling of "new rooms" to the client, though the support is poor. Known bugs: * [ ] Regenerates the entire room list when a new room is seen. * [ ] Doesn't handle 2+ filters at the same time very well (see gif. will need a priority/ordering of some sort). * [ ] Doesn't handle room order changes within a tag yet, despite the docs implying it does.
1 parent 3d8e9f9 commit 73a8e77

14 files changed

+556
-27
lines changed

src/@types/global.d.ts

+2
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import ContentMessages from "../ContentMessages";
1919
import { IMatrixClientPeg } from "../MatrixClientPeg";
2020
import ToastStore from "../stores/ToastStore";
2121
import DeviceListener from "../DeviceListener";
22+
import { RoomListStore2 } from "../stores/room-list/RoomListStore2";
2223

2324
declare global {
2425
interface Window {
@@ -31,6 +32,7 @@ declare global {
3132
mx_ContentMessages: ContentMessages;
3233
mx_ToastStore: ToastStore;
3334
mx_DeviceListener: DeviceListener;
35+
mx_RoomListStore2: RoomListStore2;
3436
}
3537

3638
// workaround for https://github.com/microsoft/TypeScript/issues/30933

src/components/views/rooms/RoomList2.tsx

+18
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { Dispatcher } from "flux";
2828
import dis from "../../../dispatcher/dispatcher";
2929
import RoomSublist2 from "./RoomSublist2";
3030
import { ActionPayload } from "../../../dispatcher/payloads";
31+
import { IFilterCondition } from "../../../stores/room-list/filters/IFilterCondition";
32+
import { NameFilterCondition } from "../../../stores/room-list/filters/NameFilterCondition";
3133

3234
/*******************************************************************
3335
* CAUTION *
@@ -130,6 +132,7 @@ export default class RoomList2 extends React.Component<IProps, IState> {
130132
private sublistCollapseStates: { [tagId: string]: boolean } = {};
131133
private unfilteredLayout: Layout;
132134
private filteredLayout: Layout;
135+
private searchFilter: NameFilterCondition = new NameFilterCondition();
133136

134137
constructor(props: IProps) {
135138
super(props);
@@ -139,6 +142,21 @@ export default class RoomList2 extends React.Component<IProps, IState> {
139142
this.prepareLayouts();
140143
}
141144

145+
public componentDidUpdate(prevProps: Readonly<IProps>): void {
146+
if (prevProps.searchFilter !== this.props.searchFilter) {
147+
const hadSearch = !!this.searchFilter.search.trim();
148+
const haveSearch = !!this.props.searchFilter.trim();
149+
this.searchFilter.search = this.props.searchFilter;
150+
if (!hadSearch && haveSearch) {
151+
// started a new filter - add the condition
152+
RoomListStore.instance.addFilter(this.searchFilter);
153+
} else if (hadSearch && !haveSearch) {
154+
// cleared a filter - remove the condition
155+
RoomListStore.instance.removeFilter(this.searchFilter);
156+
} // else the filter hasn't changed enough for us to care here
157+
}
158+
}
159+
142160
public componentDidMount(): void {
143161
RoomListStore.instance.on(LISTS_UPDATE_EVENT, (store) => {
144162
console.log("new lists", store.orderedLists);

src/stores/room-list/README.md

+15
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,21 @@ an object containing the tags it needs to worry about and the rooms within. The
111111
decide which tags need rendering (as it commonly filters out empty tags in most cases), and will deal with
112112
all kinds of filtering.
113113

114+
## Filtering
115+
116+
Filters are provided to the store as condition classes, which are then passed along to the algorithm
117+
implementations. The implementations then get to decide how to actually filter the rooms, however in
118+
practice the base `Algorithm` class deals with the filtering in a more optimized/generic way.
119+
120+
The results of filters get cached to avoid needlessly iterating over potentially thousands of rooms,
121+
as the old room list store does. When a filter condition changes, it emits an update which (in this
122+
case) the `Algorithm` class will pick up and act accordingly. Typically, this also means filtering a
123+
minor subset where possible to avoid over-iterating rooms.
124+
125+
All filter conditions are considered "stable" by the consumers, meaning that the consumer does not
126+
expect a change in the condition unless the condition says it has changed. This is intentional to
127+
maintain the caching behaviour described above.
128+
114129
## Class breakdowns
115130

116131
The `RoomListStore` is the major coordinator of various `Algorithm` implementations, which take care

src/stores/room-list/RoomListStore2.ts

+75-12
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ limitations under the License.
1818
import { MatrixClient } from "matrix-js-sdk/src/client";
1919
import SettingsStore from "../../settings/SettingsStore";
2020
import { DefaultTagID, OrderedDefaultTagIDs, RoomUpdateCause, TagID } from "./models";
21-
import { Algorithm } from "./algorithms/list-ordering/Algorithm";
21+
import { Algorithm, LIST_UPDATED_EVENT } from "./algorithms/list-ordering/Algorithm";
2222
import TagOrderStore from "../TagOrderStore";
2323
import { AsyncStore } from "../AsyncStore";
2424
import { Room } from "matrix-js-sdk/src/models/room";
@@ -27,6 +27,8 @@ import { getListAlgorithmInstance } from "./algorithms/list-ordering";
2727
import { ActionPayload } from "../../dispatcher/payloads";
2828
import defaultDispatcher from "../../dispatcher/dispatcher";
2929
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
30+
import { IFilterCondition } from "./filters/IFilterCondition";
31+
import { TagWatcher } from "./TagWatcher";
3032

3133
interface IState {
3234
tagsEnabled?: boolean;
@@ -41,11 +43,13 @@ interface IState {
4143
*/
4244
export const LISTS_UPDATE_EVENT = "lists_update";
4345

44-
class _RoomListStore extends AsyncStore<ActionPayload> {
45-
private matrixClient: MatrixClient;
46+
export class RoomListStore2 extends AsyncStore<ActionPayload> {
47+
private _matrixClient: MatrixClient;
4648
private initialListsGenerated = false;
4749
private enabled = false;
4850
private algorithm: Algorithm;
51+
private filterConditions: IFilterCondition[] = [];
52+
private tagWatcher = new TagWatcher(this);
4953

5054
private readonly watchedSettings = [
5155
'RoomList.orderAlphabetically',
@@ -65,6 +69,10 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
6569
return this.algorithm.getOrderedRooms();
6670
}
6771

72+
public get matrixClient(): MatrixClient {
73+
return this._matrixClient;
74+
}
75+
6876
// TODO: Remove enabled flag when the old RoomListStore goes away
6977
private checkEnabled() {
7078
this.enabled = SettingsStore.isFeatureEnabled("feature_new_room_list");
@@ -96,7 +104,7 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
96104
this.checkEnabled();
97105
if (!this.enabled) return;
98106

99-
this.matrixClient = payload.matrixClient;
107+
this._matrixClient = payload.matrixClient;
100108

101109
// Update any settings here, as some may have happened before we were logically ready.
102110
console.log("Regenerating room lists: Startup");
@@ -111,7 +119,7 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
111119
// Reset state without causing updates as the client will have been destroyed
112120
// and downstream code will throw NPE errors.
113121
this.reset(null, true);
114-
this.matrixClient = null;
122+
this._matrixClient = null;
115123
this.initialListsGenerated = false; // we'll want to regenerate them
116124
}
117125

@@ -152,8 +160,21 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
152160

153161
const roomId = eventPayload.event.getRoomId();
154162
const room = this.matrixClient.getRoom(roomId);
155-
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${roomId}`);
156-
await this.handleRoomUpdate(room, RoomUpdateCause.Timeline);
163+
const tryUpdate = async (updatedRoom: Room) => {
164+
console.log(`[RoomListDebug] Live timeline event ${eventPayload.event.getId()} in ${updatedRoom.roomId}`);
165+
await this.handleRoomUpdate(updatedRoom, RoomUpdateCause.Timeline);
166+
};
167+
if (!room) {
168+
console.warn(`Live timeline event ${eventPayload.event.getId()} received without associated room`);
169+
console.warn(`Queuing failed room update for retry as a result.`);
170+
setTimeout(async () => {
171+
const updatedRoom = this.matrixClient.getRoom(roomId);
172+
await tryUpdate(updatedRoom);
173+
}, 100); // 100ms should be enough for the room to show up
174+
return;
175+
} else {
176+
await tryUpdate(room);
177+
}
157178
} else if (payload.action === 'MatrixActions.Event.decrypted') {
158179
const eventPayload = (<any>payload); // TODO: Type out the dispatcher types
159180
const roomId = eventPayload.event.getRoomId();
@@ -171,11 +192,20 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
171192
// TODO: Update DMs
172193
console.log(payload);
173194
} else if (payload.action === 'MatrixActions.Room.myMembership') {
195+
// TODO: Improve new room check
196+
const membershipPayload = (<any>payload); // TODO: Type out the dispatcher types
197+
if (!membershipPayload.oldMembership && membershipPayload.membership === "join") {
198+
console.log(`[RoomListDebug] Handling new room ${membershipPayload.room.roomId}`);
199+
await this.algorithm.handleRoomUpdate(membershipPayload.room, RoomUpdateCause.NewRoom);
200+
}
201+
174202
// TODO: Update room from membership change
175203
console.log(payload);
176204
} else if (payload.action === 'MatrixActions.Room') {
177-
// TODO: Update room from creation/join
178-
console.log(payload);
205+
// TODO: Improve new room check
206+
// const roomPayload = (<any>payload); // TODO: Type out the dispatcher types
207+
// console.log(`[RoomListDebug] Handling new room ${roomPayload.room.roomId}`);
208+
// await this.algorithm.handleRoomUpdate(roomPayload.room, RoomUpdateCause.NewRoom);
179209
} else if (payload.action === 'view_room') {
180210
// TODO: Update sticky room
181211
console.log(payload);
@@ -211,11 +241,22 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
211241
}
212242

213243
private setAlgorithmClass() {
244+
if (this.algorithm) {
245+
this.algorithm.off(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
246+
}
214247
this.algorithm = getListAlgorithmInstance(this.state.preferredAlgorithm);
248+
this.algorithm.setFilterConditions(this.filterConditions);
249+
this.algorithm.on(LIST_UPDATED_EVENT, this.onAlgorithmListUpdated);
215250
}
216251

252+
private onAlgorithmListUpdated = () => {
253+
console.log("Underlying algorithm has triggered a list update - refiring");
254+
this.emit(LISTS_UPDATE_EVENT, this);
255+
};
256+
217257
private async regenerateAllLists() {
218258
console.warn("Regenerating all room lists");
259+
219260
const tags: ITagSortingMap = {};
220261
for (const tagId of OrderedDefaultTagIDs) {
221262
tags[tagId] = this.getSortAlgorithmFor(tagId);
@@ -234,16 +275,38 @@ class _RoomListStore extends AsyncStore<ActionPayload> {
234275

235276
this.emit(LISTS_UPDATE_EVENT, this);
236277
}
278+
279+
public addFilter(filter: IFilterCondition): void {
280+
console.log("Adding filter condition:", filter);
281+
this.filterConditions.push(filter);
282+
if (this.algorithm) {
283+
this.algorithm.addFilterCondition(filter);
284+
}
285+
}
286+
287+
public removeFilter(filter: IFilterCondition): void {
288+
console.log("Removing filter condition:", filter);
289+
const idx = this.filterConditions.indexOf(filter);
290+
if (idx >= 0) {
291+
this.filterConditions.splice(idx, 1);
292+
293+
if (this.algorithm) {
294+
this.algorithm.removeFilterCondition(filter);
295+
}
296+
}
297+
}
237298
}
238299

239300
export default class RoomListStore {
240-
private static internalInstance: _RoomListStore;
301+
private static internalInstance: RoomListStore2;
241302

242-
public static get instance(): _RoomListStore {
303+
public static get instance(): RoomListStore2 {
243304
if (!RoomListStore.internalInstance) {
244-
RoomListStore.internalInstance = new _RoomListStore();
305+
RoomListStore.internalInstance = new RoomListStore2();
245306
}
246307

247308
return RoomListStore.internalInstance;
248309
}
249310
}
311+
312+
window.mx_RoomListStore2 = RoomListStore.instance;

src/stores/room-list/TagWatcher.ts

+80
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
Copyright 2020 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { RoomListStore2 } from "./RoomListStore2";
18+
import TagOrderStore from "../TagOrderStore";
19+
import { CommunityFilterCondition } from "./filters/CommunityFilterCondition";
20+
import { arrayDiff, arrayHasDiff, iteratorToArray } from "../../utils/arrays";
21+
22+
/**
23+
* Watches for changes in tags/groups to manage filters on the provided RoomListStore
24+
*/
25+
export class TagWatcher {
26+
// TODO: Support custom tags, somehow (deferred to later work - need support elsewhere)
27+
private filters = new Map<string, CommunityFilterCondition>();
28+
29+
constructor(private store: RoomListStore2) {
30+
TagOrderStore.addListener(this.onTagsUpdated);
31+
}
32+
33+
private onTagsUpdated = () => {
34+
const lastTags = iteratorToArray(this.filters.keys());
35+
const newTags = TagOrderStore.getSelectedTags();
36+
37+
if (arrayHasDiff(lastTags, newTags)) {
38+
// Selected tags changed, do some filtering
39+
40+
if (!this.store.matrixClient) {
41+
console.warn("Tag update without an associated matrix client - ignoring");
42+
return;
43+
}
44+
45+
const newFilters = new Map<string, CommunityFilterCondition>();
46+
47+
// TODO: Support custom tags properly
48+
const filterableTags = newTags.filter(t => t.startsWith("+"));
49+
50+
for (const tag of filterableTags) {
51+
const group = this.store.matrixClient.getGroup(tag);
52+
if (!group) {
53+
console.warn(`Group selected with no group object available: ${tag}`);
54+
continue;
55+
}
56+
57+
newFilters.set(tag, new CommunityFilterCondition(group));
58+
}
59+
60+
// Update the room list store's filters
61+
const diff = arrayDiff(lastTags, newTags);
62+
for (const tag of diff.added) {
63+
// TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters)
64+
const filter = newFilters.get(tag);
65+
if (!filter) continue;
66+
67+
this.store.addFilter(filter);
68+
}
69+
for (const tag of diff.removed) {
70+
// TODO: Remove this check when custom tags are supported (as we shouldn't be losing filters)
71+
const filter = this.filters.get(tag);
72+
if (!filter) continue;
73+
74+
this.store.removeFilter(filter);
75+
}
76+
77+
this.filters = newFilters;
78+
}
79+
};
80+
}

0 commit comments

Comments
 (0)