RoomListStore: Support specific sorting requirements for muted rooms (#29665)

* Sort muted rooms to the bottom of the room list

* Re-insert room on mute/unmute

* Write tests

* Fix broken playwright test

Muted rooms are at the bottom, so we need to scroll.
This commit is contained in:
R Midhun Suresh
2025-04-03 18:26:00 +05:30
committed by GitHub
parent d07a02fe3d
commit 149b3b1049
7 changed files with 144 additions and 16 deletions

View File

@@ -27,6 +27,7 @@ import DMRoomMap from "../../../../src/utils/DMRoomMap";
import { SortingAlgorithm } from "../../../../src/stores/room-list-v3/skip-list/sorters";
import SettingsStore from "../../../../src/settings/SettingsStore";
import * as utils from "../../../../src/utils/notifications";
import * as roomMute from "../../../../src/stores/room-list/utils/roomMute";
describe("RoomListStoreV3", () => {
async function getRoomListStore() {
@@ -635,4 +636,83 @@ describe("RoomListStoreV3", () => {
});
});
});
describe("Muted rooms", () => {
async function getRoomListStoreWithMutedRooms() {
const client = stubClient();
const rooms = getMockedRooms(client);
// Let's say that rooms 34, 84, 64, 14, 57 are muted
const mutedIndices = [34, 84, 64, 14, 57];
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();
return { client, rooms, mutedIndices, mutedRooms, store, dispatcher };
}
it("Muted rooms are sorted to the bottom of the list", async () => {
const { store, mutedRooms, client } = await getRoomListStoreWithMutedRooms();
const lastFiveRooms = store.getSortedRooms().slice(95);
const expectedRooms = new RecencySorter(client.getSafeUserId()).sort(mutedRooms);
// We expect the muted rooms to be at the bottom sorted by recency
expect(lastFiveRooms).toEqual(expectedRooms);
});
it("Muted rooms are sorted within themselves", async () => {
const { store, rooms } = await getRoomListStoreWithMutedRooms();
// Let's say that rooms 14 and 34 get new messages in that order
let ts = 1000;
for (const room of [rooms[14], rooms[34]]) {
const event = mkMessage({ room: room.roomId, user: `@foo${3}:matrix.org`, ts: 1000, event: true });
room.timeline.push(event);
const payload = {
action: "MatrixActions.Room.timeline",
event,
isLiveEvent: true,
isLiveUnfilteredRoomTimelineEvent: true,
room,
};
dispatcher.dispatch(payload, true);
ts = ts + 1;
}
const lastFiveRooms = store.getSortedRooms().slice(95);
// The order previously would have been 84, 64, 57, 34, 14
// Expected new order is 34, 14, 84, 64, 57
const expectedRooms = [rooms[34], rooms[14], rooms[84], rooms[64], rooms[57]];
expect(lastFiveRooms).toEqual(expectedRooms);
});
it("Muted room is correctly sorted when unmuted", async () => {
const { store, mutedRooms, rooms, client } = await getRoomListStoreWithMutedRooms();
// Let's say that muted room 64 becomes un-muted.
const unmutedRoom = rooms[64];
jest.spyOn(roomMute, "getChangedOverrideRoomMutePushRules").mockImplementation(() => [unmutedRoom.roomId]);
client.getRoom = jest.fn().mockReturnValue(unmutedRoom);
const payload = {
action: "MatrixActions.accountData",
event_type: EventType.PushRules,
};
mutedRooms.splice(2, 1);
dispatcher.dispatch(payload, true);
const lastFiveRooms = store.getSortedRooms().slice(95);
// We expect room at index 64 to no longer be at the bottom
expect(lastFiveRooms).not.toContain(unmutedRoom);
// Room 64 should go to index 34 since we're sorting by recency
expect(store.getSortedRooms()[34]).toEqual(unmutedRoom);
});
});
});

View File

@@ -9,6 +9,7 @@ import { shuffle } from "lodash";
import type { Room } from "matrix-js-sdk/src/matrix";
import type { Sorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters";
import type { RoomNotificationState } from "../../../../../src/stores/notifications/RoomNotificationState";
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";
@@ -16,6 +17,7 @@ import { AlphabeticSorter } from "../../../../../src/stores/room-list-v3/skip-li
import { getMockedRooms } from "./getMockedRooms";
import SpaceStore from "../../../../../src/stores/spaces/SpaceStore";
import { MetaSpace } from "../../../../../src/stores/spaces";
import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore";
describe("RoomSkipList", () => {
function generateSkipList(roomCount?: number): {
@@ -36,6 +38,12 @@ describe("RoomSkipList", () => {
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());
jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation(() => {
const state = {
mute: false,
} as unknown as RoomNotificationState;
return state;
});
});
it("Rooms are in sorted order after initial seed", () => {