From 231ab20dcf58b25aca2f1e32880f1f79a32725d2 Mon Sep 17 00:00:00 2001 From: R Midhun Suresh Date: Wed, 4 Jun 2025 23:21:49 +0530 Subject: [PATCH] RoomListStore: Sort low priority rooms to the bottom of the list (#30070) * Sort low priority rooms to the bottom of the list * Write test --- .../skip-list/sorters/RecencySorter.ts | 31 ++++++++++-- .../room-list-v3/RoomListStoreV3-test.ts | 49 +++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) diff --git a/src/stores/room-list-v3/skip-list/sorters/RecencySorter.ts b/src/stores/room-list-v3/skip-list/sorters/RecencySorter.ts index 07c902e3ee..53e8ae4331 100644 --- a/src/stores/room-list-v3/skip-list/sorters/RecencySorter.ts +++ b/src/stores/room-list-v3/skip-list/sorters/RecencySorter.ts @@ -9,6 +9,7 @@ import type { Room } from "matrix-js-sdk/src/matrix"; import { type Sorter, SortingAlgorithm } from "."; import { getLastTs } from "../../../room-list/algorithms/tag-sorting/RecentAlgorithm"; import { RoomNotificationStateStore } from "../../../notifications/RoomNotificationStateStore"; +import { DefaultTagID } from "../../../room-list/models"; export class RecencySorter implements Sorter { public constructor(private myUserId: string) {} @@ -19,11 +20,9 @@ export class RecencySorter implements Sorter { } public comparator(roomA: Room, roomB: Room, cache?: any): number { - // Check mute status first; muted rooms should be at the bottom - const isRoomAMuted = RoomNotificationStateStore.instance.getRoomState(roomA).muted; - const isRoomBMuted = RoomNotificationStateStore.instance.getRoomState(roomB).muted; - if (isRoomAMuted && !isRoomBMuted) return 1; - if (isRoomBMuted && !isRoomAMuted) return -1; + // First check if the rooms are low priority or muted + const exceptionalOrdering = this.getScore(roomA) - this.getScore(roomB); + if (exceptionalOrdering !== 0) return exceptionalOrdering; // Then check recency; recent rooms should be at the top const roomALastTs = this.getTs(roomA, cache); @@ -35,6 +34,28 @@ export class RecencySorter implements Sorter { return SortingAlgorithm.Recency; } + /** + * This sorter mostly sorts rooms by recency but there are two exceptions: + * 1. Muted rooms are sorted to the bottom of the list. + * 2. Low priority rooms are sorted to the bottom of the list but before muted rooms. + * + * The following method provides a numerical value that takes care of this + * exceptional ordering. For two rooms A and B, it works as follows: + * - If getScore(A) - getScore(B) > 0, A should come after B + * - If getScore(A) - getScore(B) < 0, A should come before B + * - If getScore(A) - getScore(B) = 0, no special ordering needed, just use recency + */ + private getScore(room: Room): number { + const isLowPriority = !!room.tags[DefaultTagID.LowPriority]; + const isMuted = RoomNotificationStateStore.instance.getRoomState(room).muted; + // These constants are chosen so that the following order is maintained: + // Low priority rooms -> Low priority and muted rooms -> Muted rooms + if (isMuted && isLowPriority) return 5; + else if (isMuted) return 10; + else if (isLowPriority) return 2; + else return 0; + } + private getTs(room: Room, cache?: { [roomId: string]: number }): number { const ts = cache?.[room.roomId] ?? getLastTs(room, this.myUserId); if (cache) { diff --git a/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts b/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts index 0c0cdfc091..47b48d437b 100644 --- a/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts +++ b/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts @@ -798,4 +798,53 @@ describe("RoomListStoreV3", () => { expect(store.getSortedRooms()[34]).toEqual(unmutedRoom); }); }); + + describe("Low priority rooms", () => { + async function getRoomListStoreWithRooms() { + const client = stubClient(); + const rooms = getMockedRooms(client); + + // Let's say that rooms 34, 84, 64, 14, 57 are low priority + const lowPriorityIndices = [34, 84, 64, 14, 57]; + const lowPriorityRooms = lowPriorityIndices.map((i) => rooms[i]); + for (const room of lowPriorityRooms) { + room.tags[DefaultTagID.LowPriority] = {}; + } + + // Let's say that rooms 14, 57, 65, 78, 82, 5, 36 are muted + const mutedIndices = [14, 57, 65, 78, 82, 5, 36]; + const mutedRooms = mutedIndices.map((i) => rooms[i]); + jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => { + const state = { + muted: mutedRooms.includes(room), + } as unknown as RoomNotificationState; + return state; + }); + + client.getVisibleRooms = jest.fn().mockReturnValue(rooms); + jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client); + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + + // We expect the following order: Low Priority -> Low Priority & Muted -> Muted + const expectedRoomIds = [84, 64, 34, 57, 14, 82, 78, 65, 36, 5].map((i) => rooms[i].roomId); + + return { + client, + rooms, + expectedRoomIds, + store, + dispatcher, + }; + } + + it("Low priority rooms are pushed to the bottom of the list just before muted rooms", async () => { + const { store, expectedRoomIds } = await getRoomListStoreWithRooms(); + const result = store + .getSortedRooms() + .slice(90) + .map((r) => r.roomId); + expect(result).toEqual(expectedRoomIds); + }); + }); });