From 0ca6449f3c7b4c676942c626f44a43569ac6facc Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Mon, 24 Feb 2025 21:44:05 +0530 Subject: [PATCH] Make it possible to swap sorting algorithm --- src/stores/room-list-v3/RoomListStoreV3.ts | 33 +++++++++++++------ .../room-list-v3/skip-list/RoomSkipList.ts | 21 ++++++++++-- .../skip-list/sorters/AlphabeticSorter.ts | 2 +- .../skip-list/RoomSkipList-test.ts | 22 +++++++++++-- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/src/stores/room-list-v3/RoomListStoreV3.ts b/src/stores/room-list-v3/RoomListStoreV3.ts index 10791dd9f4..4b661c9702 100644 --- a/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/src/stores/room-list-v3/RoomListStoreV3.ts @@ -15,6 +15,7 @@ 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"; export class RoomListStoreV3Class extends AsyncStoreWithClient { private roomSkipList?: RoomSkipList; @@ -25,17 +26,36 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); } + public getRooms(): Room[] { + let rooms = this.matrixClient?.getVisibleRooms(this.msc3946ProcessDynamicPredecessor) ?? []; + rooms = rooms.filter((r) => VisibilityProvider.instance.isRoomVisible(r)); + return rooms; + } + public getSortedRooms(): Room[] { if (this.roomSkipList?.initialized) return Array.from(this.roomSkipList); else return []; } + public useAlphabeticSorting(): void { + if (this.roomSkipList) { + const sorter = new AlphabeticSorter(); + this.roomSkipList.useNewSorter(sorter, this.getRooms()); + } + } + + 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() ?? ""); + const sorter = new RecencySorter(this.matrixClient.getSafeUserId()); this.roomSkipList = new RoomSkipList(sorter); - const rooms = this.fetchRoomsFromSdk(); - if (!rooms) return; + const rooms = this.getRooms(); this.roomSkipList.seed(rooms); this.emit(LISTS_UPDATE_EVENT); } @@ -43,13 +63,6 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { protected async onAction(payload: ActionPayload): Promise { return; } - - private fetchRoomsFromSdk(): Room[] | null { - if (!this.matrixClient) return null; - let rooms = this.matrixClient.getVisibleRooms(this.msc3946ProcessDynamicPredecessor); - rooms = rooms.filter((r) => VisibilityProvider.instance.isRoomVisible(r)); - return rooms; - } } export default class RoomListStoreV3 { diff --git a/src/stores/room-list-v3/skip-list/RoomSkipList.ts b/src/stores/room-list-v3/skip-list/RoomSkipList.ts index 971d095a8c..260786594f 100644 --- a/src/stores/room-list-v3/skip-list/RoomSkipList.ts +++ b/src/stores/room-list-v3/skip-list/RoomSkipList.ts @@ -16,11 +16,16 @@ import { Level } from "./Level"; * See See https://en.wikipedia.org/wiki/Skip_list */ export class RoomSkipList implements Iterable { - private readonly levels: Level[] = [new Level(0)]; - private readonly roomNodeMap: Map = new Map(); + private levels: Level[] = [new Level(0)]; + private roomNodeMap: Map = new Map(); public initialized: boolean = false; - public constructor(private readonly sorter: Sorter) {} + public constructor(private sorter: Sorter) {} + + private reset(): void { + this.levels = [new Level(0)]; + this.roomNodeMap = new Map(); + } /** * Seed the list with an initial list of rooms. @@ -42,6 +47,16 @@ export class RoomSkipList implements Iterable { this.initialized = true; } + /** + * Change the sorting algorithm used by the skip list. + * This will reset the list and will rebuild from scratch. + */ + public useNewSorter(sorter: Sorter, rooms: Room[]): void { + this.reset(); + this.sorter = sorter; + this.seed(rooms); + } + /** * Removes a given room from the skip list. */ diff --git a/src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter.ts b/src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter.ts index 9b1d358238..5c279efb08 100644 --- a/src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter.ts +++ b/src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter.ts @@ -12,7 +12,7 @@ export class AlphabeticSorter implements Sorter { private readonly collator = new Intl.Collator(); public sort(rooms: Room[]): Room[] { - return rooms.sort((a, b) => { + return [...rooms].sort((a, b) => { return this.comparator(a, b); }); } 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 bd24929ac3..3172307a81 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 @@ -8,9 +8,11 @@ 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 { Sorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters"; import { mkMessage, mkStubRoom, 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"; describe("RoomSkipList", () => { function getMockedRooms(client: MatrixClient, roomCount: number = 100): Room[] { @@ -25,13 +27,18 @@ describe("RoomSkipList", () => { return rooms; } - function generateSkipList(roomCount?: number): { skipList: RoomSkipList; rooms: Room[]; totalRooms: number } { + function generateSkipList(roomCount?: number): { + skipList: RoomSkipList; + rooms: Room[]; + totalRooms: number; + sorter: Sorter; + } { const client = stubClient(); const sorter = new RecencySorter(client.getSafeUserId()); const skipList = new RoomSkipList(sorter); const rooms = getMockedRooms(client, roomCount); skipList.seed(rooms); - return { skipList, rooms, totalRooms: rooms.length }; + return { skipList, rooms, totalRooms: rooms.length, sorter }; } it("Rooms are in sorted order after initial seed", () => { @@ -81,6 +88,17 @@ describe("RoomSkipList", () => { } }); + it("Re-sort works when sorter is swapped", () => { + const { skipList, rooms, sorter } = generateSkipList(); + const sortedByRecency = [...rooms].sort((a, b) => sorter.comparator(a, b)); + expect(sortedByRecency).toEqual([...skipList]); + // Now switch over to alphabetic sorter + const newSorter = new AlphabeticSorter(); + skipList.useNewSorter(newSorter, rooms); + const sortedByAlphabet = [...rooms].sort((a, b) => newSorter.comparator(a, b)); + expect(sortedByAlphabet).toEqual([...skipList]); + }); + describe("Empty skip list functionality", () => { it("Insertions into empty skip list works", () => { // Create an empty skip list