Skip to content

Commit 21e9d93

Browse files
Room List Store: Filter rooms by active space (#29399)
* Add method to await space store setup Otherwise, the room list store will get incorrect information about spaces and thus will produce an incorrect roomlist. * Implement a way to filter by active space Implement a way to filter by active space * Fix broken jest tests * Fix typo * Rename `isReady` to `storeReadyPromise` * Fix mock in test
1 parent ffa8971 commit 21e9d93

File tree

6 files changed

+153
-2
lines changed

6 files changed

+153
-2
lines changed

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

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import { RecencySorter } from "./skip-list/sorters/RecencySorter";
2121
import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter";
2222
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
2323
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership";
24+
import SpaceStore from "../spaces/SpaceStore";
25+
import { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
2426

2527
/**
2628
* This store allows for fast retrieval of the room list in a sorted and filtered manner.
@@ -34,6 +36,10 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
3436
public constructor(dispatcher: MatrixDispatcher) {
3537
super(dispatcher);
3638
this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
39+
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, () => {
40+
this.onActiveSpaceChanged();
41+
});
42+
SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged());
3743
}
3844

3945
/**
@@ -53,6 +59,14 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
5359
else return [];
5460
}
5561

62+
/**
63+
* Get a list of sorted rooms that belong to the currently active space.
64+
*/
65+
public getSortedRoomsInActiveSpace(): Room[] {
66+
if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList.getRoomsInActiveSpace());
67+
else return [];
68+
}
69+
5670
/**
5771
* Re-sort the list of rooms by alphabetic order.
5872
*/
@@ -78,6 +92,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
7892
const sorter = new RecencySorter(this.matrixClient.getSafeUserId());
7993
this.roomSkipList = new RoomSkipList(sorter);
8094
const rooms = this.getRooms();
95+
await SpaceStore.instance.storeReadyPromise;
8196
this.roomSkipList.seed(rooms);
8297
this.emit(LISTS_UPDATE_EVENT);
8398
}
@@ -178,6 +193,12 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
178193
this.roomSkipList.addRoom(room);
179194
this.emit(LISTS_UPDATE_EVENT);
180195
}
196+
197+
private onActiveSpaceChanged(): void {
198+
if (!this.roomSkipList) return;
199+
this.roomSkipList.calculateActiveSpaceForNodes();
200+
this.emit(LISTS_UPDATE_EVENT);
201+
}
181202
}
182203

183204
export default class RoomListStoreV3 {

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,16 @@ 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 SpaceStore from "../../spaces/SpaceStore";
910

1011
/**
1112
* Room skip list stores room nodes.
1213
* These hold the actual room object and provides references to other nodes
1314
* in different levels.
1415
*/
1516
export class RoomNode {
17+
private _isInActiveSpace: boolean = false;
18+
1619
public constructor(public readonly room: Room) {}
1720

1821
/**
@@ -26,4 +29,23 @@ export class RoomNode {
2629
* eg: previous[i] gives the previous room node from this room node in level i.
2730
*/
2831
public previous: RoomNode[] = [];
32+
33+
/**
34+
* Whether the room associated with this room node belongs to
35+
* the currently active space.
36+
* @see {@link SpaceStoreClass#activeSpace} to understand what active
37+
* space means.
38+
*/
39+
public get isInActiveSpace(): boolean {
40+
return this._isInActiveSpace;
41+
}
42+
43+
/**
44+
* Check if this room belongs to the active space and store the result
45+
* in {@link RoomNode#isInActiveSpace}.
46+
*/
47+
public checkIfRoomBelongsToActiveSpace(): void {
48+
const activeSpace = SpaceStore.instance.activeSpace;
49+
this._isInActiveSpace = SpaceStore.instance.isRoomInSpace(activeSpace, this.room.roomId);
50+
}
2951
}

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,22 @@ export class RoomSkipList implements Iterable<Room> {
4444
this.levels[currentLevel.level] = currentLevel;
4545
currentLevel = currentLevel.generateNextLevel();
4646
} while (currentLevel.size > 1);
47+
48+
// 3. Go through the list of rooms and mark nodes in active space
49+
this.calculateActiveSpaceForNodes();
50+
4751
this.initialized = true;
4852
}
4953

