From 825fab65959ce1b6ca76fd246fc54fada39268fc Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 13 Jul 2026 15:31:39 +0100 Subject: [PATCH] Remove SpaceStore singleton (#34088) * Remove dead code * Remove LegacyCallHandler singleton Route via SDKContext to cut import cycles * Remove SpaceStore singleton Route via SDKContext to cut import cycles * Fix tests * Fix tests * Fix tests * Fix tests * Iterate * Post-merge fixup * Iterate --- .../src/components/structures/LeftPanel.tsx | 15 +- .../dialogs/AddExistingToSpaceDialog.tsx | 32 ++-- .../dialogs/ConfirmSpaceUserActionDialog.tsx | 4 +- .../views/dialogs/LeaveSpaceDialog.tsx | 6 +- .../ManageRestrictedJoinRuleDialog.tsx | 8 +- .../dialogs/spotlight/SpotlightDialog.tsx | 9 +- .../components/views/rooms/NewRoomIntro.tsx | 18 +-- .../rooms/RoomListPanel/RoomListPanel.tsx | 7 +- .../views/settings/JoinRuleSettings.tsx | 16 +- .../components/views/spaces/SpacePanel.tsx | 52 +++--- .../views/spaces/SpaceTreeLevel.tsx | 23 +-- apps/web/src/contexts/SDKContextClass.ts | 9 +- apps/web/src/createRoom.ts | 4 +- apps/web/src/stores/AsyncStore.ts | 2 +- .../stores/room-list-v3/RoomListStoreV3.ts | 12 +- apps/web/src/stores/room-list-v3/section.ts | 6 +- .../stores/room-list-v3/skip-list/RoomNode.ts | 8 +- apps/web/src/stores/spaces/SpaceStore.test.ts | 78 +++++---- apps/web/src/stores/spaces/SpaceStore.ts | 52 ++---- apps/web/src/utils/RoomUpgrade.ts | 4 +- apps/web/src/utils/i18n-helpers.ts | 4 +- apps/web/src/utils/leave-behaviour.test.ts | 17 +- apps/web/src/utils/leave-behaviour.ts | 9 +- .../room-list/RoomListHeaderViewModel.ts | 10 +- .../viewmodels/room-list/RoomListViewModel.ts | 4 +- apps/web/test/test-utils/test-utils.ts | 3 + apps/web/test/unit-tests/TestSDKContext.ts | 4 +- .../structures/LoggedInView-test.tsx | 15 +- .../structures/MessagePanel-test.tsx | 11 +- .../structures/SpaceRoomView-test.tsx | 6 +- .../ManageRestrictedJoinRuleDialog-test.tsx | 8 +- .../views/dialogs/SpotlightDialog-test.tsx | 4 +- .../views/elements/AppTile-test.tsx | 2 + .../views/rooms/NewRoomIntro-test.tsx | 16 +- .../RoomHeader/CallGuestLinkButton-test.tsx | 1 + .../views/settings/JoinRuleSettings-test.tsx | 14 +- .../views/spaces/SpacePanel-test.tsx | 12 +- .../views/spaces/SpaceTreeLevel-test.tsx | 34 ++-- .../test/unit-tests/slash-commands/utils.ts | 2 + .../unit-tests/stores/RoomViewStore-test.ts | 4 +- .../room-list-v3/RoomListStoreV3-test.ts | 30 ++-- .../stores/room-list-v3/section-test.ts | 22 +-- .../skip-list/RoomSkipList-test.ts | 12 +- .../unit-tests/utils/i18n-helpers-test.ts | 10 +- .../room-list/RoomListHeaderViewModel-test.ts | 101 ++++++------ .../room-list/RoomListViewModel-test.tsx | 149 +++++++++--------- 46 files changed, 466 insertions(+), 403 deletions(-) diff --git a/apps/web/src/components/structures/LeftPanel.tsx b/apps/web/src/components/structures/LeftPanel.tsx index f261e22292..5c14f0f027 100644 --- a/apps/web/src/components/structures/LeftPanel.tsx +++ b/apps/web/src/components/structures/LeftPanel.tsx @@ -10,9 +10,9 @@ import React from "react"; import classNames from "classnames"; import type ResizeNotifier from "../../utils/ResizeNotifier"; -import SpaceStore from "../../stores/spaces/SpaceStore"; import { type SpaceKey, UPDATE_SELECTED_SPACE } from "../../stores/spaces"; import { RoomListPanel } from "../views/rooms/RoomListPanel"; +import { SDKContext } from "../../contexts/SDKContext.ts"; interface IProps { isMinimized: boolean; @@ -24,20 +24,23 @@ interface IState { } export default class LeftPanel extends React.Component { - public constructor(props: IProps) { - super(props); + public static contextType = SDKContext; + declare public context: React.ContextType; + + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { - activeSpace: SpaceStore.instance.activeSpace, + activeSpace: context.spaceStore.activeSpace, }; } public componentDidMount(): void { - SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); + this.context.spaceStore.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); } public componentWillUnmount(): void { - SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); + this.context.spaceStore.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); } private updateActiveSpace = (activeSpace: SpaceKey): void => { diff --git a/apps/web/src/components/views/dialogs/AddExistingToSpaceDialog.tsx b/apps/web/src/components/views/dialogs/AddExistingToSpaceDialog.tsx index 113274a94d..a0368bade6 100644 --- a/apps/web/src/components/views/dialogs/AddExistingToSpaceDialog.tsx +++ b/apps/web/src/components/views/dialogs/AddExistingToSpaceDialog.tsx @@ -19,7 +19,6 @@ import { _t, _td } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; import Dropdown from "../elements/Dropdown"; import SearchBox from "../../structures/SearchBox"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import RoomAvatar from "../avatars/RoomAvatar"; import { getDisplayAliasForRoom } from "../../../Rooms"; import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; @@ -35,6 +34,7 @@ import LazyRenderList from "../elements/LazyRenderList"; import { useSettingValue } from "../../../hooks/useSettings"; import { filterBoolean } from "../../../utils/arrays"; import { type NonEmptyArray } from "../../../@types/common"; +import { SDKContextClass } from "../../../contexts/SDKContextClass.ts"; // These values match CSS const ROW_HEIGHT = 32 + 12; @@ -155,8 +155,14 @@ export const AddExistingToSpace: React.FC = ({ const [query, setQuery] = useState(""); const lcQuery = query.toLowerCase().trim(); - const existingSubspacesSet = useMemo(() => new Set(SpaceStore.instance.getChildSpaces(space.roomId)), [space]); - const existingRoomsSet = useMemo(() => new Set(SpaceStore.instance.getChildRooms(space.roomId)), [space]); + const existingSubspacesSet = useMemo( + () => new Set(SDKContextClass.instance.spaceStore.getChildSpaces(space.roomId)), + [space], + ); + const existingRoomsSet = useMemo( + () => new Set(SDKContextClass.instance.spaceStore.getChildRooms(space.roomId)), + [space], + ); const [spaces, rooms, dms] = useMemo(() => { let rooms = visibleRooms; @@ -201,15 +207,17 @@ export const AddExistingToSpace: React.FC = ({ for (const room of selectedToAdd) { const via = calculateRoomVia(room); try { - await SpaceStore.instance.addRoomToSpace(space, room.roomId, via).catch(async (e): Promise => { - if (e.errcode === "M_LIMIT_EXCEEDED") { - await sleep(e.data.retry_after_ms); - await SpaceStore.instance.addRoomToSpace(space, room.roomId, via); // retry - return; - } + await SDKContextClass.instance.spaceStore + .addRoomToSpace(space, room.roomId, via) + .catch(async (e): Promise => { + if (e.errcode === "M_LIMIT_EXCEEDED") { + await sleep(e.data.retry_after_ms); + await SDKContextClass.instance.spaceStore.addRoomToSpace(space, room.roomId, via); // retry + return; + } - throw e; - }); + throw e; + }); setProgress((i) => (i ?? 0) + 1); } catch (e) { logger.error("Failed to add rooms to space", e); @@ -398,7 +406,7 @@ export const SubspaceSelector: React.FC = ({ title, spac const options = useMemo(() => { return [ space, - ...SpaceStore.instance.getChildSpaces(space.roomId).filter((space) => { + ...SDKContextClass.instance.spaceStore.getChildSpaces(space.roomId).filter((space) => { return space.currentState.maySendStateEvent(EventType.SpaceChild, space.client.getSafeUserId()); }), ]; diff --git a/apps/web/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx b/apps/web/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx index b686b4cc4a..a23311faae 100644 --- a/apps/web/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx +++ b/apps/web/src/components/views/dialogs/ConfirmSpaceUserActionDialog.tsx @@ -11,8 +11,8 @@ import { type Room } from "matrix-js-sdk/src/matrix"; import { InfoSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import ConfirmUserActionDialog from "./ConfirmUserActionDialog"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker"; +import { SDKContextClass } from "../../../contexts/SDKContextClass.ts"; type BaseProps = ComponentProps; interface IProps extends Omit { @@ -36,7 +36,7 @@ const ConfirmSpaceUserActionDialog: React.FC = ({ ...props }) => { const spaceChildren = useMemo(() => { - const children = SpaceStore.instance.getChildren(space.roomId); + const children = SDKContextClass.instance.spaceStore.getChildren(space.roomId); if (spaceChildFilter) { return children.filter(spaceChildFilter); } diff --git a/apps/web/src/components/views/dialogs/LeaveSpaceDialog.tsx b/apps/web/src/components/views/dialogs/LeaveSpaceDialog.tsx index ffa780ccc9..3a8645bc70 100644 --- a/apps/web/src/components/views/dialogs/LeaveSpaceDialog.tsx +++ b/apps/web/src/components/views/dialogs/LeaveSpaceDialog.tsx @@ -13,10 +13,10 @@ import { InfoSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icon import { _t } from "../../../languageHandler"; import DialogButtons from "../elements/DialogButtons"; import BaseDialog from "../dialogs/BaseDialog"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import SpaceChildrenPicker from "../spaces/SpaceChildrenPicker"; import { filterBoolean } from "../../../utils/arrays"; import { isOnlyAdmin } from "../../../utils/membership"; +import { SDKContextClass } from "../../../contexts/SDKContextClass.ts"; interface IProps { space: Room; @@ -25,8 +25,8 @@ interface IProps { const LeaveSpaceDialog: React.FC = ({ space, onFinished }) => { const spaceChildren = useMemo(() => { - const roomSet = new Set(SpaceStore.instance.getSpaceFilteredRoomIds(space.roomId)); - SpaceStore.instance.traverseSpace( + const roomSet = new Set(SDKContextClass.instance.spaceStore.getSpaceFilteredRoomIds(space.roomId)); + SDKContextClass.instance.spaceStore.traverseSpace( space.roomId, (spaceId) => { if (space.roomId === spaceId) return; // skip the root node diff --git a/apps/web/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx b/apps/web/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx index eb6fbc5df8..7e13ab1759 100644 --- a/apps/web/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx +++ b/apps/web/src/components/views/dialogs/ManageRestrictedJoinRuleDialog.tsx @@ -15,12 +15,12 @@ import { AutoHideScrollbar } from "@element-hq/web-shared-components"; import { _t } from "../../../languageHandler"; import BaseDialog from "./BaseDialog"; import SearchBox from "../../structures/SearchBox"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import RoomAvatar from "../avatars/RoomAvatar"; import AccessibleButton from "../elements/AccessibleButton"; import StyledCheckbox from "../elements/StyledCheckbox"; import MatrixClientContext from "../../../contexts/MatrixClientContext"; import { filterBoolean } from "../../../utils/arrays"; +import { SDKContextClass } from "../../../contexts/SDKContextClass.ts"; interface IProps { room: Room; @@ -38,7 +38,7 @@ const Entry: React.FC<{ let description; if (localRoom) { description = _t("common|n_members", { count: room.getJoinedMemberCount() }); - const numChildRooms = SpaceStore.instance.getChildRooms(room.roomId).length; + const numChildRooms = SDKContextClass.instance.spaceStore.getChildRooms(room.roomId).length; if (numChildRooms > 0) { description += " ยท " + _t("common|n_rooms", { count: numChildRooms }); } @@ -67,7 +67,7 @@ const Entry: React.FC<{ const addAllParents = (set: Set, room: Room): void => { const cli = room.client; - const parents = Array.from(SpaceStore.instance.getKnownParents(room.roomId)).map((parentId) => + const parents = Array.from(SDKContextClass.instance.spaceStore.getKnownParents(room.roomId)).map((parentId) => cli.getRoom(parentId), ); @@ -90,7 +90,7 @@ const ManageRestrictedJoinRuleDialog: React.FC = ({ room, selected, onFi return [ Array.from(parents), - SpaceStore.instance.spacePanelSpaces.filter((s) => !parents.has(s)), + SDKContextClass.instance.spaceStore.spacePanelSpaces.filter((s) => !parents.has(s)), filterBoolean( selected?.map((roomId) => { const room = cli.getRoom(roomId); diff --git a/apps/web/src/components/views/dialogs/spotlight/SpotlightDialog.tsx b/apps/web/src/components/views/dialogs/spotlight/SpotlightDialog.tsx index 2c5caa402d..f1441d40b4 100644 --- a/apps/web/src/components/views/dialogs/spotlight/SpotlightDialog.tsx +++ b/apps/web/src/components/views/dialogs/spotlight/SpotlightDialog.tsx @@ -72,7 +72,6 @@ import { RoomNotificationStateStore } from "../../../../stores/notifications/Roo import { compareRoomsByRecency } from "../../../../utils/room/sortRoomsByRecency"; import { SDKContextClass } from "../../../../contexts/SDKContextClass"; import { getMetaSpaceName, MetaSpace } from "../../../../stores/spaces"; -import SpaceStore from "../../../../stores/spaces/SpaceStore"; import { DirectoryMember, type Member, startDmOnFirstMessage } from "../../../../utils/direct-messages"; import DMRoomMap from "../../../../utils/DMRoomMap"; import { makeUserPermalink } from "../../../../utils/permalinks/Permalinks"; @@ -420,13 +419,13 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n } return [ - ...SpaceStore.instance.enabledMetaSpaces.map((spaceKey) => ({ + ...SDKContextClass.instance.spaceStore.enabledMetaSpaces.map((spaceKey) => ({ section: Section.Spaces, filter: [] as Filter[], avatar:
{metaspaceToIcon(spaceKey)}
, - name: getMetaSpaceName(spaceKey, SpaceStore.instance.allRoomsInHome), + name: getMetaSpaceName(spaceKey, SDKContextClass.instance.spaceStore.allRoomsInHome), onClick() { - SpaceStore.instance.setActiveSpace(spaceKey); + SDKContextClass.instance.spaceStore.setActiveSpace(spaceKey); }, })), ...roomResults, @@ -517,7 +516,7 @@ const SpotlightDialog: React.FC = ({ initialText = "", initialFilter = n const numResults = sum(Object.values(results).map((it) => it.length)); useWebSearchMetrics(numResults, query.length, true); - const activeSpace = SpaceStore.instance.activeSpaceRoom; + const activeSpace = SDKContextClass.instance.spaceStore.activeSpaceRoom; const [spaceResults, spaceResultsLoading] = useSpaceResults(activeSpace ?? undefined, query); const setQuery = (e: ChangeEvent): void => { diff --git a/apps/web/src/components/views/rooms/NewRoomIntro.tsx b/apps/web/src/components/views/rooms/NewRoomIntro.tsx index c4f4a169fa..748c644282 100644 --- a/apps/web/src/components/views/rooms/NewRoomIntro.tsx +++ b/apps/web/src/components/views/rooms/NewRoomIntro.tsx @@ -12,7 +12,6 @@ import { KnownMembership } from "matrix-js-sdk/src/types"; import { ErrorSolidIcon, UserAddIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { EventTileBubble, LinkedText } from "@element-hq/web-shared-components"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import DMRoomMap from "../../../utils/DMRoomMap"; import { _t, _td } from "../../../languageHandler"; import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton"; @@ -21,10 +20,8 @@ import RoomAvatar from "../avatars/RoomAvatar"; import defaultDispatcher from "../../../dispatcher/dispatcher"; import { type ViewUserPayload } from "../../../dispatcher/payloads/ViewUserPayload"; import { Action } from "../../../dispatcher/actions"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import { showSpaceInvite } from "../../../utils/space"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog-tab"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents"; import { UIComponent } from "../../../settings/UIFeature"; import { privateShouldBeEncrypted } from "../../../utils/rooms"; @@ -33,6 +30,7 @@ import { shouldEncryptRoomWithSingle3rdPartyInvite } from "../../../utils/room/s import { useScopedRoomContext } from "../../../contexts/ScopedRoomContext.tsx"; import { useTopic } from "../../../hooks/room/useTopic"; import { topicToHtml } from "../../../HtmlUtils"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; function hasExpectedEncryptionSettings(matrixClient: MatrixClient, room: Room): boolean { const isEncrypted: boolean = matrixClient.isRoomEncrypted(room.roomId); @@ -53,7 +51,8 @@ const determineIntroMessage = (room: Room, encryptedSingle3rdPartyInvite: boolea }; const NewRoomIntro: React.FC = () => { - const cli = useContext(MatrixClientContext); + const sdkContext = useContext(SDKContext); + const cli = sdkContext.client!; const { room, roomId } = useScopedRoomContext("room", "roomId"); const topic = useTopic(room); const isLocalRoom = room instanceof LocalRoom; @@ -179,10 +178,10 @@ const NewRoomIntro: React.FC = () => { let parentSpace: Room | undefined; if ( - SpaceStore.instance.activeSpaceRoom?.canInvite(cli.getSafeUserId()) && - SpaceStore.instance.isRoomInSpace(SpaceStore.instance.activeSpace!, room.roomId) + sdkContext.spaceStore.activeSpaceRoom?.canInvite(cli.getSafeUserId()) && + sdkContext.spaceStore.isRoomInSpace(sdkContext.spaceStore.activeSpace!, room.roomId) ) { - parentSpace = SpaceStore.instance.activeSpaceRoom; + parentSpace = sdkContext.spaceStore.activeSpaceRoom; } let buttons: JSX.Element | undefined; @@ -278,10 +277,7 @@ const NewRoomIntro: React.FC = () => { const subText = _t("room|intro|private_unencrypted_warning"); let subButton: JSX.Element | undefined; - if ( - room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, MatrixClientPeg.safeGet()) && - !isLocalRoom - ) { + if (room.currentState.mayClientSendStateEvent(EventType.RoomEncryption, cli) && !isLocalRoom) { subButton = ( {_t("room|intro|enable_encryption_prompt")} diff --git a/apps/web/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx b/apps/web/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx index aa7c0cf76b..282c8e7e42 100644 --- a/apps/web/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx +++ b/apps/web/src/components/views/rooms/RoomListPanel/RoomListPanel.tsx @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com Please see LICENSE files in the repository root for full details. */ -import React, { useState, useCallback } from "react"; +import React, { useState, useCallback, useContext } from "react"; import { Flex, RoomListHeaderView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components"; import { shouldShowComponent } from "../../../../customisations/helpers/UIComponents"; @@ -19,7 +19,7 @@ import { Landmark, LandmarkNavigation } from "../../../../accessibility/Landmark import { type IState as IRovingTabIndexState } from "../../../../accessibility/RovingTabIndex"; import { RoomListHeaderViewModel } from "../../../../viewmodels/room-list/RoomListHeaderViewModel"; import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext"; -import SpaceStore from "../../../../stores/spaces/SpaceStore"; +import { SDKContext } from "../../../../contexts/SDKContext.ts"; type RoomListPanelProps = { /** @@ -33,6 +33,7 @@ type RoomListPanelProps = { * The panel of the room list */ export const RoomListPanel: React.FC = ({ activeSpace }) => { + const sdkContext = useContext(SDKContext); const displayRoomSearch = shouldShowComponent(UIComponent.FilterContainer); const [focusedElement, setFocusedElement] = useState(null); @@ -62,7 +63,7 @@ export const RoomListPanel: React.FC = ({ activeSpace }) => const matrixClient = useMatrixClientContext(); const vm = useCreateAutoDisposedViewModel( - () => new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }), + () => new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }), ); return ( diff --git a/apps/web/src/components/views/settings/JoinRuleSettings.tsx b/apps/web/src/components/views/settings/JoinRuleSettings.tsx index 86ef7af7a6..2c7bdf7530 100644 --- a/apps/web/src/components/views/settings/JoinRuleSettings.tsx +++ b/apps/web/src/components/views/settings/JoinRuleSettings.tsx @@ -14,7 +14,6 @@ import StyledRadioGroup, { type IDefinition } from "../elements/StyledRadioGroup import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; import RoomAvatar from "../avatars/RoomAvatar"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import Modal from "../../../Modal"; import ManageRestrictedJoinRuleDialog from "../dialogs/ManageRestrictedJoinRuleDialog"; import RoomUpgradeWarningDialog, { type IFinishedOpts } from "../dialogs/RoomUpgradeWarningDialog"; @@ -28,6 +27,7 @@ import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPaylo import { doesRoomVersionSupport, PreferredRoomVersions } from "../../../utils/PreferredRoomVersions"; import SettingsStore from "../../../settings/SettingsStore"; import LabelledCheckbox from "../elements/LabelledCheckbox"; +import { SDKContextClass } from "../../../contexts/SDKContextClass.ts"; export interface JoinRuleSettingsProps { room: Room; @@ -92,8 +92,8 @@ const JoinRuleSettings: React.FC = ({ const editRestrictedRoomIds = async (): Promise => { let selected = restrictedAllowRoomIds; - if (!selected?.length && SpaceStore.instance.activeSpaceRoom) { - selected = [SpaceStore.instance.activeSpaceRoom.roomId]; + if (!selected?.length && SDKContextClass.instance.spaceStore.activeSpaceRoom) { + selected = [SDKContextClass.instance.spaceStore.activeSpaceRoom.roomId]; } const { finished } = Modal.createDialog( @@ -296,12 +296,12 @@ const JoinRuleSettings: React.FC = ({ ); - } else if (SpaceStore.instance.activeSpaceRoom) { + } else if (SDKContextClass.instance.spaceStore.activeSpaceRoom) { description = _t( "room_settings|security|join_rule_restricted_description_active_space", {}, { - spaceName: () => {SpaceStore.instance.activeSpaceRoom!.name}, + spaceName: () => {SDKContextClass.instance.spaceStore.activeSpaceRoom!.name}, }, ); } else { @@ -365,9 +365,9 @@ const JoinRuleSettings: React.FC = ({ let warning: JSX.Element | undefined; const userId = cli.getUserId()!; - const unableToUpdateSomeParents = Array.from(SpaceStore.instance.getKnownParents(room.roomId)).some( - (roomId) => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId), - ); + const unableToUpdateSomeParents = Array.from( + SDKContextClass.instance.spaceStore.getKnownParents(room.roomId), + ).some((roomId) => !cli.getRoom(roomId)?.currentState.maySendStateEvent(EventType.SpaceChild, userId)); if (unableToUpdateSomeParents) { warning = {_t("room_settings|security|join_rule_restricted_upgrade_warning")}; } diff --git a/apps/web/src/components/views/spaces/SpacePanel.tsx b/apps/web/src/components/views/spaces/SpacePanel.tsx index 03023c5d49..cf21d26404 100644 --- a/apps/web/src/components/views/spaces/SpacePanel.tsx +++ b/apps/web/src/components/views/spaces/SpacePanel.tsx @@ -38,7 +38,6 @@ import { useContextMenu } from "../../structures/ContextMenu"; import SpaceCreateMenu from "./SpaceCreateMenu"; import { SpaceButton, SpaceItem } from "./SpaceTreeLevel"; import { useEventEmitter, useEventEmitterState } from "../../../hooks/useEventEmitter"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import { getMetaSpaceName, MetaSpace, @@ -49,10 +48,7 @@ import { UPDATE_TOP_LEVEL_SPACES, } from "../../../stores/spaces"; import { RovingTabIndexProvider } from "../../../accessibility/RovingTabIndex"; -import { - RoomNotificationStateStore, - UPDATE_STATUS_INDICATOR, -} from "../../../stores/notifications/RoomNotificationStateStore"; +import { UPDATE_STATUS_INDICATOR } from "../../../stores/notifications/RoomNotificationStateStore"; import type SpaceContextMenu from "../context_menus/SpaceContextMenu"; import IconizedContextMenu, { IconizedContextMenuCheckbox, @@ -82,18 +78,20 @@ import { useModuleSpacePanelItems } from "../../../modules/ExtrasApi.ts"; import { UserMenuViewModel } from "../../../viewmodels/menus/UserMenuViewModel.ts"; import { SDKContext } from "../../../contexts/SDKContext.ts"; import { OwnProfileStore } from "../../../stores/OwnProfileStore.ts"; +import { type SDKContextClass } from "../../../contexts/SDKContextClass.ts"; const useSpaces = (): [Room[], MetaSpace[], Room[], SpaceKey] => { - const invites = useEventEmitterState(SpaceStore.instance, UPDATE_INVITED_SPACES, () => { - return SpaceStore.instance.invitedSpaces; + const sdkContext = useContext(SDKContext); + const invites = useEventEmitterState(sdkContext.spaceStore, UPDATE_INVITED_SPACES, () => { + return sdkContext.spaceStore.invitedSpaces; }); const [metaSpaces, actualSpaces] = useEventEmitterState<[MetaSpace[], Room[]]>( - SpaceStore.instance, + sdkContext.spaceStore, UPDATE_TOP_LEVEL_SPACES, - () => [SpaceStore.instance.enabledMetaSpaces, SpaceStore.instance.spacePanelSpaces], + () => [sdkContext.spaceStore.enabledMetaSpaces, sdkContext.spaceStore.spacePanelSpaces], ); - const activeSpace = useEventEmitterState(SpaceStore.instance, UPDATE_SELECTED_SPACE, () => { - return SpaceStore.instance.activeSpace; + const activeSpace = useEventEmitterState(sdkContext.spaceStore, UPDATE_SELECTED_SPACE, () => { + return sdkContext.spaceStore.activeSpace; }); return [invites, metaSpaces, actualSpaces, activeSpace]; }; @@ -144,22 +142,23 @@ const MetaSpaceButton: React.FC = ({ selected, isPanelCol ); }; -const getHomeNotificationState = (): NotificationState => { - return SpaceStore.instance.allRoomsInHome - ? RoomNotificationStateStore.instance.globalState - : SpaceStore.instance.getNotificationState(MetaSpace.Home); +const getHomeNotificationState = (sdkContext: SDKContextClass): NotificationState => { + return sdkContext.spaceStore.allRoomsInHome + ? sdkContext.roomNotificationStateStore.globalState + : sdkContext.spaceStore.getNotificationState(MetaSpace.Home); }; const HomeButton: React.FC = ({ selected, isPanelCollapsed }) => { - const allRoomsInHome = useEventEmitterState(SpaceStore.instance, UPDATE_HOME_BEHAVIOUR, () => { - return SpaceStore.instance.allRoomsInHome; + const sdkContext = useContext(SDKContext); + const allRoomsInHome = useEventEmitterState(sdkContext.spaceStore, UPDATE_HOME_BEHAVIOUR, () => { + return sdkContext.spaceStore.allRoomsInHome; }); - const [notificationState, setNotificationState] = useState(getHomeNotificationState()); + const [notificationState, setNotificationState] = useState(getHomeNotificationState(sdkContext)); const updateNotificationState = useCallback(() => { - setNotificationState(getHomeNotificationState()); - }, []); + setNotificationState(getHomeNotificationState(sdkContext)); + }, [sdkContext]); useEffect(updateNotificationState, [updateNotificationState, allRoomsInHome]); - useEventEmitter(RoomNotificationStateStore.instance, UPDATE_STATUS_INDICATOR, updateNotificationState); + useEventEmitter(sdkContext.roomNotificationStateStore, UPDATE_STATUS_INDICATOR, updateNotificationState); return ( = ({ selected, isPanelCollapsed }; const OrphansButton: React.FC = ({ selected, isPanelCollapsed }) => { + const sdkContext = useContext(SDKContext); return ( } /> @@ -191,13 +191,14 @@ const OrphansButton: React.FC = ({ selected, isPanelCollap }; const VideoRoomsButton: React.FC = ({ selected, isPanelCollapsed }) => { + const sdkContext = useContext(SDKContext); return ( } /> @@ -271,6 +272,7 @@ interface IInnerSpacePanelProps extends DroppableProvidedProps { // Optimisation based on https://github.com/atlassian/react-beautiful-dnd/blob/master/docs/api/droppable.md#recommended-droppable--performance-optimisation const InnerSpacePanel = React.memo( ({ children, isPanelCollapsed, setPanelCollapsed, isDraggingOver, innerRef, ...props }) => { + const sdkContext = useContext(SDKContext); const [invites, metaSpaces, actualSpaces, activeSpace] = useSpaces(); const activeSpaces = activeSpace ? [activeSpace] : []; @@ -342,7 +344,7 @@ const InnerSpacePanel = React.memo( size="32px" selected={activeSpace === item.spaceKey} onClick={() => { - SpaceStore.instance.setActiveSpace(item.spaceKey); + sdkContext.spaceStore.setActiveSpace(item.spaceKey); item.onSelected?.(); }} /> @@ -403,7 +405,7 @@ const SpacePanel: React.FC = () => { onDragEnd={(result) => { setDragging(false); if (!result.destination) return; // dropped outside the list - SpaceStore.instance.moveRootSpace(result.source.index, result.destination.index); + sdkContext.spaceStore.moveRootSpace(result.source.index, result.destination.index); onDragEndHandler(); }} > diff --git a/apps/web/src/components/views/spaces/SpaceTreeLevel.tsx b/apps/web/src/components/views/spaces/SpaceTreeLevel.tsx index 94ba7cff3e..8f44627b37 100644 --- a/apps/web/src/components/views/spaces/SpaceTreeLevel.tsx +++ b/apps/web/src/components/views/spaces/SpaceTreeLevel.tsx @@ -15,6 +15,7 @@ import React, { type InputHTMLAttributes, type LegacyRef, type RefObject, + useContext, } from "react"; import classNames from "classnames"; import { type Room, RoomEvent } from "matrix-js-sdk/src/matrix"; @@ -27,7 +28,6 @@ import { } from "@vector-im/compound-design-tokens/assets/web/icons"; import RoomAvatar from "../avatars/RoomAvatar"; -import SpaceStore from "../../../stores/spaces/SpaceStore"; import { type SpaceKey } from "../../../stores/spaces"; import SpaceTreeLevelLayoutStore from "../../../stores/spaces/SpaceTreeLevelLayoutStore"; import NotificationBadge from "../rooms/NotificationBadge"; @@ -47,6 +47,7 @@ import { type NotificationState } from "../../../stores/notifications/Notificati import SpaceContextMenu from "../context_menus/SpaceContextMenu"; import { useRovingTabIndex } from "../../../accessibility/RovingTabIndex"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; type ButtonProps = Omit< AccessibleButtonProps, @@ -83,6 +84,7 @@ export const SpaceButton = ({ ContextMenuComponent, ...props }: ButtonProps): JSX.Element => { + const sdkContext = useContext(SDKContext); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(innerRef); const [onFocus, isActive, ref] = useRovingTabIndex(handle); const tabIndex = isActive ? 0 : -1; @@ -108,7 +110,7 @@ export const SpaceButton = ({ const jumpToNotification = (ev: MouseEvent): void => { ev.stopPropagation(); ev.preventDefault(); - SpaceStore.instance.setActiveRoomInSpace(spaceKey); + sdkContext.spaceStore.setActiveRoomInSpace(spaceKey); }; notifBadge = ( @@ -139,7 +141,7 @@ export const SpaceButton = ({ // space is set here because of the assignment condition of onClick defaultDispatcher.dispatch({ action: Action.ViewRoom, room_id: space!.roomId }); const activateSpace = (): void => { - if (spaceKey) SpaceStore.instance.setActiveSpace(spaceKey); + if (spaceKey) sdkContext.spaceStore.setActiveSpace(spaceKey); }; const onClick = props.onClick ?? (selected && space ? viewSpaceHome : activateSpace); @@ -203,10 +205,13 @@ interface IItemState { } export class SpaceItem extends React.PureComponent { + public static contextType = SDKContext; + declare public context: React.ContextType; + private buttonRef = createRef(); - public constructor(props: IItemProps) { - super(props); + public constructor(props: IItemProps, context: React.ContextType) { + super(props, context); const collapsed = SpaceTreeLevelLayoutStore.instance.getSpaceCollapsedState( props.space.roomId, @@ -222,12 +227,12 @@ export class SpaceItem extends React.PureComponent { } public componentDidMount(): void { - SpaceStore.instance.on(this.props.space.roomId, this.onSpaceUpdate); + this.context.spaceStore.on(this.props.space.roomId, this.onSpaceUpdate); this.props.space.on(RoomEvent.Name, this.onRoomNameChange); } public componentWillUnmount(): void { - SpaceStore.instance.off(this.props.space.roomId, this.onSpaceUpdate); + this.context.spaceStore.off(this.props.space.roomId, this.onSpaceUpdate); this.props.space.off(RoomEvent.Name, this.onRoomNameChange); } @@ -244,7 +249,7 @@ export class SpaceItem extends React.PureComponent { }; private get childSpaces(): Room[] { - return SpaceStore.instance + return this.context.spaceStore .getChildSpaces(this.props.space.roomId) .filter((s) => !this.props.parents?.has(s.roomId)); } @@ -334,7 +339,7 @@ export class SpaceItem extends React.PureComponent { const notificationState = isInvite ? StaticNotificationState.forSymbol("!", NotificationLevel.Highlight) - : SpaceStore.instance.getNotificationState(space.roomId); + : this.context.spaceStore.getNotificationState(space.roomId); const hasChildren = this.state.childSpaces?.length; diff --git a/apps/web/src/contexts/SDKContextClass.ts b/apps/web/src/contexts/SDKContextClass.ts index 115ccfa239..7670d2dfbb 100644 --- a/apps/web/src/contexts/SDKContextClass.ts +++ b/apps/web/src/contexts/SDKContextClass.ts @@ -17,7 +17,7 @@ import { MemberListStore } from "../stores/MemberListStore"; import { RoomNotificationStateStore } from "../stores/notifications/RoomNotificationStateStore"; import RightPanelStore from "../stores/right-panel/RightPanelStore"; import { RoomViewStore } from "../stores/RoomViewStore"; -import SpaceStore, { type SpaceStoreClass } from "../stores/spaces/SpaceStore"; +import SpaceStore from "../stores/spaces/SpaceStore"; import TypingStore from "../stores/TypingStore"; import { UserProfilesStore } from "../stores/UserProfilesStore"; import { WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore"; @@ -65,7 +65,7 @@ export class SDKContextClass { protected _WidgetStore?: WidgetStore; protected _PosthogAnalytics?: PosthogAnalytics; protected _SlidingSyncManager?: SlidingSyncManager; - protected _SpaceStore?: SpaceStoreClass; + protected _SpaceStore?: SpaceStore; protected _LegacyCallHandler?: LegacyCallHandler; protected _TypingStore?: TypingStore; protected _UserProfilesStore?: UserProfilesStore; @@ -153,9 +153,10 @@ export class SDKContextClass { } return this._SlidingSyncManager; } - public get spaceStore(): SpaceStoreClass { + public get spaceStore(): SpaceStore { if (!this._SpaceStore) { - this._SpaceStore = SpaceStore.instance; + this._SpaceStore = new SpaceStore(defaultDispatcher, this); + this._SpaceStore.start(); } return this._SpaceStore; } diff --git a/apps/web/src/createRoom.ts b/apps/web/src/createRoom.ts index fc31c7db54..0d28834e7f 100644 --- a/apps/web/src/createRoom.ts +++ b/apps/web/src/createRoom.ts @@ -33,7 +33,6 @@ import { _t, UserFriendlyError } from "./languageHandler"; import dis from "./dispatcher/dispatcher"; import * as Rooms from "./Rooms"; import { getAddressType } from "./UserAddress"; -import SpaceStore from "./stores/spaces/SpaceStore"; import { makeSpaceParentEvent } from "./utils/space"; import { JitsiCall, ElementCall } from "./models/Call"; import { Action } from "./dispatcher/actions"; @@ -48,6 +47,7 @@ import { doesRoomVersionSupport, PreferredRoomVersions } from "./utils/Preferred import { MEGOLM_ENCRYPTION_ALGORITHM } from "./utils/crypto"; import { ElementCallMemberEventType } from "./call-types"; import { htmlSerializeFromMdIfNeeded } from "./editor/serialize"; +import { SDKContextClass } from "./contexts/SDKContextClass.ts"; import SdkConfig from "./SdkConfig"; // we define a number of interfaces which take their names from the js-sdk @@ -384,7 +384,7 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro }) .then(() => { if (opts.parentSpace) { - return SpaceStore.instance.addRoomToSpace( + return SDKContextClass.instance.spaceStore.addRoomToSpace( opts.parentSpace, roomId, [client.getDomain()!], diff --git a/apps/web/src/stores/AsyncStore.ts b/apps/web/src/stores/AsyncStore.ts index 8d97714cf1..b6aeb41368 100644 --- a/apps/web/src/stores/AsyncStore.ts +++ b/apps/web/src/stores/AsyncStore.ts @@ -45,7 +45,7 @@ export abstract class AsyncStore extends EventEmitter { * @param {T} initialState The initial state for the store. */ protected constructor( - private dispatcher: MatrixDispatcher, + protected dispatcher: MatrixDispatcher, initialState: T = {}, ) { super(); diff --git a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts index 200abab9be..00c14be075 100644 --- a/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts +++ b/apps/web/src/stores/room-list-v3/RoomListStoreV3.ts @@ -19,7 +19,6 @@ import { RecencySorter } from "./skip-list/sorters/RecencySorter"; import { AlphabeticSorter } from "./skip-list/sorters/AlphabeticSorter"; import { readReceiptChangeIsFor } from "../../utils/read-receipts"; import { EffectiveMembership, getEffectiveMembership, getEffectiveMembershipTag } from "../../utils/membership"; -import SpaceStore from "../spaces/SpaceStore"; import { type SpaceKey, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../spaces"; import { FavouriteFilter } from "./skip-list/filters/FavouriteFilter"; import { UnreadFilter } from "./skip-list/filters/UnreadFilter"; @@ -49,6 +48,7 @@ import { reorderSection, } from "./section"; import { DefaultTagID, type TagID } from "./skip-list/tag"; +import { SDKContextClass } from "../../contexts/SDKContextClass.ts"; /** * These are the filters passed to the room skip list. @@ -131,10 +131,10 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { public constructor(dispatcher: MatrixDispatcher) { super(dispatcher); this.msc3946ProcessDynamicPredecessor = SettingsStore.getValue("feature_dynamic_room_predecessors"); - SpaceStore.instance.on(UPDATE_SELECTED_SPACE, () => { + SDKContextClass.instance.spaceStore.on(UPDATE_SELECTED_SPACE, () => { this.onActiveSpaceChanged(); }); - SpaceStore.instance.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged()); + SDKContextClass.instance.spaceStore.on(UPDATE_HOME_BEHAVIOUR, () => this.onActiveSpaceChanged()); SettingsStore.watchSetting("RoomList.OrderedCustomSections", null, () => this.onOrderedCustomSectionsChange()); this.loadCustomSections(); @@ -173,7 +173,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 = SpaceStore.instance.activeSpace; + const spaceId = SDKContextClass.instance.spaceStore.activeSpace; const areSectionsEnabled = SettingsStore.getValue("RoomList.showSections"); const sections = areSectionsEnabled @@ -238,7 +238,7 @@ export class RoomListStoreV3Class extends AsyncStoreWithClient { this.roomSkipList = new RoomSkipList(sorter, this.getSkipListFilters()); - await SpaceStore.instance.storeReadyPromise; + await SDKContextClass.instance.spaceStore.storeReadyPromise; const rooms = this.getRooms(); this.roomSkipList.seed(rooms); this.emit(LISTS_LOADED_EVENT); @@ -522,7 +522,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(SpaceStore.instance.activeSpace); + const tag = await createSection(SDKContextClass.instance.spaceStore.activeSpace); if (!tag) return; this.emit(SECTION_CREATED_EVENT, tag); return tag; diff --git a/apps/web/src/stores/room-list-v3/section.ts b/apps/web/src/stores/room-list-v3/section.ts index 09aebf3c96..a8e09e3248 100644 --- a/apps/web/src/stores/room-list-v3/section.ts +++ b/apps/web/src/stores/room-list-v3/section.ts @@ -14,7 +14,7 @@ import { CreateSectionDialog } from "../../components/views/dialogs/CreateSectio import { RemoveSectionDialog } from "../../components/views/dialogs/RemoveSectionDialog"; import { DefaultTagID, type TagID } from "./skip-list/tag"; import { isMetaSpace, MetaSpace, type SpaceKey } from "../spaces"; -import SpaceStore from "../spaces/SpaceStore"; +import { SDKContextClass } from "../../contexts/SDKContextClass.ts"; /** * A synthetic tag used to represent the "Chats" section, which contains @@ -105,8 +105,8 @@ function isReorderableSection(tag: string, customData: CustomSectionsData): tag * Returns true if the given space key corresponds to an enabled meta-space or a known top-level space room. */ function doesSpaceExist(spaceId: SpaceKey): boolean { - if (isMetaSpace(spaceId)) return SpaceStore.instance.enabledMetaSpaces.includes(spaceId); - return SpaceStore.instance.spacePanelSpaces.some((room) => room.roomId === spaceId); + if (isMetaSpace(spaceId)) return SDKContextClass.instance.spaceStore.enabledMetaSpaces.includes(spaceId); + return SDKContextClass.instance.spaceStore.spacePanelSpaces.some((room) => room.roomId === spaceId); } /** diff --git a/apps/web/src/stores/room-list-v3/skip-list/RoomNode.ts b/apps/web/src/stores/room-list-v3/skip-list/RoomNode.ts index 6fbcb65588..3e44d21d4a 100644 --- a/apps/web/src/stores/room-list-v3/skip-list/RoomNode.ts +++ b/apps/web/src/stores/room-list-v3/skip-list/RoomNode.ts @@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details. import type { Room } from "matrix-js-sdk/src/matrix"; import type { Filter, FilterKey } from "./filters"; -import SpaceStore from "../../spaces/SpaceStore"; +import { SDKContextClass } from "../../../contexts/SDKContextClass.ts"; /** * Room skip list stores room nodes. @@ -34,7 +34,7 @@ export class RoomNode { /** * Whether the room associated with this room node belongs to * the currently active space. - * @see {@link SpaceStoreClass#activeSpace} to understand what active + * @see {@link SpaceStore#activeSpace} to understand what active * space means. */ public get isInActiveSpace(): boolean { @@ -46,8 +46,8 @@ export class RoomNode { * in {@link RoomNode#isInActiveSpace}. */ public checkIfRoomBelongsToActiveSpace(): void { - const activeSpace = SpaceStore.instance.activeSpace; - this._isInActiveSpace = SpaceStore.instance.isRoomInSpace(activeSpace, this.room.roomId); + const activeSpace = SDKContextClass.instance.spaceStore.activeSpace; + this._isInActiveSpace = SDKContextClass.instance.spaceStore.isRoomInSpace(activeSpace, this.room.roomId); } /** diff --git a/apps/web/src/stores/spaces/SpaceStore.test.ts b/apps/web/src/stores/spaces/SpaceStore.test.ts index 94f4b751db..148a000755 100644 --- a/apps/web/src/stores/spaces/SpaceStore.test.ts +++ b/apps/web/src/stores/spaces/SpaceStore.test.ts @@ -23,7 +23,7 @@ import { } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import * as testUtils from "test-utils"; -import { mkEvent, stubClient } from "test-utils"; +import { mkEvent, setupAsyncStoreWithClient, stubClient } from "test-utils"; import SpaceStore from "./SpaceStore"; import { @@ -44,6 +44,7 @@ import { DefaultTagID } from "../room-list-v3/skip-list/tag"; import { RoomNotificationStateStore } from "../notifications/RoomNotificationStateStore"; import { NotificationLevel } from "../notifications/NotificationLevel"; import { storeRoomAliasInCache } from "../../RoomAliasCache.ts"; +import { TestSDKContext } from "../../../test/unit-tests/TestSDKContext.ts"; vi.useFakeTimers(); @@ -100,8 +101,10 @@ DMRoomMap.sharedInstance = { getUserIdForRoomId, getDMRoomsForUserId }; describe("SpaceStore", () => { stubClient(); - const store = SpaceStore.instance; const client = MatrixClientPeg.safeGet(); + const sdkContext = new TestSDKContext(); + sdkContext._client = client; + const store = sdkContext.spaceStore; const spyDispatcher = vi.spyOn(defaultDispatcher, "dispatch"); @@ -1231,39 +1234,39 @@ describe("SpaceStore", () => { rootSpace.getMyMembership.mockReturnValue(KnownMembership.Invite); client.emit(ClientEvent.Room, rootSpace); vi.runOnlyPendingTimers(); - expect(SpaceStore.instance.invitedSpaces).toStrictEqual([rootSpace]); - expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([]); + expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([rootSpace]); + expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([]); // accept invite to space rootSpace.getMyMembership.mockReturnValue(KnownMembership.Join); client.emit(RoomEvent.MyMembership, rootSpace, KnownMembership.Join, KnownMembership.Invite); vi.runOnlyPendingTimers(); - expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); - expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]); + expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]); + expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([rootSpace]); // join room in space - expect(SpaceStore.instance.isRoomInSpace(space1, room1)).toBeFalsy(); + expect(sdkContext.spaceStore.isRoomInSpace(space1, room1)).toBeFalsy(); const rootSpaceRoom1 = mkRoom(room1); rootSpaceRoom1.getMyMembership.mockReturnValue(KnownMembership.Join); client.emit(ClientEvent.Room, rootSpaceRoom1); vi.runOnlyPendingTimers(); - expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); - expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]); - expect(SpaceStore.instance.isRoomInSpace(space1, room1)).toBeTruthy(); - expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy(); - expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, room1)).toBeFalsy(); + expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]); + expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([rootSpace]); + expect(sdkContext.spaceStore.isRoomInSpace(space1, room1)).toBeTruthy(); + expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Home, room1)).toBeFalsy(); + expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Orphans, room1)).toBeFalsy(); // receive room invite - expect(SpaceStore.instance.isRoomInSpace(space1, room2)).toBeFalsy(); + expect(sdkContext.spaceStore.isRoomInSpace(space1, room2)).toBeFalsy(); const rootSpaceRoom2 = mkRoom(room2); rootSpaceRoom2.getMyMembership.mockReturnValue(KnownMembership.Invite); client.emit(ClientEvent.Room, rootSpaceRoom2); vi.runOnlyPendingTimers(); - expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); - expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]); - expect(SpaceStore.instance.isRoomInSpace(space1, room2)).toBeTruthy(); - expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, room2)).toBeTruthy(); - expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, room2)).toBeFalsy(); + expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]); + expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([rootSpace]); + expect(sdkContext.spaceStore.isRoomInSpace(space1, room2)).toBeTruthy(); + expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Home, room2)).toBeTruthy(); + expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Orphans, room2)).toBeFalsy(); // start DM in space const myRootSpaceMember = new RoomMember(space1, testUserId); @@ -1280,7 +1283,7 @@ describe("SpaceStore", () => { } return null; }); - expect(SpaceStore.instance.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeFalsy(); + expect(sdkContext.spaceStore.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeFalsy(); const memberEvent = mkEvent({ event: true, type: EventType.RoomMember, @@ -1293,25 +1296,25 @@ describe("SpaceStore", () => { }); client.emit(RoomStateEvent.Members, memberEvent, rootSpace.currentState, dm1Partner); vi.runOnlyPendingTimers(); - expect(SpaceStore.instance.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeTruthy(); + expect(sdkContext.spaceStore.getSpaceFilteredUserIds(space1)!.has(dm1Partner.userId)).toBeTruthy(); const dm1Room = mkRoom(dm1); dm1Room.getMyMembership.mockReturnValue(KnownMembership.Join); client.emit(ClientEvent.Room, dm1Room); vi.runOnlyPendingTimers(); - expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); - expect(SpaceStore.instance.spacePanelSpaces).toStrictEqual([rootSpace]); - expect(SpaceStore.instance.isRoomInSpace(space1, dm1)).toBeTruthy(); - expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy(); - expect(SpaceStore.instance.isRoomInSpace(MetaSpace.Orphans, dm1)).toBeFalsy(); + expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]); + expect(sdkContext.spaceStore.spacePanelSpaces).toStrictEqual([rootSpace]); + expect(sdkContext.spaceStore.isRoomInSpace(space1, dm1)).toBeTruthy(); + expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Home, dm1)).toBeTruthy(); + expect(sdkContext.spaceStore.isRoomInSpace(MetaSpace.Orphans, dm1)).toBeFalsy(); // join subspace const subspace = mkSpace(space2); subspace.getMyMembership.mockReturnValue(KnownMembership.Join); - const prom = testUtils.emitPromise(SpaceStore.instance, space1); + const prom = testUtils.emitPromise(sdkContext.spaceStore, space1); client.emit(ClientEvent.Room, subspace); vi.runOnlyPendingTimers(); - expect(SpaceStore.instance.invitedSpaces).toStrictEqual([]); - expect(SpaceStore.instance.spacePanelSpaces.map((r) => r.roomId)).toStrictEqual([rootSpace.roomId]); + expect(sdkContext.spaceStore.invitedSpaces).toStrictEqual([]); + expect(sdkContext.spaceStore.spacePanelSpaces.map((r) => r.roomId)).toStrictEqual([rootSpace.roomId]); await prom; }); @@ -1340,7 +1343,7 @@ describe("SpaceStore", () => { it("Favourites and People meta spaces should not be returned", async () => { await run(); // Favourites and People meta spaces are not part of the meta space order - expect(SpaceStore.instance.enabledMetaSpaces).toStrictEqual([MetaSpace.Home, MetaSpace.Orphans]); + expect(sdkContext.spaceStore.enabledMetaSpaces).toStrictEqual([MetaSpace.Home, MetaSpace.Orphans]); }); describe("when feature_dynamic_room_predecessors is not enabled", () => { @@ -1349,7 +1352,7 @@ describe("SpaceStore", () => { (settingName) => settingName === "Spaces.allRoomsInHome", ); // @ts-ignore calling a private function - SpaceStore.instance.onAction({ + sdkContext.spaceStore.onAction({ action: Action.SettingUpdated, settingName: "feature_dynamic_room_predecessors", roomId: null, @@ -1379,8 +1382,12 @@ describe("SpaceStore", () => { }); it("passes that value in calls to getVisibleRooms during getSpaceFilteredRoomIds", () => { + const sdkContext = new TestSDKContext(); + sdkContext._client = client; // Given a store - const store = SpaceStore.testInstance(); + const store = new SpaceStore(defaultDispatcher, sdkContext); + sdkContext._SpaceStore = store; + setupAsyncStoreWithClient(store, client); // When we ask for filtered room ids store.getSpaceFilteredRoomIds(MetaSpace.Home); @@ -1399,7 +1406,7 @@ describe("SpaceStore", () => { settingName === "Spaces.allRoomsInHome" || settingName === "feature_dynamic_room_predecessors", ); // @ts-ignore calling a private function - SpaceStore.instance.onAction({ + sdkContext.spaceStore.onAction({ action: Action.SettingUpdated, settingName: "feature_dynamic_room_predecessors", roomId: null, @@ -1429,8 +1436,13 @@ describe("SpaceStore", () => { }); it("passes that value in calls to getVisibleRooms during getSpaceFilteredRoomIds", () => { + const sdkContext = new TestSDKContext(); + sdkContext._client = client; // Given a store - const store = SpaceStore.testInstance(); + const store = new SpaceStore(defaultDispatcher, sdkContext); + sdkContext._SpaceStore = store; + setupAsyncStoreWithClient(store, client); + // When we ask for filtered room ids store.getSpaceFilteredRoomIds(MetaSpace.Home); diff --git a/apps/web/src/stores/spaces/SpaceStore.ts b/apps/web/src/stores/spaces/SpaceStore.ts index 88d65a2d4b..6976d2f265 100644 --- a/apps/web/src/stores/spaces/SpaceStore.ts +++ b/apps/web/src/stores/spaces/SpaceStore.ts @@ -23,7 +23,7 @@ import { KnownMembership } from "matrix-js-sdk/src/types"; import { logger } from "matrix-js-sdk/src/logger"; import { AsyncStoreWithClient } from "../AsyncStoreWithClient"; -import defaultDispatcher from "../../dispatcher/dispatcher"; +import { type MatrixDispatcher } from "../../dispatcher/dispatcher.ts"; import RoomListStoreV3 from "../room-list-v3/RoomListStoreV3"; import SettingsStore from "../../settings/SettingsStore"; import DMRoomMap from "../../utils/DMRoomMap"; @@ -59,7 +59,7 @@ import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload" import { type ViewHomePagePayload } from "../../dispatcher/payloads/ViewHomePagePayload"; import { type SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload"; import { type AfterLeaveRoomPayload } from "../../dispatcher/payloads/AfterLeaveRoomPayload"; -import { SDKContextClass } from "../../contexts/SDKContextClass"; +import { type SDKContextClass } from "../../contexts/SDKContextClass"; import { ModuleApi } from "../../modules/Api.ts"; const ACTIVE_SPACE_LS_KEY = "mx_active_space"; @@ -110,7 +110,7 @@ type SpaceStoreActions = | SwitchSpacePayload | AfterLeaveRoomPayload; -export class SpaceStoreClass extends AsyncStoreWithClient { +export default class SpaceStore extends AsyncStoreWithClient { // The spaces representing the roots of the various tree-like hierarchies private rootSpaces: Room[] = []; // Map from room/space ID to set of spaces which list it as a child @@ -142,8 +142,11 @@ export class SpaceStoreClass extends AsyncStoreWithClient { private _msc3946ProcessDynamicPredecessor: boolean = SettingsStore.getValue("feature_dynamic_room_predecessors"); private _storeReadyDeferred = Promise.withResolvers(); - public constructor() { - super(defaultDispatcher, {}); + public constructor( + dispatcher: MatrixDispatcher, + private readonly sdkContext: SDKContextClass, + ) { + super(dispatcher, {}); SettingsStore.monitorSetting("Spaces.allRoomsInHome", null); SettingsStore.monitorSetting("Spaces.enabledMetaSpaces", null); @@ -208,7 +211,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } if (!!roomId) { - defaultDispatcher.dispatch({ + this.dispatcher.dispatch({ action: Action.ViewRoom, room_id: roomId, context_switch: true, @@ -252,14 +255,14 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.matrixClient.getRoom(roomId)?.getMyMembership() === KnownMembership.Join && this.isRoomInSpace(space, roomId) ) { - defaultDispatcher.dispatch({ + this.dispatcher.dispatch({ action: Action.ViewRoom, room_id: roomId, context_switch: true, metricsTrigger: "WebSpaceContextSwitch", }); } else if (cliSpace) { - defaultDispatcher.dispatch({ + this.dispatcher.dispatch({ action: Action.ViewRoom, room_id: space, context_switch: true, @@ -268,7 +271,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { } else if (ModuleApi.instance.extras.spacePanelItems.has(space)) { // module will handle this } else { - defaultDispatcher.dispatch({ + this.dispatcher.dispatch({ action: Action.ViewHomePage, context_switch: true, }); @@ -283,7 +286,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Load all members for the selected space and its subspaces, // so we can correctly show DMs we have with members of this space. - SpaceStore.instance.traverseSpace( + this.traverseSpace( space, (roomId) => { this.matrixClient?.getRoom(roomId)?.loadMembersIfNeeded(); @@ -718,7 +721,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // Method for resolving the impact of a single user's membership change in the given Space and its hierarchy private onMemberUpdate = (space: Room, userId: string): void => { - const inSpace = SpaceStoreClass.isInSpace(space.getMember(userId)); + const inSpace = SpaceStore.isInSpace(space.getMember(userId)); if (inSpace) { this.userIdsBySpace.get(space.roomId)?.add(userId); @@ -867,7 +870,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.updateNotificationStates([...changeSet]); }; - private switchSpaceIfNeeded = (roomId = SDKContextClass.instance.roomViewStore.getRoomId()): void => { + private switchSpaceIfNeeded = (roomId = this.sdkContext.roomViewStore.getRoomId()): void => { if (!roomId) return; if (!this.isRoomInSpace(this.activeSpace, roomId) && !this.matrixClient?.getRoom(roomId)?.isSpaceRoom()) { this.switchToRelatedSpace(roomId); @@ -923,7 +926,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { // if the room currently being viewed was just joined then switch to its related space if ( newMembership === KnownMembership.Join && - room.roomId === SDKContextClass.instance.roomViewStore.getRoomId() + room.roomId === this.sdkContext.roomViewStore.getRoomId() ) { this.switchSpaceIfNeeded(room.roomId); } @@ -951,7 +954,7 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.emit(room.roomId); } - if (membership === KnownMembership.Join && room.roomId === SDKContextClass.instance.roomViewStore.getRoomId()) { + if (membership === KnownMembership.Join && room.roomId === this.sdkContext.roomViewStore.getRoomId()) { // if the user was looking at the space and then joined: select that space this.setActiveSpace(room.roomId, false); } else if (membership === KnownMembership.Leave && room.roomId === this.activeSpace) { @@ -1349,24 +1352,3 @@ export class SpaceStoreClass extends AsyncStoreWithClient { this.notifyIfOrderChanged(); } } - -export default class SpaceStore { - private static readonly internalInstance = (() => { - const instance = new SpaceStoreClass(); - instance.start(); - return instance; - })(); - - public static get instance(): SpaceStoreClass { - return SpaceStore.internalInstance; - } - - /** - * @internal for test only - */ - public static testInstance(): SpaceStoreClass { - const store = new SpaceStoreClass(); - store.start(); - return store; - } -} diff --git a/apps/web/src/utils/RoomUpgrade.ts b/apps/web/src/utils/RoomUpgrade.ts index 3bbdcb64bf..7bed4b68a4 100644 --- a/apps/web/src/utils/RoomUpgrade.ts +++ b/apps/web/src/utils/RoomUpgrade.ts @@ -14,9 +14,9 @@ import { showAnyInviteErrors } from "../RoomInvite"; import Modal, { type IHandle } from "../Modal"; import { _t } from "../languageHandler"; import ErrorDialog from "../components/views/dialogs/ErrorDialog"; -import SpaceStore from "../stores/spaces/SpaceStore"; import Spinner from "../components/views/elements/Spinner"; import MultiInviter, { type MultiInviterOptions } from "./MultiInviter"; +import { SDKContextClass } from "../contexts/SDKContextClass.ts"; export interface RoomUpgradeProgress { roomUpgraded: boolean; @@ -73,7 +73,7 @@ export async function upgradeRoom( let parentsToRelink: Room[] = []; if (updateSpaces) { - parentsToRelink = Array.from(SpaceStore.instance.getKnownParents(room.roomId)) + parentsToRelink = Array.from(SDKContextClass.instance.spaceStore.getKnownParents(room.roomId)) .map((roomId) => cli.getRoom(roomId)) .filter((parent) => parent?.currentState.maySendStateEvent(EventType.SpaceChild, cli.getUserId()!), diff --git a/apps/web/src/utils/i18n-helpers.ts b/apps/web/src/utils/i18n-helpers.ts index 408ec269e5..2b61b29d74 100644 --- a/apps/web/src/utils/i18n-helpers.ts +++ b/apps/web/src/utils/i18n-helpers.ts @@ -8,10 +8,10 @@ Please see LICENSE files in the repository root for full details. import { type Room } from "matrix-js-sdk/src/matrix"; -import SpaceStore from "../stores/spaces/SpaceStore"; import { _t } from "../languageHandler"; import DMRoomMap from "./DMRoomMap"; import { formatList } from "./FormattingUtils"; +import { SDKContextClass } from "../contexts/SDKContextClass.ts"; export interface RoomContextDetails { details: string | null; @@ -26,7 +26,7 @@ export function roomContextDetails(room: Room): RoomContextDetails | null { return { details: dmPartner }; } - const [parent, secondParent, ...otherParents] = SpaceStore.instance.getKnownParents(room.roomId); + const [parent, secondParent, ...otherParents] = SDKContextClass.instance.spaceStore.getKnownParents(room.roomId); if (secondParent && !otherParents?.length) { // exactly 2 edge case for improved i18n const space1Name = room.client.getRoom(parent)?.name; diff --git a/apps/web/src/utils/leave-behaviour.test.ts b/apps/web/src/utils/leave-behaviour.test.ts index ce67c3a298..03070a47be 100644 --- a/apps/web/src/utils/leave-behaviour.test.ts +++ b/apps/web/src/utils/leave-behaviour.test.ts @@ -20,7 +20,6 @@ import { Action } from "../dispatcher/actions"; import { leaveRoomBehaviour } from "./leave-behaviour"; import { SDKContextClass } from "../contexts/SDKContextClass"; import DMRoomMap from "../utils/DMRoomMap"; -import SpaceStore from "../stores/spaces/SpaceStore"; import { MetaSpace } from "../stores/spaces"; import { type ActionPayload } from "../dispatcher/payloads"; import SettingsStore from "../settings/SettingsStore"; @@ -55,12 +54,12 @@ describe("leaveRoomBehaviour", () => { } }); - await setupAsyncStoreWithClient(SpaceStore.instance, client); + await setupAsyncStoreWithClient(SDKContextClass.instance.spaceStore, client); }); afterEach(async () => { - SpaceStore.instance.setActiveSpace(MetaSpace.Home); - await resetAsyncStoreWithClient(SpaceStore.instance); + SDKContextClass.instance.spaceStore.setActiveSpace(MetaSpace.Home); + await resetAsyncStoreWithClient(SDKContextClass.instance.spaceStore); vi.restoreAllMocks(); }); @@ -114,11 +113,11 @@ describe("leaveRoomBehaviour", () => { }); it("returns to the parent space after leaving a room inside of a space that was being viewed", async () => { - vi.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) => + vi.spyOn(SDKContextClass.instance.spaceStore, "getCanonicalParent").mockImplementation((roomId) => roomId === room.roomId ? space : null, ); viewRoom(room); - SpaceStore.instance.setActiveSpace(space.roomId, false); + SDKContextClass.instance.spaceStore.setActiveSpace(space.roomId, false); await leaveRoomBehaviour(client, room.roomId); await expectDispatch({ @@ -130,7 +129,7 @@ describe("leaveRoomBehaviour", () => { it("returns to the home page after leaving a top-level space that was being viewed", async () => { viewRoom(space); - SpaceStore.instance.setActiveSpace(space.roomId, false); + SDKContextClass.instance.spaceStore.setActiveSpace(space.roomId, false); await leaveRoomBehaviour(client, space.roomId); await expectDispatch({ action: Action.ViewHomePage }); @@ -138,11 +137,11 @@ describe("leaveRoomBehaviour", () => { it("returns to the parent space after leaving a subspace that was being viewed", async () => { room.isSpaceRoom.mockReturnValue(true); - vi.spyOn(SpaceStore.instance, "getCanonicalParent").mockImplementation((roomId) => + vi.spyOn(SDKContextClass.instance.spaceStore, "getCanonicalParent").mockImplementation((roomId) => roomId === room.roomId ? space : null, ); viewRoom(room); - SpaceStore.instance.setActiveSpace(room.roomId, false); + SDKContextClass.instance.spaceStore.setActiveSpace(room.roomId, false); await leaveRoomBehaviour(client, room.roomId); await expectDispatch({ diff --git a/apps/web/src/utils/leave-behaviour.ts b/apps/web/src/utils/leave-behaviour.ts index 12c3e81168..ddbcd47623 100644 --- a/apps/web/src/utils/leave-behaviour.ts +++ b/apps/web/src/utils/leave-behaviour.ts @@ -16,7 +16,6 @@ import Spinner from "../components/views/elements/Spinner"; import { _t } from "../languageHandler"; import ErrorDialog from "../components/views/dialogs/ErrorDialog"; import { isMetaSpace } from "../stores/spaces"; -import SpaceStore from "../stores/spaces/SpaceStore"; import dis from "../dispatcher/dispatcher"; import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../dispatcher/actions"; @@ -165,11 +164,11 @@ export async function leaveRoomBehaviour( // accidentally viewing the next room in the list and clearing its // notifications, switch to a neutral ground such as the home page or // space landing page. - if (isMetaSpace(SpaceStore.instance.activeSpace)) { + if (isMetaSpace(SDKContextClass.instance.spaceStore.activeSpace)) { dis.dispatch({ action: Action.ViewHomePage }); - } else if (SpaceStore.instance.activeSpace === roomId) { + } else if (SDKContextClass.instance.spaceStore.activeSpace === roomId) { // View the parent space, if there is one - const parent = SpaceStore.instance.getCanonicalParent(roomId); + const parent = SDKContextClass.instance.spaceStore.getCanonicalParent(roomId); if (parent !== null) { dis.dispatch({ action: Action.ViewRoom, @@ -182,7 +181,7 @@ export async function leaveRoomBehaviour( } else { dis.dispatch({ action: Action.ViewRoom, - room_id: SpaceStore.instance.activeSpace, + room_id: SDKContextClass.instance.spaceStore.activeSpace, metricsTrigger: undefined, // other }); } diff --git a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts index 548aa8f03d..017a16a068 100644 --- a/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListHeaderViewModel.ts @@ -17,7 +17,7 @@ import defaultDispatcher from "../../dispatcher/dispatcher"; import PosthogTrackers from "../../PosthogTrackers"; import { Action } from "../../dispatcher/actions"; import { getMetaSpaceName, type MetaSpace, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../../stores/spaces"; -import { type SpaceStoreClass } from "../../stores/spaces/SpaceStore"; +import type SpaceStore from "../../stores/spaces/SpaceStore"; import { shouldShowSpaceSettings, showCreateNewRoom, @@ -42,7 +42,7 @@ export interface Props { /** * The space store instance. */ - spaceStore: SpaceStoreClass; + spaceStore: SpaceStore; } /** @@ -271,7 +271,7 @@ export class RoomListHeaderViewModel * @param matrixClient - The Matrix client instance. * @returns */ -function getInitialSnapshot(spaceStore: SpaceStoreClass, matrixClient: MatrixClient): RoomListHeaderViewSnapshot { +function getInitialSnapshot(spaceStore: SpaceStore, matrixClient: MatrixClient): RoomListHeaderViewSnapshot { const sortingAlgorithm = SettingsStore.getValue("RoomList.preferredSorting"); let activeSortOption: SortOption; @@ -300,7 +300,7 @@ function getInitialSnapshot(spaceStore: SpaceStoreClass, matrixClient: MatrixCli * Get the header title based on the active space. * @param spaceStore - The space store instance. */ -function getHeaderTitle(spaceStore: SpaceStoreClass): string { +function getHeaderTitle(spaceStore: SpaceStore): string { const activeSpace = spaceStore.activeSpaceRoom; const spaceName = activeSpace?.name; return spaceName ?? getMetaSpaceName(spaceStore.activeSpace as MetaSpace, spaceStore.allRoomsInHome); @@ -321,7 +321,7 @@ function getCanCreateVideoRoom(canCreateRoom: boolean): boolean { * @returns The header space state containing title, permissions, and display flags. */ function computeHeaderSpaceState( - spaceStore: SpaceStoreClass, + spaceStore: SpaceStore, matrixClient: MatrixClient, ): Omit { const displaySectionReleaseAnnouncement = diff --git a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts index a4abe130f7..b1799ec287 100644 --- a/apps/web/src/viewmodels/room-list/RoomListViewModel.ts +++ b/apps/web/src/viewmodels/room-list/RoomListViewModel.ts @@ -22,7 +22,7 @@ import dispatcher from "../../dispatcher/dispatcher"; import { type ViewRoomDeltaPayload } from "../../dispatcher/payloads/ViewRoomDeltaPayload"; import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; import { type RoomListSectionsCollapseStateChangedPayload } from "../../dispatcher/payloads/RoomListSectionsCollapseStateChangedPayload"; -import { type SpaceStoreClass } from "../../stores/spaces/SpaceStore"; +import type SpaceStore from "../../stores/spaces/SpaceStore"; import RoomListStoreV3, { RoomListStoreV3Event, type RoomsResult, @@ -59,7 +59,7 @@ interface StickyRoomPosition { interface RoomListViewModelProps { client: MatrixClient; roomViewStore: RoomViewStore; - spaceStore: SpaceStoreClass; + spaceStore: SpaceStore; } const filterKeyToIdMap: Map = new Map([ diff --git a/apps/web/test/test-utils/test-utils.ts b/apps/web/test/test-utils/test-utils.ts index bc159fcfb0..4066c78370 100644 --- a/apps/web/test/test-utils/test-utils.ts +++ b/apps/web/test/test-utils/test-utils.ts @@ -49,6 +49,7 @@ import { EnhancedMap } from "../../src/utils/maps"; import { type AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient"; import MatrixClientBackedSettingsHandler from "../../src/settings/handlers/MatrixClientBackedSettingsHandler"; import { vi } from "../setup/adapter.ts"; +import { SDKContextClass } from "../../src/contexts/SDKContextClass.ts"; /** * Stub out the MatrixClient, and configure the MatrixClientPeg object to @@ -76,6 +77,8 @@ export function stubClient(): MatrixClient { peg.get = () => client; peg.safeGet = () => client; MatrixClientBackedSettingsHandler.matrixClient = client; + // @ts-ignore + SDKContextClass.instance._client = client; return client; } diff --git a/apps/web/test/unit-tests/TestSDKContext.ts b/apps/web/test/unit-tests/TestSDKContext.ts index 893ea4d7af..0649791965 100644 --- a/apps/web/test/unit-tests/TestSDKContext.ts +++ b/apps/web/test/unit-tests/TestSDKContext.ts @@ -14,7 +14,7 @@ import { type SlidingSyncManager } from "../../src/SlidingSyncManager"; import { type RoomNotificationStateStore } from "../../src/stores/notifications/RoomNotificationStateStore"; import type RightPanelStore from "../../src/stores/right-panel/RightPanelStore"; import { type RoomViewStore } from "../../src/stores/RoomViewStore"; -import { type SpaceStoreClass } from "../../src/stores/spaces/SpaceStore"; +import type SpaceStore from "../../src/stores/spaces/SpaceStore"; import { type WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore"; import { type WidgetPermissionStore } from "../../src/stores/widgets/WidgetPermissionStore"; import type WidgetStore from "../../src/stores/WidgetStore"; @@ -34,7 +34,7 @@ export class TestSDKContext extends SDKContextClass { declare public _WidgetStore?: WidgetStore; declare public _PosthogAnalytics?: PosthogAnalytics; declare public _SlidingSyncManager?: SlidingSyncManager; - declare public _SpaceStore?: SpaceStoreClass; + declare public _SpaceStore?: SpaceStore; declare public _LegacyCallHandler?: LegacyCallHandler; constructor() { diff --git a/apps/web/test/unit-tests/components/structures/LoggedInView-test.tsx b/apps/web/test/unit-tests/components/structures/LoggedInView-test.tsx index a7f8a1038a..7e430ec283 100644 --- a/apps/web/test/unit-tests/components/structures/LoggedInView-test.tsx +++ b/apps/web/test/unit-tests/components/structures/LoggedInView-test.tsx @@ -30,7 +30,13 @@ import LoggedInView from "../../../../src/components/structures/LoggedInView"; import { SDKContext } from "../../../../src/contexts/SDKContext"; import { StandardActions } from "../../../../src/notifications/StandardActions"; import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; -import { flushPromises, getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../test-utils"; +import { + flushPromises, + getMockClientWithEventEmitter, + mockClientMethodsRooms, + mockClientMethodsServer, + mockClientMethodsUser, +} from "../../../test-utils"; import { TestSDKContext } from "../../TestSDKContext"; import defaultDispatcher from "../../../../src/dispatcher/dispatcher"; import SettingsStore from "../../../../src/settings/SettingsStore"; @@ -45,9 +51,9 @@ describe("", () => { const userId = "@alice:domain.org"; const mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), - getClientWellKnown: jest.fn(), + ...mockClientMethodsServer(), + ...mockClientMethodsRooms([]), getAccountData: jest.fn(), - getRoom: jest.fn(), getSyncState: jest.fn().mockReturnValue(null), getSyncStateData: jest.fn().mockReturnValue(null), getMediaHandler: jest.fn(), @@ -57,6 +63,9 @@ describe("", () => { setExtendedProfileProperty: jest.fn().mockResolvedValue(undefined), deleteExtendedProfileProperty: jest.fn().mockResolvedValue(undefined), doesServerSupportExtendedProfiles: jest.fn().mockResolvedValue(true), + matrixRTC: { + on: jest.fn(), + }, getAuthMetadata: jest.fn().mockRejectedValue(new Error("Legacy auth")), }); const mediaHandler = new MediaHandler(mockClient); diff --git a/apps/web/test/unit-tests/components/structures/MessagePanel-test.tsx b/apps/web/test/unit-tests/components/structures/MessagePanel-test.tsx index 18d2fe2ca7..4e1508356c 100644 --- a/apps/web/test/unit-tests/components/structures/MessagePanel-test.tsx +++ b/apps/web/test/unit-tests/components/structures/MessagePanel-test.tsx @@ -25,13 +25,14 @@ import { makeBeaconInfoEvent, mockClientMethodsCrypto, mockClientMethodsEvents, + mockClientMethodsServer, mockClientMethodsUser, mockClientPushProcessor, } from "../../../test-utils"; import type ResizeNotifier from "../../../../src/utils/ResizeNotifier"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; -import { SDKContextClass } from "../../../../src/contexts/SDKContextClass"; +import { TestSDKContext } from "../../TestSDKContext.ts"; jest.mock("../../../../src/utils/beacon", () => ({ useBeacon: jest.fn(), @@ -47,14 +48,15 @@ describe("MessagePanel", function () { ...mockClientMethodsEvents(), ...mockClientMethodsCrypto(), ...mockClientPushProcessor(), + ...mockClientMethodsServer(), getAccountData: jest.fn(), isUserIgnored: jest.fn().mockReturnValue(false), isRoomEncrypted: jest.fn().mockReturnValue(false), getRoom: jest.fn(), - getClientWellKnown: jest.fn().mockReturnValue({}), supportsThreads: jest.fn().mockReturnValue(true), + getVisibleRooms: jest.fn().mockReturnValue([]), }); - let sdkContext: SDKContextClass; + let sdkContext: TestSDKContext; jest.spyOn(MatrixClientPeg, "get").mockReturnValue(client); const room = new Room(roomId, client, userId); @@ -106,7 +108,8 @@ describe("MessagePanel", function () { return arg === "showDisplaynameChanges"; }); - sdkContext = new SDKContextClass(); + sdkContext = new TestSDKContext(); + sdkContext._client = client; DMRoomMap.makeShared(client); }); diff --git a/apps/web/test/unit-tests/components/structures/SpaceRoomView-test.tsx b/apps/web/test/unit-tests/components/structures/SpaceRoomView-test.tsx index 6610587a50..9dc5646067 100644 --- a/apps/web/test/unit-tests/components/structures/SpaceRoomView-test.tsx +++ b/apps/web/test/unit-tests/components/structures/SpaceRoomView-test.tsx @@ -18,7 +18,7 @@ import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalink import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore.ts"; import DMRoomMap from "../../../../src/utils/DMRoomMap.ts"; import { type IOpts } from "../../../../src/createRoom.ts"; -import SpaceStore from "../../../../src/stores/spaces/SpaceStore.ts"; +import { SDKContextClass } from "../../../../src/contexts/SDKContextClass.ts"; describe("SpaceRoomView", () => { let cli: MockedObject; @@ -132,7 +132,7 @@ describe("SpaceRoomView", () => { describe("Spaces: creating a new community space", () => { it("asks what topics you want to discuss, creates rooms for them and offers to share", async () => { cli.createRoom.mockResolvedValueOnce({ room_id: "room1" }).mockResolvedValueOnce({ room_id: "room2" }); - SpaceStore.instance.addRoomToSpace = jest.fn(); + SDKContextClass.instance.spaceStore.addRoomToSpace = jest.fn(); // Given we are creating a space const view = await renderSpaceRoomView({ @@ -243,7 +243,7 @@ describe("SpaceRoomView", () => { describe("Spaces: creating a new private space", () => { it("creates rooms inside a private space for a team", async () => { cli.createRoom.mockResolvedValueOnce({ room_id: "room1" }).mockResolvedValueOnce({ room_id: "room2" }); - SpaceStore.instance.addRoomToSpace = jest.fn(); + SDKContextClass.instance.spaceStore.addRoomToSpace = jest.fn(); // When I create a private space const view = await renderSpaceRoomView({ diff --git a/apps/web/test/unit-tests/components/views/dialogs/ManageRestrictedJoinRuleDialog-test.tsx b/apps/web/test/unit-tests/components/views/dialogs/ManageRestrictedJoinRuleDialog-test.tsx index 7321f36b7a..73dcd9b7b2 100644 --- a/apps/web/test/unit-tests/components/views/dialogs/ManageRestrictedJoinRuleDialog-test.tsx +++ b/apps/web/test/unit-tests/components/views/dialogs/ManageRestrictedJoinRuleDialog-test.tsx @@ -10,16 +10,16 @@ import React from "react"; import { render } from "jest-matrix-react"; import { Room } from "matrix-js-sdk/src/matrix"; -import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; +import { getMockClientWithEventEmitter, mockClientMethodsRooms, mockClientMethodsUser } from "../../../../test-utils"; import ManageRestrictedJoinRuleDialog from "../../../../../src/components/views/dialogs/ManageRestrictedJoinRuleDialog"; -import SpaceStore from "../../../../../src/stores/spaces/SpaceStore"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; describe("", () => { const userId = "@alice:server.org"; const mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), - getRoom: jest.fn(), + ...mockClientMethodsRooms([]), }); const room = new Room("!roomId:server", mockClient, userId); mockClient.getRoom.mockReturnValue(room); @@ -36,7 +36,7 @@ describe("", () => { it("should list spaces which are not parents of the room", () => { const space1 = new Room("!space:server", mockClient, userId); space1.name = "Other Space"; - jest.spyOn(SpaceStore.instance, "spacePanelSpaces", "get").mockReturnValue([space1]); + jest.spyOn(SDKContextClass.instance.spaceStore, "spacePanelSpaces", "get").mockReturnValue([space1]); expect(getComponent().asFragment()).toMatchSnapshot(); }); diff --git a/apps/web/test/unit-tests/components/views/dialogs/SpotlightDialog-test.tsx b/apps/web/test/unit-tests/components/views/dialogs/SpotlightDialog-test.tsx index 4e22afea18..08e6cdc66e 100644 --- a/apps/web/test/unit-tests/components/views/dialogs/SpotlightDialog-test.tsx +++ b/apps/web/test/unit-tests/components/views/dialogs/SpotlightDialog-test.tsx @@ -34,7 +34,7 @@ import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import SdkConfig from "../../../../../src/SdkConfig"; import { Action } from "../../../../../src/dispatcher/actions"; import { MetaSpace } from "../../../../../src/stores/spaces"; -import SpaceStore from "../../../../../src/stores/spaces/SpaceStore.ts"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; jest.useFakeTimers(); @@ -704,7 +704,7 @@ describe("Spotlight Dialog", () => { describe("metaspaces", () => { beforeEach(() => { - jest.spyOn(SpaceStore.instance, "enabledMetaSpaces", "get").mockReturnValue([ + jest.spyOn(SDKContextClass.instance.spaceStore, "enabledMetaSpaces", "get").mockReturnValue([ MetaSpace.Home, MetaSpace.Orphans, ]); diff --git a/apps/web/test/unit-tests/components/views/elements/AppTile-test.tsx b/apps/web/test/unit-tests/components/views/elements/AppTile-test.tsx index 7737c4df79..8d8e084849 100644 --- a/apps/web/test/unit-tests/components/views/elements/AppTile-test.tsx +++ b/apps/web/test/unit-tests/components/views/elements/AppTile-test.tsx @@ -73,6 +73,8 @@ describe("AppTile", () => { r1 = new Room("r1", cli, "@name:example.com"); r2 = new Room("r2", cli, "@name:example.com"); + jest.spyOn(r1, "getPendingEvents").mockReturnValue([]); + jest.spyOn(r2, "getPendingEvents").mockReturnValue([]); jest.spyOn(cli, "getRoom").mockImplementation((roomId) => { if (roomId === "r1") return r1; if (roomId === "r2") return r2; diff --git a/apps/web/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx b/apps/web/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx index 8f0798cfa6..f0db2282f6 100644 --- a/apps/web/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx +++ b/apps/web/test/unit-tests/components/views/rooms/NewRoomIntro-test.tsx @@ -15,29 +15,29 @@ import { LinkedTextContext } from "@element-hq/web-shared-components"; import { LocalRoom } from "../../../../../src/models/LocalRoom"; import { + clientAndSDKContextRenderOptions, filterConsole, mkEvent, mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient, } from "../../../../test-utils"; -import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import NewRoomIntro from "../../../../../src/components/views/rooms/NewRoomIntro"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import { DirectoryMember } from "../../../../../src/utils/direct-messages"; import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => { render( - - - - - - - , + + + + + , + clientAndSDKContextRenderOptions(client, SDKContextClass.instance), ); }; diff --git a/apps/web/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx b/apps/web/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx index 59572e0d96..fbeba4a764 100644 --- a/apps/web/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx +++ b/apps/web/test/unit-tests/components/views/rooms/RoomHeader/CallGuestLinkButton-test.tsx @@ -77,6 +77,7 @@ describe("", () => { const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(), sendStateEvent: jest.fn(), + getVisibleRooms: jest.fn().mockReturnValue([]), }); sdkContext = new TestSDKContext(); sdkContext._client = client; diff --git a/apps/web/test/unit-tests/components/views/settings/JoinRuleSettings-test.tsx b/apps/web/test/unit-tests/components/views/settings/JoinRuleSettings-test.tsx index adedbb8737..22881b9916 100644 --- a/apps/web/test/unit-tests/components/views/settings/JoinRuleSettings-test.tsx +++ b/apps/web/test/unit-tests/components/views/settings/JoinRuleSettings-test.tsx @@ -26,6 +26,8 @@ import { clearAllModals, flushPromises, getMockClientWithEventEmitter, + mockClientMethodsRooms, + mockClientMethodsServer, mockClientMethodsUser, } from "../../../../test-utils"; import { filterBoolean } from "../../../../../src/utils/arrays"; @@ -33,14 +35,16 @@ import JoinRuleSettings, { type JoinRuleSettingsProps, } from "../../../../../src/components/views/settings/JoinRuleSettings"; import { PreferredRoomVersions } from "../../../../../src/utils/PreferredRoomVersions"; -import SpaceStore from "../../../../../src/stores/spaces/SpaceStore"; import SettingsStore from "../../../../../src/settings/SettingsStore"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; +import DMRoomMap from "../../../../../src/utils/DMRoomMap.ts"; describe("", () => { const userId = "@alice:server.org"; const client = getMockClientWithEventEmitter({ ...mockClientMethodsUser(userId), - getRoom: jest.fn(), + ...mockClientMethodsServer(), + ...mockClientMethodsRooms([]), getDomain: jest.fn(), getLocalAliases: jest.fn().mockReturnValue([]), sendStateEvent: jest.fn(), @@ -50,6 +54,7 @@ describe("", () => { isRoomEncrypted: jest.fn().mockReturnValue(false), getRoomDirectoryVisibility: jest.fn(), setRoomDirectoryVisibility: jest.fn(), + matrixRTC: { on: jest.fn() }, }); const roomId = "!room:server.org"; const newRoomId = "!roomUpgraded:server.org"; @@ -112,6 +117,7 @@ describe("", () => { client.upgradeRoom.mockResolvedValue({ replacement_room: newRoomId }); client.getRoom.mockReturnValue(null); jest.spyOn(SettingsStore, "getValue").mockImplementation((setting) => setting === "feature_ask_to_join"); + DMRoomMap.makeShared(client); }); type TestCase = [string, { label: string; unsupportedRoomVersion: string; preferredRoomVersion: string }]; @@ -165,7 +171,9 @@ describe("", () => { // room that doesn't support the join rule const room = new Room(roomId, client, userId); const parentSpace = new Room("!parentSpace:server.org", client, userId); - jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(new Set([parentSpace.roomId])); + jest.spyOn(SDKContextClass.instance.spaceStore, "getKnownParents").mockReturnValue( + new Set([parentSpace.roomId]), + ); setRoomStateEvents(room, unsupportedRoomVersion); const memberAlice = new RoomMember(roomId, "@alice:server.org"); const memberBob = new RoomMember(roomId, "@bob:server.org"); diff --git a/apps/web/test/unit-tests/components/views/spaces/SpacePanel-test.tsx b/apps/web/test/unit-tests/components/views/spaces/SpacePanel-test.tsx index a2e3698ced..7f7e4212e8 100644 --- a/apps/web/test/unit-tests/components/views/spaces/SpacePanel-test.tsx +++ b/apps/web/test/unit-tests/components/views/spaces/SpacePanel-test.tsx @@ -18,7 +18,6 @@ import { shouldShowComponent } from "../../../../../src/customisations/helpers/U import { UIComponent } from "../../../../../src/settings/UIFeature"; import { mkStubRoom, wrapInMatrixClientContext, wrapInSdkContext } from "../../../../test-utils"; import { TestSDKContext } from "../../../TestSDKContext.ts"; -import SpaceStore from "../../../../../src/stores/spaces/SpaceStore"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import { type SpaceNotificationState } from "../../../../../src/stores/notifications/SpaceNotificationState"; import SettingsStore from "../../../../../src/settings/SettingsStore"; @@ -99,10 +98,9 @@ jest.mock("../../../../../src/stores/spaces/SpaceStore", () => { getNotificationState = () => null as SpaceNotificationState | null; setActiveSpace = jest.fn(); moveRootSpace = jest.fn(); + start = jest.fn(); } - return { - instance: new MockSpaceStore(), - }; + return MockSpaceStore; }); jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({ @@ -135,7 +133,7 @@ describe("", () => { }); beforeEach(() => { - SpaceStore.instance.enabledMetaSpaces.push(MetaSpace.Home, MetaSpace.Orphans, MetaSpace.VideoRooms); + sdkContext.spaceStore.enabledMetaSpaces.push(MetaSpace.Home, MetaSpace.Orphans, MetaSpace.VideoRooms); mocked(shouldShowComponent).mockClear().mockReturnValue(true); }); afterEach(() => { @@ -186,7 +184,7 @@ describe("", () => { }); it("should allow rearranging via drag and drop", async () => { - (SpaceStore.instance.spacePanelSpaces as any) = [ + (sdkContext.spaceStore.spacePanelSpaces as any) = [ mkStubRoom("!room1:server", "Room 1", mockClient), mkStubRoom("!room2:server", "Room 2", mockClient), mkStubRoom("!room3:server", "Room 3", mockClient), @@ -201,7 +199,7 @@ describe("", () => { await move(room1, DragDirection.DOWN); await drop(room1); - expect(SpaceStore.instance.moveRootSpace).toHaveBeenCalledWith(0, 1); + expect(sdkContext.spaceStore.moveRootSpace).toHaveBeenCalledWith(0, 1); }); it("should be able to open the user menu via dispatcher", async () => { diff --git a/apps/web/test/unit-tests/components/views/spaces/SpaceTreeLevel-test.tsx b/apps/web/test/unit-tests/components/views/spaces/SpaceTreeLevel-test.tsx index 87e52f79df..ea38579a8a 100644 --- a/apps/web/test/unit-tests/components/views/spaces/SpaceTreeLevel-test.tsx +++ b/apps/web/test/unit-tests/components/views/spaces/SpaceTreeLevel-test.tsx @@ -10,16 +10,15 @@ import React from "react"; import { fireEvent, getByTestId, render } from "jest-matrix-react"; import { mocked } from "jest-mock"; -import { mkRoom, stubClient } from "../../../../test-utils"; -import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; +import { clientAndSDKContextRenderOptions, mkRoom, stubClient } from "../../../../test-utils"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import defaultDispatcher from "../../../../../src/dispatcher/dispatcher"; import { Action } from "../../../../../src/dispatcher/actions"; import { SpaceItem, SpaceButton } from "../../../../../src/components/views/spaces/SpaceTreeLevel"; import { MetaSpace, type SpaceKey } from "../../../../../src/stores/spaces"; -import SpaceStore from "../../../../../src/stores/spaces/SpaceStore"; import { StaticNotificationState } from "../../../../../src/stores/notifications/StaticNotificationState"; import { NotificationLevel } from "../../../../../src/stores/notifications/NotificationLevel"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass"; jest.mock("../../../../../src/stores/spaces/SpaceStore", () => { // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -29,15 +28,16 @@ jest.mock("../../../../../src/stores/spaces/SpaceStore", () => { setActiveSpace = jest.fn(); getChildSpaces = jest.fn(); getNotificationState = jest.fn(); + start = jest.fn(); } - return { instance: new MockSpaceStore() }; + return MockSpaceStore; }); describe("SpaceButton", () => { - stubClient(); - const space = mkRoom(MatrixClientPeg.safeGet(), "!1:example.org"); - DMRoomMap.makeShared(MatrixClientPeg.safeGet()); + const cli = stubClient(); + const space = mkRoom(cli, "!1:example.org"); + DMRoomMap.makeShared(cli); const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch"); @@ -53,11 +53,12 @@ describe("SpaceButton", () => { data-testid="create-space-button" size="32px" />, + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), ); - expect(SpaceStore.instance.setActiveSpace).not.toHaveBeenCalled(); + expect(SDKContextClass.instance.spaceStore.setActiveSpace).not.toHaveBeenCalled(); fireEvent.click(getByTestId(container, "create-space-button")); - expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith("!1:example.org"); + expect(SDKContextClass.instance.spaceStore.setActiveSpace).toHaveBeenCalledWith("!1:example.org"); }); it("navigates to the space home on click if already active", () => { @@ -87,11 +88,12 @@ describe("SpaceButton", () => { data-testid="create-space-button" size="32px" />, + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), ); - expect(SpaceStore.instance.setActiveSpace).not.toHaveBeenCalled(); + expect(SDKContextClass.instance.spaceStore.setActiveSpace).not.toHaveBeenCalled(); fireEvent.click(getByTestId(container, "create-space-button")); - expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith(MetaSpace.Home); + expect(SDKContextClass.instance.spaceStore.setActiveSpace).toHaveBeenCalledWith(MetaSpace.Home); }); it("does nothing on click if already active", () => { @@ -103,12 +105,13 @@ describe("SpaceButton", () => { data-testid="create-space-button" size="32px" />, + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), ); fireEvent.click(getByTestId(container, "create-space-button")); expect(dispatchSpy).not.toHaveBeenCalled(); // Re-activating the metaspace is a no-op - expect(SpaceStore.instance.setActiveSpace).toHaveBeenCalledWith(MetaSpace.Home); + expect(SDKContextClass.instance.spaceStore.setActiveSpace).toHaveBeenCalledWith(MetaSpace.Home); }); it("should render notificationState if one is provided", () => { @@ -139,11 +142,14 @@ describe("SpaceItem", () => { subspace.name = "Subspace"; it("should render a space with subspaces", () => { - mocked(SpaceStore.instance.getChildSpaces).mockImplementation((spaceId) => + mocked(SDKContextClass.instance.spaceStore.getChildSpaces).mockImplementation((spaceId) => spaceId === space.roomId ? [subspace] : [], ); - const { asFragment, queryByText, getByLabelText } = render(); + const { asFragment, queryByText, getByLabelText } = render( + , + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), + ); expect(queryByText("Root Space")).toBeVisible(); expect(queryByText("Subspace")).toBeNull(); diff --git a/apps/web/test/unit-tests/slash-commands/utils.ts b/apps/web/test/unit-tests/slash-commands/utils.ts index 08c676d1ef..a46d69d817 100644 --- a/apps/web/test/unit-tests/slash-commands/utils.ts +++ b/apps/web/test/unit-tests/slash-commands/utils.ts @@ -13,6 +13,7 @@ import { getCommand } from "../../../src/slash-commands/SlashCommands"; import { stubClient } from "../../test-utils"; import { SDKContextClass } from "../../../src/contexts/SDKContextClass"; import { LocalRoom } from "../../../src/models/LocalRoom"; +import DMRoomMap from "../../../src/utils/DMRoomMap.ts"; export function setUpCommandTest( roomId: string, @@ -30,6 +31,7 @@ export function setUpCommandTest( // createTestClient here instead of stubClient (i.e. avoid setting // MatrixClientPeg.) const client = stubClient(); + DMRoomMap.makeShared(client); const { cmd: command, args } = getCommand(roomId, input); let room: Room; diff --git a/apps/web/test/unit-tests/stores/RoomViewStore-test.ts b/apps/web/test/unit-tests/stores/RoomViewStore-test.ts index 5c52122219..d741fe3a21 100644 --- a/apps/web/test/unit-tests/stores/RoomViewStore-test.ts +++ b/apps/web/test/unit-tests/stores/RoomViewStore-test.ts @@ -34,7 +34,7 @@ import { TimelineRenderingType } from "../../../src/contexts/RoomContext"; import { MatrixDispatcher } from "../../../src/dispatcher/dispatcher"; import { UPDATE_EVENT } from "../../../src/stores/AsyncStore"; import { type ActiveRoomChangedPayload } from "../../../src/dispatcher/payloads/ActiveRoomChangedPayload"; -import { SpaceStoreClass } from "../../../src/stores/spaces/SpaceStore"; +import SpaceStore from "../../../src/stores/spaces/SpaceStore"; import { TestSDKContext } from "../TestSDKContext"; import { type ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload"; import Modal from "../../../src/Modal"; @@ -60,7 +60,7 @@ const MockPosthogAnalytics = >(PosthogAnaly jest.mock("../../../src/SlidingSyncManager"); const MockSlidingSyncManager = >(SlidingSyncManager); jest.mock("../../../src/stores/spaces/SpaceStore"); -const MockSpaceStore = >(SpaceStoreClass); +const MockSpaceStore = >(SpaceStore); // mock VoiceRecording because it contains all the audio APIs jest.mock("../../../src/audio/VoiceRecording", () => ({ 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 73e1ec82d6..9878a4bb40 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 @@ -24,7 +24,6 @@ import { mkEvent, mkMessage, mkSpace, mkStubRoom, stubClient, upsertRoomStateEve import { getMockedRooms } from "./skip-list/getMockedRooms"; import { AlphabeticSorter } from "../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter"; import dispatcher from "../../../../src/dispatcher/dispatcher"; -import SpaceStore from "../../../../src/stores/spaces/SpaceStore"; import { MetaSpace, UPDATE_SELECTED_SPACE } from "../../../../src/stores/spaces"; import { DefaultTagID } from "../../../../src/stores/room-list-v3/skip-list/tag"; import { FilterEnum } from "../../../../src/stores/room-list-v3/skip-list/filters"; @@ -37,6 +36,7 @@ import * as utilsRLS from "../../../../src/stores/room-list-v3/utils.ts"; 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"; describe("RoomListStoreV3", () => { async function getRoomListStore() { @@ -54,9 +54,13 @@ describe("RoomListStoreV3", () => { cb(0); return 0; }); - jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space) => space === MetaSpace.Home); - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => MetaSpace.Home); - jest.spyOn(SpaceStore.instance, "storeReadyPromise", "get").mockImplementation(() => Promise.resolve()); + jest.spyOn(SDKContextClass.instance.spaceStore, "isRoomInSpace").mockImplementation( + (space) => space === MetaSpace.Home, + ); + jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpace", "get").mockImplementation(() => MetaSpace.Home); + jest.spyOn(SDKContextClass.instance.spaceStore, "storeReadyPromise", "get").mockImplementation(() => + Promise.resolve(), + ); jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation((room) => { const state = { isUnread: false, @@ -448,12 +452,14 @@ describe("RoomListStoreV3", () => { } function setupMocks(spaceRoom: Room, roomIds: string[]) { - jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space, id) => { + jest.spyOn(SDKContextClass.instance.spaceStore, "isRoomInSpace").mockImplementation((space, id) => { if (space === MetaSpace.Home && !roomIds.includes(id)) return true; if (space === spaceRoom.roomId && roomIds.includes(id)) return true; return false; }); - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoom.roomId); + jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpace", "get").mockImplementation( + () => spaceRoom.roomId, + ); } function getClientAndRooms() { @@ -497,7 +503,7 @@ describe("RoomListStoreV3", () => { const { spaceRoom, roomIds } = createSpace(rooms, [6, 8, 13, 27, 75], client); // Mock the space store - jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space, id) => { + jest.spyOn(SDKContextClass.instance.spaceStore, "isRoomInSpace").mockImplementation((space, id) => { if (space === MetaSpace.Home && !roomIds.includes(id)) return true; if (space === spaceRoom.roomId && roomIds.includes(id)) return true; return false; @@ -518,8 +524,10 @@ describe("RoomListStoreV3", () => { } // Lets switch to the space - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoom.roomId); - SpaceStore.instance.emit(UPDATE_SELECTED_SPACE); + jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpace", "get").mockImplementation( + () => spaceRoom.roomId, + ); + SDKContextClass.instance.spaceStore.emit(UPDATE_SELECTED_SPACE); expect(fn).toHaveBeenCalled(); const result2 = store .getSortedRoomsInActiveSpace() @@ -1152,11 +1160,11 @@ describe("RoomListStoreV3", () => { const spaceRoomId = "!space1:matrix.org"; const inSpaceIds = [3, 10, 20].map((i) => rooms[i].roomId); - jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space, id) => { + jest.spyOn(SDKContextClass.instance.spaceStore, "isRoomInSpace").mockImplementation((space, id) => { if (space === spaceRoomId && inSpaceIds.includes(id)) return true; return false; }); - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => spaceRoomId); + jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpace", "get").mockImplementation(() => spaceRoomId); const store = new RoomListStoreV3Class(dispatcher); await store.start(); diff --git a/apps/web/test/unit-tests/stores/room-list-v3/section-test.ts b/apps/web/test/unit-tests/stores/room-list-v3/section-test.ts index bcd9ab94f2..39b63edadf 100644 --- a/apps/web/test/unit-tests/stores/room-list-v3/section-test.ts +++ b/apps/web/test/unit-tests/stores/room-list-v3/section-test.ts @@ -25,7 +25,7 @@ import { CreateSectionDialog } from "../../../../src/components/views/dialogs/Cr import { RemoveSectionDialog } from "../../../../src/components/views/dialogs/RemoveSectionDialog"; import { DefaultTagID } from "../../../../src/stores/room-list-v3/skip-list/tag"; import { MetaSpace } from "../../../../src/stores/spaces"; -import SpaceStore from "../../../../src/stores/spaces/SpaceStore"; +import { SDKContextClass } from "../../../../src/contexts/SDKContextClass.ts"; describe("section", () => { afterEach(() => { @@ -39,8 +39,8 @@ describe("section", () => { beforeEach(() => { // Default: no known spaces - jest.spyOn(SpaceStore.instance, "enabledMetaSpaces", "get").mockReturnValue([]); - jest.spyOn(SpaceStore.instance, "spacePanelSpaces", "get").mockReturnValue([]); + jest.spyOn(SDKContextClass.instance.spaceStore, "enabledMetaSpaces", "get").mockReturnValue([]); + jest.spyOn(SDKContextClass.instance.spaceStore, "spacePanelSpaces", "get").mockReturnValue([]); }); it.each([null, false, 42, "string", []] as const)("returns an empty object when the raw value is %p", (raw) => { @@ -80,7 +80,9 @@ describe("section", () => { }); it("keeps spaceId when the meta-space is enabled", () => { - jest.spyOn(SpaceStore.instance, "enabledMetaSpaces", "get").mockReturnValue([MetaSpace.Home]); + jest.spyOn(SDKContextClass.instance.spaceStore, "enabledMetaSpaces", "get").mockReturnValue([ + MetaSpace.Home, + ]); jest.spyOn(SettingsStore, "getValue").mockReturnValue({ [validTag]: { ...validEntry, spaceId: MetaSpace.Home }, }); @@ -89,7 +91,9 @@ describe("section", () => { it("keeps spaceId when the real space room exists", () => { const spaceId = "!space:server"; - jest.spyOn(SpaceStore.instance, "spacePanelSpaces", "get").mockReturnValue([{ roomId: spaceId } as Room]); + jest.spyOn(SDKContextClass.instance.spaceStore, "spacePanelSpaces", "get").mockReturnValue([ + { roomId: spaceId } as Room, + ]); jest.spyOn(SettingsStore, "getValue").mockReturnValue({ [validTag]: { ...validEntry, spaceId }, }); @@ -101,8 +105,8 @@ describe("section", () => { const tag = "element.io.section.abc"; beforeEach(() => { - jest.spyOn(SpaceStore.instance, "enabledMetaSpaces", "get").mockReturnValue([]); - jest.spyOn(SpaceStore.instance, "spacePanelSpaces", "get").mockReturnValue([]); + jest.spyOn(SDKContextClass.instance.spaceStore, "enabledMetaSpaces", "get").mockReturnValue([]); + jest.spyOn(SDKContextClass.instance.spaceStore, "spacePanelSpaces", "get").mockReturnValue([]); }); it("returns an empty array when the raw value is not an array", () => { @@ -131,8 +135,8 @@ describe("section", () => { beforeEach(() => { jest.spyOn(SettingsStore, "getValue").mockReturnValue(null); jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined); - jest.spyOn(SpaceStore.instance, "enabledMetaSpaces", "get").mockReturnValue([]); - jest.spyOn(SpaceStore.instance, "spacePanelSpaces", "get").mockReturnValue([]); + jest.spyOn(SDKContextClass.instance.spaceStore, "enabledMetaSpaces", "get").mockReturnValue([]); + jest.spyOn(SDKContextClass.instance.spaceStore, "spacePanelSpaces", "get").mockReturnValue([]); }); it.each([ diff --git a/apps/web/test/unit-tests/stores/room-list-v3/skip-list/RoomSkipList-test.ts b/apps/web/test/unit-tests/stores/room-list-v3/skip-list/RoomSkipList-test.ts index 8640f3491e..55daf541ca 100644 --- a/apps/web/test/unit-tests/stores/room-list-v3/skip-list/RoomSkipList-test.ts +++ b/apps/web/test/unit-tests/stores/room-list-v3/skip-list/RoomSkipList-test.ts @@ -15,12 +15,12 @@ import { RoomSkipList } from "../../../../../src/stores/room-list-v3/skip-list/R import { RecencySorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/RecencySorter"; import { AlphabeticSorter } from "../../../../../src/stores/room-list-v3/skip-list/sorters/AlphabeticSorter"; import { getMockedRooms } from "./getMockedRooms"; -import SpaceStore from "../../../../../src/stores/spaces/SpaceStore"; import { MetaSpace } from "../../../../../src/stores/spaces"; import { RoomNotificationStateStore } from "../../../../../src/stores/notifications/RoomNotificationStateStore"; import { FavouriteFilter } from "../../../../../src/stores/room-list-v3/skip-list/filters/FavouriteFilter"; import { FilterEnum } from "../../../../../src/stores/room-list-v3/skip-list/filters"; import { DefaultTagID } from "../../../../../src/stores/room-list-v3/skip-list/tag"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; describe("RoomSkipList", () => { function generateSkipList(roomCount?: number): { @@ -38,9 +38,13 @@ describe("RoomSkipList", () => { } beforeEach(() => { - jest.spyOn(SpaceStore.instance, "isRoomInSpace").mockImplementation((space) => space === MetaSpace.Home); - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockImplementation(() => MetaSpace.Home); - jest.spyOn(SpaceStore.instance, "storeReadyPromise", "get").mockImplementation(() => Promise.resolve()); + jest.spyOn(SDKContextClass.instance.spaceStore, "isRoomInSpace").mockImplementation( + (space) => space === MetaSpace.Home, + ); + jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpace", "get").mockImplementation(() => MetaSpace.Home); + jest.spyOn(SDKContextClass.instance.spaceStore, "storeReadyPromise", "get").mockImplementation(() => + Promise.resolve(), + ); jest.spyOn(RoomNotificationStateStore.instance, "getRoomState").mockImplementation(() => { const state = { mute: false, diff --git a/apps/web/test/unit-tests/utils/i18n-helpers-test.ts b/apps/web/test/unit-tests/utils/i18n-helpers-test.ts index 04e7212264..96fe1b5d53 100644 --- a/apps/web/test/unit-tests/utils/i18n-helpers-test.ts +++ b/apps/web/test/unit-tests/utils/i18n-helpers-test.ts @@ -9,10 +9,10 @@ Please see LICENSE files in the repository root for full details. import { Room } from "matrix-js-sdk/src/matrix"; import { mocked } from "jest-mock"; -import SpaceStore from "../../../src/stores/spaces/SpaceStore"; import { stubClient } from "../../test-utils"; import { roomContextDetails } from "../../../src/utils/i18n-helpers"; import DMRoomMap from "../../../src/utils/DMRoomMap"; +import { SDKContextClass } from "../../../src/contexts/SDKContextClass.ts"; describe("roomContextDetails", () => { const client = stubClient(); @@ -30,21 +30,23 @@ describe("roomContextDetails", () => { }); it("should return 1-parent variant", () => { - jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(new Set([parent1.roomId])); + jest.spyOn(SDKContextClass.instance.spaceStore, "getKnownParents").mockReturnValue(new Set([parent1.roomId])); const res = roomContextDetails(room); expect(res!.details).toMatchInlineSnapshot(`"Alpha"`); expect(res!.ariaLabel).toMatchInlineSnapshot(`"In Alpha."`); }); it("should return 2-parent variant", () => { - jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue(new Set([parent2.roomId, parent3.roomId])); + jest.spyOn(SDKContextClass.instance.spaceStore, "getKnownParents").mockReturnValue( + new Set([parent2.roomId, parent3.roomId]), + ); const res = roomContextDetails(room); expect(res!.details).toMatchInlineSnapshot(`"Beta and Charlie"`); expect(res!.ariaLabel).toMatchInlineSnapshot(`"In spaces Beta and Charlie."`); }); it("should return n-parent variant", () => { - jest.spyOn(SpaceStore.instance, "getKnownParents").mockReturnValue( + jest.spyOn(SDKContextClass.instance.spaceStore, "getKnownParents").mockReturnValue( new Set([parent1.roomId, parent2.roomId, parent3.roomId]), ); const res = roomContextDetails(room); diff --git a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts index 290003ae32..3f9415debf 100644 --- a/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts +++ b/apps/web/test/viewmodels/room-list/RoomListHeaderViewModel-test.ts @@ -10,7 +10,6 @@ import { JoinRule, type MatrixClient, type Room, RoomEvent, RoomType } from "mat import { RoomListHeaderViewModel } from "../../../src/viewmodels/room-list/RoomListHeaderViewModel"; import { MetaSpace, UPDATE_HOME_BEHAVIOUR, UPDATE_SELECTED_SPACE } from "../../../src/stores/spaces"; -import SpaceStore from "../../../src/stores/spaces/SpaceStore"; import defaultDispatcher from "../../../src/dispatcher/dispatcher"; import { Action } from "../../../src/dispatcher/actions"; import SettingsStore from "../../../src/settings/SettingsStore"; @@ -27,6 +26,7 @@ import { createTestClient, mkSpace } from "../../test-utils"; import { createRoom, hasCreateRoomRights } from "../../../src/viewmodels/room-list/utils"; import PosthogTrackers from "../../../src/PosthogTrackers"; import { ReleaseAnnouncementStore } from "../../../src/stores/ReleaseAnnouncementStore"; +import { TestSDKContext } from "../../unit-tests/TestSDKContext.ts"; jest.mock("../../../src/PosthogTrackers", () => ({ trackInteraction: jest.fn(), @@ -51,9 +51,12 @@ describe("RoomListHeaderViewModel", () => { let matrixClient: MatrixClient; let mockSpace: Room; let vm: RoomListHeaderViewModel; + let sdkContext: TestSDKContext; beforeEach(() => { matrixClient = createTestClient(); + sdkContext = new TestSDKContext(); + sdkContext._client = matrixClient; mockSpace = mkSpace(matrixClient, "!space:server"); @@ -79,10 +82,10 @@ describe("RoomListHeaderViewModel", () => { describe("snapshot", () => { it("should compute snapshot for Home space", () => { - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(MetaSpace.Home); - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null); + jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(MetaSpace.Home); + jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); const snapshot = vm.getSnapshot(); expect(snapshot.title).toBe("Home"); @@ -93,10 +96,10 @@ describe("RoomListHeaderViewModel", () => { }); it("should compute snapshot for active space", () => { - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId); - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace); + jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); + jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); const snapshot = vm.getSnapshot(); expect(snapshot.title).toBe(mockSpace.roomId); @@ -108,7 +111,7 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); expect(vm.getSnapshot().canCreateVideoRoom).toBe(false); }); @@ -118,31 +121,31 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); expect(vm.getSnapshot().activeSortOption).toBe("alphabetical"); }); it("should show invite option when space is public", () => { - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId); - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace); + jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); + jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace); jest.spyOn(mockSpace, "getJoinRule").mockReturnValue(JoinRule.Public); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); 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: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); expect(vm.getSnapshot().canInviteInSpace).toBe(false); }); it("should hide space settings when user cannot access them", () => { - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId); + jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); mocked(shouldShowSpaceSettings).mockReturnValue(false); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); expect(vm.getSnapshot().canAccessSpaceSettings).toBe(false); }); @@ -152,7 +155,7 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true); }); @@ -162,7 +165,7 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); expect(vm.getSnapshot().areSectionsEnabled).toBe(true); }); @@ -173,7 +176,7 @@ describe("RoomListHeaderViewModel", () => { return "watcher-id"; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); expect(vm.getSnapshot().areSectionsEnabled).toBe(false); // Enable sections @@ -191,7 +194,7 @@ describe("RoomListHeaderViewModel", () => { "room_list_section", ); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); expect(vm.getSnapshot().displaySectionReleaseAnnouncement).toBe(true); }); }); @@ -200,24 +203,24 @@ describe("RoomListHeaderViewModel", () => { it.each([UPDATE_SELECTED_SPACE, UPDATE_HOME_BEHAVIOUR])( "should update snapshot when %s event is emitted", (event) => { - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(MetaSpace.Home); - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null); + jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(MetaSpace.Home); + jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId); - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace); - SpaceStore.instance.emit(event); + jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); + jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace); + sdkContext.spaceStore.emit(event); expect(vm.getSnapshot().title).toBe(mockSpace.roomId); }, ); it("should update snapshot when space name changes", () => { - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId); - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace); + jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); + jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); mockSpace.name = "new name"; mockSpace.emit(RoomEvent.Name, mockSpace); @@ -228,20 +231,20 @@ describe("RoomListHeaderViewModel", () => { describe("actions", () => { beforeEach(() => { - jest.spyOn(SpaceStore.instance, "activeSpace", "get").mockReturnValue(mockSpace.roomId); - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(mockSpace); + jest.spyOn(sdkContext.spaceStore, "activeSpace", "get").mockReturnValue(mockSpace.roomId); + jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(mockSpace); }); it("should fire CreateChat action when createChatRoom is called", () => { const fireSpy = jest.spyOn(defaultDispatcher, "fire"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); 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: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.createRoom(new Event("click")); expect(createRoom).toHaveBeenCalledWith(mockSpace); @@ -253,16 +256,16 @@ describe("RoomListHeaderViewModel", () => { return false; }); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.createVideoRoom(); expect(showCreateNewRoom).toHaveBeenCalledWith(mockSpace, RoomType.ElementVideo); }); it("should use UnstableCall type when element_call_video_rooms is enabled", () => { - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null); + jest.spyOn(sdkContext.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null); const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.createVideoRoom(); expect(dispatchSpy).toHaveBeenCalledWith({ @@ -273,7 +276,7 @@ describe("RoomListHeaderViewModel", () => { it("should dispatch ViewRoom action when openSpaceHome is called", () => { const dispatchSpy = jest.spyOn(defaultDispatcher, "dispatch"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.openSpaceHome(); expect(dispatchSpy).toHaveBeenCalledWith({ @@ -284,21 +287,21 @@ describe("RoomListHeaderViewModel", () => { }); it("should show space invite dialog when inviteInSpace is called", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.inviteInSpace(); expect(showSpaceInvite).toHaveBeenCalledWith(mockSpace); }); it("should show space preferences dialog when openSpacePreferences is called", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.openSpacePreferences(); expect(showSpacePreferences).toHaveBeenCalledWith(mockSpace); }); it("should show space settings dialog when openSpaceSettings is called", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.openSpaceSettings(); expect(showSpaceSettings).toHaveBeenCalledWith(mockSpace); @@ -310,7 +313,7 @@ describe("RoomListHeaderViewModel", () => { ["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: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.sort(option); expect(resortSpy).toHaveBeenCalledWith(expectedAlgorithm); }); @@ -321,7 +324,7 @@ describe("RoomListHeaderViewModel", () => { ); PosthogTrackers.trackRoomListSortingAlgorithmChange = jest.fn(); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); jest.spyOn(RoomListStoreV3.instance, "resort").mockImplementation(jest.fn()); vm.sort("unread-first"); @@ -335,7 +338,7 @@ describe("RoomListHeaderViewModel", () => { const createSectionSpy = jest .spyOn(RoomListStoreV3.instance, "createSection") .mockResolvedValue("element.io.section.work"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.createSection(); expect(createSectionSpy).toHaveBeenCalled(); }); @@ -343,7 +346,7 @@ describe("RoomListHeaderViewModel", () => { describe("collapseOrExpandSections", () => { it("should dispatch RoomListCollapseAllSections when collapseSections is not 'expand'", () => { const fireSpy = jest.spyOn(defaultDispatcher, "fire"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.collapseOrExpandSections(); @@ -352,7 +355,7 @@ describe("RoomListHeaderViewModel", () => { it("should dispatch RoomListExpandAllSections when collapseSections is 'expand'", () => { const fireSpy = jest.spyOn(defaultDispatcher, "fire"); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); // Drive the VM into the "expand" state by simulating all sections collapsed defaultDispatcher.dispatch( @@ -371,7 +374,7 @@ describe("RoomListHeaderViewModel", () => { describe("RoomListSectionsCollapseStateChanged handling", () => { it("should set collapseSections to 'expand' when collapseSections is collapse", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); defaultDispatcher.dispatch( { @@ -385,7 +388,7 @@ describe("RoomListHeaderViewModel", () => { }); it("should set collapseSections to 'collapse' when collapseSections is expand", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); defaultDispatcher.dispatch( { @@ -399,7 +402,7 @@ describe("RoomListHeaderViewModel", () => { }); it("should set collapseSections to undefined when collapseSections is undefined", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); // First drive it into a non-undefined state defaultDispatcher.dispatch( @@ -430,7 +433,7 @@ describe("RoomListHeaderViewModel", () => { }); const setValueSpy = jest.spyOn(SettingsStore, "setValue").mockImplementation(jest.fn()); - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); expect(vm.getSnapshot().isMessagePreviewEnabled).toBe(true); vm.toggleMessagePreview(); @@ -440,7 +443,7 @@ describe("RoomListHeaderViewModel", () => { }); it("should call nextReleaseAnnouncement and set displaySectionReleaseAnnouncement to false when closeSectionReleaseAnnouncement is called", () => { - vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: SpaceStore.instance }); + vm = new RoomListHeaderViewModel({ matrixClient, spaceStore: sdkContext.spaceStore }); vm.closeSectionReleaseAnnouncement(); diff --git a/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx b/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx index abafda7dc5..1310182434 100644 --- a/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx +++ b/apps/web/test/viewmodels/room-list/RoomListViewModel-test.tsx @@ -18,7 +18,6 @@ import { TestSDKContext, } from "../../test-utils"; import RoomListStoreV3, { RoomListStoreV3Event } from "../../../src/stores/room-list-v3/RoomListStoreV3"; -import SpaceStore from "../../../src/stores/spaces/SpaceStore"; import { FilterEnum } from "../../../src/stores/room-list-v3/skip-list/filters"; import dispatcher from "../../../src/dispatcher/dispatcher"; import { Action } from "../../../src/dispatcher/actions"; @@ -77,7 +76,7 @@ describe("RoomListViewModel", () => { }); jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(false); - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(null); + jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpaceRoom", "get").mockReturnValue(null); jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null); mocked(hasCreateRoomRights).mockReturnValue(false); @@ -92,7 +91,7 @@ describe("RoomListViewModel", () => { it("should initialize with correct snapshot", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -113,7 +112,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -126,7 +125,7 @@ describe("RoomListViewModel", () => { mocked(hasCreateRoomRights).mockReturnValue(true); viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -138,7 +137,7 @@ describe("RoomListViewModel", () => { it("should update room list when ListsUpdate event fires", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -162,7 +161,7 @@ describe("RoomListViewModel", () => { jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(true); viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -177,7 +176,7 @@ describe("RoomListViewModel", () => { it("should keep existing view model when ListsUpdate event fires", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -196,7 +195,7 @@ describe("RoomListViewModel", () => { it("should update room list when space changes", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -207,7 +206,9 @@ describe("RoomListViewModel", () => { sections: [{ tag: CHATS_TAG, rooms: spaceRoomList }], }); - jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue("!room1:server"); + jest.spyOn(SDKContextClass.instance.spaceStore, "getLastSelectedRoomIdForSpace").mockReturnValue( + "!room1:server", + ); RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); @@ -218,7 +219,7 @@ describe("RoomListViewModel", () => { it("should clear view models when space changes", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -244,7 +245,7 @@ describe("RoomListViewModel", () => { it("should clear roomsMap when space changes and repopulate with new rooms", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -254,7 +255,7 @@ describe("RoomListViewModel", () => { spaceId: "!space:server", sections: [{ tag: CHATS_TAG, rooms: [newSpaceRoom] }], }); - jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null); + jest.spyOn(SDKContextClass.instance.spaceStore, "getLastSelectedRoomIdForSpace").mockReturnValue(null); RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); @@ -269,7 +270,7 @@ describe("RoomListViewModel", () => { it("should update active room index when room is selected", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -289,7 +290,7 @@ describe("RoomListViewModel", () => { it("should return undefined active room index when no room is selected", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -311,7 +312,7 @@ describe("RoomListViewModel", () => { it("should keep selected room at same index when room list updates", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -341,7 +342,7 @@ describe("RoomListViewModel", () => { it("should not apply sticky behavior when user changes rooms", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -371,7 +372,7 @@ describe("RoomListViewModel", () => { it("should toggle filter on", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -392,7 +393,7 @@ describe("RoomListViewModel", () => { it("should toggle filter off", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -507,7 +508,7 @@ describe("RoomListViewModel", () => { it("should create room item view model on demand", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -520,7 +521,7 @@ describe("RoomListViewModel", () => { it("should reuse existing room item view model", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -533,7 +534,7 @@ describe("RoomListViewModel", () => { it("should return undefined for non-existent room", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -543,7 +544,7 @@ describe("RoomListViewModel", () => { it("should not throw when requesting view model for a room removed from the list but still in roomsMap", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -561,7 +562,7 @@ describe("RoomListViewModel", () => { it("should return undefined for a room from old space after space change", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -572,7 +573,7 @@ describe("RoomListViewModel", () => { spaceId: "!space:server", sections: [{ tag: CHATS_TAG, rooms: [spaceRoom] }], }); - jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null); + jest.spyOn(SDKContextClass.instance.spaceStore, "getLastSelectedRoomIdForSpace").mockReturnValue(null); RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); @@ -582,7 +583,7 @@ describe("RoomListViewModel", () => { it("should recover when roomsMap is stale but roomsResult has the room", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -596,7 +597,7 @@ describe("RoomListViewModel", () => { it("should dispose view models for rooms no longer visible", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -623,7 +624,7 @@ describe("RoomListViewModel", () => { it("should dispatch CreateChat action when createChatRoom is called", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -637,7 +638,7 @@ describe("RoomListViewModel", () => { it("should dispatch CreateRoom action without parent space", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -652,11 +653,11 @@ describe("RoomListViewModel", () => { it("should dispatch CreateRoom action with parent space", () => { const spaceRoom = mkStubRoom("!space:server", "Space", matrixClient); - jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(spaceRoom); + jest.spyOn(SDKContextClass.instance.spaceStore, "activeSpaceRoom", "get").mockReturnValue(spaceRoom); viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -680,7 +681,7 @@ describe("RoomListViewModel", () => { it("should navigate to next room when delta is 1", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -707,7 +708,7 @@ describe("RoomListViewModel", () => { it("should navigate to previous room when delta is -1", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -734,7 +735,7 @@ describe("RoomListViewModel", () => { it("should wrap around to last room when navigating backwards from first room", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -761,7 +762,7 @@ describe("RoomListViewModel", () => { it("should not navigate when current room is not found", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -789,7 +790,7 @@ describe("RoomListViewModel", () => { it("should not navigate when no room is selected", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -826,7 +827,7 @@ describe("RoomListViewModel", () => { it("should dispose all room item view models on dispose", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -846,7 +847,7 @@ describe("RoomListViewModel", () => { it("should show toast when SectionCreated event fires", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated); @@ -856,7 +857,7 @@ describe("RoomListViewModel", () => { it("should show toast when RoomTagged event fires", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); RoomListStoreV3.instance.emit(RoomListStoreV3Event.RoomTagged); @@ -866,7 +867,7 @@ describe("RoomListViewModel", () => { it("should clear toast when closeToast is called", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -880,7 +881,7 @@ describe("RoomListViewModel", () => { it("should auto-close toast after 15 seconds", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -894,7 +895,7 @@ describe("RoomListViewModel", () => { it("should reset the auto-close timer when a new section is created", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -923,7 +924,7 @@ describe("RoomListViewModel", () => { mockRoom3Unread(); viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -937,7 +938,7 @@ describe("RoomListViewModel", () => { mockRoom3Unread(); viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); viewModel.updateVisibleFold(1); @@ -980,7 +981,7 @@ describe("RoomListViewModel", () => { it("should initialize with multiple sections", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -997,7 +998,7 @@ describe("RoomListViewModel", () => { it("should not be a flat list when multiple sections exist", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1016,7 +1017,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1037,7 +1038,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1048,7 +1049,7 @@ describe("RoomListViewModel", () => { it("should exclude favourite and low_priority from filter list", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1072,7 +1073,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1084,7 +1085,7 @@ describe("RoomListViewModel", () => { it("should create section header view models on demand", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1097,7 +1098,7 @@ describe("RoomListViewModel", () => { it("should reuse section header view models", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1109,7 +1110,7 @@ describe("RoomListViewModel", () => { it("should hide room IDs when a section is collapsed", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1132,7 +1133,7 @@ describe("RoomListViewModel", () => { it("should compute activeRoomIndex relative to visible rooms when a section is collapsed", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1159,7 +1160,7 @@ describe("RoomListViewModel", () => { it("should restore room IDs when a section is re-expanded", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1178,7 +1179,7 @@ describe("RoomListViewModel", () => { it("should update sections when room list changes", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1202,7 +1203,7 @@ describe("RoomListViewModel", () => { it("should preserve section collapse state across list updates", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1221,7 +1222,7 @@ describe("RoomListViewModel", () => { it("should track section collapse state per space", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1240,7 +1241,7 @@ describe("RoomListViewModel", () => { { tag: DefaultTagID.LowPriority, rooms: [] }, ], }); - jest.spyOn(SpaceStore.instance, "getLastSelectedRoomIdForSpace").mockReturnValue(null); + jest.spyOn(SDKContextClass.instance.spaceStore, "getLastSelectedRoomIdForSpace").mockReturnValue(null); RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate); @@ -1280,7 +1281,7 @@ describe("RoomListViewModel", () => { it("should apply filters across all sections", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1309,8 +1310,10 @@ describe("RoomListViewModel", () => { const customTag = `${CUSTOM_SECTION_TAG_PREFIX}test-uuid` as const; beforeEach(() => { - jest.spyOn(SpaceStore.instance, "enabledMetaSpaces", "get").mockReturnValue([MetaSpace.Home]); - jest.spyOn(SpaceStore.instance, "spacePanelSpaces", "get").mockReturnValue([ + jest.spyOn(SDKContextClass.instance.spaceStore, "enabledMetaSpaces", "get").mockReturnValue([ + MetaSpace.Home, + ]); + jest.spyOn(SDKContextClass.instance.spaceStore, "spacePanelSpaces", "get").mockReturnValue([ mkStubRoom("!space:server", "My Space", matrixClient), ]); jest.spyOn(SettingsStore, "getValue").mockImplementation((setting: string) => { @@ -1338,7 +1341,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1356,7 +1359,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1374,7 +1377,7 @@ describe("RoomListViewModel", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1386,7 +1389,7 @@ describe("RoomListViewModel", () => { it("should collapse all sections when Action.RoomListCollapseAllSections is dispatched", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1408,7 +1411,7 @@ describe("RoomListViewModel", () => { it("should expand all sections when Action.RoomListExpandAllSections is dispatched", async () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1433,7 +1436,7 @@ describe("RoomListViewModel", () => { it("should dispatch collapseSections=expand when all sections are expanded (default)", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1449,7 +1452,7 @@ describe("RoomListViewModel", () => { it("should dispatch collapseSection=collapse when all sections are collapsed", () => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1481,7 +1484,7 @@ describe("RoomListViewModel", () => { }); viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1499,7 +1502,7 @@ describe("RoomListViewModel", () => { stubClient(); viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); @@ -1535,7 +1538,7 @@ describe("RoomListViewModel", () => { beforeEach(() => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); // Ensure section header VMs are created before tests that interact with them @@ -1643,7 +1646,7 @@ describe("RoomListViewModel", () => { beforeEach(() => { viewModel = new RoomListViewModel({ client: matrixClient, - spaceStore: SpaceStore.instance, + spaceStore: SDKContextClass.instance.spaceStore, roomViewStore: SDKContextClass.instance.roomViewStore, }); mocked(tagRoom).mockClear();