From 2da21248bb4406d22a08689c00510bf4708b9b10 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Mon, 3 Mar 2025 16:42:00 +0530 Subject: [PATCH] Room List - Implement a minimal view model (#29357) * Implement enough of the new store to get a list of rooms * Make it possible to swap sorting algorithm * Don't attach to window object We don't want the store to be created if the labs flag is off * Remove the store class Probably best to include this PR with the minimal vm implmentation * Create a new room list store that wraps around the skip list * Create a minimal view model * Fix CI * Add some basic tests for the store * Write more tests * Add some jsdoc comments * Add more documentation * Add more docs --- .../viewmodels/roomlist/RoomListViewModel.tsx | 25 +++++ .../rooms/RoomListPanel/RoomListPanel.tsx | 23 +++++ src/stores/room-list-v3/RoomListStoreV3.ts | 97 +++++++++++++++++++ .../__snapshots__/RoomListPanel-test.tsx.snap | 52 ++++++++++ .../room-list-v3/RoomListStoreV3-test.ts | 53 ++++++++++ .../skip-list/RoomSkipList-test.ts | 17 +--- .../room-list-v3/skip-list/getMockedRooms.ts | 21 ++++ 7 files changed, 274 insertions(+), 14 deletions(-) create mode 100644 src/components/viewmodels/roomlist/RoomListViewModel.tsx create mode 100644 src/stores/room-list-v3/RoomListStoreV3.ts create mode 100644 test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts create mode 100644 test/unit-tests/stores/room-list-v3/skip-list/getMockedRooms.ts diff --git a/src/components/viewmodels/roomlist/RoomListViewModel.tsx b/src/components/viewmodels/roomlist/RoomListViewModel.tsx new file mode 100644 index 0000000000..1dacd030e2 --- /dev/null +++ b/src/components/viewmodels/roomlist/RoomListViewModel.tsx @@ -0,0 +1,25 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import type { Room } from "matrix-js-sdk/src/matrix"; +import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3"; + +export interface RoomListViewState { + /** + * A list of rooms to be displayed in the left panel. + */ + rooms: Room[]; +} + +/** + * View model for the new room list + * @see {@link RoomListViewState} for more information about what this view model returns. + */ +export function useRoomListViewModel(): RoomListViewState { + const rooms = RoomListStoreV3.instance.getSortedRooms(); + return { rooms }; +} diff --git a/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx b/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx index e5c1cbfa30..a52b619651 100644 --- a/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx +++ b/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx @@ -6,11 +6,14 @@ Please see LICENSE files in the repository root for full details. */ import React from "react"; +import { AutoSizer, List } from "react-virtualized"; +import type { ListRowProps } from "react-virtualized"; import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../../settings/UIFeature"; import { RoomListSearch } from "./RoomListSearch"; import { RoomListHeaderView } from "./RoomListHeaderView"; +import { useRoomListViewModel } from "../../../viewmodels/roomlist/RoomListViewModel"; type RoomListPanelProps = { /** @@ -25,11 +28,31 @@ type RoomListPanelProps = { */ export const RoomListPanel: React.FC = ({ activeSpace }) => { const displayRoomSearch = shouldShowComponent(UIComponent.FilterContainer); + const { rooms } = useRoomListViewModel(); + + const rowRenderer = ({ key, index, style }: ListRowProps): React.JSX.Element => { + return ( +
+ {rooms[index].name} +
+ ); + }; return (
{displayRoomSearch && } + + {({ height, width }) => ( + + )} +
); }; diff --git a/src/stores/room-list-v3/RoomListStoreV3.ts b/src/stores/room-list-v3/RoomListStoreV3.ts new file mode 100644 index 0000000000..2904680ea8 --- /dev/null +++ b/src/stores/room-list-v3/RoomListStoreV3.ts @@ -0,0 +1,97 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import type { EmptyObject, Room } from "matrix-js-sdk/src/matrix"; +import type { MatrixDispatcher } from "../../dispatcher/dispatcher"; +import type { ActionPayload } from "../../dispatcher/payloads"; +import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; +import SettingsStore from "../../settings/SettingsStore"; +import { VisibilityProvider } from "../room-list/filters/VisibilityProvider"; +import defaultDispatcher from "../../dispatcher/dispatcher"; +import { LISTS_UPDATE_EVENT } from "../room-list/RoomListStore"; +import { RoomSkipList } from "./skip-list/RoomSkipList"; +import { RecencySorter } from "./skip-list/sorters/RecencySorter"; +import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter"; + +/** + * This store allows for fast retrieval of the room list in a sorted and filtered manner. + * This is the third such implementation hence the "V3". + * This store is being actively developed so expect the methods to change in future. + */ +export class RoomListStoreV3Class extends AsyncStoreWithClient { + private roomSkipList?: RoomSkipList; + private readonly msc3946ProcessDynamicPredecessor: boolean; + + public constructor(dispatcher: MatrixDispatcher) { + super(dispatcher); + this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); + } + + /** + * Get a list of unsorted, unfiltered rooms. + */ + public getRooms(): Room[] { + let rooms = this.matrixClient?.getVisibleRooms(this.msc3946ProcessDynamicPredecessor) ?? []; + rooms = rooms.filter((r) => VisibilityProvider.instance.isRoomVisible(r)); + return rooms; + } + + /** + * Get a list of sorted rooms. + */ + public getSortedRooms(): Room[] { + if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList); + else return []; + } + + /** + * Re-sort the list of rooms by alphabetic order. + */ + public useAlphabeticSorting(): void { + if (this.roomSkipList) { + const sorter = new AlphabeticSorter(); + this.roomSkipList.useNewSorter(sorter, this.getRooms()); + } + } + + /** + * Re-sort the list of rooms by recency. + */ + public useRecencySorting(): void { + if (this.roomSkipList && this.matrixClient) { + const sorter = new RecencySorter(this.matrixClient?.getSafeUserId() ?? ""); + this.roomSkipList.useNewSorter(sorter, this.getRooms()); + } + } + + protected async onReady(): Promise { + if (this.roomSkipList?.initialized || !this.matrixClient) return; + const sorter = new RecencySorter(this.matrixClient.getSafeUserId()); + this.roomSkipList = new RoomSkipList(sorter); + const rooms = this.getRooms(); + this.roomSkipList.seed(rooms); + this.emit(LISTS_UPDATE_EVENT); + } + + protected async onAction(payload: ActionPayload): Promise { + return; + } +} + +export default class RoomListStoreV3 { + private static internalInstance: RoomListStoreV3Class; + + public static get instance(): RoomListStoreV3Class { + if (!RoomListStoreV3.internalInstance) { + const instance = new RoomListStoreV3Class(defaultDispatcher); + instance.start(); + RoomListStoreV3.internalInstance = instance; + } + + return this.internalInstance; + } +} diff --git a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap index 5125b5ed54..35643e394f 100644 --- a/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap +++ b/test/unit-tests/components/views/rooms/RoomListPanel/__snapshots__/RoomListPanel-test.tsx.snap @@ -23,6 +23,32 @@ exports[` should not render the RoomListSearch component when U +
+
+
+
+
+
+
+
+
`; @@ -141,6 +167,32 @@ exports[` should render the RoomListSearch component when UICom
+
+
+
+
+
+
+
+
+
`; diff --git a/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts b/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts new file mode 100644 index 0000000000..cd37b04e34 --- /dev/null +++ b/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts @@ -0,0 +1,53 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import type { MatrixDispatcher } from "../../../../src/dispatcher/dispatcher"; +import { RoomListStoreV3Class } from "../../../../src/stores/room-list-v3/RoomListStoreV3"; +import { AsyncStoreWithClient } from "../../../../src/stores/AsyncStoreWithClient"; +import { RecencySorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter"; +import { stubClient } from "../../../test-utils"; +import { getMockedRooms } from "./skip-list/getMockedRooms"; +import { AlphabeticSorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter"; + +describe("RoomListStoreV3", () => { + async function getRoomListStore() { + const client = stubClient(); + const rooms = getMockedRooms(client); + client.getVisibleRooms = jest.fn().mockReturnValue(rooms); + jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client); + const fakeDispatcher = { register: jest.fn() } as unknown as MatrixDispatcher; + const store = new RoomListStoreV3Class(fakeDispatcher); + store.start(); + return { client, rooms, store }; + } + + it("Provides an unsorted list of rooms", async () => { + const { store, rooms } = await getRoomListStore(); + expect(store.getRooms()).toEqual(rooms); + }); + + it("Provides a sorted list of rooms", async () => { + const { store, rooms, client } = await getRoomListStore(); + const sorter = new RecencySorter(client.getSafeUserId()); + const sortedRooms = sorter.sort(rooms); + expect(store.getSortedRooms()).toEqual(sortedRooms); + }); + + it("Provides a way to resort", async () => { + const { store, rooms, client } = await getRoomListStore(); + + // List is sorted by recency, sort by alphabetical now + store.useAlphabeticSorting(); + let sortedRooms = new AlphabeticSorter().sort(rooms); + expect(store.getSortedRooms()).toEqual(sortedRooms); + + // Go back to recency sorting + store.useRecencySorting(); + sortedRooms = new RecencySorter(client.getSafeUserId()).sort(rooms); + expect(store.getSortedRooms()).toEqual(sortedRooms); + }); +}); diff --git a/test/unit-tests/stores/room-list-v3/skip-list/RoomSkipList-test.ts b/test/unit-tests/stores/room-list-v3/skip-list/RoomSkipList-test.ts index 3172307a81..b644aa30e9 100644 --- a/test/unit-tests/stores/room-list-v3/skip-list/RoomSkipList-test.ts +++ b/test/unit-tests/stores/room-list-v3/skip-list/RoomSkipList-test.ts @@ -7,26 +7,15 @@ Please see LICENSE files in the repository root for full details. import { shuffle } from "lodash"; -import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import type { Room } from "matrix-js-sdk/src/matrix"; import type { Sorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters"; -import { mkMessage, mkStubRoom, stubClient } from "../../../../test-utils"; +import { mkMessage, stubClient } from "../../../../test-utils"; import { RoomSkipList } from "../../../../../src/stores/room-list-v3/skip-list/RoomSkipList"; 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 { getMockedRooms } from "./getMockedRooms"; describe("RoomSkipList", () => { - function getMockedRooms(client: MatrixClient, roomCount: number = 100): Room[] { - const rooms: Room[] = []; - for (let i = 0; i < roomCount; ++i) { - const roomId = `!foo${i}:matrix.org`; - const room = mkStubRoom(roomId, `Foo Room ${i}`, client); - const event = mkMessage({ room: roomId, user: `@foo${i}:matrix.org`, ts: i + 1, event: true }); - room.timeline.push(event); - rooms.push(room); - } - return rooms; - } - function generateSkipList(roomCount?: number): { skipList: RoomSkipList; rooms: Room[]; diff --git a/test/unit-tests/stores/room-list-v3/skip-list/getMockedRooms.ts b/test/unit-tests/stores/room-list-v3/skip-list/getMockedRooms.ts new file mode 100644 index 0000000000..d895ba944b --- /dev/null +++ b/test/unit-tests/stores/room-list-v3/skip-list/getMockedRooms.ts @@ -0,0 +1,21 @@ +/* +Copyright 2025 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial +Please see LICENSE files in the repository root for full details. +*/ + +import type { MatrixClient, Room } from "matrix-js-sdk/src/matrix"; +import { mkMessage, mkStubRoom } from "../../../../test-utils"; + +export function getMockedRooms(client: MatrixClient, roomCount: number = 100): Room[] { + const rooms: Room[] = []; + for (let i = 0; i < roomCount; ++i) { + const roomId = `!foo${i}:matrix.org`; + const room = mkStubRoom(roomId, `Foo Room ${i}`, client); + const event = mkMessage({ room: roomId, user: `@foo${i}:matrix.org`, ts: i + 1, event: true }); + room.timeline.push(event); + rooms.push(room); + } + return rooms; +}