Expose SDKContext on the window & avoid using singleton store .instance (#34098)

* Expose SDKContextClass via window for debugging

* Remove stores from window if they are exposed via sdkContext

* Avoid usages of global store instance where React context is accessible

* Fix tests by adding SDKContext.Provider

* Fix tests by adding SDKContext.Provider

* Fix tests by adding SDKContext.Provider

* Fix tests by adding SDKContext.Provider

* Iterate

* Discard changes to apps/web/src/components/views/elements/AppTile.tsx
This commit is contained in:
Michael Telatynski
2026-07-10 09:51:38 +00:00
committed by GitHub
parent 15c85ec50f
commit aeea53dfde
36 changed files with 613 additions and 232 deletions
+2 -10
View File
@@ -19,13 +19,8 @@ import { type PlatformPeg } from "../PlatformPeg";
import { type IntegrationManagers } from "../integrations/IntegrationManagers";
import { type ModalManager } from "../Modal";
import type SettingsStore from "../settings/SettingsStore";
import type RightPanelStore from "../stores/right-panel/RightPanelStore";
import type WidgetStore from "../stores/WidgetStore";
import type UserActivity from "../UserActivity";
import { type ModalWidgetStore } from "../stores/ModalWidgetStore";
import { type WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
import { type SpaceStoreClass } from "../stores/spaces/SpaceStore";
import type TypingStore from "../stores/TypingStore";
import { type EventIndexPeg } from "../indexing/EventIndexPeg";
import { type VoiceRecordingStore } from "../stores/VoiceRecordingStore";
import type PerformanceMonitor from "../performance";
@@ -41,6 +36,7 @@ import type MatrixChat from "../components/structures/MatrixChat";
import { type InitialCryptoSetupStore } from "../stores/InitialCryptoSetupStore";
import { type ModuleApiType } from "../modules/Api.ts";
import type { RoomListStoreV3Class } from "../stores/room-list-v3/RoomListStoreV3.ts";
import { type SDKContextClass } from "../contexts/SDKContextClass.ts";
/* eslint-disable @typescript-eslint/naming-convention */
@@ -95,14 +91,9 @@ declare global {
mxIntegrationManagers: typeof IntegrationManagers;
singletonModalManager: ModalManager;
mxSettingsStore: SettingsStore;
mxRightPanelStore: RightPanelStore;
mxWidgetStore: WidgetStore;
mxWidgetLayoutStore: WidgetLayoutStore;
mxUserActivity: UserActivity;
mxModalWidgetStore: ModalWidgetStore;
mxSpaceStore: SpaceStoreClass;
mxVoiceRecordingStore: VoiceRecordingStore;
mxTypingStore: TypingStore;
mxEventIndexPeg: EventIndexPeg;
mxPerformanceMonitor: PerformanceMonitor;
mxPerformanceEntryNames: any;
@@ -114,6 +105,7 @@ declare global {
mxOnRecaptchaLoaded?: () => void;
mxModuleLoader: ModuleLoader;
mxModuleApi: ModuleApiType;
mxSdkContext: SDKContextClass;
// electron-only
electron?: Electron;
@@ -93,7 +93,6 @@ import PerformanceMonitor, { PerformanceEntryNames } from "../../performance";
import UIStore, { UI_EVENTS } from "../../stores/UIStore";
import SoftLogout from "./auth/SoftLogout";
import { copyPlaintext } from "../../utils/strings";
import { PosthogAnalytics } from "../../PosthogAnalytics";
import { initSentry } from "../../sentry";
import { showSpaceInvite } from "../../utils/space";
import { type ButtonEvent } from "../views/elements/AccessibleButton";
@@ -245,6 +244,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
super(props);
this.stores = SDKContextClass.instance;
this.stores.constructEagerStores();
window.mxSdkContext = this.stores;
this.state = {
view: Views.LOADING,
@@ -1792,7 +1792,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
// Cannot be done in OnLoggedIn as at that point the AccountSettingsHandler doesn't yet have a client
// Will be moved to a pre-login flow as well
if (PosthogAnalytics.instance.isEnabled() && SettingsStore.isLevelSupported(SettingLevel.ACCOUNT)) {
if (this.stores.posthogAnalytics.isEnabled() && SettingsStore.isLevelSupported(SettingLevel.ACCOUNT)) {
this.initPosthogAnalyticsToast();
}
@@ -6,7 +6,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, { type JSX, createRef, type ReactNode, type TransitionEventHandler } from "react";
import React, { type JSX, createRef, type ReactNode, type TransitionEventHandler, useContext } from "react";
import classNames from "classnames";
import {
type Room,
@@ -58,6 +58,7 @@ import { _t } from "../../languageHandler";
import { getLateEventInfo } from "./grouper/LateEventGrouper";
import { DateSeparatorViewModel } from "../../viewmodels/room/timeline/DateSeparatorViewModel";
import { isEligibleForSpecialReceipt } from "../../viewmodels/room/timeline/event-tile/EventTileReceiptState";
import { SDKContext } from "../../contexts/SDKContext.ts";
const CONTINUATION_MAX_INTERVAL = 5 * 60 * 1000; // 5 minutes
const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
@@ -66,7 +67,10 @@ const continuedTypes = [EventType.Sticker, EventType.RoomMessage];
* Creates and auto-disposes the DateSeparatorViewModel for message panel rendering.
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
const sdkContext = useContext(SDKContext);
const vm = useCreateAutoDisposedViewModel(
() => new DateSeparatorViewModel({ roomId, ts, roomViewStore: sdkContext.roomViewStore }),
);
return <DateSeparatorView vm={vm} className="mx_TimelineSeparator" />;
}
@@ -12,7 +12,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { useCreateAutoDisposedViewModel, WidgetPipView } from "@element-hq/web-shared-components";
import LegacyCallView from "../views/voip/LegacyCallView";
import type LegacyCallHandler from "../../LegacyCallHandler";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import PictureInPictureDragger, { type CreatePipChildren } from "./PictureInPictureDragger";
import dis from "../../dispatcher/dispatcher";
@@ -54,44 +53,6 @@ interface IState {
showWidgetInPip: boolean;
}
// Splits a list of calls into one 'primary' one and a list
// (which should be a single element) of other calls.
// The primary will be the one not on hold, or an arbitrary one
// if they're all on hold)
function getPrimarySecondaryCallsForPip(
legacyCallHandler: LegacyCallHandler,
roomId: string | null,
): [MatrixCall | null, MatrixCall[]] {
if (!roomId) return [null, []];
const calls = legacyCallHandler.getAllActiveCallsForPip(roomId);
let primary: MatrixCall | null = null;
let secondaries: MatrixCall[] = [];
for (const call of calls) {
if (!SHOW_CALL_IN_STATES.includes(call.state)) continue;
if (!call.isRemoteOnHold() && primary === null) {
primary = call;
} else {
secondaries.push(call);
}
}
if (primary === null && secondaries.length > 0) {
primary = secondaries[0];
secondaries = secondaries.slice(1);
}
if (secondaries.length > 1) {
// We should never be in more than two calls so this shouldn't happen
logger.log("Found more than 1 secondary call! Other calls will not be shown.");
}
return [primary, secondaries];
}
/**
* PipContainer shows a small version of the LegacyCallView or a sticky widget hovering over the UI in
* 'picture-in-picture' (PiP mode). It displays the call(s) which is *not* in the room the user is currently viewing
@@ -103,11 +64,11 @@ class PipContainerInner extends React.Component<IProps, IState> {
declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
super(props);
super(props, context);
const roomId = context.roomViewStore.getRoomId();
const roomId = this.context.roomViewStore.getRoomId();
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(context.legacyCallHandler, roomId);
const [primaryCall, secondaryCalls] = this.getPrimarySecondaryCallsForPip(roomId);
this.state = {
viewedRoomId: roomId || undefined,
@@ -147,6 +108,43 @@ class PipContainerInner extends React.Component<IProps, IState> {
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Undock, this.onWidgetDockChanges);
}
/**
* Splits a list of calls into one 'primary' one and a list
* (which should be a single element) of other calls.
* The primary will be the one not on hold, or an arbitrary one
* if they're all on hold)
*/
private getPrimarySecondaryCallsForPip(roomId: string | null): [MatrixCall | null, MatrixCall[]] {
if (!roomId) return [null, []];
const calls = this.context.legacyCallHandler.getAllActiveCallsForPip(roomId);
let primary: MatrixCall | null = null;
let secondaries: MatrixCall[] = [];
for (const call of calls) {
if (!SHOW_CALL_IN_STATES.includes(call.state)) continue;
if (!call.isRemoteOnHold() && primary === null) {
primary = call;
} else {
secondaries.push(call);
}
}
if (primary === null && secondaries.length > 0) {
primary = secondaries[0];
secondaries = secondaries.slice(1);
}
if (secondaries.length > 1) {
// We should never be in more than two calls so this shouldn't happen
logger.log("Found more than 1 secondary call! Other calls will not be shown.");
}
return [primary, secondaries];
}
private onMove = (): void => this.props.movePersistedElement.current?.();
private onRoomViewStoreUpdate = (): void => {
@@ -165,7 +163,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
}
if (!newRoomId) return;
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.context.legacyCallHandler, newRoomId);
const [primaryCall, secondaryCalls] = this.getPrimarySecondaryCallsForPip(newRoomId);
this.setState({
viewedRoomId: newRoomId,
primaryCall: primaryCall,
@@ -184,10 +182,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
private updateCalls = (): void => {
if (!this.state.viewedRoomId) return;
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(
this.context.legacyCallHandler,
this.state.viewedRoomId,
);
const [primaryCall, secondaryCalls] = this.getPrimarySecondaryCallsForPip(this.state.viewedRoomId);
this.setState({
primaryCall: primaryCall,
@@ -198,10 +193,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
private onCallRemoteHold = (): void => {
if (!this.state.viewedRoomId) return;
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(
this.context.legacyCallHandler,
this.state.viewedRoomId,
);
const [primaryCall, secondaryCalls] = this.getPrimarySecondaryCallsForPip(this.state.viewedRoomId);
this.setState({
primaryCall: primaryCall,
@@ -6,7 +6,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, { type ReactNode } from "react";
import React, { type ReactNode, useContext } from "react";
import { EventType, M_BEACON_INFO, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { DateSeparatorView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
@@ -19,6 +19,7 @@ import { _t } from "../../../languageHandler";
import NewRoomIntro from "../../views/rooms/NewRoomIntro";
import GenericEventListSummary from "../../views/elements/GenericEventListSummary";
import { DateSeparatorViewModel } from "../../../viewmodels/room/timeline/DateSeparatorViewModel";
import { SDKContext } from "../../../contexts/SDKContext.ts";
// Wrap initial room creation events into a GenericEventListSummary
// Grouping only events sent by the same user that sent the `m.room.create` and only until
@@ -28,7 +29,10 @@ import { DateSeparatorViewModel } from "../../../viewmodels/room/timeline/DateSe
* Creates and auto-disposes the DateSeparatorViewModel for creation-group rendering.
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): ReactNode {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
const sdkContext = useContext(SDKContext);
const vm = useCreateAutoDisposedViewModel(
() => new DateSeparatorViewModel({ roomId, ts, roomViewStore: sdkContext.roomViewStore }),
);
return <DateSeparatorView vm={vm} className="mx_TimelineSeparator" />;
}
@@ -6,7 +6,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, { type ReactNode } from "react";
import React, { type ReactNode, useContext } from "react";
import { EventType, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { DateSeparatorView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
@@ -18,6 +18,7 @@ import { MatrixClientPeg } from "../../../MatrixClientPeg";
import HistoryTile from "../../views/rooms/HistoryTile";
import EventListSummary from "../../views/elements/EventListSummary";
import { DateSeparatorViewModel } from "../../../viewmodels/room/timeline/DateSeparatorViewModel";
import { SDKContext } from "../../../contexts/SDKContext.ts";
const groupedStateEvents = [
EventType.RoomMember,
@@ -30,7 +31,10 @@ const groupedStateEvents = [
* Creates and auto-disposes the DateSeparatorViewModel for grouped timeline rendering.
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): ReactNode {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
const sdkContext = useContext(SDKContext);
const vm = useCreateAutoDisposedViewModel(
() => new DateSeparatorViewModel({ roomId, ts, roomViewStore: sdkContext.roomViewStore }),
);
return <DateSeparatorView vm={vm} className="mx_TimelineSeparator" />;
}
@@ -20,6 +20,7 @@ import ScrollPanel from "../../structures/ScrollPanel";
import Spinner from "../elements/Spinner";
import EditHistoryMessage from "../messages/EditHistoryMessage";
import { DateSeparatorViewModel } from "../../../viewmodels/room/timeline/DateSeparatorViewModel";
import { SDKContextClass } from "../../../contexts/SDKContextClass.ts";
interface IProps {
mxEvent: MatrixEvent;
@@ -54,7 +55,7 @@ export default class MessageEditHistoryDialog extends React.PureComponent<IProps
const key = `${roomId}-${ts}`;
let vm = this.dateSeparatorVms.get(key);
if (!vm) {
vm = new DateSeparatorViewModel({ roomId, ts });
vm = new DateSeparatorViewModel({ roomId, ts, roomViewStore: SDKContextClass.instance.roomViewStore });
this.dateSeparatorVms.set(key, vm);
}
return vm;
@@ -41,9 +41,8 @@ import { type ICompletion } from "../../../autocomplete/Autocompleter";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { ALTERNATE_KEY_NAME, KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { _t } from "../../../languageHandler";
import { SDKContextClass } from "../../../contexts/SDKContextClass";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Landmark, LandmarkNavigation } from "../../../accessibility/LandmarkNavigation";
import { SDKContext } from "../../../contexts/SDKContext.ts";
// matches emoticons which follow the start of a line or whitespace
const REGEX_EMOTICON_WHITESPACE = new RegExp("(?:^|\\s)(" + EMOTICON_REGEX.source + ")\\s|:^$");
@@ -115,6 +114,9 @@ interface IState {
}
export default class BasicMessageEditor extends React.Component<IProps, IState> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
public readonly editorRef = createRef<HTMLDivElement>();
private autocompleteRef = createRef<Autocomplete>();
private formatBarRef = createRef<MessageComposerFormatBar>();
@@ -245,17 +247,13 @@ export default class BasicMessageEditor extends React.Component<IProps, IState>
const { cmd } = parseCommandString(this.props.model.parts[0].text);
const command = CommandMap.get(cmd!);
if (
!command?.isEnabled(MatrixClientPeg.get(), this.props.room.roomId) ||
!command?.isEnabled(this.context.client!, this.props.room.roomId) ||
command.category !== CommandCategories.messages
) {
isTyping = false;
}
}
SDKContextClass.instance.typingStore.setSelfTyping(
this.props.room.roomId,
this.props.threadId ?? null,
isTyping,
);
this.context.typingStore.setSelfTyping(this.props.room.roomId, this.props.threadId ?? null, isTyping);
this.props.onChange?.(selection, inputType, diff);
};
@@ -5,7 +5,7 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { useCallback, type JSX, type ReactNode } from "react";
import React, { useCallback, type JSX, type ReactNode, useContext } from "react";
import {
RoomListView as SharedRoomListView,
useCreateAutoDisposedViewModel,
@@ -13,21 +13,28 @@ import {
} from "@element-hq/web-shared-components";
import { type Room } from "matrix-js-sdk/src/matrix";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext";
import { RoomAvatarView } from "../../avatars/RoomAvatarView";
import { getKeyBindingsManager } from "../../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts";
import { Landmark, LandmarkNavigation } from "../../../../accessibility/LandmarkNavigation";
import { RoomListViewModel } from "../../../../viewmodels/room-list/RoomListViewModel";
import { SDKContext } from "../../../../contexts/SDKContext.ts";
/**
* RoomListView component using shared components with proper MVVM pattern.
*/
export function RoomListView(): JSX.Element {
const matrixClient = useMatrixClientContext();
const sdkContext = useContext(SDKContext);
// Create and auto-dispose ViewModel instance
const vm = useCreateAutoDisposedViewModel(() => new RoomListViewModel({ client: matrixClient }));
const vm = useCreateAutoDisposedViewModel(
() =>
new RoomListViewModel({
client: sdkContext.client!,
roomViewStore: sdkContext.roomViewStore,
spaceStore: sdkContext.spaceStore,
}),
);
// Render avatar for each room - memoized to prevent re-renders
const renderAvatar = useCallback((room: SharedRoom): ReactNode => {
@@ -7,7 +7,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, { type JSX } from "react";
import React, { type JSX, useContext } from "react";
import { type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { DateSeparatorView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
@@ -22,6 +22,7 @@ import { buildLegacyCallEventGroupers } from "../../structures/LegacyCallEventGr
import { haveRendererForEvent } from "../../../events/EventTileFactory";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { DateSeparatorViewModel } from "../../../viewmodels/room/timeline/DateSeparatorViewModel";
import { SDKContext } from "../../../contexts/SDKContext.ts";
interface IProps {
// a list of strings to be highlighted in the results
@@ -39,7 +40,10 @@ interface IProps {
* Creates and auto-disposes the DateSeparatorViewModel for search result rendering.
*/
function DateSeparatorWrapper({ roomId, ts }: { roomId: string; ts: number }): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new DateSeparatorViewModel({ roomId, ts }));
const sdkContext = useContext(SDKContext);
const vm = useCreateAutoDisposedViewModel(
() => new DateSeparatorViewModel({ roomId, ts, roomViewStore: sdkContext.roomViewStore }),
);
return <DateSeparatorView vm={vm} className="mx_TimelineSeparator" />;
}
@@ -6,7 +6,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, { type JSX } from "react";
import React, { type JSX, useContext } from "react";
import classNames from "classnames";
import { SettingsSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import { IconButton, Text, Tooltip } from "@vector-im/compound-web";
@@ -20,14 +20,15 @@ import { Action } from "../../../dispatcher/actions";
import QuickThemeSwitcher from "./QuickThemeSwitcher";
import Modal from "../../../Modal";
import DevtoolsDialog from "../dialogs/DevtoolsDialog";
import { SDKContextClass } from "../../../contexts/SDKContextClass";
import { SDKContext } from "../../../contexts/SDKContext.ts";
const QuickSettingsButton: React.FC<{
isPanelCollapsed: boolean;
}> = ({ isPanelCollapsed = false }) => {
const sdkContext = useContext(SDKContext);
const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu<HTMLButtonElement>();
const currentRoomId = SDKContextClass.instance.roomViewStore.getRoomId();
const currentRoomId = sdkContext.roomViewStore.getRoomId();
const developerModeEnabled = useSettingValue("developerMode");
let contextMenu: JSX.Element | undefined;
@@ -10,12 +10,11 @@ import React, { type FC, useContext, useEffect, type AriaRole, useCallback } fro
import type { Room } from "matrix-js-sdk/src/matrix";
import { type Call, CallEvent } from "../../../models/Call";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AppTile from "../elements/AppTile";
import { CallStore } from "../../../stores/CallStore";
import { SDKContextClass } from "../../../contexts/SDKContextClass";
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import { useCall } from "../../../hooks/useCall";
import { SDKContext } from "../../../contexts/SDKContext.ts";
interface JoinCallViewProps {
room: Room;
@@ -26,7 +25,7 @@ interface JoinCallViewProps {
}
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, role, onClose }) => {
const cli = useContext(MatrixClientContext);
const sdkContext = useContext(SDKContext);
useTypedEventEmitter(call, CallEvent.Close, onClose);
useEffect(() => {
@@ -38,17 +37,17 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, role, onClo
// The stickyPromise has to resolve before the widget actually becomes sticky.
// We only let the widget become sticky after disconnecting all other active calls.
const calls = [...CallStore.instance.connectedCalls].filter(
(call) => SDKContextClass.instance.roomViewStore.getRoomId() !== call.roomId,
(call) => sdkContext.roomViewStore.getRoomId() !== call.roomId,
);
await Promise.all(calls.map(async (call) => await call.disconnect()));
}, []);
}, [sdkContext.roomViewStore]);
return (
<div className="mx_CallView" role={role}>
<AppTile
app={call.widget}
room={room}
userId={cli.credentials.userId!}
userId={sdkContext.client?.credentials.userId ?? undefined}
creatorUserId={call.widget.creatorUserId}
waitForIframeLoad={call.widget.waitForIframeLoad}
showMenubar={false}
-1
View File
@@ -162,7 +162,6 @@ export class SDKContextClass {
public get typingStore(): TypingStore {
if (!this._TypingStore) {
this._TypingStore = new TypingStore(this);
window.mxTypingStore = this._TypingStore;
}
return this._TypingStore;
}
+5 -4
View File
@@ -8,7 +8,7 @@ Please see LICENSE files in the repository root for full details.
import { type Room } from "matrix-js-sdk/src/matrix";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { type ReactNode, useCallback, useEffect, useMemo, useState } from "react";
import { type ReactNode, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { logger as rootLogger } from "matrix-js-sdk/src/logger";
import type React from "react";
@@ -37,8 +37,8 @@ import { type InteractionName } from "../../PosthogTrackers";
import { ElementCallMemberEventType } from "../../call-types";
import { LocalRoom, LocalRoomState } from "../../models/LocalRoom";
import { useScopedRoomContext } from "../../contexts/ScopedRoomContext";
import { SDKContext } from "../../contexts/SDKContext.ts";
import SdkConfig from "../../SdkConfig";
import { SDKContextClass } from "../../contexts/SDKContextClass";
const logger = rootLogger.getChild("useRoomCall");
@@ -107,6 +107,7 @@ export const useRoomCall = (
showVideoCallButton: boolean;
showVoiceCallButton: boolean;
} => {
const sdkContext = useContext(SDKContext);
const roomViewStore = useScopedRoomContext("roomViewStore").roomViewStore;
// settings
const widgetsFeatureEnabled = useSettingValue(UIFeature.Widgets);
@@ -133,9 +134,9 @@ export const useRoomCall = (
}, [useElementCallExclusively, serverIsConfiguredForElementCall]);
const hasLegacyCall = useEventEmitterState(
SDKContextClass.instance.legacyCallHandler,
sdkContext.legacyCallHandler,
LegacyCallHandlerEvent.CallsChanged,
() => SDKContextClass.instance.legacyCallHandler.getCallForRoom(room.roomId) !== null,
() => sdkContext.legacyCallHandler.getCallForRoom(room.roomId) !== null,
);
// settings
const widgets = useWidgets(room);
+4 -1
View File
@@ -69,7 +69,10 @@ describe("useRoomCall", () => {
});
function render() {
return renderHook(() => useRoomCall(room), withContexts({ matrixClient: client, roomContext }));
return renderHook(
() => useRoomCall(room),
withContexts({ matrixClient: client, roomContext, sdkContext: SDKContextClass.instance }),
);
}
describe("Element Call focus detection", () => {
-2
View File
@@ -202,5 +202,3 @@ export default class WidgetStore extends AsyncStoreWithClient<EmptyObject> {
this.emit(UPDATE_EVENT, roomId);
}
}
window.mxWidgetStore = WidgetStore.instance;
@@ -454,5 +454,3 @@ export default class RightPanelStore extends ReadyWatchingStore {
return this.internalInstance;
}
}
window.mxRightPanelStore = RightPanelStore.instance;
-2
View File
@@ -1370,5 +1370,3 @@ export default class SpaceStore {
return store;
}
}
window.mxSpaceStore = SpaceStore.instance;
@@ -514,5 +514,3 @@ export class WidgetLayoutStore extends ReadyWatchingStore {
this.recalculateRoom(room); // call to try local echo on changes (the catch above undoes any errors)
}
}
window.mxWidgetLayoutStore = WidgetLayoutStore.instance;
@@ -262,6 +262,7 @@ export default class HTMLExporter extends Exporter {
roomId: event.getRoomId()!,
ts,
forExport: true,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
try {
const dateSeparator = (
@@ -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 SpaceStore from "../../stores/spaces/SpaceStore";
import { type SpaceStoreClass } from "../../stores/spaces/SpaceStore";
import RoomListStoreV3, {
RoomListStoreV3Event,
type RoomsResult,
@@ -34,7 +34,6 @@ import {
UPDATE_STATUS_INDICATOR,
} from "../../stores/notifications/RoomNotificationStateStore";
import { RoomListItemViewModel } from "./RoomListItemViewModel";
import { SDKContextClass } from "../../contexts/SDKContextClass";
import { hasCreateRoomRights } from "./utils";
import { keepIfSame } from "../../utils/keepIfSame";
import { DefaultTagID } from "../../stores/room-list-v3/skip-list/tag";
@@ -43,6 +42,7 @@ import { getCustomSectionData, isCustomSectionTag, CHATS_TAG } from "../../store
import { tagRoom } from "../../utils/room/tagRoom";
import { getSectionTagForRoom } from "../../utils/room/getSectionTagForRoom";
import SettingsStore from "../../settings/SettingsStore";
import { type RoomViewStore } from "../../stores/RoomViewStore.tsx";
/**
* Tracks the position of the active room within a specific section.
@@ -58,6 +58,8 @@ interface StickyRoomPosition {
interface RoomListViewModelProps {
client: MatrixClient;
roomViewStore: RoomViewStore;
spaceStore: SpaceStoreClass;
}
const filterKeyToIdMap: Map<FilterEnum, FilterId> = new Map([
@@ -158,7 +160,7 @@ export class RoomListViewModel
private scrollToIndex?: (index: number) => void;
public constructor(props: RoomListViewModelProps) {
const activeSpace = SpaceStore.instance.activeSpaceRoom;
const activeSpace = props.spaceStore.activeSpaceRoom;
// Get initial rooms
const roomsResult = RoomListStoreV3.instance.getSortedRoomsInActiveSpace(undefined);
@@ -575,7 +577,7 @@ export class RoomListViewModel
* Migrated from useRoomListNavigation hook.
*/
private handleViewRoomDelta(payload: ViewRoomDeltaPayload): void {
const currentRoomId = SDKContextClass.instance.roomViewStore.getRoomId();
const currentRoomId = this.props.roomViewStore.getRoomId();
if (!currentRoomId) return;
const { delta, unread } = payload;
@@ -646,7 +648,7 @@ export class RoomListViewModel
}
// Space changed - get the last selected room for the new space to prevent flicker
const lastSelectedRoom = SpaceStore.instance.getLastSelectedRoomIdForSpace(newSpaceId);
const lastSelectedRoom = this.props.spaceStore.getLastSelectedRoomIdForSpace(newSpaceId);
this.updateRoomListData(true, lastSelectedRoom);
return;
@@ -753,7 +755,7 @@ export class RoomListViewModel
): Promise<void> {
// Determine the room ID to use for calculations
// Use override if provided (e.g., during space changes), otherwise fall back to RoomViewStore
const roomId = roomIdOverride ?? SDKContextClass.instance.roomViewStore.getRoomId();
const roomId = roomIdOverride ?? this.props.roomViewStore.getRoomId();
// Apply sticky room logic to keep selected room at same position within its section
const stickySections = this.applyStickyRoom(isRoomChange, roomId);
@@ -858,7 +860,7 @@ export class RoomListViewModel
};
public createRoom = (): void => {
const activeSpace = SpaceStore.instance.activeSpaceRoom;
const activeSpace = this.props.spaceStore.activeSpaceRoom;
if (activeSpace) {
dispatcher.dispatch({
action: Action.CreateRoom,
@@ -28,7 +28,7 @@ import { UIFeature } from "../../../settings/UIFeature";
import ErrorDialog from "../../../components/views/dialogs/ErrorDialog";
import BugReportDialog from "../../../components/views/dialogs/BugReportDialog";
import AccessibleButton from "../../../components/views/elements/AccessibleButton";
import { SDKContextClass } from "../../../contexts/SDKContextClass";
import { type RoomViewStore } from "../../../stores/RoomViewStore.tsx";
export interface DateSeparatorViewModelProps {
/**
@@ -43,6 +43,10 @@ export interface DateSeparatorViewModelProps {
* Export mode disables relative date labels and jump-to-date menu UI.
*/
forExport?: boolean;
/**
* The room view store instance to use
*/
roomViewStore: RoomViewStore;
}
/**
@@ -159,7 +163,7 @@ export class DateSeparatorViewModel
// Only try to navigate to the room if the user is still viewing the same
// room. We don't want to jump someone back to a room after a slow request
// if they've already navigated away to another room.
const currentRoomId = SDKContextClass.instance.roomViewStore.getRoomId();
const currentRoomId = this.props.roomViewStore.getRoomId();
if (currentRoomId === roomIdForJumpRequest) {
dispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@@ -185,7 +189,7 @@ export class DateSeparatorViewModel
// don't want to worry someone about an error in a room they no longer care
// about after a slow request if they've already navigated away to another
// room.
const currentRoomId = SDKContextClass.instance.roomViewStore.getRoomId();
const currentRoomId = this.props.roomViewStore.getRoomId();
if (currentRoomId === roomIdForJumpRequest) {
let friendlyErrorMessage = "An error occured while trying to find and jump to the given date.";
let submitDebugLogsContent: React.ReactElement = <></>;
+1
View File
@@ -188,6 +188,7 @@ export const mockClientMethodsCrypto = (): Partial<
});
export const mockClientMethodsRooms = (rooms: Room[] = []): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getVisibleRooms: vi.fn().mockReturnValue(rooms),
getRooms: vi.fn().mockReturnValue(rooms),
getRoom: vi.fn((roomId) => rooms.find((r) => r.roomId === roomId) ?? null),
isRoomEncrypted: vi.fn(),
@@ -37,6 +37,7 @@ import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomCo
import { untilDispatch } from "../../../test-utils/utilities.ts";
import { TimelineRenderingType } from "../../../../src/contexts/RoomContext.ts";
import { type ComposerInsertPayload, ComposerType } from "../../../../src/dispatcher/payloads/ComposerInsertPayload.ts";
import { SDKContext } from "../../../../src/contexts/SDKContext.ts";
describe("ThreadView", () => {
const ROOM_ID = "!roomId:example.org";
@@ -73,7 +74,11 @@ describe("ThreadView", () => {
}
async function getComponent(initialEvent?: MatrixEvent): Promise<RenderResult> {
const renderResult = render(<TestThreadView initialEvent={initialEvent} />);
const renderResult = render(<TestThreadView initialEvent={initialEvent} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
await waitFor(() => {
expect(() => getByTestId(renderResult.container, "spinner")).toThrow();
@@ -24,7 +24,6 @@ import { clientAndSDKContextRenderOptions, stubClient } from "../../../../test-u
import { Action } from "../../../../../src/dispatcher/actions";
import dis from "../../../../../src/dispatcher/dispatcher";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore";
@@ -194,13 +193,11 @@ describe("AppTile", () => {
);
renderResult.rerender(
<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r2}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r2, r2.roomId)}
/>
</MatrixClientContext.Provider>,
<RightPanel
room={r2}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r2, r2.roomId)}
/>,
);
expect(renderResult.queryByText("Example 1")).not.toBeInTheDocument();
@@ -278,13 +275,11 @@ describe("AppTile", () => {
}),
);
renderResult.rerender(
<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r2}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r2, r2.roomId)}
/>
</MatrixClientContext.Provider>,
<RightPanel
room={r2}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r2, r2.roomId)}
/>,
);
await waitFor(() => {
@@ -283,6 +283,14 @@ exports[`<ExtensionsCard /> should render widgets 1`] = `
</svg>
</div>
</div>
<a
class="_link_k9ljz_8"
data-kind="primary"
data-size="md"
rel="noreferrer noopener"
>
Set layout for everyone
</a>
</div>
</div>
</DocumentFragment>
@@ -20,6 +20,7 @@ import { CommandPartCreator } from "../../../../../src/editor/parts";
import DocumentOffset from "../../../../../src/editor/offset";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SDKContext } from "../../../../../src/contexts/SDKContext.ts";
describe("BasicMessageComposer", () => {
const renderer = createRenderer();
@@ -35,7 +36,11 @@ describe("BasicMessageComposer", () => {
it("should allow a user to paste a URL without it being mangled", async () => {
const model = new EditorModel([], pc, renderer);
render(<BasicMessageComposer model={model} room={room} />);
render(<BasicMessageComposer model={model} room={room} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
const testUrl = "https://element.io";
const mockDataTransfer = generateMockDataTransferForString(testUrl);
await userEvent.paste(mockDataTransfer);
@@ -51,7 +56,11 @@ describe("BasicMessageComposer", () => {
});
userEvent.setup();
const model = new EditorModel([], pc, renderer);
render(<BasicMessageComposer model={model} room={room} />);
render(<BasicMessageComposer model={model} room={room} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
const tranformations = [
{ before: "4:3 video", after: "4:3 video" },
@@ -81,7 +90,11 @@ describe("BasicMessageComposer", () => {
it("should not mangle shift-enter when the autocomplete is open", async () => {
const model = new EditorModel([], pc, renderer);
render(<BasicMessageComposer model={model} room={room} />);
render(<BasicMessageComposer model={model} room={room} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
const input = screen.getByRole("textbox");
@@ -93,7 +106,11 @@ describe("BasicMessageComposer", () => {
it("should escape single quote in placeholder", async () => {
const model = new EditorModel([], pc, renderer);
const composer = render(<BasicMessageComposer placeholder="Don't" model={model} room={room} />);
const composer = render(<BasicMessageComposer placeholder="Don't" model={model} room={room} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
const input = composer.queryAllByRole("textbox");
const placeholder = input[0].style.getPropertyValue("--placeholder");
expect(placeholder).toMatch("'Don\\'t'");
@@ -101,7 +118,11 @@ describe("BasicMessageComposer", () => {
it("should escape backslash in placeholder", async () => {
const model = new EditorModel([], pc, renderer);
const composer = render(<BasicMessageComposer placeholder={"w\\e"} model={model} room={room} />);
const composer = render(<BasicMessageComposer placeholder={"w\\e"} model={model} room={room} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
const input = composer.queryAllByRole("textbox");
const placeholder = input[0].style.getPropertyValue("--placeholder");
expect(placeholder).toMatch("'w\\\\e'");
@@ -116,7 +137,11 @@ describe("BasicMessageComposer", () => {
// spy on typingStore.setSelfTyping
const spy = jest.spyOn(SDKContextClass.instance.typingStore, "setSelfTyping");
render(<BasicMessageComposer model={model} room={room} />);
render(<BasicMessageComposer model={model} room={room} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
// simulate typing by updating the model - this will call the component's update callback
await model.update(commandPart.text, "insertText", new DocumentOffset(commandPart.text.length, true));
@@ -128,7 +153,11 @@ describe("BasicMessageComposer", () => {
it("should ignore keydown events during IME composition", () => {
const model = new EditorModel([], pc, renderer);
render(<BasicMessageComposer model={model} room={room} />);
render(<BasicMessageComposer model={model} room={room} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
const input = screen.getByRole("textbox");
// Start IME composition
@@ -167,7 +196,11 @@ describe("BasicMessageComposer", () => {
it("should handle keydown events normally when not composing", () => {
const model = new EditorModel([], pc, renderer);
render(<BasicMessageComposer model={model} room={room} />);
render(<BasicMessageComposer model={model} room={room} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
const input = screen.getByRole("textbox");
// Simulate Tab key when NOT composing
@@ -33,6 +33,8 @@ import NotifProvider from "../../../../../src/autocomplete/NotifProvider";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import type { RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
import { SDKContext } from "../../../../../src/contexts/SDKContext.ts";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
describe("<EditMessageComposer/>", () => {
const userId = "@alice:server.org";
@@ -78,9 +80,11 @@ describe("<EditMessageComposer/>", () => {
const getComponent = (editState: EditorStateTransfer, roomContext: RoomContextType = defaultRoomContext) =>
render(<EditMessageComposerWithMatrixClient editState={editState} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
<SDKContext.Provider value={SDKContextClass.instance}>
<MatrixClientContext.Provider value={mockClient}>
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
</SDKContext.Provider>
),
});
@@ -51,6 +51,8 @@ import SettingsStore from "../../../../../src/settings/SettingsStore";
import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import PlatformPeg from "../../../../../src/PlatformPeg";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
import { SDKContext } from "../../../../../src/contexts/SDKContext.ts";
function getTile(container: HTMLElement): HTMLElement {
const tile = container.querySelector(".mx_EventTile");
@@ -1652,7 +1654,11 @@ describe("EventTile", () => {
return Element.prototype.matches.call(this, selector);
});
const { container, rerender } = render(<WrappedEventTiles events={events} editEvent={firstEvent} />);
const { container, rerender } = render(<WrappedEventTiles events={events} editEvent={firstEvent} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
const editingTile = container.querySelector(".mx_EventTile_isEditing");
expect(editingTile).not.toBeNull();
@@ -37,6 +37,8 @@ import { Action } from "../../../../../src/dispatcher/actions";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import { TimelineRenderingType, type RoomContextType } from "../../../../../src/contexts/RoomContext.ts";
import { RoomUploadContextProvider } from "../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
import { SDKContext } from "../../../../../src/contexts/SDKContext.ts";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
const openStickerPicker = async (): Promise<void> => {
await userEvent.click(screen.getByLabelText("More options"));
@@ -469,13 +471,15 @@ function wrapAndRender(
};
const getRawComponent = (props = {}, context = roomContext, client = mockClient) => (
<MatrixClientContext.Provider value={client}>
<ScopedRoomContextProvider {...context}>
<RoomUploadContextProvider>
<MessageComposer {...defaultProps} {...props} />
</RoomUploadContextProvider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
<SDKContext.Provider value={SDKContextClass.instance}>
<MatrixClientContext.Provider value={client}>
<ScopedRoomContextProvider {...context}>
<RoomUploadContextProvider>
<MessageComposer {...defaultProps} {...props} />
</RoomUploadContextProvider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
</SDKContext.Provider>
);
return {
rawComponent: getRawComponent(props, roomContext, mockClient),
@@ -59,6 +59,7 @@ import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore
import { UIFeature } from "../../../../../../src/settings/UIFeature";
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
import { ElementCallMemberEventType } from "../../../../../../src/call-types";
import { SDKContext } from "../../../../../../src/contexts/SDKContext";
import { SDKContextClass } from "../../../../../../src/contexts/SDKContextClass.ts";
jest.mock("../../../../../../src/utils/ShieldUtils");
@@ -93,9 +94,11 @@ describe("RoomHeader", () => {
function getWrapper(): RenderOptions {
return {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={client}>
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
<SDKContext.Provider value={SDKContextClass.instance}>
<MatrixClientContext.Provider value={client}>
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
</SDKContext.Provider>
),
};
}
@@ -10,9 +10,10 @@ import React from "react";
import { MatrixEvent, Room, EventType } from "matrix-js-sdk/src/matrix";
import { render, type RenderResult } from "jest-matrix-react";
import { stubClient, withClientContextRenderOptions } from "../../../../test-utils";
import { clientAndSDKContextRenderOptions, stubClient } from "../../../../test-utils";
import SearchResultTile from "../../../../../src/components/views/rooms/SearchResultTile";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts";
const ROOM_ID = "!qPewotXpIctQySfjSy:localhost";
@@ -30,7 +31,7 @@ describe("SearchResultTile", () => {
function renderComponent(props: Partial<Props>): RenderResult {
return render(
<SearchResultTile timeline={[]} ourEventsIndexes={[1]} {...props} />,
withClientContextRenderOptions(MatrixClientPeg.safeGet()),
clientAndSDKContextRenderOptions(MatrixClientPeg.safeGet(), SDKContextClass.instance),
);
}
@@ -35,6 +35,7 @@ import { addTextToComposer } from "../../../../test-utils/composer";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass";
import { RoomUploadContextProvider } from "../../../../../src/viewmodels/room/RoomUploadViewModel.tsx";
import { SDKContext } from "../../../../../src/contexts/SDKContext.ts";
jest.mock("../../../../../src/utils/local-room", () => ({
doMaybeLocalRoomAction: jest.fn(),
@@ -199,7 +200,11 @@ describe("<SendMessageComposer/>", () => {
</MatrixClientContext.Provider>
);
const getComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => {
return render(getRawComponent(props, roomContext, client));
return render(getRawComponent(props, roomContext, client), {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
};
it("renders text and placeholder correctly", () => {
@@ -447,6 +452,11 @@ describe("<SendMessageComposer/>", () => {
</RoomUploadContextProvider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
{
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
},
);
const composer = container.querySelector<HTMLDivElement>(".mx_BasicMessageComposer_input")!;
@@ -14,12 +14,17 @@ import { mocked } from "jest-mock";
import QuickSettingsButton from "../../../../../src/components/views/spaces/QuickSettingsButton";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass";
import { SDKContext } from "../../../../../src/contexts/SDKContext.ts";
describe("QuickSettingsButton", () => {
const roomId = "!room:example.com";
const renderQuickSettingsButton = () => {
render(<QuickSettingsButton isPanelCollapsed={true} />);
render(<QuickSettingsButton isPanelCollapsed={true} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
};
const getQuickSettingsButton = () => {
@@ -37,7 +42,11 @@ describe("QuickSettingsButton", () => {
});
it("should render the quick settings button in expanded mode", () => {
const { asFragment } = render(<QuickSettingsButton isPanelCollapsed={false} />);
const { asFragment } = render(<QuickSettingsButton isPanelCollapsed={false} />, {
wrapper: ({ children }) => (
<SDKContext.Provider value={SDKContextClass.instance}>{children}</SDKContext.Provider>
),
});
expect(asFragment()).toMatchSnapshot();
});
@@ -9,7 +9,14 @@ import { type MatrixClient, type Room } from "matrix-js-sdk/src/matrix";
import { mocked } from "jest-mock";
import { waitFor } from "jest-matrix-react";
import { createTestClient, flushPromises, flushPromisesWithFakeTimers, mkStubRoom, stubClient } from "../../test-utils";
import {
createTestClient,
flushPromises,
flushPromisesWithFakeTimers,
mkStubRoom,
stubClient,
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";
@@ -44,6 +51,7 @@ jest.mock("../../../src/viewmodels/room-list/utils", () => ({
describe("RoomListViewModel", () => {
let matrixClient: MatrixClient;
let sdkContext: TestSDKContext;
let room1: Room;
let room2: Room;
let room3: Room;
@@ -51,6 +59,8 @@ describe("RoomListViewModel", () => {
beforeEach(() => {
matrixClient = createTestClient();
sdkContext = new TestSDKContext();
sdkContext._client = matrixClient;
room1 = mkStubRoom("!room1:server", "Room 1", matrixClient);
room2 = mkStubRoom("!room2:server", "Room 2", matrixClient);
room3 = mkStubRoom("!room3:server", "Room 3", matrixClient);
@@ -80,7 +90,11 @@ describe("RoomListViewModel", () => {
describe("Initialization", () => {
it("should initialize with correct snapshot", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const snapshot = viewModel.getSnapshot();
expect(snapshot.sections[0].roomIds).toEqual(["!room1:server", "!room2:server", "!room3:server"]);
@@ -97,7 +111,11 @@ describe("RoomListViewModel", () => {
sections: [{ tag: CHATS_TAG, rooms: [] }],
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().sections).toEqual([]);
expect(viewModel.getSnapshot().isRoomListEmpty).toBe(true);
@@ -106,7 +124,11 @@ describe("RoomListViewModel", () => {
it("should set canCreateRoom based on user rights", () => {
mocked(hasCreateRoomRights).mockReturnValue(true);
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().canCreateRoom).toBe(true);
});
@@ -114,7 +136,11 @@ describe("RoomListViewModel", () => {
describe("Room list updates", () => {
it("should update room list when ListsUpdate event fires", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const newRoom = mkStubRoom("!room4:server", "Room 4", matrixClient);
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
@@ -134,7 +160,11 @@ describe("RoomListViewModel", () => {
it("should update loading state when ListsLoaded event fires", () => {
jest.spyOn(RoomListStoreV3.instance, "isLoadingRooms", "get").mockReturnValue(true);
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().isLoadingRooms).toBe(true);
@@ -145,7 +175,11 @@ describe("RoomListViewModel", () => {
// This test ensures that the room list item vms are preserved when the room list is changing
it("should keep existing view model when ListsUpdate event fires", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Create view model for room1
const room1VM = viewModel.getRoomItemViewModel("!room1:server");
@@ -160,7 +194,11 @@ describe("RoomListViewModel", () => {
describe("Space switching", () => {
it("should update room list when space changes", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const spaceRoomList = [room1, room2];
@@ -178,7 +216,11 @@ describe("RoomListViewModel", () => {
});
it("should clear view models when space changes", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Get view models for visible rooms
const vm1 = viewModel.getRoomItemViewModel("!room1:server")!;
@@ -200,7 +242,11 @@ describe("RoomListViewModel", () => {
});
it("should clear roomsMap when space changes and repopulate with new rooms", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const newSpaceRoom = mkStubRoom("!spaceroom:server", "Space Room", matrixClient);
@@ -221,7 +267,11 @@ describe("RoomListViewModel", () => {
describe("Active room tracking", () => {
it("should update active room index when room is selected", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server");
@@ -237,7 +287,11 @@ describe("RoomListViewModel", () => {
});
it("should return undefined active room index when no room is selected", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null);
@@ -255,7 +309,11 @@ describe("RoomListViewModel", () => {
describe("Sticky room behavior", () => {
it("should keep selected room at same index when room list updates", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Select room at index 1
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server");
@@ -281,7 +339,11 @@ describe("RoomListViewModel", () => {
});
it("should not apply sticky behavior when user changes rooms", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Select room at index 1
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server");
@@ -307,7 +369,11 @@ describe("RoomListViewModel", () => {
describe("Filters", () => {
it("should toggle filter on", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().activeFilterId).toBeUndefined();
@@ -324,7 +390,11 @@ describe("RoomListViewModel", () => {
});
it("should toggle filter off", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Turn filter on
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
@@ -363,7 +433,11 @@ describe("RoomListViewModel", () => {
it("hides the Favourites and Low Priority filters when sections are enabled", () => {
mockShowSections(true);
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
roomViewStore: sdkContext.roomViewStore,
spaceStore: sdkContext.spaceStore,
});
const { filterIds } = viewModel.getSnapshot();
expect(filterIds).not.toContain("favourite");
@@ -372,7 +446,11 @@ describe("RoomListViewModel", () => {
it("shows the Favourites and Low Priority filters when sections are disabled", () => {
mockShowSections(false);
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
roomViewStore: sdkContext.roomViewStore,
spaceStore: sdkContext.spaceStore,
});
const { filterIds } = viewModel.getSnapshot();
expect(filterIds).toContain("favourite");
@@ -393,7 +471,11 @@ describe("RoomListViewModel", () => {
return "watcher-id";
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
roomViewStore: sdkContext.roomViewStore,
spaceStore: sdkContext.spaceStore,
});
expect(viewModel.getSnapshot().filterIds).toContain("favourite");
// Activate the Favourites filter
@@ -423,7 +505,11 @@ describe("RoomListViewModel", () => {
describe("Room item view models", () => {
it("should create room item view model on demand", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const itemViewModel = viewModel.getRoomItemViewModel("!room1:server");
@@ -432,7 +518,11 @@ describe("RoomListViewModel", () => {
});
it("should reuse existing room item view model", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const itemViewModel1 = viewModel.getRoomItemViewModel("!room1:server");
const itemViewModel2 = viewModel.getRoomItemViewModel("!room1:server");
@@ -441,13 +531,21 @@ describe("RoomListViewModel", () => {
});
it("should return undefined for non-existent room", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getRoomItemViewModel("!nonexistent:server")).toBeUndefined();
});
it("should not throw when requesting view model for a room removed from the list but still in roomsMap", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Normal list update removes room2 from the list
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
@@ -461,7 +559,11 @@ describe("RoomListViewModel", () => {
});
it("should return undefined for a room from old space after space change", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const spaceRoom = mkStubRoom("!newroom:server", "New Room", matrixClient);
@@ -478,7 +580,11 @@ describe("RoomListViewModel", () => {
});
it("should recover when roomsMap is stale but roomsResult has the room", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Manually clear roomsMap to simulate stale cache, but keep roomsResult intact
(viewModel as any).roomsMap.clear();
@@ -488,7 +594,11 @@ describe("RoomListViewModel", () => {
});
it("should dispose view models for rooms no longer visible", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const vm1 = viewModel.getRoomItemViewModel("!room1:server")!;
const vm2 = viewModel.getRoomItemViewModel("!room2:server")!;
@@ -511,7 +621,11 @@ describe("RoomListViewModel", () => {
describe("Room creation", () => {
it("should dispatch CreateChat action when createChatRoom is called", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const dispatchSpy = jest.spyOn(dispatcher, "fire");
@@ -521,7 +635,11 @@ describe("RoomListViewModel", () => {
});
it("should dispatch CreateRoom action without parent space", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
@@ -536,7 +654,11 @@ describe("RoomListViewModel", () => {
const spaceRoom = mkStubRoom("!space:server", "Space", matrixClient);
jest.spyOn(SpaceStore.instance, "activeSpaceRoom", "get").mockReturnValue(spaceRoom);
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
@@ -556,7 +678,11 @@ describe("RoomListViewModel", () => {
});
it("should navigate to next room when delta is 1", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room1:server");
@@ -579,7 +705,11 @@ describe("RoomListViewModel", () => {
});
it("should navigate to previous room when delta is -1", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room2:server");
@@ -602,7 +732,11 @@ describe("RoomListViewModel", () => {
});
it("should wrap around to last room when navigating backwards from first room", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!room1:server");
@@ -625,7 +759,11 @@ describe("RoomListViewModel", () => {
});
it("should not navigate when current room is not found", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!unknown:server");
@@ -649,7 +787,11 @@ describe("RoomListViewModel", () => {
});
it("should not navigate when no room is selected", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue(null);
@@ -682,7 +824,11 @@ describe("RoomListViewModel", () => {
});
it("should dispose all room item view models on dispose", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const vm1 = viewModel.getRoomItemViewModel("!room1:server")!;
const vm2 = viewModel.getRoomItemViewModel("!room2:server")!;
@@ -698,19 +844,31 @@ describe("RoomListViewModel", () => {
describe("Toast", () => {
it("should show toast when SectionCreated event fires", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated);
expect(viewModel.getSnapshot().toast).toBe("section_created");
});
it("should show toast when RoomTagged event fires", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.RoomTagged);
expect(viewModel.getSnapshot().toast).toBe("chat_moved");
});
it("should clear toast when closeToast is called", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated);
expect(viewModel.getSnapshot().toast).toBe("section_created");
@@ -720,7 +878,11 @@ describe("RoomListViewModel", () => {
});
it("should auto-close toast after 15 seconds", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated);
expect(viewModel.getSnapshot().toast).toBe("section_created");
@@ -730,7 +892,11 @@ describe("RoomListViewModel", () => {
});
it("should reset the auto-close timer when a new section is created", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
RoomListStoreV3.instance.emit(RoomListStoreV3Event.SectionCreated);
jest.advanceTimersByTime(10 * 1000);
@@ -755,7 +921,11 @@ describe("RoomListViewModel", () => {
it("should show the unread-activity toast when an unread room is below the fold", () => {
mockRoom3Unread();
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// room1/room2 visible, room3 (unread) scrolled below the fold.
viewModel.updateVisibleFold(1);
@@ -765,7 +935,11 @@ describe("RoomListViewModel", () => {
it("should prefer the event toast over the unread-activity toast, restoring it on auto-close", () => {
mockRoom3Unread();
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
viewModel.updateVisibleFold(1);
expect(viewModel.getSnapshot().toast).toBe("unread_activity");
@@ -804,7 +978,11 @@ describe("RoomListViewModel", () => {
});
it("should initialize with multiple sections", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const snapshot = viewModel.getSnapshot();
expect(snapshot.sections).toHaveLength(3);
@@ -817,7 +995,11 @@ describe("RoomListViewModel", () => {
});
it("should not be a flat list when multiple sections exist", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().isFlatList).toBe(false);
});
@@ -832,7 +1014,11 @@ describe("RoomListViewModel", () => {
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().isFlatList).toBe(true);
expect(viewModel.getSnapshot().sections).toHaveLength(1);
@@ -849,14 +1035,22 @@ describe("RoomListViewModel", () => {
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().isFlatList).toBe(true);
expect(viewModel.getSnapshot().sections).toHaveLength(0);
});
it("should exclude favourite and low_priority from filter list", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const snapshot = viewModel.getSnapshot();
expect(snapshot.filterIds).not.toContain("favourite");
@@ -876,7 +1070,11 @@ describe("RoomListViewModel", () => {
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const snapshot = viewModel.getSnapshot();
expect(snapshot.sections).toHaveLength(1);
@@ -884,7 +1082,11 @@ describe("RoomListViewModel", () => {
});
it("should create section header view models on demand", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const headerVM = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
expect(headerVM).toBeDefined();
@@ -893,7 +1095,11 @@ describe("RoomListViewModel", () => {
});
it("should reuse section header view models", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const headerVM1 = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
const headerVM2 = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
@@ -901,7 +1107,11 @@ describe("RoomListViewModel", () => {
});
it("should hide room IDs when a section is collapsed", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Collapse the favourite section
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
@@ -920,7 +1130,11 @@ describe("RoomListViewModel", () => {
});
it("should compute activeRoomIndex relative to visible rooms when a section is collapsed", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Collapse the favourite section (which has 2 rooms: fav1, fav2)
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
@@ -943,7 +1157,11 @@ describe("RoomListViewModel", () => {
});
it("should restore room IDs when a section is re-expanded", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
@@ -958,7 +1176,11 @@ describe("RoomListViewModel", () => {
});
it("should update sections when room list changes", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const newFav = mkStubRoom("!fav3:server", "Fav 3", matrixClient);
@@ -978,7 +1200,11 @@ describe("RoomListViewModel", () => {
});
it("should preserve section collapse state across list updates", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Collapse favourites
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
@@ -993,7 +1219,11 @@ describe("RoomListViewModel", () => {
});
it("should track section collapse state per space", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Collapse favourites in the home space
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
@@ -1048,7 +1278,11 @@ describe("RoomListViewModel", () => {
});
it("should apply filters across all sections", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Only favRoom1 is unread
jest.spyOn(RoomListStoreV3.instance, "getSortedRoomsInActiveSpace").mockReturnValue({
@@ -1102,7 +1336,11 @@ describe("RoomListViewModel", () => {
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().sections.some((s) => s.id === customTag)).toBe(true);
});
@@ -1116,7 +1354,11 @@ describe("RoomListViewModel", () => {
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().sections.some((s) => s.id === customTag)).toBe(false);
});
@@ -1130,7 +1372,11 @@ describe("RoomListViewModel", () => {
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
expect(viewModel.getSnapshot().sections.some((s) => s.id === customTag)).toBe(true);
});
@@ -1138,7 +1384,11 @@ describe("RoomListViewModel", () => {
describe("Collapse/expand all sections", () => {
it("should collapse all sections when Action.RoomListCollapseAllSections is dispatched", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
const chatsHeader = viewModel.getSectionHeaderViewModel(CHATS_TAG);
@@ -1156,7 +1406,11 @@ describe("RoomListViewModel", () => {
});
it("should expand all sections when Action.RoomListExpandAllSections is dispatched", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Collapse first
const favHeader = viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
@@ -1177,7 +1431,11 @@ describe("RoomListViewModel", () => {
describe("notifyCollapseState", () => {
it("should dispatch collapseSections=expand when all sections are expanded (default)", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
@@ -1189,7 +1447,11 @@ describe("RoomListViewModel", () => {
});
it("should dispatch collapseSection=collapse when all sections are collapsed", () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Collapse all sections
viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite).isExpanded = false;
@@ -1217,7 +1479,11 @@ describe("RoomListViewModel", () => {
{ tag: DefaultTagID.LowPriority, rooms: [] },
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
const dispatchSpy = jest.spyOn(dispatcher, "dispatch");
RoomListStoreV3.instance.emit(RoomListStoreV3Event.ListsUpdate);
@@ -1231,7 +1497,11 @@ describe("RoomListViewModel", () => {
it("should apply sticky room within the correct section", async () => {
stubClient();
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Select favRoom1 (index 0 globally, index 0 in favourites section)
jest.spyOn(SDKContextClass.instance.roomViewStore, "getRoomId").mockReturnValue("!fav1:server");
@@ -1263,7 +1533,11 @@ describe("RoomListViewModel", () => {
describe("Drag and drop", () => {
beforeEach(() => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
// Ensure section header VMs are created before tests that interact with them
viewModel.getSectionHeaderViewModel(DefaultTagID.Favourite);
viewModel.getSectionHeaderViewModel(CHATS_TAG);
@@ -1367,7 +1641,11 @@ describe("RoomListViewModel", () => {
describe("changeRoomSection", () => {
beforeEach(() => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
spaceStore: SpaceStore.instance,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
mocked(tagRoom).mockClear();
});
@@ -1406,7 +1684,11 @@ describe("RoomListViewModel", () => {
});
it("should scroll a room into view in a flat list", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
roomViewStore: sdkContext.roomViewStore,
spaceStore: sdkContext.spaceStore,
});
const scrollSpy = jest.fn();
viewModel.setScrollToIndex(scrollSpy);
@@ -1432,7 +1714,11 @@ describe("RoomListViewModel", () => {
{ tag: CHATS_TAG, rooms: [regularRoom1] },
],
});
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
roomViewStore: sdkContext.roomViewStore,
spaceStore: sdkContext.spaceStore,
});
const scrollSpy = jest.fn();
viewModel.setScrollToIndex(scrollSpy);
@@ -1448,7 +1734,11 @@ describe("RoomListViewModel", () => {
});
it("should not scroll when the room is not in the current list", async () => {
viewModel = new RoomListViewModel({ client: matrixClient });
viewModel = new RoomListViewModel({
client: matrixClient,
roomViewStore: sdkContext.roomViewStore,
spaceStore: sdkContext.spaceStore,
});
const scrollSpy = jest.fn();
viewModel.setScrollToIndex(scrollSpy);
@@ -70,7 +70,11 @@ describe("DateSeparatorViewModel", () => {
const createViewModel = (
props: Partial<typeof defaultProps> & { forExport?: boolean } = {},
): DateSeparatorViewModel => {
return new DateSeparatorViewModel({ ...defaultProps, ...props });
return new DateSeparatorViewModel({
...defaultProps,
...props,
roomViewStore: SDKContextClass.instance.roomViewStore,
});
};
beforeEach(() => {