54+
/**
55+
* Go through all the room nodes and check if they belong to the active space.
56+
*/
57+
public calculateActiveSpaceForNodes(): void {
58+
for (const node of this.roomNodeMap.values()) {
59+
node.checkIfRoomBelongsToActiveSpace();
60+
}
61+
}
62+
5063
/**
5164
* Change the sorting algorithm used by the skip list.
5265
* This will reset the list and will rebuild from scratch.
@@ -81,6 +94,7 @@ export class RoomSkipList implements Iterable<Room> {
8194
this.removeRoom(room);
8295

8396
const newNode = new RoomNode(room);
97+
newNode.checkIfRoomBelongsToActiveSpace();
8498
this.roomNodeMap.set(room.roomId, newNode);
8599

86100
/**
@@ -159,6 +173,10 @@ export class RoomSkipList implements Iterable<Room> {
159173
return new SortedRoomIterator(this.levels[0].head!);
160174
}
161175

176+
public getRoomsInActiveSpace(): SortedSpaceFilteredIterator {
177+
return new SortedSpaceFilteredIterator(this.levels[0].head!);
178+
}
179+
162180
/**
163181
* The number of rooms currently in the skip list.
164182
*/
@@ -179,3 +197,23 @@ class SortedRoomIterator implements Iterator<Room> {
179197
};
180198
}
181199
}
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+
}

src/stores/spaces/SpaceStore.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
} from "matrix-js-sdk/src/matrix";
2222
import { KnownMembership } from "matrix-js-sdk/src/types";
2323
import { logger } from "matrix-js-sdk/src/logger";
24+
import { defer } from "matrix-js-sdk/src/utils";
2425

2526
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
2627
import defaultDispatcher from "../../dispatcher/dispatcher";
@@ -152,6 +153,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
152153
private _enabledMetaSpaces: MetaSpace[] = [];
153154
/** Whether the feature flag is set for MSC3946 */
154155
private _msc3946ProcessDynamicPredecessor: boolean = SettingsStore.getValue("feature_dynamic_room_predecessors");
156+
private _storeReadyDeferred = defer();
155157

156158
public constructor() {
157159
super(defaultDispatcher, {});
@@ -162,6 +164,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
162164
SettingsStore.monitorSetting("feature_dynamic_room_predecessors", null);
163165
}
164166

