Skip to content

Commit 7ff1fd2

Browse files
Room List Store: Support filters by implementing just the favourite filter (#29433)
* Implement the favourite filter * Make the room node capable of dealing with filters - Holds data to indicate which filters apply - Provides method to check if a given set of filters apply to this node - Provides a method to recalculate which filters apply * Wire up the filtering mechanism in skip list * Use filters in the store * Remove else * Use a set instead of map
1 parent 8d891cd commit 7ff1fd2

File tree

7 files changed

+245
-49
lines changed

7 files changed

+245
-49
lines changed

src/stores/room-list-v3/RoomListStoreV3.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { EventType } from "matrix-js-sdk/src/matrix";
1111
import type { EmptyObject, Room, RoomState } from "matrix-js-sdk/src/matrix";
1212
import type { MatrixDispatcher } from "../../dispatcher/dispatcher";
1313
import type { ActionPayload } from "../../dispatcher/payloads";
14+
import type { FilterKey } from "./skip-list/filters";
1415
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
1516
import SettingsStore from "../../settings/SettingsStore";
1617
import { VisibilityProvider } from "../room-list/filters/VisibilityProvider";
@@ -23,6 +24,7 @@ import { readReceiptChangeIsFor } from "../../utils/read-receipts";
2324
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership";
2425
import SpaceStore from "../spaces/SpaceStore";
2526
import { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
27+
import { FavouriteFilter } from "./skip-list/filters/FavouriteFilter";
2628

2729
/**
2830
* This store allows for fast retrieval of the room list in a sorted and filtered manner.
@@ -61,9 +63,13 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
6163

6264
/**
6365
* Get a list of sorted rooms that belong to the currently active space.
66+
* If filterKeys is passed, only the rooms that match the given filters are
67+
* returned.
68+
69+
* @param filterKeys Optional array of filters that the rooms must match against.
6470
*/
65-
public getSortedRoomsInActiveSpace(): Room[] {
66-
if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList.getRoomsInActiveSpace());
71+
public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): Room[] {
72+
if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList.getRoomsInActiveSpace(filterKeys));
6773
else return [];
6874
}
6975

@@ -90,7 +96,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
9096
protected async onReady(): Promise<any> {
9197
if (this.roomSkipList?.initialized || !this.matrixClient) return;
9298
const sorter = new RecencySorter(this.matrixClient.getSafeUserId());
93-
this.roomSkipList = new RoomSkipList(sorter);
99+
this.roomSkipList = new RoomSkipList(sorter, [new FavouriteFilter()]);
94100
const rooms = this.getRooms();
95101
await SpaceStore.instance.storeReadyPromise;
96102
this.roomSkipList.seed(rooms);

src/stores/room-list-v3/skip-list/RoomNode.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details.
66
*/
77

88
import type { Room } from "matrix-js-sdk/src/matrix";
9+
import type { Filter, FilterKey } from "./filters";
910
import SpaceStore from "../../spaces/SpaceStore";
1011

1112
/**
@@ -48,4 +49,31 @@ export class RoomNode {
4849
const activeSpace = SpaceStore.instance.activeSpace;
4950
this._isInActiveSpace = SpaceStore.instance.isRoomInSpace(activeSpace, this.room.roomId);
5051
}
52+
53+
/**
54+
* Aggregates all the filter keys that apply to this room.
55+
* eg: if filterKeysSet.has(Filter.FavouriteFilter) is true, then this room is a favourite room.
56+
*/
57+
private filterKeysSet: Set<FilterKey> = new Set();
58+
59+
/**
60+
* Returns true if the associated room matches all the provided filters.
61+
* Returns false otherwise.
62+
* @param filterKeys An array of filter keys to check against.
63+
*/
64+
public doesRoomMatchFilters(filterKeys: FilterKey[]): boolean {
65+
return !filterKeys.some((key) => !this.filterKeysSet.has(key));
66+
}
67+
68+
/**
69+
* Populates {@link RoomNode#filterKeysSet} by checking if the associated room
70+
* satisfies the given filters.
71+
* @param filters A list of filters
72+
*/
73+
public applyFilters(filters: Filter[]): void {
74+
this.filterKeysSet = new Set();
75+
for (const filter of filters) {
76+
if (filter.matches(this.room)) this.filterKeysSet.add(filter.key);
77+
}
78+
}
5179
}

src/stores/room-list-v3/skip-list/RoomSkipList.ts

Lines changed: 24 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ Please see LICENSE files in the repository root for full details.
77

88
import type { Room } from "matrix-js-sdk/src/matrix";
99
import type { Sorter } from "./sorters";
10+
import type { Filter, FilterKey } from "./filters";
1011
import { RoomNode } from "./RoomNode";
1112
import { shouldPromote } from "./utils";
1213
import { Level } from "./Level";
14+
import { SortedRoomIterator, SortedSpaceFilteredIterator } from "./iterators";
1315

