From 526b4b8c330482a97adba4869b90e5f28f0f0f20 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 29 Jun 2026 13:46:14 +0200 Subject: [PATCH] Room list: remove usage of old room list store in `LoggedInView` & `InviteDialog` (#33973) * feat: add functions to dm and server notice in rls * feat: use these functions in dialogs to get rid of old rls --- .../components/structures/LoggedInView.tsx | 11 +-- .../components/views/dialogs/InviteDialog.tsx | 9 +- .../stores/room-list-v3/RoomListStoreV3.ts | 27 ++++- .../room-list-v3/RoomListStoreV3-test.ts | 98 +++++++++++++++++++ 4 files changed, 133 insertions(+), 12 deletions(-) diff --git a/apps/web/src/components/structures/LoggedInView.tsx b/apps/web/src/components/structures/LoggedInView.tsx index 09a8d97515..a5256dde07 100644 --- a/apps/web/src/components/structures/LoggedInView.tsx +++ b/apps/web/src/components/structures/LoggedInView.tsx @@ -34,12 +34,11 @@ import { SettingLevel } from "../../settings/SettingLevel"; import ResizeHandle from "../views/elements/ResizeHandle"; import { CollapseDistributor, Resizer } from "../../resizer"; import PlatformPeg from "../../PlatformPeg"; -import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag"; import { hideToast as hideServerLimitToast, showToast as showServerLimitToast } from "../../toasts/ServerLimitToast"; import { Action } from "../../dispatcher/actions"; import LeftPanel from "./LeftPanel"; import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; -import RoomListStore from "../../stores/room-list/RoomListStore"; +import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import NonUrgentToastContainer from "./NonUrgentToastContainer"; import { type IOOBData, type IThreepidInvite } from "../../stores/ThreepidInviteStore"; import Modal from "../../Modal"; @@ -389,8 +388,8 @@ class LoggedInView extends React.Component { }; private onRoomStateEvents = (ev: MatrixEvent): void => { - const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; - if (serverNoticeList?.some((r) => r.roomId === ev.getRoomId())) { + const serverNoticeList = RoomListStoreV3.instance.getServerNoticeRooms(); + if (serverNoticeList.some((r) => r.roomId === ev.getRoomId())) { this.updateServerNoticeEvents(); } }; @@ -422,8 +421,8 @@ class LoggedInView extends React.Component { } private updateServerNoticeEvents = async (): Promise => { - const serverNoticeList = RoomListStore.instance.orderedLists[DefaultTagID.ServerNotice]; - if (!serverNoticeList) return; + const serverNoticeList = RoomListStoreV3.instance.getServerNoticeRooms(); + if (!serverNoticeList.length) return; const events: MatrixEvent[] = []; let pinnedEventTs = 0; diff --git a/apps/web/src/components/views/dialogs/InviteDialog.tsx b/apps/web/src/components/views/dialogs/InviteDialog.tsx index a927129241..e5d08582a1 100644 --- a/apps/web/src/components/views/dialogs/InviteDialog.tsx +++ b/apps/web/src/components/views/dialogs/InviteDialog.tsx @@ -26,8 +26,7 @@ import { abbreviateUrl } from "../../../utils/UrlUtils"; import IdentityAuthClient from "../../../IdentityAuthClient"; import { showAnyInviteErrors } from "../../../RoomInvite"; import { Action } from "../../../dispatcher/actions"; -import { DefaultTagID } from "../../../stores/room-list-v3/skip-list/tag"; -import RoomListStore from "../../../stores/room-list/RoomListStore"; +import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3"; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import { SearchResultAvatar } from "../avatars/SearchResultAvatar"; @@ -279,9 +278,9 @@ export default class InviteDialog extends React.PureComponent): Result[] { const rooms = DMRoomMap.shared().getUniqueRoomsWithIndividuals(); // map of userId => js-sdk Room - // Also pull in all the rooms tagged as DefaultTagID.DM so we don't miss anything. Sometimes the - // room list doesn't tag the room for the DMRoomMap, but does for the room list. - const dmTaggedRooms = RoomListStore.instance.orderedLists[DefaultTagID.DM] || []; + // Also pull in all the rooms that the room list tags as DMs so we don't miss anything: sometimes + // a room is absent from getUniqueRoomsWithIndividuals() above but is still tagged as a DM. + const dmTaggedRooms = RoomListStoreV3.instance.getDmRooms(); const myUserId = MatrixClientPeg.safeGet().getUserId(); for (const dmRoom of dmTaggedRooms) { const otherMembers = dmRoom.getJoinedMembers().filter((u) => u.userId !== myUserId); diff --git a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts index 5514745f10..2746fcc2c0 100644 --- a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts @@ -36,6 +36,7 @@ import { UnreadSorter } from "./skip-list/sorters/UnreadSorter"; import { getChangedOverrideRoomMutePushRules } from "./utils"; import { isRoomVisible } from "./isRoomVisible"; import { RoomSkipList } from "./skip-list/RoomSkipList"; +import { getTagsForRoom } from "../../utils/room/getTagsForRoom"; import { ExcludeTagsFilter } from "./skip-list/filters/ExcludeTagsFilter"; import { TagFilter } from "./skip-list/filters/TagFilter"; import { filterBoolean } from "../../utils/arrays"; @@ -47,7 +48,7 @@ import { getOrderedReorderableSections, reorderSection, } from "./section"; -import { DefaultTagID } from "./skip-list/tag"; +import { DefaultTagID, type TagID } from "./skip-list/tag"; /** * These are the filters passed to the room skip list. @@ -184,6 +185,30 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { }; } + /** + * Get the rooms in the currently active space that are tagged with the given tag. + * @param tag The tag to filter the rooms by. + */ + private getRoomsWithTagInActiveSpace(tag: TagID): Room[] { + return this.getSortedRoomsInActiveSpace() + .sections.flatMap((s) => s.rooms) + .filter((room) => getTagsForRoom(room).includes(tag)); + } + + /** + * Get the server notice rooms in the currently active space. + */ + public getServerNoticeRooms(): Room[] { + return this.getRoomsWithTagInActiveSpace(DefaultTagID.ServerNotice); + } + + /** + * Get the direct message (DM) rooms in the currently active space. + */ + public getDmRooms(): Room[] { + return this.getRoomsWithTagInActiveSpace(DefaultTagID.DM); + } + /** * Resort the list of rooms using a different algorithm. * @param algorithm The sorting algorithm to use. diff --git a/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts b/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts index 4ca1a710f0..fc2a01254c 100644 --- a/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts +++ b/apps/web/test/unit-tests/stores/room-list-v3/RoomListStoreV3-test.ts @@ -826,6 +826,104 @@ describe("RoomListStoreV3", () => { ).toContain(room); }); }); + + describe("getServerNoticeRooms", () => { + it("returns only rooms tagged as server notice", async () => { + const { rooms } = getClientAndRooms(); + + // Tag rooms 8 and 27 as server notice rooms + [8, 27].forEach((i) => (rooms[i].tags[DefaultTagID.ServerNotice] = {})); + + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + + const result = store.getServerNoticeRooms(); + expect(result).toHaveLength(2); + for (const i of [8, 27]) { + expect(result).toContain(rooms[i]); + } + }); + + it("returns an empty array when there are no server notice rooms", async () => { + getClientAndRooms(); + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + expect(store.getServerNoticeRooms()).toEqual([]); + }); + + it("only returns rooms that belong to the active space", async () => { + const { client, rooms } = getClientAndRooms(); + // Put rooms 6, 8, 13, 27, 75 into a space + const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client); + + // Room 8 (in the space) and room 50 (in Home only) are both server notices + rooms[8].tags[DefaultTagID.ServerNotice] = {}; + rooms[50].tags[DefaultTagID.ServerNotice] = {}; + + // Activate the space + setupMocks(spaceRoom, roomIds); + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + + // Only the server notice room within the active space is returned + const result = store.getServerNoticeRooms(); + expect(result).toEqual([rooms[8]]); + }); + }); + + describe("getDmRooms", () => { + it("returns only rooms tagged as DM", async () => { + const { rooms } = getClientAndRooms(); + + // Rooms 8 and 27 are DMs (no explicit tags + present in the DM map) + const ids = [8, 27].map((i) => rooms[i].roomId); + jest.spyOn(DMRoomMap, "shared").mockImplementation((() => { + return { + getUserIdForRoomId: (id: string) => (ids.includes(id) ? "@myuser:matrix.org" : ""), + }; + }) as () => DMRoomMap); + + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + + const result = store.getDmRooms(); + expect(result).toHaveLength(2); + for (const i of [8, 27]) { + expect(result).toContain(rooms[i]); + } + }); + + it("returns an empty array when there are no DM rooms", async () => { + // The top-level beforeEach mocks the DM map to match no rooms. + getClientAndRooms(); + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + expect(store.getDmRooms()).toEqual([]); + }); + + it("only returns rooms that belong to the active space", async () => { + const { client, rooms } = getClientAndRooms(); + // Put rooms 6, 8, 13, 27, 75 into a space + const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client); + + // Room 8 (in the space) and room 50 (in Home only) are both DMs + const ids = [rooms[8].roomId, rooms[50].roomId]; + jest.spyOn(DMRoomMap, "shared").mockImplementation((() => { + return { + getUserIdForRoomId: (id: string) => (ids.includes(id) ? "@myuser:matrix.org" : ""), + }; + }) as () => DMRoomMap); + + // Activate the space + setupMocks(spaceRoom, roomIds); + const store = new RoomListStoreV3Class(dispatcher); + await store.start(); + + // Only the DM room within the active space is returned + const result = store.getDmRooms(); + expect(result).toEqual([rooms[8]]); + }); + }); }); describe("Sections", () => {