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 = <></>;