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
This commit is contained in:
@@ -21,6 +21,8 @@ import { RecencySorter } from "./skip-list/sorters/RecencySorter";
|
|||||||
import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter";
|
import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter";
|
||||||
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
import { readReceiptChangeIsFor } from "../../utils/read-receipts";
|
||||||
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership";
|
import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership";
|
||||||
|
import SpaceStore from "../spaces/SpaceStore";
|
||||||
|
import { UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This store allows for fast retrieval of the room list in a sorted and filtered manner.
|
* 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> {
|
|||||||
public constructor(dispatcher: MatrixDispatcher) {
|
public constructor(dispatcher: MatrixDispatcher) {
|
||||||
super(dispatcher);
|
super(dispatcher);
|
||||||
this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
|
this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors");
|
||||||
|
SpaceStore.instance.on(UPDATE_SELECTED_SPACE, () => {
|
||||||
|
this.onActiveSpaceChanged();
|
||||||
|
});
|
||||||
|
SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,6 +59,14 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
else return [];
|
else return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a list of sorted rooms that belong to the currently active space.
|
||||||
|
*/
|
||||||
|
public getSortedRoomsInActiveSpace(): Room[] {
|
||||||
|
if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList.getRoomsInActiveSpace());
|
||||||
|
else return [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Re-sort the list of rooms by alphabetic order.
|
* Re-sort the list of rooms by alphabetic order.
|
||||||
*/
|
*/
|
||||||
@@ -78,6 +92,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
const sorter = new RecencySorter(this.matrixClient.getSafeUserId());
|
const sorter = new RecencySorter(this.matrixClient.getSafeUserId());
|
||||||
this.roomSkipList = new RoomSkipList(sorter);
|
this.roomSkipList = new RoomSkipList(sorter);
|
||||||
const rooms = this.getRooms();
|
const rooms = this.getRooms();
|
||||||
|
await SpaceStore.instance.storeReadyPromise;
|
||||||
this.roomSkipList.seed(rooms);
|
this.roomSkipList.seed(rooms);
|
||||||
this.emit(LISTS_UPDATE_EVENT);
|
this.emit(LISTS_UPDATE_EVENT);
|
||||||
}
|
}
|
||||||
@@ -178,6 +193,12 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
this.roomSkipList.addRoom(room);
|
this.roomSkipList.addRoom(room);
|
||||||
this.emit(LISTS_UPDATE_EVENT);
|
this.emit(LISTS_UPDATE_EVENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private onActiveSpaceChanged(): void {
|
||||||
|
if (!this.roomSkipList) return;
|
||||||
|
this.roomSkipList.calculateActiveSpaceForNodes();
|
||||||
|
this.emit(LISTS_UPDATE_EVENT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class RoomListStoreV3 {
|
export default class RoomListStoreV3 {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||||
|
import SpaceStore from "../../spaces/SpaceStore";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Room skip list stores room nodes.
|
* Room skip list stores room nodes.
|
||||||
@@ -13,6 +14,8 @@ import type { Room } from "matrix-js-sdk/src/matrix";
|
|||||||
* in different levels.
|
* in different levels.
|
||||||
*/
|
*/
|
||||||
export class RoomNode {
|
export class RoomNode {
|
||||||
|
private _isInActiveSpace: boolean = false;
|
||||||
|
|
||||||
public constructor(public readonly room: Room) {}
|
public constructor(public readonly room: Room) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,4 +29,23 @@ export class RoomNode {
|
|||||||
* eg: previous[i] gives the previous room node from this room node in level i.
|
* eg: previous[i] gives the previous room node from this room node in level i.
|
||||||
*/
|
*/
|
||||||
public previous: RoomNode[] = [];
|
public previous: RoomNode[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the room associated with this room node belongs to
|
||||||
|
* the currently active space.
|
||||||
|
* @see {@link SpaceStoreClass#activeSpace} to understand what active
|
||||||
|
* space means.
|
||||||
|
*/
|
||||||
|
public get isInActiveSpace(): boolean {
|
||||||
|
return this._isInActiveSpace;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this room belongs to the active space and store the result
|
||||||
|
* in {@link RoomNode#isInActiveSpace}.
|
||||||
|
*/
|
||||||
|
public checkIfRoomBelongsToActiveSpace(): void {
|
||||||
|
const activeSpace = SpaceStore.instance.activeSpace;
|
||||||
|
this._isInActiveSpace = SpaceStore.instance.isRoomInSpace(activeSpace, this.room.roomId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,9 +44,22 @@ export class RoomSkipList implements Iterable<Room> {
|
|||||||
this.levels[currentLevel.level] = currentLevel;
|
this.levels[currentLevel.level] = currentLevel;
|
||||||
currentLevel = currentLevel.generateNextLevel();
|
currentLevel = currentLevel.generateNextLevel();
|
||||||
} while (currentLevel.size > 1);
|
} while (currentLevel.size > 1);
|
||||||
|
|
||||||
|
// 3. Go through the list of rooms and mark nodes in active space
|
||||||
|
this.calculateActiveSpaceForNodes();
|
||||||
|
|
||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Go through all the room nodes and check if they belong to the active space.
|
||||||
|
*/
|
||||||
|
public calculateActiveSpaceForNodes(): void {
|
||||||
|
for (const node of this.roomNodeMap.values()) {
|
||||||
|
node.checkIfRoomBelongsToActiveSpace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the sorting algorithm used by the skip list.
|
* Change the sorting algorithm used by the skip list.
|
||||||
* This will reset the list and will rebuild from scratch.
|
* This will reset the list and will rebuild from scratch.
|
||||||
@@ -81,6 +94,7 @@ export class RoomSkipList implements Iterable<Room> {
|
|||||||
this.removeRoom(room);
|
this.removeRoom(room);
|
||||||
|
|
||||||
const newNode = new RoomNode(room);
|
const newNode = new RoomNode(room);
|
||||||
|
newNode.checkIfRoomBelongsToActiveSpace();
|
||||||
this.roomNodeMap.set(room.roomId, newNode);
|
this.roomNodeMap.set(room.roomId, newNode);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -159,6 +173,10 @@ export class RoomSkipList implements Iterable<Room> {
|
|||||||
return new SortedRoomIterator(this.levels[0].head!);
|
return new SortedRoomIterator(this.levels[0].head!);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getRoomsInActiveSpace(): SortedSpaceFilteredIterator {
|
||||||
|
return new SortedSpaceFilteredIterator(this.levels[0].head!);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The number of rooms currently in the skip list.
|
* The number of rooms currently in the skip list.
|
||||||
*/
|
*/
|
||||||
@@ -179,3 +197,23 @@ class SortedRoomIterator implements Iterator<Room> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SortedSpaceFilteredIterator implements Iterator<Room> {
|
||||||
|
public constructor(private current: RoomNode) {}
|
||||||
|
|
||||||
|
public [Symbol.iterator](): SortedSpaceFilteredIterator {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public next(): IteratorResult<Room> {
|
||||||
|
let current = this.current;
|
||||||
|
while (current && !current.isInActiveSpace) {
|
||||||
|
current = current.next[0];
|
||||||
|
}
|
||||||
|
if (!current) return { value: undefined, done: true };
|
||||||
|
this.current = current.next[0];
|
||||||
|
return {
|
||||||
|
value: current.room,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
} from "matrix-js-sdk/src/matrix";
|
} from "matrix-js-sdk/src/matrix";
|
||||||
import { KnownMembership } from "matrix-js-sdk/src/types";
|
import { KnownMembership } from "matrix-js-sdk/src/types";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
import { defer } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "../AsyncStoreWithClient";
|
||||||
import defaultDispatcher from "../../dispatcher/dispatcher";
|
import defaultDispatcher from "../../dispatcher/dispatcher";
|
||||||
@@ -152,6 +153,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
private _enabledMetaSpaces: MetaSpace[] = [];
|
private _enabledMetaSpaces: MetaSpace[] = [];
|
||||||
/** Whether the feature flag is set for MSC3946 */
|
/** Whether the feature flag is set for MSC3946 */
|
||||||
private _msc3946ProcessDynamicPredecessor: boolean = SettingsStore.getValue("feature_dynamic_room_predecessors");
|
private _msc3946ProcessDynamicPredecessor: boolean = SettingsStore.getValue("feature_dynamic_room_predecessors");
|
||||||
|
private _storeReadyDeferred = defer();
|
||||||
|
|
||||||
public constructor() {
|
public constructor() {
|
||||||
super(defaultDispatcher, {});
|
super(defaultDispatcher, {});
|
||||||
@@ -162,6 +164,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
SettingsStore.monitorSetting("feature_dynamic_room_predecessors", null);
|
SettingsStore.monitorSetting("feature_dynamic_room_predecessors", null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A promise that resolves when the space store is ready.
|
||||||
|
* This happens after an initial hierarchy of spaces and rooms has been computed.
|
||||||
|
*/
|
||||||
|
public get storeReadyPromise(): Promise<void> {
|
||||||
|
return this._storeReadyDeferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the order of meta spaces to display in the space panel.
|
* Get the order of meta spaces to display in the space panel.
|
||||||
*
|
*
|
||||||
@@ -1201,6 +1211,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
} else {
|
} else {
|
||||||
this.switchSpaceIfNeeded();
|
this.switchSpaceIfNeeded();
|
||||||
}
|
}
|
||||||
|
this._storeReadyDeferred.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
private sendUserProperties(): void {
|
private sendUserProperties(): void {
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ import { logger } from "matrix-js-sdk/src/logger";
|
|||||||
import { RoomListStoreV3Class } from "../../../../src/stores/room-list-v3/RoomListStoreV3";
|
import { RoomListStoreV3Class } from "../../../../src/stores/room-list-v3/RoomListStoreV3";
|
||||||
import { AsyncStoreWithClient } from "../../../../src/stores/AsyncStoreWithClient";
|
import { AsyncStoreWithClient } from "../../../../src/stores/AsyncStoreWithClient";
|
||||||
import { RecencySorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
|
import { RecencySorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
|
||||||
import { mkEvent, mkMessage, stubClient, upsertRoomStateEvents } from "../../../test-utils";
|
import { mkEvent, mkMessage, mkSpace, stubClient, upsertRoomStateEvents } from "../../../test-utils";
|
||||||
import { getMockedRooms } from "./skip-list/getMockedRooms";
|
import { getMockedRooms } from "./skip-list/getMockedRooms";
|
||||||
import { AlphabeticSorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter";
|
import { AlphabeticSorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter";
|
||||||
import { LISTS_UPDATE_EVENT } from "../../../../src/stores/room-list/RoomListStore";
|
import { LISTS_UPDATE_EVENT } from "../../../../src/stores/room-list/RoomListStore";
|
||||||
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
import dispatcher from "../../../../src/dispatcher/dispatcher";
|
||||||
|
import SpaceStore from "../../../../src/stores/spaces/SpaceStore";
|
||||||
|
import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../../src/stores/spaces";
|
||||||
|
|
||||||
describe("RoomListStoreV3", () => {
|
describe("RoomListStoreV3", () => {
|
||||||
async function getRoomListStore() {
|
async function getRoomListStore() {
|
||||||
@@ -24,10 +26,16 @@ describe("RoomListStoreV3", () => {
|
|||||||
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
|
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
|
||||||
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
|
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
|
||||||
const store = new RoomListStoreV3Class(dispatcher);
|
const store = new RoomListStoreV3Class(dispatcher);
|
||||||
store.start();
|
await store.start();
|
||||||
return { client, rooms, store, dispatcher };
|
return { client, rooms, store, dispatcher };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space) => space === MetaSpace.Home);
|
||||||
|
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => MetaSpace.Home);
|
||||||
|
jest.spyOn(SpaceStore.instance, "storeReadyPromise", "get").mockImplementation(() => Promise.resolve());
|
||||||
|
});
|
||||||
|
|
||||||
it("Provides an unsorted list of rooms", async () => {
|
it("Provides an unsorted list of rooms", async () => {
|
||||||
const { store, rooms } = await getRoomListStore();
|
const { store, rooms } = await getRoomListStore();
|
||||||
expect(store.getRooms()).toEqual(rooms);
|
expect(store.getRooms()).toEqual(rooms);
|
||||||
@@ -264,5 +272,48 @@ describe("RoomListStoreV3", () => {
|
|||||||
expect(fn).not.toHaveBeenCalled();
|
expect(fn).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Spaces", () => {
|
||||||
|
it("Filtering by spaces work", async () => {
|
||||||
|
const client = stubClient();
|
||||||
|
const rooms = getMockedRooms(client);
|
||||||
|
|
||||||
|
// Let's choose 5 rooms to put in space
|
||||||
|
const indexes = [6, 8, 13, 27, 75];
|
||||||
|
const roomIds = indexes.map((i) => rooms[i].roomId);
|
||||||
|
const spaceRoom = mkSpace(client, "!space1:matrix.org", [], roomIds);
|
||||||
|
rooms.push(spaceRoom);
|
||||||
|
|
||||||
|
client.getVisibleRooms = jest.fn().mockReturnValue(rooms);
|
||||||
|
jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client);
|
||||||
|
|
||||||
|
// Mock the space store
|
||||||
|
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space, id) => {
|
||||||
|
if (space === MetaSpace.Home && !roomIds.includes(id)) return true;
|
||||||
|
if (space === spaceRoom.roomId && roomIds.includes(id)) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = new RoomListStoreV3Class(dispatcher);
|
||||||
|
await store.start();
|
||||||
|
const fn = jest.fn();
|
||||||
|
store.on(LISTS_UPDATE_EVENT, fn);
|
||||||
|
|
||||||
|
// The rooms which belong to the space should not be shown
|
||||||
|
const result = store.getSortedRoomsInActiveSpace().map((r) => r.roomId);
|
||||||
|
for (const id of roomIds) {
|
||||||
|
expect(result).not.toContain(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lets switch to the space
|
||||||
|
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoom.roomId);
|
||||||
|
SpaceStore.instance.emit(UPDATE_SELECTED_SPACE);
|
||||||
|
expect(fn).toHaveBeenCalled();
|
||||||
|
const result2 = store.getSortedRoomsInActiveSpace().map((r) => r.roomId);
|
||||||
|
for (const id of roomIds) {
|
||||||
|
expect(result2).toContain(id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import { RoomSkipList } from "../../../../../src/stores/room-list-v3/skip-list/R
|
|||||||
import { RecencySorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
|
import { RecencySorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter";
|
||||||
import { AlphabeticSorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter";
|
import { AlphabeticSorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter";
|
||||||
import { getMockedRooms } from "./getMockedRooms";
|
import { getMockedRooms } from "./getMockedRooms";
|
||||||
|
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
|
||||||
|
import { MetaSpace } from "../../../../../src/stores/spaces";
|
||||||
|
|
||||||
describe("RoomSkipList", () => {
|
describe("RoomSkipList", () => {
|
||||||
function generateSkipList(roomCount?: number): {
|
function generateSkipList(roomCount?: number): {
|
||||||
@@ -30,6 +32,12 @@ describe("RoomSkipList", () => {
|
|||||||
return { skipList, rooms, totalRooms: rooms.length, sorter };
|
return { skipList, rooms, totalRooms: rooms.length, sorter };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space) => space === MetaSpace.Home);
|
||||||
|
jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => MetaSpace.Home);
|
||||||
|
jest.spyOn(SpaceStore.instance, "storeReadyPromise", "get").mockImplementation(() => Promise.resolve());
|
||||||
|
});
|
||||||
|
|
||||||
it("Rooms are in sorted order after initial seed", () => {
|
it("Rooms are in sorted order after initial seed", () => {
|
||||||
const { skipList, totalRooms } = generateSkipList();
|
const { skipList, totalRooms } = generateSkipList();
|
||||||
expect(skipList.size).toEqual(totalRooms);
|
expect(skipList.size).toEqual(totalRooms);
|
||||||
|
|||||||
Reference in New Issue
Block a user