diff --git a/apps/web/src/@types/global.d.ts b/apps/web/src/@types/global.d.ts index 9d7c1ddf09f..e56ce4cfaf7 100644 --- a/apps/web/src/@types/global.d.ts +++ b/apps/web/src/@types/global.d.ts @@ -40,7 +40,6 @@ import { type DeepReadonly } from "./common"; import type MatrixChat from "../components/structures/MatrixChat"; import { type InitialCryptoSetupStore } from "../stores/InitialCryptoSetupStore"; import { type ModuleApiType } from "../modules/Api.ts"; -import type { RoomListStoreV3Class } from "../stores/room-list-v3/RoomListStoreV3.ts"; /* eslint-disable @typescript-eslint/naming-convention */ @@ -90,7 +89,6 @@ declare global { mxContentMessages: ContentMessages; mxToastStore: ToastStore; mxDeviceListener: DeviceListener; - getRoomListStoreV3: () => RoomListStoreV3Class; mxPlatformPeg: PlatformPeg; mxIntegrationManagers: typeof IntegrationManagers; singletonModalManager: ModalManager; diff --git a/apps/web/src/components/structures/LoggedInView.tsx b/apps/web/src/components/structures/LoggedInView.tsx index 1b49bf528ff..3a4e665410c 100644 --- a/apps/web/src/components/structures/LoggedInView.tsx +++ b/apps/web/src/components/structures/LoggedInView.tsx @@ -36,7 +36,6 @@ import { hideToast as hideServerLimitToast, showToast as showServerLimitToast } import { Action } from "../../dispatcher/actions"; import LeftPanel from "./LeftPanel"; import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; -import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import NonUrgentToastContainer from "./NonUrgentToastContainer"; import { type IOOBData, type IThreepidInvite } from "../../stores/ThreepidInviteStore"; import Modal from "../../Modal"; @@ -53,7 +52,6 @@ import UserView from "./UserView"; import { mediaFromMxc } from "../../customisations/Media"; import { UserTab } from "../views/dialogs/UserTab"; import { type OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; -import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { TimelineRenderingType } from "../../contexts/RoomContext"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; import { type SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload"; @@ -296,7 +294,7 @@ class LoggedInView extends React.Component { }; private onRoomStateEvents = (ev: MatrixEvent): void => { - const serverNoticeList = RoomListStoreV3.instance.getServerNoticeRooms(); + const serverNoticeList = this.context.roomListStore.getServerNoticeRooms(); if (serverNoticeList.some((r) => r.roomId === ev.getRoomId())) { this.updateServerNoticeEvents(); } @@ -329,7 +327,7 @@ class LoggedInView extends React.Component { } private updateServerNoticeEvents = async (): Promise => { - const serverNoticeList = RoomListStoreV3.instance.getServerNoticeRooms(); + const serverNoticeList = this.context.roomListStore.getServerNoticeRooms(); if (!serverNoticeList.length) return; const events: MatrixEvent[] = []; @@ -494,7 +492,7 @@ class LoggedInView extends React.Component { break; case KeyBindingAction.ToggleRoomSidePanel: if (this.props.page_type === "room_view") { - RightPanelStore.instance.togglePanel(null); + this.context.rightPanelStore.togglePanel(null); handled = true; } break; diff --git a/apps/web/src/components/views/dialogs/InviteDialog.tsx b/apps/web/src/components/views/dialogs/InviteDialog.tsx index d7492369774..f38b47e18f4 100644 --- a/apps/web/src/components/views/dialogs/InviteDialog.tsx +++ b/apps/web/src/components/views/dialogs/InviteDialog.tsx @@ -26,7 +26,6 @@ import { abbreviateUrl } from "../../../utils/UrlUtils"; import IdentityAuthClient from "../../../IdentityAuthClient"; import { showAnyInviteErrors } from "../../../RoomInvite"; import { Action } from "../../../dispatcher/actions"; -import RoomListStoreV3 from "../../../stores/room-list-v3/RoomListStoreV3"; import SettingsStore from "../../../settings/SettingsStore"; import { UIFeature } from "../../../settings/UIFeature"; import { SearchResultAvatar } from "../avatars/SearchResultAvatar"; @@ -279,7 +278,7 @@ export default class InviteDialog extends React.PureComponent u.userId !== myUserId); diff --git a/apps/web/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx b/apps/web/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx index 282c8e7e42a..3e0769fb09c 100644 --- a/apps/web/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx +++ b/apps/web/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx @@ -63,7 +63,12 @@ export const RoomListPanel: React.FC = ({ activeSpace }) => const matrixClient = useMatrixClientContext(); const vm = useCreateAutoDisposedViewModel( - () => new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }), + () => + new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }), ); return ( diff --git a/apps/web/src/components/views/rooms/RoomListPanel/RoomListView.tsx b/apps/web/src/components/views/rooms/RoomListPanel/RoomListView.tsx index 0cbc4b3fd9e..50809610319 100644 --- a/apps/web/src/components/views/rooms/RoomListPanel/RoomListView.tsx +++ b/apps/web/src/components/views/rooms/RoomListPanel/RoomListView.tsx @@ -30,7 +30,12 @@ export function RoomListView(): JSX.Element { // Create and auto-dispose ViewModel instance const vm = useCreateAutoDisposedViewModel( - () => new RoomListViewModel({ client: matrixClient, spaceStore: sdkContext.spaceStore }), + () => + new RoomListViewModel({ + client: matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }), ); // Render avatar for each room - memoized to prevent re-renders diff --git a/apps/web/src/contexts/SDKContextClass.ts b/apps/web/src/contexts/SDKContextClass.ts index 8689b36b3b4..9dd1a5d48e9 100644 --- a/apps/web/src/contexts/SDKContextClass.ts +++ b/apps/web/src/contexts/SDKContextClass.ts @@ -30,6 +30,7 @@ import { Action } from "../dispatcher/actions.ts"; import { type OnLoggedInPayload } from "../dispatcher/payloads/OnLoggedInPayload.ts"; import Notifier from "../Notifier.ts"; import SettingController from "../settings/controllers/SettingController.ts"; +import RoomListStoreV3 from "../stores/room-list-v3/RoomListStoreV3.ts"; /** * A class which (mostly) lazily initialises stores as and when they are requested, ensuring they remain @@ -71,6 +72,7 @@ export class SDKContextClass { protected _UserProfilesStore?: UserProfilesStore; protected _ResizeNotifier?: ResizeNotifier; protected _MultiRoomViewStore?: MultiRoomViewStore; + protected _RoomListStore?: RoomListStoreV3; protected _Notifier?: Notifier; public constructor() { @@ -198,6 +200,14 @@ export class SDKContextClass { return this._MultiRoomViewStore; } + public get roomListStore(): RoomListStoreV3 { + if (!this._RoomListStore) { + this._RoomListStore = new RoomListStoreV3(defaultDispatcher, this); + this._RoomListStore.start(); + } + return this._RoomListStore; + } + public get notifier(): Notifier { if (!this._Notifier) { this._Notifier = new Notifier(defaultDispatcher, this); diff --git a/apps/web/src/modules/Api.ts b/apps/web/src/modules/Api.ts index 789e8144d29..c5c9e4d47ea 100644 --- a/apps/web/src/modules/Api.ts +++ b/apps/web/src/modules/Api.ts @@ -35,6 +35,7 @@ import { WidgetApi } from "./WidgetApi.ts"; import { CustomisationsApi } from "./customisationsApi.ts"; import { ComposerApi } from "./ComposerApi.ts"; import defaultDispatcher from "../dispatcher/dispatcher.ts"; +import { SDKContextClass } from "../contexts/SDKContextClass.ts"; const legacyCustomisationsFactory = (baseCustomisations: T) => { let used = false; @@ -96,7 +97,7 @@ export class ModuleApi implements Api { public readonly widget = new WidgetApi(); public readonly rootNode = document.getElementById("matrixchat")!; public readonly client = new ClientApi(); - public readonly stores = new StoresApi(); + public readonly stores = new StoresApi(SDKContextClass.instance); public readonly composer = new ComposerApi(defaultDispatcher); public createRoot(element: Element): Root { diff --git a/apps/web/src/modules/StoresApi.ts b/apps/web/src/modules/StoresApi.ts index f1d6add95e8..f6f08e9e7f3 100644 --- a/apps/web/src/modules/StoresApi.ts +++ b/apps/web/src/modules/StoresApi.ts @@ -11,73 +11,31 @@ import { Watchable, } from "@element-hq/element-web-module-api"; -import type { RoomListStoreV3Class, RoomListStoreV3Event } from "../stores/room-list-v3/RoomListStoreV3"; +import type RoomListStoreV3 from "../stores/room-list-v3/RoomListStoreV3"; +import { RoomListStoreV3Event } from "../stores/room-list-v3/RoomListStoreV3"; import { Room as ModuleRoom } from "./models/Room"; - -interface RlsEvents { - LISTS_LOADED_EVENT: RoomListStoreV3Event.ListsLoaded; - LISTS_UPDATE_EVENT: RoomListStoreV3Event.ListsUpdate; -} +import { type SDKContextClass } from "../contexts/SDKContextClass.ts"; export class RoomListStoreApi implements IRoomListStore { - private rls?: RoomListStoreV3Class; - private LISTS_LOADED_EVENT?: RoomListStoreV3Event.ListsLoaded; - private LISTS_UPDATE_EVENT?: RoomListStoreV3Event.ListsUpdate; - public readonly moduleLoadPromise: Promise; - - public constructor() { - this.moduleLoadPromise = this.init(); - } - - /** - * Load the RLS through a dynamic import. This is necessary to prevent - * circular dependency issues. - */ - private async init(): Promise { - const module = await import("../stores/room-list-v3/RoomListStoreV3"); - this.rls = module.default.instance; - this.LISTS_LOADED_EVENT = module.LISTS_LOADED_EVENT; - this.LISTS_UPDATE_EVENT = module.LISTS_UPDATE_EVENT; - } + public constructor(private readonly sdkContext: SDKContextClass) {} public getRooms(): RoomsWatchable { - return new RoomsWatchable(this.roomListStore, this.events); - } - - private get events(): RlsEvents { - if (!this.LISTS_LOADED_EVENT || !this.LISTS_UPDATE_EVENT) { - throw new Error("Event type was not loaded correctly, did you forget to await waitForReady()?"); - } - return { LISTS_LOADED_EVENT: this.LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT: this.LISTS_UPDATE_EVENT }; - } - - private get roomListStore(): RoomListStoreV3Class { - if (!this.rls) { - throw new Error("rls is undefined, did you forget to await waitForReady()?"); - } - return this.rls; + return new RoomsWatchable(this.sdkContext.roomListStore); } public async waitForReady(): Promise { - // Wait for the module to load first - await this.moduleLoadPromise; - // Check if RLS is already loaded - if (!this.roomListStore.isLoadingRooms) return; + if (!this.sdkContext.roomListStore.isLoadingRooms) return; // Await a promise that resolves when RLS has loaded const { promise, resolve } = Promise.withResolvers(); - const { LISTS_LOADED_EVENT } = this.events; - this.roomListStore.once(LISTS_LOADED_EVENT, resolve); + this.sdkContext.roomListStore.once(RoomListStoreV3Event.ListsLoaded, resolve); await promise; } } class RoomsWatchable extends Watchable { - public constructor( - private readonly rls: RoomListStoreV3Class, - private readonly events: RlsEvents, - ) { + public constructor(private readonly rls: RoomListStoreV3) { super(rls.getSortedRooms().map((sdkRoom) => new ModuleRoom(sdkRoom))); } @@ -86,20 +44,22 @@ class RoomsWatchable extends Watchable { }; protected onFirstWatch(): void { - this.rls.on(this.events.LISTS_UPDATE_EVENT, this.onRlsUpdate); + this.rls.on(RoomListStoreV3Event.ListsUpdate, this.onRlsUpdate); } protected onLastWatch(): void { - this.rls.off(this.events.LISTS_UPDATE_EVENT, this.onRlsUpdate); + this.rls.off(RoomListStoreV3Event.ListsUpdate, this.onRlsUpdate); } } export class StoresApi implements IStoresApi { private roomListStoreApi?: IRoomListStore; + public constructor(private readonly sdkContext: SDKContextClass) {} + public get roomListStore(): IRoomListStore { if (!this.roomListStoreApi) { - this.roomListStoreApi = new RoomListStoreApi(); + this.roomListStoreApi = new RoomListStoreApi(this.sdkContext); } return this.roomListStoreApi; } diff --git a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts index 00c14be0752..d60e34f1318 100644 --- a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts @@ -14,7 +14,6 @@ import type { ActionPayload } from "../../dispatcher/payloads"; import type { Filter, FilterKey } from "./skip-list/filters"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import SettingsStore from "../../settings/SettingsStore"; -import defaultDispatcher from "../../dispatcher/dispatcher"; import { RecencySorter } from "./skip-list/sorters/RecencySorter"; import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter"; import { readReceiptChangeIsFor } from "../../utils/read-receipts"; @@ -48,7 +47,7 @@ import { reorderSection, } from "./section"; import { DefaultTagID, type TagID } from "./skip-list/tag"; -import { SDKContextClass } from "../../contexts/SDKContextClass.ts"; +import { type SDKContextClass } from "../../contexts/SDKContextClass.ts"; /** * These are the filters passed to the room skip list. @@ -104,7 +103,7 @@ export const ROOM_TAGGED_EVENT = RoomListStoreV3Event.RoomTagged; * 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 { +export default class RoomListStoreV3 extends AsyncStoreWithClient { /** * Contains all the rooms in the active space */ @@ -128,13 +127,16 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { */ private pendingEmit = false; - public constructor(dispatcher: MatrixDispatcher) { + public constructor( + dispatcher: MatrixDispatcher, + private readonly sdkContext: SDKContextClass, + ) { super(dispatcher); this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); - SDKContextClass.instance.spaceStore.on(UPDATE_SELECTED_SPACE, () => { + this.sdkContext.spaceStore.on(UPDATE_SELECTED_SPACE, () => { this.onActiveSpaceChanged(); }); - SDKContextClass.instance.spaceStore.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged()); + this.sdkContext.spaceStore.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged()); SettingsStore.watchSetting("RoomList.OrderedCustomSections", null, () => this.onOrderedCustomSectionsChange()); this.loadCustomSections(); @@ -173,7 +175,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { * @param filterKeys Optional array of filters that the rooms must match against. */ public getSortedRoomsInActiveSpace(filterKeys?: FilterKey[]): RoomsResult { - const spaceId = SDKContextClass.instance.spaceStore.activeSpace; + const spaceId = this.sdkContext.spaceStore.activeSpace; const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections"); const sections = areSectionsEnabled @@ -238,7 +240,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { this.roomSkipList = new RoomSkipList(sorter, this.getSkipListFilters()); - await SDKContextClass.instance.spaceStore.storeReadyPromise; + await this.sdkContext.spaceStore.storeReadyPromise; const rooms = this.getRooms(); this.roomSkipList.seed(rooms); this.emit(LISTS_LOADED_EVENT); @@ -522,7 +524,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { * Emits {@link SECTION_CREATED_EVENT} if the section was successfully created. */ public async createSection(): Promise { - const tag = await createSection(SDKContextClass.instance.spaceStore.activeSpace); + const tag = await createSection(this.sdkContext.spaceStore.activeSpace); if (!tag) return; this.emit(SECTION_CREATED_EVENT, tag); return tag; @@ -573,19 +575,3 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { this.sortedTags = [DefaultTagID.Favourite, ...reorderable, DefaultTagID.LowPriority]; } } - -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; - } -} - -window.getRoomListStoreV3 = () => RoomListStoreV3.instance; diff --git a/apps/web/src/stores/spaces/SpaceStore.test.ts b/apps/web/src/stores/spaces/SpaceStore.test.ts index 3ea8fbf0773..8b8f6d4ae97 100644 --- a/apps/web/src/stores/spaces/SpaceStore.test.ts +++ b/apps/web/src/stores/spaces/SpaceStore.test.ts @@ -39,7 +39,6 @@ import SettingsStore from "../../../src/settings/SettingsStore"; import { SettingLevel } from "../../settings/SettingLevel"; import { Action } from "../../dispatcher/actions"; import { MatrixClientPeg } from "../../MatrixClientPeg"; -import RoomListStoreV3 from "../room-list-v3/RoomListStoreV3"; import { DefaultTagID } from "../room-list-v3/skip-list/tag"; import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; import { NotificationLevel } from "../notifications/NotificationLevel"; @@ -1473,7 +1472,7 @@ describe("SpaceStore", () => { const state = RoomNotificationStateStore.instance.getRoomState(room); // @ts-ignore state._level = NotificationLevel.Notification; - vi.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + vi.spyOn(sdkContext.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: MetaSpace.Home, sections: [{ tag: DefaultTagID.Untagged, rooms: [room] }], }); diff --git a/apps/web/src/stores/spaces/SpaceStore.ts b/apps/web/src/stores/spaces/SpaceStore.ts index df8ad1101a3..865f3ec85ef 100644 --- a/apps/web/src/stores/spaces/SpaceStore.ts +++ b/apps/web/src/stores/spaces/SpaceStore.ts @@ -24,7 +24,6 @@ import { logger } from "matrix-js-sdk/src/logger"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; import { type MatrixDispatcher } from "../../dispatcher/dispatcher.ts"; -import RoomListStoreV3 from "../room-list-v3/RoomListStoreV3"; import SettingsStore from "../../settings/SettingsStore"; import DMRoomMap from "../../utils/DMRoomMap"; import { SpaceNotificationState } from "../notifications/SpaceNotificationState"; @@ -199,7 +198,7 @@ export default class SpaceStore extends AsyncStoreWithClient { let roomId: string | undefined; if (space === MetaSpace.Home && this.allRoomsInHome) { const hasMentions = RoomNotificationStateStore.instance.globalState.hasMentions; - const rooms = RoomListStoreV3.instance.getSortedRoomsInActiveSpace().sections.flatMap((s) => s.rooms); + const rooms = this.sdkContext.roomListStore.getSortedRoomsInActiveSpace().sections.flatMap((s) => s.rooms); for (const room of rooms) { const state = RoomNotificationStateStore.instance.getRoomState(room); if (hasMentions ? state.hasMentions : state.isUnread) { diff --git a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts index 017a16a068a..cf93079eee9 100644 --- a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts @@ -28,7 +28,7 @@ import { import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import type { RoomListSectionsCollapseStateChangedPayload } from "../../dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload"; import SettingsStore from "../../settings/SettingsStore"; -import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; +import type RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import { SortingAlgorithm } from "../../stores/room-list-v3/skip-list/sorters"; import { SettingLevel } from "../../settings/SettingLevel"; import { createRoom, hasCreateRoomRights } from "./utils"; @@ -43,6 +43,10 @@ export interface Props { * The space store instance. */ spaceStore: SpaceStore; + /** + * The room list store instance. + */ + roomListStore: RoomListStoreV3; } /** @@ -198,7 +202,7 @@ export class RoomListHeaderViewModel }; public sort = (option: SortOption): void => { - const oldSortingAlgorithm = RoomListStoreV3.instance.activeSortAlgorithm; + const oldSortingAlgorithm = this.props.roomListStore.activeSortAlgorithm; let newSortingAlgorithm: SortingAlgorithm; switch (option) { case "alphabetical": @@ -211,7 +215,7 @@ export class RoomListHeaderViewModel newSortingAlgorithm = SortingAlgorithm.Unread; break; } - RoomListStoreV3.instance.resort(newSortingAlgorithm); + this.props.roomListStore.resort(newSortingAlgorithm); this.snapshot.merge({ activeSortOption: option }); // Record analytics for this action @@ -229,7 +233,7 @@ export class RoomListHeaderViewModel }; public createSection = (): void => { - RoomListStoreV3.instance.createSection(); + this.props.roomListStore.createSection(); PosthogTrackers.trackSectionCreation("RoomListHeader"); }; diff --git a/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts index f7f285271ab..45cf99022fc 100644 --- a/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListItemViewModel.ts @@ -38,7 +38,7 @@ import { Action } from "../../dispatcher/actions"; import type { ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import PosthogTrackers from "../../PosthogTrackers"; import { type Call, CallEvent } from "../../models/Call"; -import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; +import type RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import { getCustomSectionData, isDefaultSectionTag } from "../../stores/room-list-v3/section"; import { _t } from "../../languageHandler"; @@ -50,6 +50,7 @@ type Sections = Array>; interface RoomItemProps { room: Room; client: MatrixClient; + roomListStore: RoomListStoreV3; } /** @@ -78,7 +79,7 @@ export class RoomListItemViewModel public constructor(props: RoomItemProps) { // Get notification state first so we can generate a complete initial snapshot const notifState = RoomNotificationStateStore.instance.getRoomState(props.room); - const availableSections = RoomListItemViewModel.computeAvailableSections(); + const availableSections = RoomListItemViewModel.computeAvailableSections(props.roomListStore); const initialItem = RoomListItemViewModel.generateItemSync( props.room, props.client, @@ -445,7 +446,7 @@ export class RoomListItemViewModel }; public onCreateSection = async (): Promise => { - const newTag = await RoomListStoreV3.instance.createSection(); + const newTag = await this.props.roomListStore.createSection(); PosthogTrackers.trackSectionCreation("RoomListItemOverflowMenu"); // Add the room to the section @@ -460,7 +461,7 @@ export class RoomListItemViewModel public onRemoveFromSection = (): void => { const roomTags = this.props.room.tags; - const sectionTag = RoomListStoreV3.instance.orderedSectionTags.find((tag) => Boolean(roomTags[tag])); + const sectionTag = this.props.roomListStore.orderedSectionTags.find((tag) => Boolean(roomTags[tag])); if (sectionTag) { tagRoom(this.props.room, sectionTag); } @@ -468,7 +469,7 @@ export class RoomListItemViewModel private onCustomSectionsChange = (): void => { // Rebuild the available sections and the sections list to reflect the new settings - this.availableSections = RoomListItemViewModel.computeAvailableSections(); + this.availableSections = RoomListItemViewModel.computeAvailableSections(this.props.roomListStore); const sections = RoomListItemViewModel.buildSections(this.props.room.tags, this.availableSections); this.snapshot.merge({ sections: keepIfSame(this.snapshot.current.sections, sections) }); }; @@ -479,11 +480,11 @@ export class RoomListItemViewModel * Reads the custom section settings, so callers should cache the result and recompute only * when those settings change. */ - private static computeAvailableSections(): Sections { + private static computeAvailableSections(roomListStore: RoomListStoreV3): Sections { const customSectionData = getCustomSectionData(); return ( - RoomListStoreV3.instance.orderedSectionTags + roomListStore.orderedSectionTags // Exclude the Chats because the user toggle the other sections to move rooms in and out of the Chats section. // Also exclude the default sections because they are available as toggles in the main context menu, and we don't want them to be duplicated in the "Move to section" submenu. .filter((tag) => !isDefaultSectionTag(tag)) diff --git a/apps/web/src/viewmodels/room-list/RoomListSectionHeaderViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListSectionHeaderViewModel.ts index 963b24e6a28..ae3ffc0a368 100644 --- a/apps/web/src/viewmodels/room-list/RoomListSectionHeaderViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListSectionHeaderViewModel.ts @@ -18,7 +18,7 @@ import { RoomNotificationStateStore } from "../../stores/notifications/RoomNotif import { NotificationStateEvents } from "../../stores/notifications/NotificationState"; import { type RoomNotificationState } from "../../stores/notifications/RoomNotificationState"; import SettingsStore from "../../settings/SettingsStore"; -import RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; +import type RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; import { CHATS_TAG, getCustomSectionData, @@ -37,6 +37,7 @@ interface RoomListSectionHeaderViewModelProps { */ spaceId: string; onToggleExpanded: (isExpanded: boolean) => void; + roomListStore: RoomListStoreV3; } export class RoomListSectionHeaderViewModel @@ -253,13 +254,13 @@ export class RoomListSectionHeaderViewModel } public editSection = async (): Promise => { - await RoomListStoreV3.instance.editSection(this.props.tag); + await this.props.roomListStore.editSection(this.props.tag); }; public removeSection = async (): Promise => { // There is one notification state per room in the section const isEmpty = this.roomNotificationStates.size === 0; - await RoomListStoreV3.instance.removeSection(this.props.tag, isEmpty); + await this.props.roomListStore.removeSection(this.props.tag, isEmpty); PosthogTrackers.trackInteraction("WebDeleteSection"); }; diff --git a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts index bc1beb2af31..0fa81e3f5d6 100644 --- a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts @@ -23,11 +23,8 @@ import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDel import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { type RoomListSectionsCollapseStateChangedPayload } from "../../dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload"; import type SpaceStore from "../../stores/spaces/SpaceStore"; -import RoomListStoreV3, { - RoomListStoreV3Event, - type RoomsResult, - type Section, -} from "../../stores/room-list-v3/RoomListStoreV3"; +import type RoomListStoreV3 from "../../stores/room-list-v3/RoomListStoreV3"; +import { RoomListStoreV3Event, type RoomsResult, type Section } from "../../stores/room-list-v3/RoomListStoreV3"; import { FilterEnum } from "../../stores/room-list-v3/skip-list/filters"; import { RoomNotificationStateStore, @@ -59,6 +56,7 @@ interface StickyRoomPosition { interface RoomListViewModelProps { client: MatrixClient; spaceStore: SpaceStore; + roomListStore: RoomListStoreV3; } const filterKeyToIdMap: Map = new Map([ @@ -142,7 +140,7 @@ export class RoomListViewModel const activeSpace = props.spaceStore.activeSpaceRoom; // Get initial rooms - const roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(undefined); + const roomsResult = props.roomListStore.getSortedRoomsInActiveSpace(undefined); const canCreateRoom = hasCreateRoomRights(props.client, activeSpace); const filterIds = [...filterKeyToIdMap.values()]; @@ -153,7 +151,7 @@ export class RoomListViewModel super(props, { // Initial view state - start with empty, will populate in async init - isLoadingRooms: RoomListStoreV3.instance.isLoadingRooms, + isLoadingRooms: props.roomListStore.isLoadingRooms, isRoomListEmpty, filterIds, activeFilterId: undefined, @@ -175,28 +173,28 @@ export class RoomListViewModel // Subscribe to room list updates this.disposables.trackListener( - RoomListStoreV3.instance, + this.props.roomListStore, RoomListStoreV3Event.ListsUpdate as any, this.onListsUpdate, ); // Subscribe to room list loaded this.disposables.trackListener( - RoomListStoreV3.instance, + this.props.roomListStore, RoomListStoreV3Event.ListsLoaded as any, this.onListsLoaded, ); // Subscribe to section creation this.disposables.trackListener( - RoomListStoreV3.instance, + this.props.roomListStore, RoomListStoreV3Event.SectionCreated as any, this.onSectionCreated as (...args: unknown[]) => void, ); // Subscribe to room tagging this.disposables.trackListener( - RoomListStoreV3.instance, + this.props.roomListStore, RoomListStoreV3Event.RoomTagged as any, this.onRoomTagged, ); @@ -242,7 +240,7 @@ export class RoomListViewModel // Update rooms result with new filter const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined; - this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys); + this.roomsResult = this.props.roomListStore.getSortedRoomsInActiveSpace(filterKeys); // Update roomsMap immediately before clearing VMs this.updateRoomsMap(this.roomsResult); @@ -307,6 +305,7 @@ export class RoomListViewModel viewModel = new RoomListItemViewModel({ room, client: this.props.client, + roomListStore: this.props.roomListStore, }); this.roomItemViewModels.set(roomId, viewModel); @@ -325,6 +324,7 @@ export class RoomListViewModel title, spaceId: this.roomsResult.spaceId, onToggleExpanded: () => this.updateRoomListData(), + roomListStore: this.props.roomListStore, }); this.roomSectionHeaderViewModels.set(tag, viewModel); return viewModel; @@ -546,7 +546,7 @@ export class RoomListViewModel const oldSpaceId = this.roomsResult.spaceId; // Refresh room data from store - this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys); + this.roomsResult = this.props.roomListStore.getSortedRoomsInActiveSpace(filterKeys); const newSpaceId = this.roomsResult.spaceId; // Detect space change @@ -730,7 +730,7 @@ export class RoomListViewModel const activeFilterId = this.activeFilter !== undefined ? filterKeyToIdMap.get(this.activeFilter) : undefined; const isRoomListEmpty = this.roomsResult.sections.every((section) => section.rooms.length === 0); - const isLoadingRooms = RoomListStoreV3.instance.isLoadingRooms; + const isLoadingRooms = this.props.roomListStore.isLoadingRooms; const previousSections = this.snapshot.current.sections; // Single atomic snapshot update @@ -794,7 +794,7 @@ export class RoomListViewModel public onSectionCreated = (tag: string): void => { // Refresh roomsResult so the new section lands in the same snapshot as the scroll-to. const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined; - this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys); + this.roomsResult = this.props.roomListStore.getSortedRoomsInActiveSpace(filterKeys); this.updateRoomsMap(this.roomsResult); this.updateRoomListData(false, null, tag); this.showToast("section_created"); @@ -838,10 +838,10 @@ export class RoomListViewModel } public changeSectionOrder = async (sourceTag: string, targetTag: string): Promise => { - await RoomListStoreV3.instance.reorderSection(sourceTag, targetTag); + await this.props.roomListStore.reorderSection(sourceTag, targetTag); // Scroll to the section after it moved const filterKeys = this.activeFilter !== undefined ? [this.activeFilter] : undefined; - this.roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(filterKeys); + this.roomsResult = this.props.roomListStore.getSortedRoomsInActiveSpace(filterKeys); this.updateRoomsMap(this.roomsResult); this.updateRoomListData(false, null, sourceTag); }; diff --git a/apps/web/test/unit-tests/modules/StoresApi-test.ts b/apps/web/test/unit-tests/modules/StoresApi-test.ts index ccdfdf1de6e..50bdac5678a 100644 --- a/apps/web/test/unit-tests/modules/StoresApi-test.ts +++ b/apps/web/test/unit-tests/modules/StoresApi-test.ts @@ -7,19 +7,17 @@ Please see LICENSE files in the repository root for full details. import { waitFor } from "jest-matrix-react"; -import { type RoomListStoreApi, StoresApi } from "../../../src/modules/StoresApi"; -import RoomListStoreV3, { - LISTS_LOADED_EVENT, - LISTS_UPDATE_EVENT, -} from "../../../src/stores/room-list-v3/RoomListStoreV3"; +import { StoresApi } from "../../../src/modules/StoresApi"; +import { LISTS_LOADED_EVENT, LISTS_UPDATE_EVENT } from "../../../src/stores/room-list-v3/RoomListStoreV3"; import { mkRoom, stubClient } from "../../test-utils/test-utils"; import { Room } from "../../../src/modules/models/Room"; +import { SDKContextClass } from "../../../src/contexts/SDKContextClass.ts"; describe("StoresApi", () => { describe("RoomListStoreApi", () => { it("should return promise that resolves when RLS is ready", async () => { - jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(true); - const store = new StoresApi(); + jest.spyOn(SDKContextClass.instance.roomListStore, "isLoadingRooms", "get").mockReturnValue(true); + const store = new StoresApi(SDKContextClass.instance); let hasResolved = false; // The following async function will set hasResolved to false // only when waitForReady resolves. @@ -30,10 +28,8 @@ describe("StoresApi", () => { // Shouldn't have resolved yet. expect(hasResolved).toStrictEqual(false); - // Wait for the module to load so that we can test the listener. - await (store.roomListStore as RoomListStoreApi).moduleLoadPromise; // Emit the loaded event. - RoomListStoreV3.instance.emit(LISTS_LOADED_EVENT); + SDKContextClass.instance.roomListStore.emit(LISTS_LOADED_EVENT); // Should resolve now. await waitFor(() => { expect(hasResolved).toStrictEqual(true); @@ -46,10 +42,14 @@ describe("StoresApi", () => { const room1 = mkRoom(cli, "!foo1:m.org"); const room2 = mkRoom(cli, "!foo2:m.org"); const room3 = mkRoom(cli, "!foo3:m.org"); - jest.spyOn(RoomListStoreV3.instance, "getSortedRooms").mockReturnValue([room1, room2, room3]); - jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false); + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRooms").mockReturnValue([ + room1, + room2, + room3, + ]); + jest.spyOn(SDKContextClass.instance.roomListStore, "isLoadingRooms", "get").mockReturnValue(false); - const store = new StoresApi(); + const store = new StoresApi(SDKContextClass.instance); await store.roomListStore.waitForReady(); const watchable = store.roomListStore.getRooms(); expect(watchable.value).toHaveLength(3); @@ -62,10 +62,10 @@ describe("StoresApi", () => { const room2 = mkRoom(cli, "!foo2:m.org"); const rooms = [room1, room2]; - jest.spyOn(RoomListStoreV3.instance, "getSortedRooms").mockReturnValue(rooms); - jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false); + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRooms").mockReturnValue(rooms); + jest.spyOn(SDKContextClass.instance.roomListStore, "isLoadingRooms", "get").mockReturnValue(false); - const store = new StoresApi(); + const store = new StoresApi(SDKContextClass.instance); await store.roomListStore.waitForReady(); const watchable = store.roomListStore.getRooms(); const fn = jest.fn(); @@ -74,7 +74,7 @@ describe("StoresApi", () => { const room3 = mkRoom(cli, "!foo3:m.org"); rooms.push(room3); - RoomListStoreV3.instance.emit(LISTS_UPDATE_EVENT); + SDKContextClass.instance.roomListStore.emit(LISTS_UPDATE_EVENT); expect(fn).toHaveBeenCalledTimes(1); expect(watchable.value).toHaveLength(3); }); 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 9878a4bb406..1ecaa1ce43e 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 @@ -11,10 +11,9 @@ import { mocked } from "jest-mock"; import type { MatrixClient } from "matrix-js-sdk/src/matrix"; import type { RoomNotificationState } from "../../../../src/stores/notifications/RoomNotificationState"; -import { +import RoomListStoreV3, { LISTS_UPDATE_EVENT, SECTION_CREATED_EVENT, - RoomListStoreV3Class, type Section, } from "../../../../src/stores/room-list-v3/RoomListStoreV3"; import * as sectionModule from "../../../../src/stores/room-list-v3/section"; @@ -37,6 +36,7 @@ import { Action } from "../../../../src/dispatcher/actions"; import { SettingLevel } from "../../../../src/settings/SettingLevel.ts"; import { CHATS_TAG } from "../../../../src/stores/room-list-v3/section"; import { SDKContextClass } from "../../../../src/contexts/SDKContextClass.ts"; +import { TestSDKContext } from "../../TestSDKContext.ts"; describe("RoomListStoreV3", () => { async function getRoomListStore() { @@ -44,7 +44,7 @@ describe("RoomListStoreV3", () => { const rooms = getMockedRooms(client); client.getVisibleRooms = jest.fn().mockReturnValue(rooms); jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); return { client, rooms, store, dispatcher }; } @@ -475,7 +475,7 @@ describe("RoomListStoreV3", () => { const { client, rooms } = getClientAndRooms(); const infoSpy = jest.spyOn(logger, "info"); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Create a space and let the store know about it @@ -509,7 +509,7 @@ describe("RoomListStoreV3", () => { return false; }); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const fn = jest.fn(); store.on(LISTS_UPDATE_EVENT, fn); @@ -551,7 +551,7 @@ describe("RoomListStoreV3", () => { }); setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Sorted, filtered rooms should be 8, 27 and 75 @@ -575,7 +575,7 @@ describe("RoomListStoreV3", () => { }); setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Let's say 27 got unfavourited @@ -615,7 +615,7 @@ describe("RoomListStoreV3", () => { }); setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Should only give us rooms at index 8 and 27 @@ -634,7 +634,7 @@ describe("RoomListStoreV3", () => { const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client); setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Since there's no unread yet, we expect zero results @@ -674,7 +674,7 @@ describe("RoomListStoreV3", () => { }) as () => DMRoomMap); setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Should only give us rooms at index 8 and 27 @@ -710,7 +710,7 @@ describe("RoomListStoreV3", () => { // Let's choose 5 rooms to put in space const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 100, 101, 102, 103, 104], client); setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const result = store @@ -736,7 +736,7 @@ describe("RoomListStoreV3", () => { }); setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Should only give us rooms at index 8 and 27 @@ -760,7 +760,7 @@ describe("RoomListStoreV3", () => { }); setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Sorted, filtered rooms should be 8, 27 and 75 @@ -790,7 +790,7 @@ describe("RoomListStoreV3", () => { }); setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Should give us only room at 8 since that's the only room which matches both filters @@ -842,7 +842,7 @@ describe("RoomListStoreV3", () => { // Tag rooms 8 and 27 as server notice rooms [8, 27].forEach((i) => (rooms[i].tags[DefaultTagID.ServerNotice] = {})); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const result = store.getServerNoticeRooms(); @@ -854,7 +854,7 @@ describe("RoomListStoreV3", () => { it("returns an empty array when there are no server notice rooms", async () => { getClientAndRooms(); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); expect(store.getServerNoticeRooms()).toEqual([]); }); @@ -870,7 +870,7 @@ describe("RoomListStoreV3", () => { // Activate the space setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Only the server notice room within the active space is returned @@ -891,7 +891,7 @@ describe("RoomListStoreV3", () => { }; }) as () => DMRoomMap); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const result = store.getDmRooms(); @@ -904,7 +904,7 @@ describe("RoomListStoreV3", () => { 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); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); expect(store.getDmRooms()).toEqual([]); }); @@ -924,7 +924,7 @@ describe("RoomListStoreV3", () => { // Activate the space setupMocks(spaceRoom, roomIds); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Only the DM room within the active space is returned @@ -960,7 +960,7 @@ describe("RoomListStoreV3", () => { enableSections(); getClientAndRooms(); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const result = store.getSortedRoomsInActiveSpace(); @@ -987,7 +987,7 @@ describe("RoomListStoreV3", () => { rooms[3].tags[DefaultTagID.Favourite] = {}; rooms[7].tags[DefaultTagID.LowPriority] = {}; - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const { sections } = store.getSortedRoomsInActiveSpace(); @@ -1008,7 +1008,7 @@ describe("RoomListStoreV3", () => { return "watcher-id"; }); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const listsUpdateListener = jest.fn(); @@ -1031,7 +1031,7 @@ describe("RoomListStoreV3", () => { rooms[i].tags[tag] = {}; }); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const { sections } = store.getSortedRoomsInActiveSpace(); @@ -1052,7 +1052,7 @@ describe("RoomListStoreV3", () => { rooms[0].tags[DefaultTagID.Favourite] = {}; rooms[1].tags[DefaultTagID.LowPriority] = {}; - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const { sections } = store.getSortedRoomsInActiveSpace(); @@ -1077,7 +1077,7 @@ describe("RoomListStoreV3", () => { rooms[i].tags[DefaultTagID.LowPriority] = {}; }); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const { sections } = store.getSortedRoomsInActiveSpace(); @@ -1101,7 +1101,7 @@ describe("RoomListStoreV3", () => { return state; }); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const { sections } = store.getSortedRoomsInActiveSpace([FilterEnum.UnreadFilter]); @@ -1125,7 +1125,7 @@ describe("RoomListStoreV3", () => { return state; }); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // With the unread filter, only the Favourite section has matching rooms. @@ -1141,7 +1141,7 @@ describe("RoomListStoreV3", () => { getClientAndRooms(); // No rooms are tagged, so Favourite and LowPriority sections will be empty - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const { sections } = store.getSortedRoomsInActiveSpace(); @@ -1166,7 +1166,7 @@ describe("RoomListStoreV3", () => { }); jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpace", "get").mockImplementation(() => spaceRoomId); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const { sections, spaceId } = store.getSortedRoomsInActiveSpace(); @@ -1193,7 +1193,7 @@ describe("RoomListStoreV3", () => { getClientAndRooms(); jest.spyOn(sectionModule, "createSection").mockResolvedValue("element.io.section.test-tag"); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const sectionCreatedListener = jest.fn(); @@ -1210,7 +1210,7 @@ describe("RoomListStoreV3", () => { getClientAndRooms(); jest.spyOn(sectionModule, "createSection").mockResolvedValue(undefined); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const sectionCreatedListener = jest.fn(); @@ -1228,7 +1228,7 @@ describe("RoomListStoreV3", () => { getClientAndRooms(); const editSectionSpy = jest.spyOn(sectionModule, "editSection").mockResolvedValue(undefined); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); await store.editSection("element.io.section.test-tag"); @@ -1242,7 +1242,7 @@ describe("RoomListStoreV3", () => { getClientAndRooms(); jest.spyOn(sectionModule, "deleteSection").mockResolvedValue(undefined); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); const listsUpdateListener = jest.fn(); @@ -1273,7 +1273,7 @@ describe("RoomListStoreV3", () => { return false; }); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // Initial state: 3 sections (Favourite, Chats, LowPriority) @@ -1316,7 +1316,7 @@ describe("RoomListStoreV3", () => { client.getVisibleRooms = jest.fn().mockReturnValue(rooms); jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); return { client, rooms, mutedIndices, mutedRooms, store, dispatcher }; } @@ -1402,7 +1402,7 @@ describe("RoomListStoreV3", () => { client.getVisibleRooms = jest.fn().mockReturnValue(rooms); jest.spyOn(AsyncStoreWithClient.prototype, "matrixClient", "get").mockReturnValue(client); - const store = new RoomListStoreV3Class(dispatcher); + const store = new RoomListStoreV3(dispatcher, new TestSDKContext()); await store.start(); // We expect the following order: Low Priority -> Low Priority & Muted -> Muted diff --git a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts index 3f9415debfe..4d8dcdba8a8 100644 --- a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts +++ b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts @@ -14,7 +14,6 @@ import defaultDispatcher from "../../../src/dispatcher/dispatcher"; import { Action } from "../../../src/dispatcher/actions"; import SettingsStore from "../../../src/settings/SettingsStore"; import { SortingAlgorithm } from "../../../src/stores/room-list-v3/skip-list/sorters"; -import RoomListStoreV3 from "../../../src/stores/room-list-v3/RoomListStoreV3"; import { shouldShowSpaceSettings, showCreateNewRoom, @@ -85,7 +84,11 @@ describe("RoomListHeaderViewModel", () => { jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(MetaSpace.Home); jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); const snapshot = vm.getSnapshot(); expect(snapshot.title).toBe("Home"); @@ -99,7 +102,11 @@ describe("RoomListHeaderViewModel", () => { jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); const snapshot = vm.getSnapshot(); expect(snapshot.title).toBe(mockSpace.roomId); @@ -111,7 +118,11 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().canCreateVideoRoom).toBe(false); }); @@ -121,7 +132,11 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().activeSortOption).toBe("alphabetical"); }); @@ -130,14 +145,22 @@ describe("RoomListHeaderViewModel", () => { jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace); jest.spyOn(mockSpace, "getJoinRule").mockReturnValue(JoinRule.Public); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().canInviteInSpace).toBe(true); }); it("should hide invite option when user cannot invite", () => { mocked(mockSpace.canInvite).mockReturnValue(false); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().canInviteInSpace).toBe(false); }); @@ -145,7 +168,11 @@ describe("RoomListHeaderViewModel", () => { jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); mocked(shouldShowSpaceSettings).mockReturnValue(false); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().canAccessSpaceSettings).toBe(false); }); @@ -155,7 +182,11 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true); }); @@ -165,7 +196,11 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().areSectionsEnabled).toBe(true); }); @@ -176,7 +211,11 @@ describe("RoomListHeaderViewModel", () => { return "watcher-id"; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().areSectionsEnabled).toBe(false); // Enable sections @@ -194,7 +233,11 @@ describe("RoomListHeaderViewModel", () => { "room_list_section", ); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().displaySectionReleaseAnnouncement).toBe(true); }); }); @@ -206,7 +249,11 @@ describe("RoomListHeaderViewModel", () => { jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(MetaSpace.Home); jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace); @@ -220,7 +267,11 @@ describe("RoomListHeaderViewModel", () => { jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); mockSpace.name = "new name"; mockSpace.emit(RoomEvent.Name, mockSpace); @@ -237,14 +288,22 @@ describe("RoomListHeaderViewModel", () => { it("should fire CreateChat action when createChatRoom is called", () => { const fireSpy = jest.spyOn(defaultDispatcher, "fire"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.createChatRoom(new Event("click")); expect(fireSpy).toHaveBeenCalledWith(Action.CreateChat); }); it("should call createRoom with active space when in a space", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.createRoom(new Event("click")); expect(createRoom).toHaveBeenCalledWith(mockSpace); @@ -256,7 +315,11 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.createVideoRoom(); expect(showCreateNewRoom).toHaveBeenCalledWith(mockSpace, RoomType.ElementVideo); }); @@ -265,7 +328,11 @@ describe("RoomListHeaderViewModel", () => { jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null); const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.createVideoRoom(); expect(dispatchSpy).toHaveBeenCalledWith({ @@ -276,7 +343,11 @@ describe("RoomListHeaderViewModel", () => { it("should dispatch ViewRoom action when openSpaceHome is called", () => { const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.openSpaceHome(); expect(dispatchSpy).toHaveBeenCalledWith({ @@ -287,21 +358,33 @@ describe("RoomListHeaderViewModel", () => { }); it("should show space invite dialog when inviteInSpace is called", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.inviteInSpace(); expect(showSpaceInvite).toHaveBeenCalledWith(mockSpace); }); it("should show space preferences dialog when openSpacePreferences is called", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.openSpacePreferences(); expect(showSpacePreferences).toHaveBeenCalledWith(mockSpace); }); it("should show space settings dialog when openSpaceSettings is called", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.openSpaceSettings(); expect(showSpaceSettings).toHaveBeenCalledWith(mockSpace); @@ -312,20 +395,28 @@ describe("RoomListHeaderViewModel", () => { ["alphabetical" as const, SortingAlgorithm.Alphabetic], ["unread-first" as const, SortingAlgorithm.Unread], ])("should resort when sort is called with '%s'", (option, expectedAlgorithm) => { - const resortSpy = jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn()); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + const resortSpy = jest.spyOn(sdkContext.roomListStore, "resort").mockImplementation(jest.fn()); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.sort(option); expect(resortSpy).toHaveBeenCalledWith(expectedAlgorithm); }); it("should track analytics on resort", () => { - jest.spyOn(RoomListStoreV3.instance, "activeSortAlgorithm", "get").mockReturnValue( + jest.spyOn(sdkContext.roomListStore, "activeSortAlgorithm", "get").mockReturnValue( SortingAlgorithm.Alphabetic, ); PosthogTrackers.trackRoomListSortingAlgorithmChange = jest.fn(); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); - jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn()); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); + jest.spyOn(sdkContext.roomListStore, "resort").mockImplementation(jest.fn()); vm.sort("unread-first"); expect(PosthogTrackers.trackRoomListSortingAlgorithmChange).toHaveBeenCalledWith( @@ -336,9 +427,13 @@ describe("RoomListHeaderViewModel", () => { it("should call createSection on RoomListStoreV3 when createSection is called", () => { const createSectionSpy = jest - .spyOn(RoomListStoreV3.instance, "createSection") + .spyOn(sdkContext.roomListStore, "createSection") .mockResolvedValue("element.io.section.work"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.createSection(); expect(createSectionSpy).toHaveBeenCalled(); }); @@ -346,7 +441,11 @@ describe("RoomListHeaderViewModel", () => { describe("collapseOrExpandSections", () => { it("should dispatch RoomListCollapseAllSections when collapseSections is not 'expand'", () => { const fireSpy = jest.spyOn(defaultDispatcher, "fire"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.collapseOrExpandSections(); @@ -355,7 +454,11 @@ describe("RoomListHeaderViewModel", () => { it("should dispatch RoomListExpandAllSections when collapseSections is 'expand'", () => { const fireSpy = jest.spyOn(defaultDispatcher, "fire"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); // Drive the VM into the "expand" state by simulating all sections collapsed defaultDispatcher.dispatch( @@ -374,7 +477,11 @@ describe("RoomListHeaderViewModel", () => { describe("RoomListSectionsCollapseStateChanged handling", () => { it("should set collapseSections to 'expand' when collapseSections is collapse", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); defaultDispatcher.dispatch( { @@ -388,7 +495,11 @@ describe("RoomListHeaderViewModel", () => { }); it("should set collapseSections to 'collapse' when collapseSections is expand", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); defaultDispatcher.dispatch( { @@ -402,7 +513,11 @@ describe("RoomListHeaderViewModel", () => { }); it("should set collapseSections to undefined when collapseSections is undefined", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); // First drive it into a non-undefined state defaultDispatcher.dispatch( @@ -433,7 +548,11 @@ describe("RoomListHeaderViewModel", () => { }); const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockImplementation(jest.fn()); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true); vm.toggleMessagePreview(); @@ -443,7 +562,11 @@ describe("RoomListHeaderViewModel", () => { }); it("should call nextReleaseAnnouncement and set displaySectionReleaseAnnouncement to false when closeSectionReleaseAnnouncement is called", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); + vm = new RoomListHeaderViewModel({ + matrixClient, + spaceStore: sdkContext.spaceStore, + roomListStore: sdkContext.roomListStore, + }); vm.closeSectionReleaseAnnouncement(); diff --git a/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx b/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx index 3abd78c1c3f..a38e3d8c862 100644 --- a/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx +++ b/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx @@ -30,9 +30,10 @@ import { Action } from "../../../src/dispatcher/actions"; import { CallStore } from "../../../src/stores/CallStore"; import { CallEvent, type Call } from "../../../src/models/Call"; import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel"; -import RoomListStoreV3 from "../../../src/stores/room-list-v3/RoomListStoreV3"; import * as tagRoomModule from "../../../src/utils/room/tagRoom"; import { CHATS_TAG } from "../../../src/stores/room-list-v3/section"; +import { SDKContextClass } from "../../../src/contexts/SDKContextClass.ts"; +import { TestSDKContext } from "../../unit-tests/TestSDKContext.ts"; jest.mock("../../../src/viewmodels/room-list/utils", () => ({ hasAccessToOptionsMenu: jest.fn().mockReturnValue(true), @@ -59,6 +60,7 @@ describe("RoomListItemViewModel", () => { let room: Room; let notificationState: RoomNotificationState; let viewModel: RoomListItemViewModel; + const sdkContext = new TestSDKContext(); beforeEach(() => { matrixClient = createTestClient(); @@ -88,7 +90,7 @@ describe("RoomListItemViewModel", () => { jest.spyOn(MessagePreviewStore.instance, "getPreviewForRoom").mockResolvedValue(null); jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null); - jest.spyOn(RoomListStoreV3.instance, "orderedSectionTags", "get").mockReturnValue([]); + jest.spyOn(SDKContextClass.instance.roomListStore, "orderedSectionTags", "get").mockReturnValue([]); }); afterEach(() => { @@ -98,7 +100,11 @@ describe("RoomListItemViewModel", () => { describe("Initialization", () => { it("should initialize with room data", async () => { - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); // Wait for async initialization await flushPromises(); @@ -114,7 +120,11 @@ describe("RoomListItemViewModel", () => { text: "Hello world!", } as MessagePreview); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); // Wait for async message preview load await flushPromises(); @@ -125,7 +135,11 @@ describe("RoomListItemViewModel", () => { it("should not load message preview when disabled", async () => { jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -138,7 +152,11 @@ describe("RoomListItemViewModel", () => { jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true); jest.spyOn(notificationState, "count", "get").mockReturnValue(5); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -148,7 +166,11 @@ describe("RoomListItemViewModel", () => { }); it("should update when notification state changes", async () => { - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); expect(viewModel.getSnapshot().notification.count).toBe(0); @@ -163,7 +185,11 @@ describe("RoomListItemViewModel", () => { it("should show bold text when has notifications", async () => { jest.spyOn(notificationState, "hasAnyNotificationOrActivity", "get").mockReturnValue(true); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -173,7 +199,11 @@ describe("RoomListItemViewModel", () => { it("should show mention badge", async () => { jest.spyOn(notificationState, "isMention", "get").mockReturnValue(true); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -183,7 +213,11 @@ describe("RoomListItemViewModel", () => { it("should show invitation state", async () => { jest.spyOn(notificationState, "invited", "get").mockReturnValue(true); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -198,7 +232,11 @@ describe("RoomListItemViewModel", () => { text: "Initial message", } as MessagePreview); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); expect(viewModel.getSnapshot().messagePreview).toBe("Initial message"); @@ -227,7 +265,11 @@ describe("RoomListItemViewModel", () => { text: "Test message", } as MessagePreview); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); expect(viewModel.getSnapshot().messagePreview).toBeUndefined(); @@ -245,7 +287,11 @@ describe("RoomListItemViewModel", () => { it("should reflect favorite tag", async () => { room.tags = { [DefaultTagID.Favourite]: { order: 0 } }; - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -255,7 +301,11 @@ describe("RoomListItemViewModel", () => { it("should reflect low priority tag", async () => { room.tags = { [DefaultTagID.LowPriority]: { order: 0 } }; - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -264,7 +314,11 @@ describe("RoomListItemViewModel", () => { it("should update when room tags change", async () => { room.tags = {}; - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); expect(viewModel.getSnapshot().isFavourite).toBe(false); @@ -291,7 +345,11 @@ describe("RoomListItemViewModel", () => { jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -308,7 +366,11 @@ describe("RoomListItemViewModel", () => { jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -325,7 +387,11 @@ describe("RoomListItemViewModel", () => { jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -341,7 +407,11 @@ describe("RoomListItemViewModel", () => { }; jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall as unknown as Call); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); expect(viewModel.getSnapshot().notification.callType).toBeUndefined(); // Get the callback registered for call state changes @@ -368,7 +438,11 @@ describe("RoomListItemViewModel", () => { }; jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall as unknown as Call); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); // Trigger onCallStateChanged so the call is tracked and the participant listener is registered const mockCalls = (CallStore.instance.on as jest.Mock).mock.calls; @@ -398,7 +472,11 @@ describe("RoomListItemViewModel", () => { }; jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall as unknown as Call); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); expect(viewModel.getSnapshot().notification.callType).toBeUndefined(); // Simulate participant joining @@ -426,7 +504,11 @@ describe("RoomListItemViewModel", () => { }; jest.spyOn(CallStore.instance, "getCall").mockReturnValue(firstCall as unknown as Call); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); // Trigger onCallStateChanged to register the first call const mockCalls = (CallStore.instance.on as jest.Mock).mock.calls; @@ -457,7 +539,11 @@ describe("RoomListItemViewModel", () => { })() as unknown as Call; jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); expect(viewModel.getSnapshot().notification.callType).toBe("voice"); @@ -471,7 +557,11 @@ describe("RoomListItemViewModel", () => { describe("Room name updates", () => { it("should update when room name changes", async () => { - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); expect(viewModel.getSnapshot().name).toBe("Test Room"); @@ -489,7 +579,11 @@ describe("RoomListItemViewModel", () => { const dmRoomMap = DMRoomMap.shared(); jest.spyOn(dmRoomMap, "getUserIdForRoomId").mockReturnValue("@user:server"); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -501,7 +595,11 @@ describe("RoomListItemViewModel", () => { const dmRoomMap = DMRoomMap.shared(); jest.spyOn(dmRoomMap, "getUserIdForRoomId").mockReturnValue(undefined); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); await flushPromises(); @@ -511,7 +609,11 @@ describe("RoomListItemViewModel", () => { describe("Actions", () => { it("should dispatch view room action on openRoom", () => { - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); @@ -525,13 +627,21 @@ describe("RoomListItemViewModel", () => { }); it("should return room object", () => { - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); expect(viewModel.getSnapshot().room).toBe(room); }); it("should dispatch view_invite action when onInvite is called", () => { - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); viewModel.onInvite(); @@ -543,7 +653,11 @@ describe("RoomListItemViewModel", () => { }); it("should dispatch copy_room action when onCopyRoomLink is called", () => { - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); viewModel.onCopyRoomLink(); @@ -556,7 +670,11 @@ describe("RoomListItemViewModel", () => { it("should dispatch leave_room action when onLeaveRoom is called for normal room", () => { room.tags = {}; - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); viewModel.onLeaveRoom(); @@ -570,7 +688,11 @@ describe("RoomListItemViewModel", () => { it("should dispatch forget_room action when onLeaveRoom is called for archived room", () => { room.tags = { [DefaultTagID.Archived]: { order: 0 } }; - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); viewModel.onLeaveRoom(); @@ -583,11 +705,15 @@ describe("RoomListItemViewModel", () => { it("should call createSection on RoomListStoreV3 when onCreateSection is called", async () => { const createSectionSpy = jest - .spyOn(RoomListStoreV3.instance, "createSection") + .spyOn(SDKContextClass.instance.roomListStore, "createSection") .mockResolvedValue("element.io.section.work"); const tagRoomSpy = jest.spyOn(tagRoomModule, "tagRoom").mockImplementation(() => {}); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); viewModel.onCreateSection(); expect(createSectionSpy).toHaveBeenCalled(); @@ -596,7 +722,11 @@ describe("RoomListItemViewModel", () => { it("should call tagRoom when onToggleSection is called", () => { const tagRoomSpy = jest.spyOn(tagRoomModule, "tagRoom").mockImplementation(() => {}); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); viewModel.onToggleSection(DefaultTagID.Favourite); @@ -608,7 +738,7 @@ describe("RoomListItemViewModel", () => { const customTag = "element.io.section.custom1"; beforeEach(() => { - jest.spyOn(RoomListStoreV3.instance, "orderedSectionTags", "get").mockReturnValue([ + jest.spyOn(SDKContextClass.instance.roomListStore, "orderedSectionTags", "get").mockReturnValue([ DefaultTagID.Favourite, customTag, CHATS_TAG, @@ -617,7 +747,11 @@ describe("RoomListItemViewModel", () => { }); it("should include sections from orderedSectionTags excluding CHATS_TAG, favourite, and low priority", () => { - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); const sections = viewModel.getSnapshot().sections; expect(sections.map((s) => s.tag)).toEqual([customTag]); @@ -626,7 +760,11 @@ describe("RoomListItemViewModel", () => { it("should mark the room current section as selected", () => { room.tags = { [customTag]: { order: 0 } }; - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); const sections = viewModel.getSnapshot().sections; expect(sections.find((s) => s.tag === customTag)?.isSelected).toBe(true); @@ -638,7 +776,11 @@ describe("RoomListItemViewModel", () => { return { [customTag]: { name: "My Custom Section", tag: customTag } }; return false; }); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); const section = viewModel.getSnapshot().sections.find((s) => s.tag === customTag); expect(section?.name).toBe("My Custom Section"); @@ -651,11 +793,15 @@ describe("RoomListItemViewModel", () => { return "watcher-id"; }); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); expect(viewModel.getSnapshot().sections).toHaveLength(1); // Simulate reordering: custom section removed - jest.spyOn(RoomListStoreV3.instance, "orderedSectionTags", "get").mockReturnValue([ + jest.spyOn(SDKContextClass.instance.roomListStore, "orderedSectionTags", "get").mockReturnValue([ DefaultTagID.Favourite, CHATS_TAG, DefaultTagID.LowPriority, @@ -671,7 +817,11 @@ describe("RoomListItemViewModel", () => { return false; }); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: sdkContext.roomListStore, + }); expect(viewModel.getSnapshot().areSectionsEnabled).toBe(true); }); @@ -682,7 +832,11 @@ describe("RoomListItemViewModel", () => { return "watcher-id"; }); - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: sdkContext.roomListStore, + }); expect(viewModel.getSnapshot().areSectionsEnabled).toBe(false); // Enable sections @@ -698,7 +852,11 @@ describe("RoomListItemViewModel", () => { describe("Cleanup", () => { it("should unsubscribe from all events on dispose", () => { - viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + viewModel = new RoomListItemViewModel({ + room, + client: matrixClient, + roomListStore: SDKContextClass.instance.roomListStore, + }); const offSpy = jest.spyOn(notificationState, "off"); diff --git a/apps/web/test/viewmodels/room-list/RoomListSectionHeaderViewModel-test.ts b/apps/web/test/viewmodels/room-list/RoomListSectionHeaderViewModel-test.ts index e0c3130970f..6d3330807a9 100644 --- a/apps/web/test/viewmodels/room-list/RoomListSectionHeaderViewModel-test.ts +++ b/apps/web/test/viewmodels/room-list/RoomListSectionHeaderViewModel-test.ts @@ -16,9 +16,9 @@ import { CallStore } from "../../../src/stores/CallStore"; import { type Call } from "../../../src/models/Call"; import { createTestClient, mkRoom } from "../../test-utils"; import SettingsStore from "../../../src/settings/SettingsStore"; -import RoomListStoreV3 from "../../../src/stores/room-list-v3/RoomListStoreV3"; import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag"; import { CHATS_TAG } from "../../../src/stores/room-list-v3/section"; +import { SDKContextClass } from "../../../src/contexts/SDKContextClass.ts"; describe("RoomListSectionHeaderViewModel", () => { let onToggleExpanded: jest.Mock; @@ -45,6 +45,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); const snapshot = vm.getSnapshot(); @@ -59,6 +60,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(vm.isExpanded).toBe(true); @@ -79,6 +81,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); // Default space: collapse @@ -112,6 +115,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Section", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(vm.getSnapshot().displaySectionMenu).toBe(expected); }); @@ -129,6 +133,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Section", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(vm.getSnapshot().canBeReordered).toBe(expected); }); @@ -151,6 +156,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Old Title", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(vm.getSnapshot().title).toBe("Old Title"); @@ -167,6 +173,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "My Section", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); jest.spyOn(SettingsStore, "getValue").mockReturnValue({}); @@ -181,6 +188,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); watchCallback(); @@ -191,13 +199,16 @@ describe("RoomListSectionHeaderViewModel", () => { describe("editSection", () => { it("should delegate to RoomListStoreV3.instance.editSection", async () => { - const editSectionSpy = jest.spyOn(RoomListStoreV3.instance, "editSection").mockResolvedValue(undefined); + const editSectionSpy = jest + .spyOn(SDKContextClass.instance.roomListStore, "editSection") + .mockResolvedValue(undefined); const tag = "element.io.section.custom"; const vm = new RoomListSectionHeaderViewModel({ tag, title: "Section", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); await vm.editSection(); @@ -216,13 +227,16 @@ describe("RoomListSectionHeaderViewModel", () => { }); it("should delegate to RoomListStoreV3.instance.removeSection with isEmpty=true when no rooms", async () => { - const removeSectionSpy = jest.spyOn(RoomListStoreV3.instance, "removeSection").mockResolvedValue(undefined); + const removeSectionSpy = jest + .spyOn(SDKContextClass.instance.roomListStore, "removeSection") + .mockResolvedValue(undefined); const tag = "element.io.section.custom"; const vm = new RoomListSectionHeaderViewModel({ tag, title: "Section", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); await vm.removeSection(); @@ -230,13 +244,16 @@ describe("RoomListSectionHeaderViewModel", () => { }); it("should delegate to RoomListStoreV3.instance.removeSection with isEmpty=false when rooms exist", async () => { - const removeSectionSpy = jest.spyOn(RoomListStoreV3.instance, "removeSection").mockResolvedValue(undefined); + const removeSectionSpy = jest + .spyOn(SDKContextClass.instance.roomListStore, "removeSection") + .mockResolvedValue(undefined); const tag = "element.io.section.custom"; const vm = new RoomListSectionHeaderViewModel({ tag, title: "Section", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([mkRoom(matrixClient, "!room:server")]); @@ -261,6 +278,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -275,6 +293,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -298,6 +317,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -319,6 +339,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -337,6 +358,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -361,6 +383,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -395,6 +418,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room, room2]); @@ -418,6 +442,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -437,6 +462,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -477,6 +503,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room, room2]); @@ -502,6 +529,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -520,6 +548,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -541,6 +570,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); @@ -561,6 +591,7 @@ describe("RoomListSectionHeaderViewModel", () => { title: "Favourites", spaceId: "!space:server", onToggleExpanded, + roomListStore: SDKContextClass.instance.roomListStore, }); vm.setRooms([room]); diff --git a/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx b/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx index 5193d527e33..4d6a956a4ab 100644 --- a/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx +++ b/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx @@ -10,7 +10,7 @@ import { mocked } from "jest-mock"; import { waitFor } from "jest-matrix-react"; import { createTestClient, flushPromises, flushPromisesWithFakeTimers, mkStubRoom, stubClient } from "../../test-utils"; -import RoomListStoreV3, { RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3"; +import { RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3"; import { FilterEnum } from "../../../src/stores/room-list-v3/skip-list/filters"; import dispatcher from "../../../src/dispatcher/dispatcher"; import { Action } from "../../../src/dispatcher/actions"; @@ -60,12 +60,12 @@ describe("RoomListViewModel", () => { } as unknown as DMRoomMap; DMRoomMap.setShared(dmRoomMap); - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3] }], }); - jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false); + jest.spyOn(SDKContextClass.instance.roomListStore, "isLoadingRooms", "get").mockReturnValue(false); jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null); jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null); @@ -82,6 +82,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const snapshot = viewModel.getSnapshot(); @@ -94,7 +95,7 @@ describe("RoomListViewModel", () => { }); it("should initialize with empty room list", () => { - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [{ tag: CHATS_TAG, rooms: [] }], }); @@ -102,6 +103,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().sections).toEqual([]); @@ -114,6 +116,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().canCreateRoom).toBe(true); @@ -125,15 +128,16 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const newRoom = mkStubRoom("!room4:server", "Room 4", matrixClient); - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3, newRoom] }], }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); expect(viewModel.getSnapshot().sections[0].roomIds).toEqual([ "!room1:server", @@ -144,15 +148,16 @@ describe("RoomListViewModel", () => { }); it("should update loading state when ListsLoaded event fires", () => { - jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(true); + jest.spyOn(SDKContextClass.instance.roomListStore, "isLoadingRooms", "get").mockReturnValue(true); viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().isLoadingRooms).toBe(true); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsLoaded); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsLoaded); expect(viewModel.getSnapshot().isLoadingRooms).toBe(false); }); @@ -162,13 +167,14 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Create view model for room1 const room1VM = viewModel.getRoomItemViewModel("!room1:server"); expect(room1VM).toBeDefined(); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); // View model should be still valid expect(room1VM!.isDisposed).toBe(false); @@ -180,11 +186,12 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const spaceRoomList = [room1, room2]; - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "!space:server", sections: [{ tag: CHATS_TAG, rooms: spaceRoomList }], }); @@ -193,7 +200,7 @@ describe("RoomListViewModel", () => { "!room1:server", ); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); expect(viewModel.getSnapshot().roomListState.spaceId).toBe("!space:server"); expect(viewModel.getSnapshot().sections[0].roomIds).toEqual(["!room1:server", "!room2:server"]); @@ -203,6 +210,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Get view models for visible rooms @@ -213,12 +221,12 @@ describe("RoomListViewModel", () => { const disposeSpy2 = jest.spyOn(vm2, "dispose"); // Change space - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "!space:server", sections: [{ tag: CHATS_TAG, rooms: [room3] }], }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); expect(disposeSpy1).toHaveBeenCalled(); expect(disposeSpy2).toHaveBeenCalled(); @@ -228,17 +236,18 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const newSpaceRoom = mkStubRoom("!spaceroom:server", "Space Room", matrixClient); - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "!space:server", sections: [{ tag: CHATS_TAG, rooms: [newSpaceRoom] }], }); jest.spyOn(SDKContextClass.instance.spaceStore, "getLastSelectedRoomIdForSpace").mockReturnValue(null); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); // New space room should be accessible expect(() => viewModel.getRoomItemViewModel("!spaceroom:server")).not.toThrow(); @@ -252,6 +261,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server"); @@ -271,6 +281,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null); @@ -292,6 +303,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Select room at index 1 @@ -305,12 +317,12 @@ describe("RoomListViewModel", () => { expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(1); // Simulate room list update that would move room2 to front - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [{ tag: CHATS_TAG, rooms: [room2, room1, room3] }], // room2 moved to front }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); // Active room should still be at index 1 (sticky behavior) expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(1); @@ -321,6 +333,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Select room at index 1 @@ -350,11 +363,12 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().activeFilterId).toBeUndefined(); - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [{ tag: CHATS_TAG, rooms: [room1] }], filterKeys: [FilterEnum.UnreadFilter], @@ -370,10 +384,11 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Turn filter on - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [{ tag: CHATS_TAG, rooms: [room1] }], filterKeys: [FilterEnum.UnreadFilter], @@ -383,7 +398,7 @@ describe("RoomListViewModel", () => { expect(viewModel.getSnapshot().activeFilterId).toBe("unread"); // Turn filter off - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [{ tag: CHATS_TAG, rooms: [room1, room2, room3] }], }); @@ -403,6 +418,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const itemViewModel = viewModel.getRoomItemViewModel("!room1:server"); @@ -415,6 +431,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const itemViewModel1 = viewModel.getRoomItemViewModel("!room1:server"); @@ -427,6 +444,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getRoomItemViewModel("!nonexistent:server")).toBeUndefined(); @@ -436,15 +454,16 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Normal list update removes room2 from the list - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [{ tag: CHATS_TAG, rooms: [room1, room3] }], }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); expect(() => viewModel.getRoomItemViewModel("!room2:server")).not.toThrow(); }); @@ -453,18 +472,19 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const spaceRoom = mkStubRoom("!newroom:server", "New Room", matrixClient); // Space change: new space only has spaceRoom - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "!space:server", sections: [{ tag: CHATS_TAG, rooms: [spaceRoom] }], }); jest.spyOn(SDKContextClass.instance.spaceStore, "getLastSelectedRoomIdForSpace").mockReturnValue(null); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); expect(viewModel.getRoomItemViewModel("!room1:server")).toBeUndefined(); }); @@ -473,6 +493,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Manually clear roomsMap to simulate stale cache, but keep roomsResult intact @@ -486,6 +507,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const vm1 = viewModel.getRoomItemViewModel("!room1:server")!; @@ -512,6 +534,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const dispatchSpy = jest.spyOn(dispatcher, "fire"); @@ -525,6 +548,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); @@ -543,6 +567,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); @@ -566,6 +591,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room1:server"); @@ -592,6 +618,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server"); @@ -618,6 +645,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room1:server"); @@ -644,6 +672,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!unknown:server"); @@ -671,6 +700,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null); @@ -707,6 +737,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const vm1 = viewModel.getRoomItemViewModel("!room1:server")!; @@ -726,8 +757,9 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.SectionCreated); expect(viewModel.getSnapshot().toast).toBe("section_created"); }); @@ -735,8 +767,9 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.RoomTagged); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.RoomTagged); expect(viewModel.getSnapshot().toast).toBe("chat_moved"); }); @@ -744,9 +777,10 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.SectionCreated); expect(viewModel.getSnapshot().toast).toBe("section_created"); viewModel.closeToast(); @@ -757,9 +791,10 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.SectionCreated); expect(viewModel.getSnapshot().toast).toBe("section_created"); jest.advanceTimersByTime(15 * 1000); @@ -770,13 +805,14 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.SectionCreated); jest.advanceTimersByTime(10 * 1000); // Second section created — resets the timer - RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.SectionCreated); jest.advanceTimersByTime(10 * 1000); // Toast should still be visible (only 10s since last emit) @@ -798,6 +834,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // room1/room2 visible, room3 (unread) scrolled below the fold. @@ -811,12 +848,13 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); viewModel.updateVisibleFold(1); expect(viewModel.getSnapshot().toast).toBe("unread_activity"); // A transient event toast takes precedence over the persistent unread-activity toast… - RoomListStoreV3.instance.emit(RoomListStoreV3Event.RoomTagged); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.RoomTagged); expect(viewModel.getSnapshot().toast).toBe("chat_moved"); // …and once it auto-dismisses, the unread-activity toast returns. @@ -839,7 +877,7 @@ describe("RoomListViewModel", () => { regularRoom1 = mkStubRoom("!reg1:server", "Reg 1", matrixClient); regularRoom2 = mkStubRoom("!reg2:server", "Reg 2", matrixClient); - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [ { tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2] }, @@ -853,6 +891,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const snapshot = viewModel.getSnapshot(); @@ -869,13 +908,14 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().isFlatList).toBe(false); }); it("should be a flat list when only chats section has rooms", () => { - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [ { tag: DefaultTagID.Favourite, rooms: [] }, @@ -887,6 +927,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().isFlatList).toBe(true); @@ -895,7 +936,7 @@ describe("RoomListViewModel", () => { }); it("should be a flat list when the room list is empty", () => { - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [ { tag: DefaultTagID.Favourite, rooms: [] }, @@ -907,6 +948,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().isFlatList).toBe(true); @@ -917,6 +959,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const snapshot = viewModel.getSnapshot(); @@ -928,7 +971,7 @@ describe("RoomListViewModel", () => { }); it("should omit empty sections from snapshot", () => { - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [ { tag: DefaultTagID.Favourite, rooms: [] }, @@ -940,6 +983,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const snapshot = viewModel.getSnapshot(); @@ -951,6 +995,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const headerVM = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite); @@ -963,6 +1008,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const headerVM1 = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite); @@ -974,6 +1020,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Collapse the favourite section @@ -996,6 +1043,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Collapse the favourite section (which has 2 rooms: fav1, fav2) @@ -1022,6 +1070,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite); @@ -1040,11 +1089,12 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const newFav = mkStubRoom("!fav3:server", "Fav 3", matrixClient); - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [ { tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2, newFav] }, @@ -1053,7 +1103,7 @@ describe("RoomListViewModel", () => { ], }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); const snapshot = viewModel.getSnapshot(); expect(snapshot.sections[0].roomIds).toEqual(["!fav1:server", "!fav2:server", "!fav3:server"]); @@ -1063,6 +1113,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Collapse favourites @@ -1070,7 +1121,7 @@ describe("RoomListViewModel", () => { favHeader.onClick(); // Trigger a list update - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); const snapshot = viewModel.getSnapshot(); const favSection = snapshot.sections.find((s) => s.id === DefaultTagID.Favourite); @@ -1081,6 +1132,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Collapse favourites in the home space @@ -1090,7 +1142,7 @@ describe("RoomListViewModel", () => { // Switch to a different space with its own rooms const spaceFav = mkStubRoom("!spacefav:server", "Space Fav", matrixClient); const spaceReg = mkStubRoom("!spacereg:server", "Space Reg", matrixClient); - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "!space:server", sections: [ { tag: DefaultTagID.Favourite, rooms: [spaceFav] }, @@ -1100,7 +1152,7 @@ describe("RoomListViewModel", () => { }); jest.spyOn(SDKContextClass.instance.spaceStore, "getLastSelectedRoomIdForSpace").mockReturnValue(null); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); let snapshot = viewModel.getSnapshot(); // Favourites should be expanded in the new space (per-space state) @@ -1113,7 +1165,7 @@ describe("RoomListViewModel", () => { expect(chatsSection!.roomIds).toEqual(["!spacereg:server"]); // Switch back to home space - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [ { tag: DefaultTagID.Favourite, rooms: [favRoom1, favRoom2] }, @@ -1122,7 +1174,7 @@ describe("RoomListViewModel", () => { ], }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); snapshot = viewModel.getSnapshot(); // Favourites should still be collapsed in the home space @@ -1139,10 +1191,11 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Only favRoom1 is unread - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [ { tag: DefaultTagID.Favourite, rooms: [favRoom1] }, @@ -1187,7 +1240,7 @@ describe("RoomListViewModel", () => { return { [customTag]: { tag: customTag, name: "My Section", spaceId: MetaSpace.Home } }; return false; }); - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: MetaSpace.Home, sections: [ { tag: customTag, rooms: [] }, @@ -1198,13 +1251,14 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().sections.some((s) => s.id === customTag)).toBe(true); }); it("hides an empty custom section in a different space", () => { - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: MetaSpace.Home, sections: [ { tag: customTag, rooms: [] }, @@ -1215,13 +1269,14 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().sections.some((s) => s.id === customTag)).toBe(false); }); it("shows a non-empty custom section regardless of originating space", () => { - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: MetaSpace.Home, sections: [ { tag: customTag, rooms: [regularRoom1] }, @@ -1232,6 +1287,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); expect(viewModel.getSnapshot().sections.some((s) => s.id === customTag)).toBe(true); @@ -1243,6 +1299,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite); @@ -1264,6 +1321,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Collapse first @@ -1288,10 +1346,11 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); expect(dispatchSpy).toHaveBeenCalledWith({ action: Action.RoomListSectionsCollapseStateChanged, @@ -1303,6 +1362,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Collapse all sections @@ -1311,7 +1371,7 @@ describe("RoomListViewModel", () => { viewModel.getSectionHeaderViewModel(DefaultTagID.LowPriority).isExpanded = false; const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); expect(dispatchSpy).toHaveBeenCalledWith({ action: Action.RoomListSectionsCollapseStateChanged, @@ -1323,7 +1383,7 @@ describe("RoomListViewModel", () => { { label: "flat list", chatsRooms: true }, { label: "empty room list", chatsRooms: false }, ])("should dispatch collapseSection=undefined when it is a $label", ({ chatsRooms }) => { - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [ { tag: DefaultTagID.Favourite, rooms: [] }, @@ -1334,10 +1394,11 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); const dispatchSpy = jest.spyOn(dispatcher, "dispatch"); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); expect(dispatchSpy).toHaveBeenCalledWith({ action: Action.RoomListSectionsCollapseStateChanged, @@ -1351,6 +1412,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Select favRoom1 (index 0 globally, index 0 in favourites section) @@ -1364,7 +1426,7 @@ describe("RoomListViewModel", () => { expect(viewModel.getSnapshot().roomListState.activeRoomIndex).toBe(0); // Room list update moves favRoom1 to second position within favourites - jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({ + jest.spyOn(SDKContextClass.instance.roomListStore, "getSortedRoomsInActiveSpace").mockReturnValue({ spaceId: "home", sections: [ { tag: DefaultTagID.Favourite, rooms: [favRoom2, favRoom1] }, @@ -1373,7 +1435,7 @@ describe("RoomListViewModel", () => { ], }); - RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); + SDKContextClass.instance.roomListStore.emit(RoomListStoreV3Event.ListsUpdate); // Sticky room should keep favRoom1 at index 0 within the favourites section const snapshot = viewModel.getSnapshot(); @@ -1386,6 +1448,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); // Ensure section header VMs are created before tests that interact with them viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite); @@ -1395,7 +1458,7 @@ describe("RoomListViewModel", () => { it("should delegate changeSectionOrder to RoomListStoreV3.reorderSection", async () => { const reorderSpy = jest - .spyOn(RoomListStoreV3.instance, "reorderSection") + .spyOn(SDKContextClass.instance.roomListStore, "reorderSection") .mockResolvedValue(undefined); await viewModel.changeSectionOrder(DefaultTagID.Favourite, CHATS_TAG); @@ -1404,7 +1467,7 @@ describe("RoomListViewModel", () => { }); it("should scroll the moved section back into view after reordering", async () => { - jest.spyOn(RoomListStoreV3.instance, "reorderSection").mockResolvedValue(undefined); + jest.spyOn(SDKContextClass.instance.roomListStore, "reorderSection").mockResolvedValue(undefined); await viewModel.changeSectionOrder(DefaultTagID.Favourite, CHATS_TAG); expect(viewModel.getSnapshot().roomListState.scrollToSectionTag).toBe(DefaultTagID.Favourite); @@ -1493,6 +1556,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, spaceStore: SDKContextClass.instance.spaceStore, + roomListStore: SDKContextClass.instance.roomListStore, }); mocked(tagRoom).mockClear(); });