167+
/**
168+
* A promise that resolves when the space store is ready.
169+
* This happens after an initial hierarchy of spaces and rooms has been computed.
170+
*/
171+
public get storeReadyPromise(): Promise<void> {
172+
return this._storeReadyDeferred.promise;
173+
}
174+
165175
/**
166176
* Get the order of meta spaces to display in the space panel.
167177
*
@@ -1201,6 +1211,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
12011211
} else {
12021212
this.switchSpaceIfNeeded();
12031213
}
1214+
this._storeReadyDeferred.resolve();
12041215
}
12051216

12061217
private sendUserProperties(): void {

test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,13 @@ import { logger } from "matrix-js-sdk/src/logger";
1111
import { RoomListStoreV3Class } from "../../../../src/stores/room-list-v3/RoomListStoreV3";
1212
import { AsyncStoreWithClient } from "../../../../src/stores/AsyncStoreWithClient";
1313
import { RecencySorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
14-
import { mkEvent, mkMessage, stubClient, upsertRoomStateEvents } from "../../../test-utils";
14+
import { mkEvent, mkMessage, mkSpace, stubClient, upsertRoomStateEvents } from "../../../test-utils";
1515
import { getMockedRooms } from "./skip-list/getMockedRooms";
1616
import { AlphabeticSorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter";
1717
import { LISTS_UPDATE_EVENT } from "../../../../src/stores/room-list/RoomListStore";
1818
import dispatcher from "../../../../src/dispatcher/dispatcher";
19+
import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
20+
import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../../src/stores/spaces";
1921

2022
describe("RoomListStoreV3", () => {
2123
async function getRoomListStore() {
@@ -24,10 +26,16 @@ describe("RoomListStoreV3", () => {
2426
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
2527
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
2628
const store = new RoomListStoreV3Class(dispatcher);
27-
store.start();
29+
await store.start();
2830
return { client, rooms, store, dispatcher };
2931
}
3032

33+
beforeEach(() => {
34+
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space) => space === MetaSpace.Home);
35+
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => MetaSpace.Home);
36+
jest.spyOn(SpaceStore.instance, "storeReadyPromise", "get").mockImplementation(() => Promise.resolve());
37+
});
38+
3139
it("Provides an unsorted list of rooms", async () => {
3240
const { store, rooms } = await getRoomListStore();
3341
expect(store.getRooms()).toEqual(rooms);
@@ -264,5 +272,48 @@ describe("RoomListStoreV3", () => {
264272
expect(fn).not.toHaveBeenCalled();
265273
});
266274
});
275+
276+
describe("Spaces", () => {
277+
it("Filtering by spaces work", async () => {
278+
const client = stubClient();
279+
const rooms = getMockedRooms(client);
280+
281+
// Let's choose 5 rooms to put in space
282+
const indexes = [6, 8, 13, 27, 75];
283+
const roomIds = indexes.map((i) => rooms[i].roomId);
284+
const spaceRoom = mkSpace(client, "!space1:matrix.org", [], roomIds);
285+
rooms.push(spaceRoom);
286+
287+
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
288+
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
289+
290+
// Mock the space store
291+
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space, id) => {
292+
if (space === MetaSpace.Home && !roomIds.includes(id)) return true;
293+
if (space === spaceRoom.roomId && roomIds.includes(id)) return true;
294+
return false;
295+
});
296+
297+
const store = new RoomListStoreV3Class(dispatcher);
298+
await store.start();
299+
const fn = jest.fn();
300+
store.on(LISTS_UPDATE_EVENT, fn);
301+
302+
// The rooms which belong to the space should not be shown
303+
const result = store.getSortedRoomsInActiveSpace().map((r) => r.roomId);
304+
for (const id of roomIds) {
305+
expect(result).not.toContain(id);
306+
}
307+
308+
// Lets switch to the space
309+
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoom.roomId);
310+
SpaceStore.instance.emit(UPDATE_SELECTED_SPACE);
311+
expect(fn).toHaveBeenCalled();
312+
const result2 = store.getSortedRoomsInActiveSpace().map((r) => r.roomId);
313+
for (const id of roomIds) {
314+
expect(result2).toContain(id);
315+
}
316+
});
317+
});
267318
});
268319
});

test/unit-tests/stores/room-list-v3/skip-list/RoomSkipList-test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import { RoomSkipList } from "../../../../../src/stores/room-list-v3/skip-list/R
1414
import { RecencySorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
1515
import { AlphabeticSorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter";
1616
import { getMockedRooms } from "./getMockedRooms";
17+
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
18+
import { MetaSpace } from "../../../../../src/stores/spaces";
1719

1820
describe("RoomSkipList", () => {
1921
function generateSkipList(roomCount?: number): {
@@ -30,6 +32,12 @@ describe("RoomSkipList", () => {
3032
return { skipList, rooms, totalRooms: rooms.length, sorter };
3133
}
3234

35+
beforeEach(() => {
36+
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space) => space === MetaSpace.Home);
37+
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => MetaSpace.Home);
38+
jest.spyOn(SpaceStore.instance, "storeReadyPromise", "get").mockImplementation(() => Promise.resolve());
39+
});
40+
3341
it("Rooms are in sorted order after initial seed", () => {
3442
const { skipList, totalRooms } = generateSkipList();
3543
expect(skipList.size).toEqual(totalRooms);

0 commit comments

Comments
 (0)