1416
/**
1517
* Implements a skip list that stores rooms using a given sorting algorithm.
@@ -20,7 +22,10 @@ export class RoomSkipList implements Iterable<Room> {
2022
private roomNodeMap: Map<string, RoomNode> = new Map();
2123
public initialized: boolean = false;
2224

23-
public constructor(private sorter: Sorter) {}
25+
public constructor(
26+
private sorter: Sorter,
27+
private filters: Filter[] = [],
28+
) {}
2429

2530
private reset(): void {
2631
this.levels = [new Level(0)];
@@ -35,6 +40,7 @@ export class RoomSkipList implements Iterable<Room> {
3540
const sortedRoomNodes = this.sorter.sort(rooms).map((room) => new RoomNode(room));
3641
let currentLevel = this.levels[0];
3742
for (const node of sortedRoomNodes) {
43+
node.applyFilters(this.filters);
3844
currentLevel.setNext(node);
3945
this.roomNodeMap.set(node.room.roomId, node);
4046
}
@@ -95,6 +101,7 @@ export class RoomSkipList implements Iterable<Room> {
95101

96102
const newNode = new RoomNode(room);
97103
newNode.checkIfRoomBelongsToActiveSpace();
104+
newNode.applyFilters(this.filters);
98105
this.roomNodeMap.set(room.roomId, newNode);
99106

100107
/**
@@ -173,8 +180,22 @@ export class RoomSkipList implements Iterable<Room> {
173180
return new SortedRoomIterator(this.levels[0].head!);
174181
}
175182

176-
public getRoomsInActiveSpace(): SortedSpaceFilteredIterator {
177-
return new SortedSpaceFilteredIterator(this.levels[0].head!);
183+
/**
184+
* Returns an iterator that can be used to generate a list of sorted rooms that belong
185+
* to the currently active space. Passing filterKeys will further filter the list such
186+
* that only rooms that match the filters are returned.
187+
*
188+
* @example To get an array of rooms:
189+
* Array.from(RLS.getRoomsInActiveSpace());
190+
*
191+
* @example Use a for ... of loop to iterate over rooms:
192+
* for(const room of RLS.getRoomsInActiveSpace()) { something(room); }
193+
*
194+
* @example Additional filtering:
195+
* Array.from(RLS.getRoomsInActiveSpace([FilterKeys.Favourite]));
196+
*/
197+
public getRoomsInActiveSpace(filterKeys: FilterKey[] = []): SortedSpaceFilteredIterator {
198+
return new SortedSpaceFilteredIterator(this.levels[0].head!, filterKeys);
178199
}
179200

180201
/**
@@ -184,36 +205,3 @@ export class RoomSkipList implements Iterable<Room> {
184205
return this.levels[0].size;
185206
}
186207
}
187-
188-
class SortedRoomIterator implements Iterator<Room> {
189-
public constructor(private current: RoomNode) {}
190-
191-
public next(): IteratorResult<Room> {
192-
const current = this.current;
193-
if (!current) return { value: undefined, done: true };
194-
this.current = current.next[0];
195-
return {
196-
value: current.room,
197-
};
198-
}
199-
}
200-
201-
class SortedSpaceFilteredIterator implements Iterator<Room> {
202-
public constructor(private current: RoomNode) {}
203-
204-
public [Symbol.iterator](): SortedSpaceFilteredIterator {
205-
return this;
206-
}
207-
208-
public next(): IteratorResult<Room> {
209-
let current = this.current;
210-
while (current && !current.isInActiveSpace) {
211-
current = current.next[0];
212-
}
213-
if (!current) return { value: undefined, done: true };
214-
this.current = current.next[0];
215-
return {
216-
value: current.room,
217-
};
218-
}
219-
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
4+
Please see LICENSE files in the repository root for full details.
5+
*/
6+
7+
import type { Room } from "matrix-js-sdk/src/matrix";
8+
import type { Filter } from ".";
9+
import { FilterKey } from ".";
10+
import { DefaultTagID } from "../../../room-list/models";
11+
12+
export class FavouriteFilter implements Filter {
13+
public matches(room: Room): boolean {
14+
return !!room.tags[DefaultTagID.Favourite];
15+
}
16+
17+
public get key(): FilterKey.FavouriteFilter {
18+
return FilterKey.FavouriteFilter;
19+
}
20+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
4+
Please see LICENSE files in the repository root for full details.
5+
*/
6+
7+
import type { Room } from "matrix-js-sdk/src/matrix";
8+
9+
export const enum FilterKey {
10+
FavouriteFilter,
11+
}
12+
13+
export interface Filter {
14+
/**
15+
* Boolean return value indicates whether this room satisfies
16+
* the filter condition.
17+
*/
18+
matches(room: Room): boolean;
19+
20+
/**
21+
* Used to identify this particular filter.
22+
*/
23+
key: FilterKey;
24+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
Copyright 2025 New Vector Ltd.
3+
4+
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
5+
Please see LICENSE files in the repository root for full details.
6+
*/
7+
8+
import type { Room } from "matrix-js-sdk/src/matrix";
9+
import type { RoomNode } from "./RoomNode";
10+
import type { FilterKey } from "./filters";
11+
12+
export class SortedRoomIterator implements Iterator<Room> {
13+
public constructor(private current: RoomNode) {}
14+
15+
public next(): IteratorResult<Room> {
16+
const current = this.current;
17+
if (!current) return { value: undefined, done: true };
18+
this.current = current.next[0];
19+
return {
20+
value: current.room,
21+
};
22+
}
23+
}
24+
25+
export class SortedSpaceFilteredIterator implements Iterator<Room> {
26+
public constructor(
27+
private current: RoomNode,
28+
private readonly filters: FilterKey[],
29+
) {}
30+
31+
public [Symbol.iterator](): SortedSpaceFilteredIterator {
32+
return this;
33+
}
34+
35+
public next(): IteratorResult<Room> {
36+
let current = this.current;
37+
while (current) {
38+
if (current.isInActiveSpace && current.doesRoomMatchFilters(this.filters)) break;
39+
current = current.next[0];
40+
}
41+
if (!current) return { value: undefined, done: true };
42+
this.current = current.next[0];
43+
return {
44+
value: current.room,
45+
};
46+
}
47+
}

0 commit comments

Comments
 (0)