From dd68ec756014833c15dfa802c7fb0ae7ffd7fd44 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2026 10:34:06 +0100 Subject: [PATCH 1/6] Remove more usages of singleton store getter in favour of contexts --- .../components/structures/LoggedInView.tsx | 13 +++---- .../src/components/structures/RoomView.tsx | 9 ++--- .../components/structures/SpaceRoomView.tsx | 37 +++++++++---------- .../UserInfoHeaderVerificationViewModel.tsx | 13 ++++--- .../components/views/right_panel/BaseCard.tsx | 23 +++++++----- .../views/right_panel/EncryptionPanel.tsx | 21 +++++------ .../views/right_panel/ExtensionsCard.tsx | 24 ++++++------ .../components/views/right_panel/UserInfo.tsx | 10 ++--- .../views/right_panel/WidgetCard.tsx | 14 +++---- .../views/rooms/PinnedMessageBanner.tsx | 22 ++++++----- .../views/rooms/RoomHeader/RoomHeader.tsx | 22 +++++------ .../components/views/rooms/RoomInfoLine.tsx | 8 ++-- .../components/views/rooms/Stickerpicker.tsx | 19 +++++----- .../ThreadsActivityCentre.tsx | 8 ++-- .../views/toasts/VerificationRequestToast.tsx | 16 ++++---- apps/web/src/verification.ts | 18 +++++---- 16 files changed, 146 insertions(+), 131 deletions(-) diff --git a/apps/web/src/components/structures/LoggedInView.tsx b/apps/web/src/components/structures/LoggedInView.tsx index a5256dde075..d17d08fe677 100644 --- a/apps/web/src/components/structures/LoggedInView.tsx +++ b/apps/web/src/components/structures/LoggedInView.tsx @@ -46,7 +46,7 @@ import { type CollapseItem, type ICollapseConfig } from "../../resizer/distribut import { getKeyBindingsManager } from "../../KeyBindingsManager"; import { type IOpts } from "../../createRoom"; import SpacePanel from "../views/spaces/SpacePanel"; -import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; +import { LegacyCallHandlerEvent } from "../../LegacyCallHandler"; import AudioFeedArrayForLegacyCall from "../views/voip/AudioFeedArrayForLegacyCall"; import { OwnProfileStore } from "../../stores/OwnProfileStore"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; @@ -57,7 +57,6 @@ import { BackdropPanel } from "./BackdropPanel"; import { mediaFromMxc } from "../../customisations/Media"; import { UserTab } from "../views/dialogs/UserTab"; import { type OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload"; -import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { TimelineRenderingType } from "../../contexts/RoomContext"; import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts"; import { type SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload"; @@ -147,7 +146,7 @@ class LoggedInView extends React.Component { // use compact timeline view useCompactLayout: SettingsStore.getValue("useCompactLayout"), usageLimitDismissed: false, - activeCalls: LegacyCallHandler.instance.getAllActiveCalls(), + activeCalls: this.context.legacyCallHandler.getAllActiveCalls(), }; // stash the MatrixClient in case we log out before we are unmounted @@ -162,7 +161,7 @@ class LoggedInView extends React.Component { public componentDidMount(): void { document.addEventListener("keydown", this.onNativeKeyDown, false); - LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.onCallState); + this.context.legacyCallHandler.addListener(LegacyCallHandlerEvent.CallState, this.onCallState); this.updateServerNoticeEvents(); @@ -260,7 +259,7 @@ class LoggedInView extends React.Component { public componentWillUnmount(): void { document.removeEventListener("keydown", this.onNativeKeyDown, false); - LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState); + this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState); this._matrixClient.removeListener(ClientEvent.AccountData, this.onAccountData); this._matrixClient.removeListener(ClientEvent.Sync, this.onSync); this._matrixClient.removeListener(RoomStateEvent.Events, this.onRoomStateEvents); @@ -274,7 +273,7 @@ class LoggedInView extends React.Component { } private onCallState = (): void => { - const activeCalls = LegacyCallHandler.instance.getAllActiveCalls(); + const activeCalls = this.context.legacyCallHandler.getAllActiveCalls(); if (activeCalls === this.state.activeCalls) return; this.setState({ activeCalls }); }; @@ -586,7 +585,7 @@ class LoggedInView extends React.Component { break; case KeyBindingAction.ToggleRoomSidePanel: if (this.props.page_type === "room_view") { - RightPanelStore.instance.togglePanel(null); + this.context.rightPanelStore.togglePanel(null); handled = true; } break; diff --git a/apps/web/src/components/structures/RoomView.tsx b/apps/web/src/components/structures/RoomView.tsx index 1f6d3cfb9f6..90c57ae4fc4 100644 --- a/apps/web/src/components/structures/RoomView.tsx +++ b/apps/web/src/components/structures/RoomView.tsx @@ -130,7 +130,6 @@ import { WaitingForThirdPartyRoomView } from "./WaitingForThirdPartyRoomView"; import { isNotUndefined } from "../../Typeguards"; import { type CancelAskToJoinPayload } from "../../dispatcher/payloads/CancelAskToJoinPayload"; import { type SubmitAskToJoinPayload } from "../../dispatcher/payloads/SubmitAskToJoinPayload"; -import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { onView3pidInvite } from "../../stores/right-panel/action-handlers"; import RoomSearchAuxPanel from "../views/rooms/RoomSearchAuxPanel"; import { PinnedMessageBanner } from "../views/rooms/PinnedMessageBanner"; @@ -1333,23 +1332,23 @@ export class RoomView extends React.Component { case Action.ViewUser: if (payload.member) { if (payload.push) { - RightPanelStore.instance.pushCard({ + this.context.rightPanelStore.pushCard({ phase: RightPanelPhases.MemberInfo, state: { member: payload.member }, }); } else { - RightPanelStore.instance.setCards([ + this.context.rightPanelStore.setCards([ { phase: RightPanelPhases.RoomSummary }, { phase: RightPanelPhases.MemberList }, { phase: RightPanelPhases.MemberInfo, state: { member: payload.member } }, ]); } } else { - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList); + this.context.rightPanelStore.showOrHidePhase(RightPanelPhases.MemberList); } break; case Action.View3pidInvite: - onView3pidInvite(payload, RightPanelStore.instance); + onView3pidInvite(payload, this.context.rightPanelStore); break; case Action.FocusMessageSearch: if ((payload as FocusMessageSearchPayload).initialText) { diff --git a/apps/web/src/components/structures/SpaceRoomView.tsx b/apps/web/src/components/structures/SpaceRoomView.tsx index 9bb04ce552e..fff3eb710f1 100644 --- a/apps/web/src/components/structures/SpaceRoomView.tsx +++ b/apps/web/src/components/structures/SpaceRoomView.tsx @@ -20,7 +20,6 @@ import { VideoCallSolidIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; -import MatrixClientContext from "../../contexts/MatrixClientContext"; import createRoom, { type IOpts } from "../../createRoom"; import { shouldShowComponent } from "../../customisations/helpers/UIComponents"; import { Action } from "../../dispatcher/actions"; @@ -37,7 +36,6 @@ import PosthogTrackers from "../../PosthogTrackers"; import { showRoomInviteDialog } from "../../RoomInvite"; import { UIComponent } from "../../settings/UIFeature"; import { UPDATE_EVENT } from "../../stores/AsyncStore"; -import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../stores/right-panel/RightPanelStorePhases"; import type ResizeNotifier from "../../utils/ResizeNotifier"; import { @@ -77,6 +75,7 @@ import { type RoomPermalinkCreator } from "../../utils/permalinks/Permalinks"; import SpacePillButton from "./SpacePillButton.tsx"; import { useRoomName } from "../../hooks/useRoomName.ts"; import MultiInviter from "../../utils/MultiInviter.ts"; +import { SDKContext } from "../../contexts/SDKContext.ts"; interface IProps { space: Room; @@ -212,18 +211,18 @@ const SpaceLandingAddButton: React.FC<{ space: Room }> = ({ space }) => { }; const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { - const cli = useContext(MatrixClientContext); + const sdkContext = useContext(SDKContext); const myMembership = useMyRoomMembership(space); - const userId = cli.getSafeUserId(); + const userId = sdkContext.client!.getSafeUserId(); const name = useRoomName(space); const storeIsShowingSpaceMembers = useCallback( () => - RightPanelStore.instance.isOpenForRoom(space.roomId) && - RightPanelStore.instance.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.MemberList, - [space.roomId], + sdkContext.rightPanelStore.isOpenForRoom(space.roomId) && + sdkContext.rightPanelStore.currentCardForRoom(space.roomId)?.phase === RightPanelPhases.MemberList, + [space.roomId, sdkContext.rightPanelStore], ); - const isShowingMembers = useEventEmitterState(RightPanelStore.instance, UPDATE_EVENT, storeIsShowingSpaceMembers); + const isShowingMembers = useEventEmitterState(sdkContext.rightPanelStore, UPDATE_EVENT, storeIsShowingSpaceMembers); let inviteButton; if (shouldShowSpaceInvite(space) && shouldShowComponent(UIComponent.InviteUsers)) { @@ -266,7 +265,7 @@ const SpaceLanding: React.FC<{ space: Room }> = ({ space }) => { } const onMembersClick = (): void => { - RightPanelStore.instance.setCard({ phase: RightPanelPhases.MemberList }); + sdkContext.rightPanelStore.setCard({ phase: RightPanelPhases.MemberList }); }; return ( @@ -603,18 +602,18 @@ const SpaceSetupPrivateInvite: React.FC<{ }; export default class SpaceRoomView extends React.PureComponent { - public static contextType = MatrixClientContext; - declare public context: React.ContextType; + public static contextType = SDKContext; + declare public context: React.ContextType; private dispatcherRef?: string; - public constructor(props: IProps, context: React.ContextType) { + public constructor(props: IProps, context: React.ContextType) { super(props, context); let phase = Phase.Landing; const creator = this.props.space.currentState.getStateEvents(EventType.RoomCreate, "")?.getSender(); - const showSetup = this.props.justCreatedOpts && context.getSafeUserId() === creator; + const showSetup = this.props.justCreatedOpts && this.context.client?.getSafeUserId() === creator; if (showSetup) { phase = @@ -625,21 +624,21 @@ export default class SpaceRoomView extends React.PureComponent { this.state = { phase, - showRightPanel: RightPanelStore.instance.isOpenForRoom(this.props.space.roomId), + showRightPanel: this.context.rightPanelStore.isOpenForRoom(this.props.space.roomId), myMembership: this.props.space.getMyMembership(), }; } public componentDidMount(): void { this.dispatcherRef = defaultDispatcher.register(this.onAction); - RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); - this.context.on(RoomEvent.MyMembership, this.onMyMembership); + this.context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); + this.context.client?.on(RoomEvent.MyMembership, this.onMyMembership); } public componentWillUnmount(): void { defaultDispatcher.unregister(this.dispatcherRef); - RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); - this.context.off(RoomEvent.MyMembership, this.onMyMembership); + this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); + this.context.client?.off(RoomEvent.MyMembership, this.onMyMembership); } private onMyMembership = (room: Room, myMembership: string): void => { @@ -650,7 +649,7 @@ export default class SpaceRoomView extends React.PureComponent { private onRightPanelStoreUpdate = (): void => { this.setState({ - showRightPanel: RightPanelStore.instance.isOpenForRoom(this.props.space.roomId), + showRightPanel: this.context.rightPanelStore.isOpenForRoom(this.props.space.roomId), }); }; diff --git a/apps/web/src/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel.tsx b/apps/web/src/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel.tsx index 4b39d503539..b2d175989d4 100644 --- a/apps/web/src/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel.tsx +++ b/apps/web/src/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel.tsx @@ -8,10 +8,10 @@ import { type MatrixClient, type RoomMember, type User } from "matrix-js-sdk/src import { useContext } from "react"; import { type UserVerificationStatus } from "matrix-js-sdk/src/crypto-api"; -import MatrixClientContext from "../../../../contexts/MatrixClientContext"; import { type IDevice } from "../../../views/right_panel/UserInfo"; import { useAsyncMemo } from "../../../../hooks/useAsyncMemo"; import { verifyUser } from "../../../../verification"; +import { SDKContext } from "../../../../contexts/SDKContext.ts"; export interface UserInfoVerificationSectionState { /** @@ -44,21 +44,22 @@ export const useUserInfoVerificationViewModel = ( member: User | RoomMember, devices: IDevice[], ): UserInfoVerificationSectionState => { - const cli = useContext(MatrixClientContext); + const sdkContext = useContext(SDKContext); const userTrust = useAsyncMemo( - async () => cli.getCrypto()?.getUserVerificationStatus(member.userId), + async () => sdkContext.client?.getCrypto()?.getUserVerificationStatus(member.userId), [member.userId], // the user verification status is not initialized undefined, ); const hasUserVerificationStatus = Boolean(userTrust); const isUserVerified = Boolean(userTrust?.isVerified()); - const isMe = member.userId === cli.getUserId(); + const isMe = member.userId === sdkContext.client!.getUserId(); const canVerify = hasUserVerificationStatus && !isUserVerified && !isMe && devices && devices.length > 0; - const hasCrossSigningKeys = useHasCrossSigningKeys(cli, member as User, canVerify); - const verifySelectedUser = (): Promise => verifyUser(cli, member as User); + const hasCrossSigningKeys = useHasCrossSigningKeys(sdkContext.client!, member as User, canVerify); + const verifySelectedUser = (): Promise => + verifyUser(sdkContext.rightPanelStore, sdkContext.client!, member as User); return { canVerify, diff --git a/apps/web/src/components/views/right_panel/BaseCard.tsx b/apps/web/src/components/views/right_panel/BaseCard.tsx index f5d91881bd8..6606ff4d9eb 100644 --- a/apps/web/src/components/views/right_panel/BaseCard.tsx +++ b/apps/web/src/components/views/right_panel/BaseCard.tsx @@ -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, type KeyboardEvent, type Ref, type MouseEvent } from "react"; +import React, { type ReactNode, type KeyboardEvent, type Ref, type MouseEvent, useContext } from "react"; import classNames from "classnames"; import { IconButton, Text } from "@vector-im/compound-web"; import CloseIcon from "@vector-im/compound-design-tokens/assets/web/icons/close"; @@ -14,9 +14,9 @@ import ChevronLeftIcon from "@vector-im/compound-design-tokens/assets/web/icons/ import { AutoHideScrollbar } from "@element-hq/web-shared-components"; import { _t } from "../../../languageHandler"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { backLabelForPhase } from "../../../stores/right-panel/RightPanelStorePhases"; import { CardContext } from "./context"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; interface IProps { header?: ReactNode | null; @@ -37,12 +37,6 @@ interface IProps { children: ReactNode; } -function closeRightPanel(ev: MouseEvent): void { - ev.preventDefault(); - ev.stopPropagation(); - RightPanelStore.instance.popCard(); -} - const BaseCard: React.FC = ({ closeLabel, onClose, @@ -60,13 +54,15 @@ const BaseCard: React.FC = ({ closeButtonRef, ref, }: IProps) => { + const sdkContext = useContext(SDKContext); + let backButton; - const cardHistory = RightPanelStore.instance.roomPhaseHistory; + const cardHistory = sdkContext.rightPanelStore.roomPhaseHistory; if (cardHistory.length > 1 && !hideHeaderButtons) { const prevCard = cardHistory[cardHistory.length - 2]; const onBackClick = (ev: MouseEvent): void => { onBack?.(ev); - RightPanelStore.instance.popCard(); + sdkContext.rightPanelStore.popCard(); }; const label = backLabelForPhase(prevCard.phase) ?? _t("action|back"); backButton = ( @@ -84,6 +80,13 @@ const BaseCard: React.FC = ({ let closeButton; if (!hideHeaderButtons) { + // eslint-disable-next-line no-inner-declarations + function closeRightPanel(ev: MouseEvent): void { + ev.preventDefault(); + ev.stopPropagation(); + sdkContext.rightPanelStore.popCard(); + } + closeButton = ( = (props: IProps) => { - const cli = useMatrixClientContext(); + const sdkContext = useContext(SDKContext); const { verificationRequest, verificationRequestPromise, member, onClose, layout, isRoomEncrypted } = props; const [request, setRequest] = useState(verificationRequest); // state to show a spinner immediately after clicking "start verification", @@ -111,11 +110,11 @@ const EncryptionPanel: React.FC = (props: IProps) => { setRequesting(true); let verificationRequest_: VerificationRequest; try { - const roomId = await ensureDMExists(cli, member.userId); + const roomId = await ensureDMExists(sdkContext.client!, member.userId); if (!roomId) { throw new Error("Unable to create Room for verification"); } - verificationRequest_ = await cli.getCrypto()!.requestVerificationDM(member.userId, roomId); + verificationRequest_ = await sdkContext.client!.getCrypto()!.requestVerificationDM(member.userId, roomId); } catch (e) { console.error("Error starting verification", e); setRequesting(false); @@ -130,20 +129,20 @@ const EncryptionPanel: React.FC = (props: IProps) => { setRequest(verificationRequest_); setPhase(verificationRequest_.phase); // Notify the RightPanelStore about this - if (RightPanelStore.instance.currentCard.phase != RightPanelPhases.EncryptionPanel) { - RightPanelStore.instance.pushCard({ + if (sdkContext.rightPanelStore.currentCard.phase != RightPanelPhases.EncryptionPanel) { + sdkContext.rightPanelStore.pushCard({ phase: RightPanelPhases.EncryptionPanel, state: { member, verificationRequest: verificationRequest_ }, }); } - if (!RightPanelStore.instance.isOpen) RightPanelStore.instance.togglePanel(null); - }, [cli, member]); + if (!sdkContext.rightPanelStore.isOpen) sdkContext.rightPanelStore.togglePanel(null); + }, [sdkContext.client, sdkContext.rightPanelStore, member]); const requested: boolean = (!request && isRequesting) || (!!request && (phase === VerificationPhase.Requested || phase === VerificationPhase.Unsent || phase === undefined)); - const isSelfVerification = request ? request.isSelfVerification : member.userId === cli.getUserId(); + const isSelfVerification = request ? request.isSelfVerification : member.userId === sdkContext.client?.getUserId(); if (!request || requested) { const initiatedByMe = (!request && isRequesting) || (!!request && request.initiatedByMe); diff --git a/apps/web/src/components/views/right_panel/ExtensionsCard.tsx b/apps/web/src/components/views/right_panel/ExtensionsCard.tsx index c3db44e5013..290776fa01f 100644 --- a/apps/web/src/components/views/right_panel/ExtensionsCard.tsx +++ b/apps/web/src/components/views/right_panel/ExtensionsCard.tsx @@ -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, useEffect, useMemo, useState } from "react"; +import React, { type JSX, useContext, useEffect, useMemo, useState } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; import classNames from "classnames"; import { Button, Link, Separator, Text } from "@vector-im/compound-web"; @@ -21,10 +21,9 @@ import BaseCard from "./BaseCard"; import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils"; import { _t } from "../../../languageHandler"; import { useContextMenu } from "../../structures/ContextMenu"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { type IApp } from "../../../stores/WidgetStore"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; -import { MAX_PINNED, WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; +import { MAX_PINNED } from "../../../stores/widgets/WidgetLayoutStore"; import AccessibleButton from "../elements/AccessibleButton"; import WidgetAvatar from "../avatars/WidgetAvatar"; import { IntegrationManagers } from "../../../integrations/IntegrationManagers"; @@ -32,6 +31,7 @@ import EmptyState from "./EmptyState"; import { shouldShowComponent } from "../../../customisations/helpers/UIComponents.ts"; import { UIComponent } from "../../../settings/UIFeature.ts"; import { WidgetContextMenu } from "../../../viewmodels/room/right-panel/WidgetContextMenuViewModel.tsx"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; interface Props { room: Room; @@ -44,6 +44,7 @@ interface IAppRowProps { } const AppRow: React.FC = ({ app, room }) => { + const sdkContext = useContext(SDKContext); const name = WidgetUtils.getWidgetName(app); const [canModifyWidget, setCanModifyWidget] = useState(); @@ -52,24 +53,24 @@ const AppRow: React.FC = ({ app, room }) => { }, [room.client, room.roomId]); const onOpenWidgetClick = (): void => { - RightPanelStore.instance.pushCard({ + sdkContext.rightPanelStore.pushCard({ phase: RightPanelPhases.Widget, state: { widgetId: app.id }, }); }; - const isPinned = WidgetLayoutStore.instance.isInContainer(room, app, "top"); + const isPinned = sdkContext.widgetLayoutStore.isInContainer(room, app, "top"); const togglePin = isPinned ? () => { - WidgetLayoutStore.instance.moveToContainer(room, app, "right"); + sdkContext.widgetLayoutStore.moveToContainer(room, app, "right"); } : () => { - WidgetLayoutStore.instance.moveToContainer(room, app, "top"); + sdkContext.widgetLayoutStore.moveToContainer(room, app, "top"); }; const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); - const cannotPin = !isPinned && !WidgetLayoutStore.instance.canAddToContainer(room, "top"); + const cannotPin = !isPinned && !sdkContext.widgetLayoutStore.canAddToContainer(room, "top"); let pinTitle: string; if (cannotPin) { @@ -78,7 +79,7 @@ const AppRow: React.FC = ({ app, room }) => { pinTitle = isPinned ? _t("action|unpin") : _t("action|pin"); } - const isMaximised = WidgetLayoutStore.instance.isInContainer(room, app, "center"); + const isMaximised = sdkContext.widgetLayoutStore.isInContainer(room, app, "center"); let openTitle = ""; if (isPinned) { @@ -142,6 +143,7 @@ const AppRow: React.FC = ({ app, room }) => { * @param onClose callback when the card is closed */ const ExtensionsCard: React.FC = ({ room, onClose }) => { + const sdkContext = useContext(SDKContext); const apps = useWidgets(room); // Filter out virtual widgets const realApps = useMemo(() => apps.filter((app) => app.eventId !== undefined), [apps]); @@ -169,9 +171,9 @@ const ExtensionsCard: React.FC = ({ room, onClose }) => { ); } else { let copyLayoutBtn: JSX.Element | null = null; - if (WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) { + if (sdkContext.widgetLayoutStore.canCopyLayoutToRoom(room)) { copyLayoutBtn = ( - WidgetLayoutStore.instance.copyLayoutToRoom(room)}> + sdkContext.widgetLayoutStore.copyLayoutToRoom(room)}> {_t("widget|set_room_layout")} ); diff --git a/apps/web/src/components/views/right_panel/UserInfo.tsx b/apps/web/src/components/views/right_panel/UserInfo.tsx index 320ab48c233..82348ff7d31 100644 --- a/apps/web/src/components/views/right_panel/UserInfo.tsx +++ b/apps/web/src/components/views/right_panel/UserInfo.tsx @@ -23,10 +23,10 @@ import EncryptionPanel from "./EncryptionPanel"; import { useIsEncrypted } from "../../../hooks/useIsEncrypted"; import BaseCard from "./BaseCard"; import QuestionDialog from "../dialogs/QuestionDialog"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import PosthogTrackers from "../../../PosthogTrackers"; import { UserInfoHeaderView } from "./user_info/UserInfoHeaderView"; import { UserInfoBasicView } from "./user_info/UserInfoBasicView"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; export interface IDevice extends Device { ambiguous?: boolean; @@ -181,18 +181,18 @@ interface IProps { } const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPhases.MemberInfo, ...props }) => { - const cli = useContext(MatrixClientContext); + const sdkContext = useContext(SDKContext); // fetch latest room member if we have a room, so we don't show historical information, falling back to user const member = useMemo(() => (room ? room.getMember(user.userId) || user : user), [room, user]); - const isRoomEncrypted = useIsEncrypted(cli, room); + const isRoomEncrypted = useIsEncrypted(sdkContext.client!, room); const devices = useDevices(user.userId) ?? []; const classes = ["mx_UserInfo"]; const onEncryptionPanelClose = (): void => { - RightPanelStore.instance.popCard(); + sdkContext.rightPanelStore.popCard(); }; let content: JSX.Element | undefined; @@ -237,7 +237,7 @@ const UserInfo: React.FC = ({ user, room, onClose, phase = RightPanelPha onClose={onClose} closeLabel={closeLabel} onBack={(ev: ButtonEvent) => { - if (RightPanelStore.instance.previousCard.phase === RightPanelPhases.MemberList) { + if (sdkContext.rightPanelStore.previousCard.phase === RightPanelPhases.MemberList) { PosthogTrackers.trackInteraction("WebRightPanelRoomUserInfoBackButton", ev); } }} diff --git a/apps/web/src/components/views/right_panel/WidgetCard.tsx b/apps/web/src/components/views/right_panel/WidgetCard.tsx index 12b65e0702b..54b16c134e5 100644 --- a/apps/web/src/components/views/right_panel/WidgetCard.tsx +++ b/apps/web/src/components/views/right_panel/WidgetCard.tsx @@ -9,16 +9,14 @@ Please see LICENSE files in the repository root for full details. import React, { type JSX, useContext, useEffect } from "react"; import { type Room } from "matrix-js-sdk/src/matrix"; -import MatrixClientContext from "../../../contexts/MatrixClientContext"; import BaseCard from "./BaseCard"; import WidgetUtils, { useWidgets } from "../../../utils/WidgetUtils"; import AppTile from "../elements/AppTile"; import { _t } from "../../../languageHandler"; import { ContextMenuButton, useContextMenu } from "../../structures/ContextMenu"; -import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import Heading from "../typography/Heading"; import { WidgetContextMenu } from "../../../viewmodels/room/right-panel/WidgetContextMenuViewModel"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; interface IProps { room: Room; @@ -27,20 +25,20 @@ interface IProps { } const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { - const cli = useContext(MatrixClientContext); + const sdkContext = useContext(SDKContext); const apps = useWidgets(room); const app = apps.find((a) => a.id === widgetId); - const isRight = app && WidgetLayoutStore.instance.isInContainer(room, app, "right"); + const isRight = app && sdkContext.widgetLayoutStore.isInContainer(room, app, "right"); const [menuDisplayed, handle, openMenu, closeMenu] = useContextMenu(); useEffect(() => { if (!app || !isRight) { // stop showing this card - RightPanelStore.instance.popCard(); + sdkContext.rightPanelStore.popCard(); } - }, [app, isRight]); + }, [app, isRight, sdkContext.rightPanelStore]); // Don't render anything as we are about to transition if (!app || !isRight) return null; @@ -78,7 +76,7 @@ const WidgetCard: React.FC = ({ room, widgetId, onClose }) => { fullWidth showMenubar={false} room={room} - userId={cli.getSafeUserId()} + userId={sdkContext.client?.getUserId() ?? undefined} creatorUserId={app.creatorUserId} widgetPageTitle={WidgetUtils.getWidgetDataTitle(app)} waitForIframeLoad={app.waitForIframeLoad} diff --git a/apps/web/src/components/views/rooms/PinnedMessageBanner.tsx b/apps/web/src/components/views/rooms/PinnedMessageBanner.tsx index 0c1f72cf8e5..66157436839 100644 --- a/apps/web/src/components/views/rooms/PinnedMessageBanner.tsx +++ b/apps/web/src/components/views/rooms/PinnedMessageBanner.tsx @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, useContext, useEffect, useId, useRef, useState } from "react"; +import React, { type JSX, useCallback, useContext, useEffect, useId, useRef, useState } from "react"; import PinIcon from "@vector-im/compound-design-tokens/assets/web/icons/pin-solid"; import { Button } from "@vector-im/compound-web"; import { type MatrixEvent, type Room } from "matrix-js-sdk/src/matrix"; @@ -15,7 +15,6 @@ import { EventPreviewView, useCreateAutoDisposedViewModel } from "@element-hq/we import { usePinnedEvents, useSortedFetchedPinnedEvents } from "../../../hooks/usePinnedEvents"; import { _t } from "../../../languageHandler"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import { useEventEmitter } from "../../../hooks/useEventEmitter"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; @@ -254,11 +253,6 @@ function Indicator({ active, hidden }: IndicatorProps): JSX.Element { ); } -function getRightPanelPhase(roomId: string): RightPanelPhases | null { - if (!RightPanelStore.instance.isOpenForRoom(roomId)) return null; - return RightPanelStore.instance.currentCard.phase; -} - /** * The props for the {@link BannerButton} component. */ @@ -273,8 +267,18 @@ interface BannerButtonProps { * A button that allows the user to view or close the list of pinned messages. */ function BannerButton({ room }: BannerButtonProps): JSX.Element { + const sdkContext = useContext(SDKContext); + + const getRightPanelPhase = useCallback( + (roomId: string): RightPanelPhases | null => { + if (!sdkContext.rightPanelStore.isOpenForRoom(roomId)) return null; + return sdkContext.rightPanelStore.currentCard.phase; + }, + [sdkContext.rightPanelStore], + ); + const [currentPhase, setCurrentPhase] = useState(getRightPanelPhase(room.roomId)); - useEventEmitter(RightPanelStore.instance, UPDATE_EVENT, () => setCurrentPhase(getRightPanelPhase(room.roomId))); + useEventEmitter(sdkContext.rightPanelStore, UPDATE_EVENT, () => setCurrentPhase(getRightPanelPhase(room.roomId))); const isPinnedMessagesPhase = currentPhase === RightPanelPhases.PinnedMessages; return ( @@ -285,7 +289,7 @@ function BannerButton({ room }: BannerButtonProps): JSX.Element { if (isPinnedMessagesPhase) PosthogTrackers.trackInteraction("PinnedMessageBannerCloseListButton"); else PosthogTrackers.trackInteraction("PinnedMessageBannerViewAllButton"); - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.PinnedMessages); + sdkContext.rightPanelStore.showOrHidePhase(RightPanelPhases.PinnedMessages); }} > {isPinnedMessagesPhase diff --git a/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx b/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx index dbdb0f619a0..7831575faa8 100644 --- a/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx +++ b/apps/web/src/components/views/rooms/RoomHeader/RoomHeader.tsx @@ -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, useCallback, useState } from "react"; +import React, { type JSX, useCallback, useContext, useState } from "react"; import { Text, Button, IconButton, Menu, MenuItem, Tooltip } from "@vector-im/compound-web"; import VideoCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/video-call-solid"; import VoiceCallIcon from "@vector-im/compound-design-tokens/assets/web/icons/voice-call-solid"; @@ -26,7 +26,6 @@ import { HistoryIcon, UserProfileSolidIcon } from "@vector-im/compound-design-to import { useRoomName } from "../../../../hooks/useRoomName.ts"; import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases.ts"; -import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx"; import { useRoomMemberCount, useRoomMembers } from "../../../../hooks/useRoomMembers.ts"; import { _t } from "../../../../languageHandler.tsx"; import { getPlatformCallTypeProps, useRoomCall } from "../../../../hooks/room/useRoomCall.tsx"; @@ -39,7 +38,6 @@ import FacePile from "../../elements/FacePile.tsx"; import { useRoomState } from "../../../../hooks/useRoomState.ts"; import RoomAvatar from "../../avatars/RoomAvatar.tsx"; import { formatCount } from "../../../../utils/FormattingUtils.ts"; -import RightPanelStore from "../../../../stores/right-panel/RightPanelStore.ts"; import PosthogTrackers from "../../../../PosthogTrackers.ts"; import { VideoRoomChatButton } from "./VideoRoomChatButton.tsx"; import { RoomKnocksBar } from "../RoomKnocksBar.tsx"; @@ -57,6 +55,7 @@ import { ToggleableIcon } from "./toggle/ToggleableIcon.tsx"; import { CurrentRightPanelPhaseContextProvider } from "../../../../contexts/CurrentRightPanelPhaseContext.tsx"; import { LocalRoom } from "../../../../models/LocalRoom.ts"; import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts"; +import { SDKContext } from "../../../../contexts/SDKContext.ts"; function RoomHeaderButtons({ room, @@ -67,6 +66,7 @@ function RoomHeaderButtons({ legacyAdditionalButtons?: ViewRoomOpts["buttons"]; extraButtons?: JSX.Element; }): JSX.Element { + const sdkContext = useContext(SDKContext); const members = useRoomMembers(room, 2500); const memberCount = useRoomMemberCount(room, { throttleWait: 2500, includeInvited: true }); @@ -337,7 +337,7 @@ function RoomHeaderButtons({ indicator={notificationLevelToIndicator(threadNotifications)} onClick={(evt) => { evt.stopPropagation(); - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.ThreadPanel); + sdkContext.rightPanelStore.showOrHidePhase(RightPanelPhases.ThreadPanel); PosthogTrackers.trackInteraction("WebRoomHeaderButtonsThreadsButton", evt); }} aria-label={_t("common|threads")} @@ -351,7 +351,7 @@ function RoomHeaderButtons({ indicator={notificationLevelToIndicator(globalNotificationState.level)} onClick={(evt) => { evt.stopPropagation(); - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.NotificationPanel); + sdkContext.rightPanelStore.showOrHidePhase(RightPanelPhases.NotificationPanel); }} aria-label={_t("notifications|enable_prompt_toast_title")} > @@ -364,7 +364,7 @@ function RoomHeaderButtons({ { evt.stopPropagation(); - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary); + sdkContext.rightPanelStore.showOrHidePhase(RightPanelPhases.RoomSummary); }} aria-label={_t("right_panel|room_summary_card|title")} > @@ -382,7 +382,7 @@ function RoomHeaderButtons({ viewUserOnClick={false} tooltipLabel={_t("room|header_face_pile_tooltip")} onClick={(e: ButtonEvent) => { - RightPanelStore.instance.showOrHidePhase(RightPanelPhases.MemberList); + sdkContext.rightPanelStore.showOrHidePhase(RightPanelPhases.MemberList); e.stopPropagation(); }} aria-label={_t("common|n_members", { count: memberCount })} @@ -442,14 +442,14 @@ export default function RoomHeader({ legacyAdditionalButtons?: ViewRoomOpts["buttons"]; oobData?: IOOBData; }): JSX.Element { - const client = useMatrixClientContext(); + const sdkContext = useContext(SDKContext); const roomName = useRoomName(room); const joinRule = useRoomState(room, (state) => state.getJoinRule()); const historyVisibility = useRoomState(room, (state) => state.getHistoryVisibility()); const dmMember = useDmMember(room); const isDirectMessage = !!dmMember; - const isRoomEncrypted = useIsEncrypted(client, room); - const e2eStatus = useEncryptionStatus(client, room); + const isRoomEncrypted = useIsEncrypted(sdkContext.client!, room); + const e2eStatus = useEncryptionStatus(sdkContext.client!, room); const askToJoinEnabled = useFeatureEnabled("feature_ask_to_join"); const onAvatarClick = (): void => { defaultDispatcher.dispatch({ @@ -480,7 +480,7 @@ export default function RoomHeader({ onClick={ room instanceof LocalRoom ? undefined - : () => RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary) + : () => sdkContext.rightPanelStore.showOrHidePhase(RightPanelPhases.RoomSummary) } className="mx_RoomHeader_infoWrapper" > diff --git a/apps/web/src/components/views/rooms/RoomInfoLine.tsx b/apps/web/src/components/views/rooms/RoomInfoLine.tsx index 687c41da341..c020076b943 100644 --- a/apps/web/src/components/views/rooms/RoomInfoLine.tsx +++ b/apps/web/src/components/views/rooms/RoomInfoLine.tsx @@ -6,25 +6,27 @@ 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, type FC } from "react"; +import React, { type JSX, type FC, useContext } from "react"; import { type Room, JoinRule, type MatrixClient } from "matrix-js-sdk/src/matrix"; import { KnownMembership } from "matrix-js-sdk/src/types"; import { LockSolidIcon, VideoCallSolidIcon, PublicIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import { _t } from "../../../languageHandler"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import { useAsyncMemo } from "../../../hooks/useAsyncMemo"; import { useRoomState } from "../../../hooks/useRoomState"; import { useRoomMemberCount, useMyRoomMembership } from "../../../hooks/useRoomMembers"; import AccessibleButton from "../elements/AccessibleButton"; import { isVideoRoom as calcIsVideoRoom } from "../../../utils/video-rooms"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; interface IProps { room: Room; } const RoomInfoLine: FC = ({ room }) => { + const sdkContext = useContext(SDKContext); + // summary will begin as undefined whilst loading and go null if it fails to load or we are not invited. const summary = useAsyncMemo(async (): Promise> | null> => { if (room.getMyMembership() !== KnownMembership.Invite) return null; @@ -64,7 +66,7 @@ const RoomInfoLine: FC = ({ room }) => { } else if (memberCount && summary !== undefined) { // summary is not still loading const viewMembers = (): void => - RightPanelStore.instance.setCard({ + sdkContext.rightPanelStore.setCard({ phase: RightPanelPhases.MemberList, }); diff --git a/apps/web/src/components/views/rooms/Stickerpicker.tsx b/apps/web/src/components/views/rooms/Stickerpicker.tsx index 7bfe34b9380..c20e7e0fcad 100644 --- a/apps/web/src/components/views/rooms/Stickerpicker.tsx +++ b/apps/web/src/components/views/rooms/Stickerpicker.tsx @@ -12,7 +12,6 @@ import { type IWidget } from "matrix-widget-api"; import { _t, _td } from "../../../languageHandler"; import AppTile from "../elements/AppTile"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import dis from "../../../dispatcher/dispatcher"; import AccessibleButton from "../elements/AccessibleButton"; import WidgetUtils, { type UserWidget } from "../../../utils/WidgetUtils"; @@ -24,9 +23,9 @@ import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingSto import { type ActionPayload } from "../../../dispatcher/payloads"; import type ScalarAuthClient from "../../../ScalarAuthClient"; import GenericElementContextMenu from "../context_menus/GenericElementContextMenu"; -import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { UPDATE_EVENT } from "../../../stores/AsyncStore"; import StickerpackPlaceholder from "../../../../res/img/stickerpack-placeholder.png"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; // This should be below the dialog level (4000), but above the rest of the UI (1000-2000). // We sit in a context menu, so this should be given to the context menu. @@ -50,6 +49,9 @@ interface IState { } export default class Stickerpicker extends React.PureComponent { + public static contextType = SDKContext; + declare public context: React.ContextType; + public static defaultProps: Partial = { threadId: null, }; @@ -130,17 +132,16 @@ export default class Stickerpicker extends React.PureComponent { this.dispatcherRef = dis.register(this.onAction); // Track updates to widget state in account data - MatrixClientPeg.safeGet().on(ClientEvent.AccountData, this.updateWidget); + this.context.client?.on(ClientEvent.AccountData, this.updateWidget); - RightPanelStore.instance.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); + this.context.rightPanelStore.on(UPDATE_EVENT, this.onRightPanelStoreUpdate); // Initialise widget state from current account data this.updateWidget(); } public componentWillUnmount(): void { - const client = MatrixClientPeg.get(); - if (client) client.removeListener(ClientEvent.AccountData, this.updateWidget); - RightPanelStore.instance.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); + this.context.client?.removeListener(ClientEvent.AccountData, this.updateWidget); + this.context.rightPanelStore.off(UPDATE_EVENT, this.onRightPanelStoreUpdate); window.removeEventListener("resize", this.onResize); dis.unregister(this.dispatcherRef); } @@ -280,9 +281,9 @@ export default class Stickerpicker extends React.PureComponent { room={this.props.room} threadId={this.props.threadId} fullWidth={true} - userId={MatrixClientPeg.safeGet().credentials.userId!} + userId={this.context.client?.credentials.userId ?? undefined} creatorUserId={ - stickerpickerWidget.sender || MatrixClientPeg.safeGet().credentials.userId! + stickerpickerWidget.sender || this.context.client?.credentials.userId || undefined } waitForIframeLoad={true} showMenubar={true} diff --git a/apps/web/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx b/apps/web/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx index be7fa2bd0eb..286a158d6dc 100644 --- a/apps/web/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx +++ b/apps/web/src/components/views/spaces/threads-activity-centre/ThreadsActivityCentre.tsx @@ -6,7 +6,7 @@ * Please see LICENSE files in the repository root for full details. */ -import React, { type JSX, useState } from "react"; +import React, { type JSX, useContext, useState } from "react"; import { Menu, MenuItem } from "@vector-im/compound-web"; import { type Room } from "matrix-js-sdk/src/matrix"; @@ -16,7 +16,6 @@ import DecoratedRoomAvatar from "../../avatars/DecoratedRoomAvatar"; import { Action } from "../../../../dispatcher/actions"; import defaultDispatcher from "../../../../dispatcher/dispatcher"; import { type ViewRoomPayload } from "../../../../dispatcher/payloads/ViewRoomPayload"; -import RightPanelStore from "../../../../stores/right-panel/RightPanelStore"; import { RightPanelPhases } from "../../../../stores/right-panel/RightPanelStorePhases"; import { useUnreadThreadRooms } from "./useUnreadThreadRooms"; import { StatelessNotificationBadge } from "../../rooms/NotificationBadge/StatelessNotificationBadge"; @@ -25,6 +24,7 @@ import PosthogTrackers from "../../../../PosthogTrackers"; import { getKeyBindingsManager } from "../../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../../accessibility/KeyboardShortcuts"; import { useSettingValue } from "../../../../hooks/useSettings"; +import { SDKContext } from "../../../../contexts/SDKContext.ts"; interface ThreadsActivityCentreProps { /** @@ -117,6 +117,8 @@ interface ThreadsActivityRow { * Display a room with unread threads. */ function ThreadsActivityCentreRow({ room, onClick, notificationLevel }: ThreadsActivityRow): JSX.Element { + const sdkContext = useContext(SDKContext); + return ( { + public static contextType = SDKContext; + declare public context: React.ContextType; + private intervalHandle?: number; public constructor(props: IProps) { @@ -69,7 +71,7 @@ export default class VerificationRequestToast extends React.PureComponent({ @@ -113,8 +115,8 @@ export default class VerificationRequestToast extends React.PureComponent { +export async function verifyUser( + rightPanelStore: RightPanelStore, + matrixClient: MatrixClient, + user: User, +): Promise { if (matrixClient.isGuest()) { dis.dispatch({ action: "require_registration" }); return; } const existingRequest = pendingVerificationRequestForUser(matrixClient, user); - setRightPanel({ member: user, verificationRequest: existingRequest }); + setRightPanel(rightPanelStore, { member: user, verificationRequest: existingRequest }); } -function setRightPanel(state: IRightPanelCardState): void { - if (RightPanelStore.instance.roomPhaseHistory.some((card) => card.phase == RightPanelPhases.RoomSummary)) { - RightPanelStore.instance.pushCard({ phase: RightPanelPhases.EncryptionPanel, state }); +function setRightPanel(rightPanelStore: RightPanelStore, state: IRightPanelCardState): void { + if (rightPanelStore.roomPhaseHistory.some((card) => card.phase == RightPanelPhases.RoomSummary)) { + rightPanelStore.pushCard({ phase: RightPanelPhases.EncryptionPanel, state }); } else { - RightPanelStore.instance.setCards([ + rightPanelStore.setCards([ { phase: RightPanelPhases.RoomSummary }, { phase: RightPanelPhases.MemberInfo, state: { member: state.member } }, { phase: RightPanelPhases.EncryptionPanel, state }, From 64837587d85863640b6b953e2f048b8e0b5e3477 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Thu, 2 Jul 2026 10:55:50 +0100 Subject: [PATCH 2/6] Remove more usages of singleton store getter in favour of contexts --- apps/web/src/LegacyCallHandler.tsx | 4 +- .../src/components/structures/LeftPanel.tsx | 25 +++++----- .../src/components/structures/MatrixChat.tsx | 3 +- .../context_menus/LegacyCallContextMenu.tsx | 9 ++-- .../context_menus/ThreadListContextMenu.tsx | 10 ++-- .../src/components/views/rooms/AppsDrawer.tsx | 19 ++++---- .../components/views/voip/LegacyCallView.tsx | 46 ++++++++++--------- .../views/voip/LegacyCallViewForRoom.tsx | 22 ++++----- .../src/components/views/voip/VideoFeed.tsx | 10 ++-- apps/web/src/hooks/room/useRoomCall.tsx | 34 ++++++++++---- apps/web/src/toasts/IncomingCallToast.tsx | 18 ++++---- .../src/toasts/IncomingLegacyCallToast.tsx | 31 +++++++------ apps/web/src/utils/room/placeCall.ts | 5 +- 13 files changed, 133 insertions(+), 103 deletions(-) diff --git a/apps/web/src/LegacyCallHandler.tsx b/apps/web/src/LegacyCallHandler.tsx index d2f7c88cb62..d562df6880c 100644 --- a/apps/web/src/LegacyCallHandler.tsx +++ b/apps/web/src/LegacyCallHandler.tsx @@ -309,7 +309,7 @@ export default class LegacyCallHandler extends TypedEventEmitter { + public static contextType = SDKContext; + declare public context: React.ContextType; + private listContainerRef = createRef(); private roomListRef = createRef(); private focusedElement: Element | null = null; private isDoingStickyHeaders = false; - public constructor(props: IProps) { - super(props); + public constructor(props: IProps, context: React.ContextType) { + super(props, context); this.state = { - activeSpace: SpaceStore.instance.activeSpace, + activeSpace: this.context.spaceStore.activeSpace, showBreadcrumbs: LeftPanel.breadcrumbsMode, - supportsPstnProtocol: LegacyCallHandler.instance.getSupportsPstnProtocol(), + supportsPstnProtocol: this.context.legacyCallHandler.getSupportsPstnProtocol(), }; } @@ -78,8 +81,8 @@ export default class LeftPanel extends React.Component { public componentDidMount(): void { BreadcrumbsStore.instance.on(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.on(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); - SpaceStore.instance.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); - LegacyCallHandler.instance.on(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport); + this.context.spaceStore.on(UPDATE_SELECTED_SPACE, this.updateActiveSpace); + this.context.legacyCallHandler.on(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport); if (this.listContainerRef.current) { UIStore.instance.trackElementDimensions("ListContainer", this.listContainerRef.current); @@ -93,8 +96,8 @@ export default class LeftPanel extends React.Component { public componentWillUnmount(): void { BreadcrumbsStore.instance.off(UPDATE_EVENT, this.onBreadcrumbsUpdate); RoomListStore.instance.off(LISTS_UPDATE_EVENT, this.onBreadcrumbsUpdate); - SpaceStore.instance.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); - LegacyCallHandler.instance.off(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport); + this.context.spaceStore.off(UPDATE_SELECTED_SPACE, this.updateActiveSpace); + this.context.legacyCallHandler.off(LegacyCallHandlerEvent.ProtocolSupport, this.updateProtocolSupport); UIStore.instance.stopTrackingElementDimensions("ListContainer"); UIStore.instance.removeListener("ListContainer", this.refreshStickyHeaders); this.listContainerRef.current?.removeEventListener("scroll", this.onScroll); @@ -107,7 +110,7 @@ export default class LeftPanel extends React.Component { } private updateProtocolSupport = (): void => { - this.setState({ supportsPstnProtocol: LegacyCallHandler.instance.getSupportsPstnProtocol() }); + this.setState({ supportsPstnProtocol: this.context.legacyCallHandler.getSupportsPstnProtocol() }); }; private updateActiveSpace = (activeSpace: SpaceKey): void => { diff --git a/apps/web/src/components/structures/MatrixChat.tsx b/apps/web/src/components/structures/MatrixChat.tsx index 02337559d74..55451d20bad 100644 --- a/apps/web/src/components/structures/MatrixChat.tsx +++ b/apps/web/src/components/structures/MatrixChat.tsx @@ -96,7 +96,6 @@ import UIStore, { UI_EVENTS } from "../../stores/UIStore"; import SoftLogout from "./auth/SoftLogout"; import { copyPlaintext } from "../../utils/strings"; import { initSentry } from "../../sentry"; -import LegacyCallHandler from "../../LegacyCallHandler"; import { showSpaceInvite } from "../../utils/space"; import { type ButtonEvent } from "../views/elements/AccessibleButton"; import { type ActionPayload } from "../../dispatcher/payloads"; @@ -700,7 +699,7 @@ export default class MatrixChat extends React.PureComponent { } break; case "logout": - LegacyCallHandler.instance.hangupAllCalls(); + this.stores.legacyCallHandler.hangupAllCalls(); Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() => Lifecycle.logout(this.stores.oidcClientStore), ); diff --git a/apps/web/src/components/views/context_menus/LegacyCallContextMenu.tsx b/apps/web/src/components/views/context_menus/LegacyCallContextMenu.tsx index 3427ed183be..e5a23aac8ed 100644 --- a/apps/web/src/components/views/context_menus/LegacyCallContextMenu.tsx +++ b/apps/web/src/components/views/context_menus/LegacyCallContextMenu.tsx @@ -10,26 +10,29 @@ import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import { _t } from "../../../languageHandler"; import ContextMenu, { type IProps as IContextMenuProps, MenuItem } from "../../structures/ContextMenu"; -import LegacyCallHandler from "../../../LegacyCallHandler"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; interface IProps extends IContextMenuProps { call: MatrixCall; } export default class LegacyCallContextMenu extends React.Component { + public static contextType = SDKContext; + declare public context: React.ContextType; + public onHoldClick = (): void => { this.props.call.setRemoteOnHold(true); this.props.onFinished(); }; public onUnholdClick = (): void => { - LegacyCallHandler.instance.setActiveCallRoomId(this.props.call.roomId); + this.context.legacyCallHandler.setActiveCallRoomId(this.props.call.roomId); this.props.onFinished(); }; public onTransferClick = (): void => { - LegacyCallHandler.instance.showTransferDialog(this.props.call); + this.context.legacyCallHandler.showTransferDialog(this.props.call); this.props.onFinished(); }; diff --git a/apps/web/src/components/views/context_menus/ThreadListContextMenu.tsx b/apps/web/src/components/views/context_menus/ThreadListContextMenu.tsx index 2b3ee576532..8d0feefff46 100644 --- a/apps/web/src/components/views/context_menus/ThreadListContextMenu.tsx +++ b/apps/web/src/components/views/context_menus/ThreadListContextMenu.tsx @@ -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, { useCallback, useEffect } from "react"; +import React, { useCallback, useContext, useEffect } from "react"; import { type MatrixEvent } from "matrix-js-sdk/src/matrix"; import { LinkIcon, OverflowHorizontalIcon, VisibilityOnIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; @@ -18,9 +18,8 @@ import { copyPlaintext } from "../../../utils/strings"; import { ChevronFace, ContextMenuTooltipButton, type MenuProps, useContextMenu } from "../../structures/ContextMenu"; import { _t } from "../../../languageHandler"; import IconizedContextMenu, { IconizedContextMenuOption, IconizedContextMenuOptionList } from "./IconizedContextMenu"; -import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; export interface ThreadListContextMenuProps { mxEvent: MatrixEvent; @@ -42,6 +41,7 @@ const ThreadListContextMenu: React.FC = ({ onMenuToggle, ...props }) => { + const sdkContext = useContext(SDKContext); const [menuDisplayed, button, openMenu, closeThreadOptions] = useContextMenu(); const viewInRoom = useCallback( @@ -77,8 +77,8 @@ const ThreadListContextMenu: React.FC = ({ onMenuToggle?.(menuDisplayed); }, [menuDisplayed, onMenuToggle]); - const room = MatrixClientPeg.safeGet().getRoom(mxEvent.getRoomId()); - const isMainSplitTimelineShown = !!room && !WidgetLayoutStore.instance.hasMaximisedWidget(room); + const room = sdkContext.client?.getRoom(mxEvent.getRoomId()); + const isMainSplitTimelineShown = !!room && !sdkContext.widgetLayoutStore.hasMaximisedWidget(room); return ( { this.context.resizeNotifier.on("isResizing", this.onIsResizing); ScalarMessaging.startListening(); - WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps); + this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps); this.dispatcherRef = dis.register(this.onAction); } public componentWillUnmount(): void { this.unmounted = true; ScalarMessaging.stopListening(); - WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps); + this.context.widgetLayoutStore.off(WidgetLayoutStore.emissionForRoom(this.props.room), this.updateApps); dis.unregister(this.dispatcherRef); if (this.resizeContainer) { this.resizer.detach(); @@ -117,7 +117,7 @@ export default class AppsDrawer extends React.Component { }, onResizeStop: () => { this.resizeContainer?.classList.remove("mx_AppsDrawer--resizing"); - WidgetLayoutStore.instance.setResizerDistributions( + this.context.widgetLayoutStore.setResizerDistributions( this.props.room, "top", this.topApps() @@ -166,7 +166,7 @@ export default class AppsDrawer extends React.Component { }; private loadResizerPreferences = (): void => { - const distributions = WidgetLayoutStore.instance.getResizerDistributions(this.props.room, "top"); + const distributions = this.context.widgetLayoutStore.getResizerDistributions(this.props.room, "top"); if (this.state.apps && this.topApps().length - 1 === distributions.length) { distributions.forEach((size, i) => { const distributor = this.resizer.forHandleAt(i); @@ -206,8 +206,8 @@ export default class AppsDrawer extends React.Component { }; private getApps = (): IState["apps"] => ({ - ["top"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "top"), - ["center"]: WidgetLayoutStore.instance.getContainerWidgets(this.props.room, "center"), + ["top"]: this.context.widgetLayoutStore.getContainerWidgets(this.props.room, "top"), + ["center"]: this.context.widgetLayoutStore.getContainerWidgets(this.props.room, "center"), }); private topApps = (): IWidget[] => this.state.apps["top"]; private centerApps = (): IWidget[] => this.state.apps["center"]; @@ -321,7 +321,8 @@ const PersistentVResizer: React.FC = ({ resizeNotifier, children, }) => { - let defaultHeight = WidgetLayoutStore.instance.getContainerHeight(room, "top"); + const sdkContext = useContext(SDKContext); + let defaultHeight = sdkContext.widgetLayoutStore.getContainerHeight(room, "top"); // Arbitrary defaults to avoid NaN problems. 100 px or 3/4 of the visible window. if (!minHeight) minHeight = 100; @@ -352,7 +353,7 @@ const PersistentVResizer: React.FC = ({ let newHeight = defaultHeight! + d.height; newHeight = percentageOf(newHeight, minHeight, maxHeight) * 100; - WidgetLayoutStore.instance.setContainerHeight(room, "top", newHeight); + sdkContext.widgetLayoutStore.setContainerHeight(room, "top", newHeight); resizeNotifier.stopResizing(); }} diff --git a/apps/web/src/components/views/voip/LegacyCallView.tsx b/apps/web/src/components/views/voip/LegacyCallView.tsx index e6da10e5200..5010d0505c3 100644 --- a/apps/web/src/components/views/voip/LegacyCallView.tsx +++ b/apps/web/src/components/views/voip/LegacyCallView.tsx @@ -15,8 +15,6 @@ import { type CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes"; import dis from "../../../dispatcher/dispatcher"; -import LegacyCallHandler from "../../../LegacyCallHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t, _td } from "../../../languageHandler"; import VideoFeed from "./VideoFeed"; import RoomAvatar from "../avatars/RoomAvatar"; @@ -28,6 +26,7 @@ import LegacyCallViewButtons from "./LegacyCallView/LegacyCallViewButtons"; import { type ActionPayload } from "../../../dispatcher/payloads"; import { getKeyBindingsManager } from "../../../KeyBindingsManager"; import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; interface IProps { // The call for us to display @@ -81,6 +80,9 @@ function exitFullscreen(): void { } export default class LegacyCallView extends React.Component { + public static contextType = SDKContext; + declare public context: React.ContextType; + private dispatcherRef?: string; private contentWrapperRef = createRef(); private buttonsRef = createRef(); @@ -308,18 +310,18 @@ export default class LegacyCallView extends React.Component { }; private onCallResumeClick = (): void => { - const userFacingRoomId = LegacyCallHandler.instance.roomIdForCall(this.props.call); - if (userFacingRoomId) LegacyCallHandler.instance.setActiveCallRoomId(userFacingRoomId); + const userFacingRoomId = this.context.legacyCallHandler.roomIdForCall(this.props.call); + if (userFacingRoomId) this.context.legacyCallHandler.setActiveCallRoomId(userFacingRoomId); }; private onTransferClick = (): void => { - const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(this.props.call.callId); + const transfereeCall = this.context.legacyCallHandler.getTransfereeForCallId(this.props.call.callId); if (transfereeCall) this.props.call.transferToCall(transfereeCall); }; private onHangupClick = (): void => { - const roomId = LegacyCallHandler.instance.roomIdForCall(this.props.call); - if (roomId) LegacyCallHandler.instance.hangupOrReject(roomId); + const roomId = this.context.legacyCallHandler.roomIdForCall(this.props.call); + if (roomId) this.context.legacyCallHandler.hangupOrReject(roomId); }; private onToggleSidebar = (): void => { @@ -400,10 +402,10 @@ export default class LegacyCallView extends React.Component { const { pipMode, call, onResize, sidebarShown } = this.props; const { isLocalOnHold, isRemoteOnHold, primaryFeed, secondaryFeed, sidebarFeeds } = this.state; - const callRoomId = LegacyCallHandler.instance.roomIdForCall(call); - const callRoom = (callRoomId ? MatrixClientPeg.safeGet().getRoom(callRoomId) : undefined) ?? undefined; + const callRoomId = this.context.legacyCallHandler.roomIdForCall(call); + const callRoom = (callRoomId ? this.context.client?.getRoom(callRoomId) : undefined) ?? undefined; const avatarSize = pipMode ? "76px" : "160px"; - const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(call.callId); + const transfereeCall = this.context.legacyCallHandler.getTransfereeForCallId(call.callId); const isOnHold = isLocalOnHold || isRemoteOnHold; let secondaryFeedElement: React.ReactNode; @@ -421,12 +423,12 @@ export default class LegacyCallView extends React.Component { let holdTransferContent: React.ReactNode; if (transfereeCall) { - const cli = MatrixClientPeg.safeGet(); - const callRoomId = LegacyCallHandler.instance.roomIdForCall(call); - const transferTargetRoom = callRoomId ? cli.getRoom(callRoomId) : null; + const cli = this.context.client; + const callRoomId = this.context.legacyCallHandler.roomIdForCall(call); + const transferTargetRoom = callRoomId ? cli?.getRoom(callRoomId) : null; const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("voip|unknown_person"); - const transfereeCallRoomId = LegacyCallHandler.instance.roomIdForCall(transfereeCall); - const transfereeRoom = transfereeCallRoomId ? cli.getRoom(transfereeCallRoomId) : null; + const transfereeCallRoomId = this.context.legacyCallHandler.roomIdForCall(transfereeCall); + const transfereeRoom = transfereeCallRoomId ? cli?.getRoom(transfereeCallRoomId) : null; const transfereeName = transfereeRoom ? transfereeRoom.name : _t("voip|unknown_person"); holdTransferContent = ( @@ -451,7 +453,7 @@ export default class LegacyCallView extends React.Component { let onHoldText: React.ReactNode; if (isRemoteOnHold) { onHoldText = _t( - LegacyCallHandler.instance.hasAnyUnheldCall() + this.context.legacyCallHandler.hasAnyUnheldCall() ? _td("voip|call_held_switch") : _td("voip|call_held_resume"), {}, @@ -544,13 +546,13 @@ export default class LegacyCallView extends React.Component { const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader, sidebarShown } = this.props; const { sidebarFeeds } = this.state; - const client = MatrixClientPeg.safeGet(); - const callRoomId = LegacyCallHandler.instance.roomIdForCall(call); - const secondaryCallRoomId = LegacyCallHandler.instance.roomIdForCall(secondaryCall); - const callRoom = callRoomId ? client.getRoom(callRoomId) : null; + const client = this.context.client; + const callRoomId = this.context.legacyCallHandler.roomIdForCall(call); + const secondaryCallRoomId = this.context.legacyCallHandler.roomIdForCall(secondaryCall); + const callRoom = callRoomId ? client?.getRoom(callRoomId) : null; if (!callRoom) return null; - const secCallRoom = secondaryCallRoomId ? client.getRoom(secondaryCallRoomId) : null; + const secCallRoom = secondaryCallRoomId ? client?.getRoom(secondaryCallRoomId) : null; const callViewClasses = classNames({ mx_LegacyCallView: true, @@ -565,7 +567,7 @@ export default class LegacyCallView extends React.Component {
diff --git a/apps/web/src/components/views/voip/LegacyCallViewForRoom.tsx b/apps/web/src/components/views/voip/LegacyCallViewForRoom.tsx index d996fe98d6c..58278afae02 100644 --- a/apps/web/src/components/views/voip/LegacyCallViewForRoom.tsx +++ b/apps/web/src/components/views/voip/LegacyCallViewForRoom.tsx @@ -10,7 +10,7 @@ import { CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import React from "react"; import { Resizable } from "re-resizable"; -import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler"; +import { LegacyCallHandlerEvent } from "../../../LegacyCallHandler"; import LegacyCallView from "./LegacyCallView"; import { SDKContext } from "../../../contexts/SDKContext"; @@ -39,20 +39,20 @@ export default class LegacyCallViewForRoom extends React.Component { @@ -60,14 +60,14 @@ export default class LegacyCallViewForRoom extends React.Component { if (!this.state.call) return; - LegacyCallHandler.instance.setCallSidebarShown(this.state.call.callId, sidebarShown); + this.context.legacyCallHandler.setCallSidebarShown(this.state.call.callId, sidebarShown); }; public render(): React.ReactNode { diff --git a/apps/web/src/components/views/voip/VideoFeed.tsx b/apps/web/src/components/views/voip/VideoFeed.tsx index 87bee5c4ad8..9023491901b 100644 --- a/apps/web/src/components/views/voip/VideoFeed.tsx +++ b/apps/web/src/components/views/voip/VideoFeed.tsx @@ -16,9 +16,8 @@ import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventType import { MicOffSolidIcon, MicOnSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons"; import SettingsStore from "../../../settings/SettingsStore"; -import LegacyCallHandler from "../../../LegacyCallHandler"; -import { MatrixClientPeg } from "../../../MatrixClientPeg"; import RoomAvatar from "../avatars/RoomAvatar"; +import { SDKContext } from "../../../contexts/SDKContext.ts"; interface IProps { call: MatrixCall; @@ -45,6 +44,9 @@ interface IState { } export default class VideoFeed extends React.PureComponent { + public static contextType = SDKContext; + declare public context: React.ContextType; + private element?: HTMLVideoElement; public constructor(props: IProps) { @@ -192,8 +194,8 @@ export default class VideoFeed extends React.PureComponent { let content; if (this.state.videoMuted) { - const callRoomId = LegacyCallHandler.instance.roomIdForCall(this.props.call); - const callRoom = (callRoomId ? MatrixClientPeg.safeGet().getRoom(callRoomId) : undefined) ?? undefined; + const callRoomId = this.context.legacyCallHandler.roomIdForCall(this.props.call); + const callRoom = (callRoomId ? this.context.client?.getRoom(callRoomId) : undefined) ?? undefined; let avatarSize; if (pipMode && primary) avatarSize = "76px"; diff --git a/apps/web/src/hooks/room/useRoomCall.tsx b/apps/web/src/hooks/room/useRoomCall.tsx index 50e8b41f6ff..1f6d14360cd 100644 --- a/apps/web/src/hooks/room/useRoomCall.tsx +++ b/apps/web/src/hooks/room/useRoomCall.tsx @@ -215,10 +215,10 @@ export const useRoomCall = ( widget = groupCall?.widget ?? jitsiWidget; } const updateWidgetState = useCallback((): void => { - setCanPinWidget(WidgetLayoutStore.instance.canAddToContainer(room, "top")); - setWidgetPinned(!!widget && WidgetLayoutStore.instance.isInContainer(room, widget, "top")); - }, [room, widget]); - useEventEmitter(WidgetLayoutStore.instance, WidgetLayoutStore.emissionForRoom(room), updateWidgetState); + setCanPinWidget(sdkContext.widgetLayoutStore.canAddToContainer(room, "top")); + setWidgetPinned(!!widget && sdkContext.widgetLayoutStore.isInContainer(room, widget, "top")); + }, [room, widget, sdkContext.widgetLayoutStore]); + useEventEmitter(sdkContext.widgetLayoutStore, WidgetLayoutStore.emissionForRoom(room), updateWidgetState); useEffect(() => { updateWidgetState(); }, [room, jitsiWidget, groupCall, updateWidgetState]); @@ -267,25 +267,39 @@ export const useRoomCall = ( (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { evt?.stopPropagation(); if (widget && promptPinWidget) { - WidgetLayoutStore.instance.moveToContainer(room, widget, "top"); + sdkContext.widgetLayoutStore.moveToContainer(room, widget, "top"); } else { - placeCall(room, CallType.Voice, callPlatformType, evt?.shiftKey || undefined, true); + placeCall( + sdkContext.legacyCallHandler, + room, + CallType.Voice, + callPlatformType, + evt?.shiftKey || undefined, + true, + ); } }, - [promptPinWidget, room, widget], + [promptPinWidget, room, widget, sdkContext.widgetLayoutStore, sdkContext.legacyCallHandler], ); const videoCallClick = useCallback( (evt: React.MouseEvent | undefined, callPlatformType: PlatformCallType): void => { evt?.stopPropagation(); if (widget && promptPinWidget) { - WidgetLayoutStore.instance.moveToContainer(room, widget, "top"); + sdkContext.widgetLayoutStore.moveToContainer(room, widget, "top"); } else { // If we have pressed shift then always skip the lobby, otherwise `undefined` will defer // to the defaults of the call implementation. - placeCall(room, CallType.Video, callPlatformType, evt?.shiftKey || undefined, false); + placeCall( + sdkContext.legacyCallHandler, + room, + CallType.Video, + callPlatformType, + evt?.shiftKey || undefined, + false, + ); } }, - [widget, promptPinWidget, room], + [widget, promptPinWidget, room, sdkContext.widgetLayoutStore, sdkContext.legacyCallHandler], ); let voiceCallDisabledReason: string | null; diff --git a/apps/web/src/toasts/IncomingCallToast.tsx b/apps/web/src/toasts/IncomingCallToast.tsx index 9a4fa0f0eff..14f253c5224 100644 --- a/apps/web/src/toasts/IncomingCallToast.tsx +++ b/apps/web/src/toasts/IncomingCallToast.tsx @@ -16,6 +16,7 @@ import React, { useRef, useState, useId, + useContext, } from "react"; import { type Room, @@ -39,7 +40,6 @@ import { AvatarWithDetails } from "@element-hq/web-shared-components"; import { _t } from "../languageHandler"; import RoomAvatar from "../components/views/avatars/RoomAvatar"; -import { MatrixClientPeg } from "../MatrixClientPeg"; import defaultDispatcher from "../dispatcher/dispatcher"; import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload"; import { Action } from "../dispatcher/actions"; @@ -49,11 +49,12 @@ import AccessibleButton, { type ButtonEvent } from "../components/views/elements import { useDispatcher } from "../hooks/useDispatcher"; import { type ActionPayload } from "../dispatcher/payloads"; import { type Call, CallEvent } from "../models/Call"; -import LegacyCallHandler, { AudioID } from "../LegacyCallHandler"; +import { AudioID } from "../LegacyCallHandler"; import { useEventEmitter, useTypedEventEmitter } from "../hooks/useEventEmitter"; import { CallStore, CallStoreEvent } from "../stores/CallStore"; import DMRoomMap from "../utils/DMRoomMap"; import MemberAvatar from "../components/views/avatars/MemberAvatar"; +import { SDKContext } from "../contexts/SDKContext.ts"; /** * Get the key for the incoming call toast. A combination of the call ID and room ID. @@ -158,10 +159,11 @@ interface Props { } export function IncomingCallToast({ notificationEvent, toastKey }: Props): JSX.Element { + const sdkContext = useContext(SDKContext); const roomId = notificationEvent.getRoomId()!; // Use a partial type so ts still helps us to not miss any type checks. const notificationContent = notificationEvent.getContent() as Partial; - const room = MatrixClientPeg.safeGet().getRoom(roomId) ?? undefined; + const room = sdkContext.client?.getRoom(roomId) ?? undefined; const call = useCall(roomId); const [connectedCalls, setConnectedCalls] = useState(Array.from(CallStore.instance.connectedCalls)); useEventEmitter(CallStore.instance, CallStoreEvent.ConnectedCalls, () => { @@ -174,18 +176,18 @@ export function IncomingCallToast({ notificationEvent, toastKey }: Props): JSX.E // This is because `LegacyCallHandler.play` tries to load the sound and then play it asynchonously // and `LegacyCallHandler.isPlaying` will not be `true` until the sound starts playing. const isRingToast = notificationContent.notification_type === "ring"; - if (isRingToast && !soundHasStarted.current && !LegacyCallHandler.instance.isPlaying(AudioID.Ring)) { + if (isRingToast && !soundHasStarted.current && !sdkContext.legacyCallHandler.isPlaying(AudioID.Ring)) { // Start ringing if not already. soundHasStarted.current = true; - void LegacyCallHandler.instance.play(AudioID.Ring); + void sdkContext.legacyCallHandler.play(AudioID.Ring); } - }, [notificationContent.notification_type, soundHasStarted]); + }, [notificationContent.notification_type, soundHasStarted, sdkContext.legacyCallHandler]); // Stop ringing on dismiss. const dismissToast = useCallback((): void => { ToastStore.sharedInstance().dismissToast(toastKey); - LegacyCallHandler.instance.pause(AudioID.Ring); - }, [toastKey]); + sdkContext.legacyCallHandler.pause(AudioID.Ring); + }, [toastKey, sdkContext.legacyCallHandler]); // Dismiss if the notification event or call event is redacted useTypedEventEmitter(room, MatrixEventEvent.BeforeRedaction, (ev: MatrixEvent) => { diff --git a/apps/web/src/toasts/IncomingLegacyCallToast.tsx b/apps/web/src/toasts/IncomingLegacyCallToast.tsx index bbcf241587c..577a4e0539a 100644 --- a/apps/web/src/toasts/IncomingLegacyCallToast.tsx +++ b/apps/web/src/toasts/IncomingLegacyCallToast.tsx @@ -19,12 +19,12 @@ import { VolumeOnSolidIcon, } from "@vector-im/compound-design-tokens/assets/web/icons"; -import LegacyCallHandler, { LegacyCallHandlerEvent } from "../LegacyCallHandler"; -import { MatrixClientPeg } from "../MatrixClientPeg"; +import { LegacyCallHandlerEvent } from "../LegacyCallHandler"; import { _t } from "../languageHandler"; import RoomAvatar from "../components/views/avatars/RoomAvatar"; import AccessibleButton, { type ButtonEvent } from "../components/views/elements/AccessibleButton"; import { getCallStateIcon } from "../components/views/messages/LegacyCallEvent.tsx"; +import { SDKContext } from "../contexts/SDKContext.ts"; export const getIncomingLegacyCallToastKey = (callId: string): string => `call_${callId}`; @@ -37,64 +37,67 @@ interface IState { } export default class IncomingLegacyCallToast extends React.Component { + public static contextType = SDKContext; + declare public context: React.ContextType; + private readonly roomId: string; public constructor(props: IProps) { super(props); - const roomId = LegacyCallHandler.instance.roomIdForCall(this.props.call); + const roomId = this.context.legacyCallHandler.roomIdForCall(this.props.call); if (!roomId) { throw new Error("Unable to find room for incoming call"); } this.roomId = roomId; this.state = { - silenced: LegacyCallHandler.instance.isCallSilenced(this.props.call.callId), + silenced: this.context.legacyCallHandler.isCallSilenced(this.props.call.callId), }; } public componentDidMount = (): void => { - LegacyCallHandler.instance.addListener( + this.context.legacyCallHandler.addListener( LegacyCallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged, ); }; public componentWillUnmount(): void { - LegacyCallHandler.instance.removeListener( + this.context.legacyCallHandler.removeListener( LegacyCallHandlerEvent.SilencedCallsChanged, this.onSilencedCallsChanged, ); } private onSilencedCallsChanged = (): void => { - this.setState({ silenced: LegacyCallHandler.instance.isCallSilenced(this.props.call.callId) }); + this.setState({ silenced: this.context.legacyCallHandler.isCallSilenced(this.props.call.callId) }); }; private onAnswerClick = (e: ButtonEvent): void => { e.stopPropagation(); - LegacyCallHandler.instance.answerCall(this.roomId); + this.context.legacyCallHandler.answerCall(this.roomId); }; private onRejectClick = (e: ButtonEvent): void => { e.stopPropagation(); - LegacyCallHandler.instance.hangupOrReject(this.roomId, true); + this.context.legacyCallHandler.hangupOrReject(this.roomId, true); }; private onSilenceClick = (e: ButtonEvent): void => { e.stopPropagation(); const callId = this.props.call.callId; if (this.state.silenced) { - LegacyCallHandler.instance.unSilenceCall(callId); + this.context.legacyCallHandler.unSilenceCall(callId); } else { - LegacyCallHandler.instance.silenceCall(callId); + this.context.legacyCallHandler.silenceCall(callId); } }; public render(): React.ReactNode { - const room = MatrixClientPeg.safeGet().getRoom(this.roomId); + const room = this.context.client?.getRoom(this.roomId); const isVoice = this.props.call.type === CallType.Voice; - const callForcedSilent = LegacyCallHandler.instance.isForcedSilent(); + const callForcedSilent = this.context.legacyCallHandler.isForcedSilent(); let silenceButtonTooltip = this.state.silenced ? _t("voip|unsilence") : _t("voip|silence"); if (callForcedSilent) { @@ -105,7 +108,7 @@ export default class IncomingLegacyCallToast extends React.Component
- {room ? room.name : _t("voip|unknown_caller")} + {room?.name ?? _t("voip|unknown_caller")}
{getCallStateIcon(isVoice, undefined)} {isVoice ? _t("voip|voice_call") : _t("voip|video_call")} diff --git a/apps/web/src/utils/room/placeCall.ts b/apps/web/src/utils/room/placeCall.ts index 6c046116ce9..4991ba65669 100644 --- a/apps/web/src/utils/room/placeCall.ts +++ b/apps/web/src/utils/room/placeCall.ts @@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details. import { type CallType } from "matrix-js-sdk/src/webrtc/call"; import { type Room } from "matrix-js-sdk/src/matrix"; -import LegacyCallHandler from "../../LegacyCallHandler"; +import type LegacyCallHandler from "../../LegacyCallHandler"; import { getPlatformCallTypeProps, PlatformCallType } from "../../hooks/room/useRoomCall"; import defaultDispatcher from "../../dispatcher/dispatcher"; import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload"; @@ -24,6 +24,7 @@ import PosthogTrackers from "../../PosthogTrackers"; * @param skipLobby Has the user indicated they would like to skip the lobby. Otherwise, defer to platform defaults. */ export const placeCall = async ( + legacyCallHandler: LegacyCallHandler, room: Room, callType: CallType, platformCallType: PlatformCallType, @@ -34,7 +35,7 @@ export const placeCall = async ( PosthogTrackers.trackInteraction(analyticsName); if (platformCallType == PlatformCallType.LegacyCall || platformCallType == PlatformCallType.JitsiCall) { - await LegacyCallHandler.instance.placeCall(room.roomId, callType); + await legacyCallHandler.placeCall(room.roomId, callType); } else if (platformCallType == PlatformCallType.ElementCall) { defaultDispatcher.dispatch({ action: Action.ViewRoom, From 0f9877cacefae13b8bd7c7f91ce0218990264473 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jul 2026 15:26:47 +0100 Subject: [PATCH 3/6] Fix tests --- .../structures/ThreadPanel-test.tsx | 20 +++++++++++-- .../views/elements/AppTile-test.tsx | 1 + .../right_panel/RoomSummaryCardView-test.tsx | 10 +++++-- .../views/right_panel/UserInfo-test.tsx | 30 +++++++------------ .../toasts/IncomingCallToast-test.tsx | 9 +++++- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/apps/web/test/unit-tests/components/structures/ThreadPanel-test.tsx b/apps/web/test/unit-tests/components/structures/ThreadPanel-test.tsx index 591d990e93b..b9083d62eb4 100644 --- a/apps/web/test/unit-tests/components/structures/ThreadPanel-test.tsx +++ b/apps/web/test/unit-tests/components/structures/ThreadPanel-test.tsx @@ -24,10 +24,18 @@ import { _t } from "../../../../src/languageHandler"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks"; import ResizeNotifier from "../../../../src/utils/ResizeNotifier"; -import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../../test-utils"; +import { + clientAndSDKContextRenderOptions, + createTestClient, + getRoomContext, + mkRoom, + mockPlatformPeg, + stubClient, +} from "../../../test-utils"; import { mkThread } from "../../../test-utils/threads"; import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx"; import type { RoomContextType } from "../../../../src/contexts/RoomContext.ts"; +import { SDKContextClass } from "../../../../src/contexts/SDKContextClass.ts"; jest.mock("../../../../src/utils/Feedback"); @@ -215,7 +223,10 @@ describe("ThreadPanel", () => { myThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true }); myThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true }); - const renderResult = render(); + const renderResult = render( + , + clientAndSDKContextRenderOptions(createTestClient(), SDKContextClass.instance), + ); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => { const events = findEvents(renderResult.container); @@ -260,7 +271,10 @@ describe("ThreadPanel", () => { const [allThreads] = room.threadsTimelineSets; allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true }); - const renderResult = render(); + const renderResult = render( + , + clientAndSDKContextRenderOptions(createTestClient(), SDKContextClass.instance), + ); await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy()); await waitFor(() => { const events = findEvents(renderResult.container); diff --git a/apps/web/test/unit-tests/components/views/elements/AppTile-test.tsx b/apps/web/test/unit-tests/components/views/elements/AppTile-test.tsx index c9b6968b5fc..ee19d49b945 100644 --- a/apps/web/test/unit-tests/components/views/elements/AppTile-test.tsx +++ b/apps/web/test/unit-tests/components/views/elements/AppTile-test.tsx @@ -120,6 +120,7 @@ describe("AppTile", () => { // Do not carry across settings from previous tests SettingsStore.reset(); sdkContext = new SDKContextClass(); + sdkContext.client = cli; // @ts-ignore await WidgetMessagingStore.instance.onReady(); diff --git a/apps/web/test/unit-tests/components/views/right_panel/RoomSummaryCardView-test.tsx b/apps/web/test/unit-tests/components/views/right_panel/RoomSummaryCardView-test.tsx index cd38d48e093..9da593b8720 100644 --- a/apps/web/test/unit-tests/components/views/right_panel/RoomSummaryCardView-test.tsx +++ b/apps/web/test/unit-tests/components/views/right_panel/RoomSummaryCardView-test.tsx @@ -23,6 +23,8 @@ import { useRoomSummaryCardViewModel, } from "../../../../../src/components/viewmodels/right_panel/RoomSummaryCardViewModel"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; +import { SDKContext } from "../../../../../src/contexts/SDKContext.ts"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; // Mock the viewmodel hooks jest.mock("../../../../../src/components/viewmodels/right_panel/RoomSummaryCardViewModel", () => ({ @@ -45,9 +47,11 @@ describe("", () => { return render(, { wrapper: ({ children }) => ( - - {children} - + + + {children} + + ), }); }; diff --git a/apps/web/test/unit-tests/components/views/right_panel/UserInfo-test.tsx b/apps/web/test/unit-tests/components/views/right_panel/UserInfo-test.tsx index 4dc8e874828..dba9dd25c54 100644 --- a/apps/web/test/unit-tests/components/views/right_panel/UserInfo-test.tsx +++ b/apps/web/test/unit-tests/components/views/right_panel/UserInfo-test.tsx @@ -32,12 +32,12 @@ import UserInfo, { disambiguateDevices } from "../../../../../src/components/vie import { getPowerLevels } from "../../../../../src/components/viewmodels/right_panel/user_info/UserInfoBasicViewModel"; import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; -import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import Modal from "../../../../../src/Modal"; -import { clearAllModals, flushPromises } from "../../../../test-utils"; +import { clearAllModals, clientAndSDKContextRenderOptions, flushPromises } from "../../../../test-utils"; import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog"; import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents"; import { UIComponent } from "../../../../../src/settings/UIFeature"; +import { TestSDKContext } from "../../../TestSDKContext.ts"; jest.mock("../../../../../src/utils/direct-messages", () => ({ ...jest.requireActual("../../../../../src/utils/direct-messages"), @@ -78,6 +78,7 @@ const defaultUser = new User(defaultUserId); let mockRoom: Mocked; let mockClient: Mocked; let mockCrypto: Mocked; +let sdkContext: TestSDKContext; const origDate = global.Date.prototype.toLocaleString; beforeEach(() => { @@ -131,6 +132,8 @@ beforeEach(() => { setPowerLevel: jest.fn(), getCrypto: jest.fn().mockReturnValue(mockCrypto), } as unknown as MatrixClient); + sdkContext = new TestSDKContext(); + sdkContext.client = mockClient; jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mockClient); jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mockClient); @@ -162,13 +165,10 @@ describe("", () => { }; const renderComponent = (props = {}) => { - const Wrapper = (wrapperProps = {}) => { - return ; - }; - - return render(, { - wrapper: Wrapper, - }); + return render( + , + clientAndSDKContextRenderOptions(mockClient, sdkContext), + ); }; beforeEach(() => { @@ -307,11 +307,7 @@ describe("", () => { }); it("renders the message button", () => { - render( - - - , - ); + render(, clientAndSDKContextRenderOptions(mockClient, sdkContext)); screen.getByRole("button", { name: "Send message" }); }); @@ -322,11 +318,7 @@ describe("", () => { return component !== UIComponent.CreateRooms; }, () => { - render( - - - , - ); + render(, clientAndSDKContextRenderOptions(mockClient, sdkContext)); expect(screen.queryByRole("button", { name: "Message" })).toBeNull(); }, diff --git a/apps/web/test/unit-tests/toasts/IncomingCallToast-test.tsx b/apps/web/test/unit-tests/toasts/IncomingCallToast-test.tsx index 025885e3194..ee62a15b572 100644 --- a/apps/web/test/unit-tests/toasts/IncomingCallToast-test.tsx +++ b/apps/web/test/unit-tests/toasts/IncomingCallToast-test.tsx @@ -34,6 +34,7 @@ import { setupAsyncStoreWithClient, resetAsyncStoreWithClient, mkEvent, + clientAndSDKContextRenderOptions, } from "../../test-utils"; import defaultDispatcher from "../../../src/dispatcher/dispatcher"; import { Action } from "../../../src/dispatcher/actions"; @@ -50,6 +51,7 @@ import { import LegacyCallHandler, { AudioID } from "../../../src/LegacyCallHandler"; import { CallEvent } from "../../../src/models/Call"; import { type WidgetMessaging } from "../../../src/stores/widgets/WidgetMessaging"; +import { SDKContextClass } from "../../../src/contexts/SDKContextClass.ts"; function makeNotificationEvent(room: Room, content: IContent = {}): MatrixEvent { const ts = Date.now(); @@ -92,6 +94,7 @@ describe("IncomingCallToast", () => { beforeEach(async () => { stubClient(); client = mocked(MatrixClientPeg.safeGet()); + SDKContextClass.instance.client = client; const audio = document.createElement("audio"); audio.id = AudioID.Ring; @@ -146,6 +149,7 @@ describe("IncomingCallToast", () => { notificationEvent={notificationEvent} toastKey={getIncomingCallToastKey(callId, room.roomId)} />, + clientAndSDKContextRenderOptions(client, SDKContextClass.instance), ); return callId; }; @@ -197,7 +201,10 @@ describe("IncomingCallToast", () => { it("start ringing on ring notify event", () => { const notificationEvent = makeNotificationEvent(room, { notification_type: "ring" }); const playMock = jest.spyOn(LegacyCallHandler.instance, "play"); - render(); + render( + , + clientAndSDKContextRenderOptions(client, SDKContextClass.instance), + ); expect(playMock).toHaveBeenCalled(); }); From 1903eac8e88d2de21c2f66fd616b443f614b8802 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jul 2026 15:43:47 +0100 Subject: [PATCH 4/6] Fix tests --- apps/web/test/test-utils/test-utils.ts | 2 + ...erInfoHeaderVerificationViewModel-test.tsx | 9 +++- .../views/right_panel/ExtensionsCard-test.tsx | 41 +++++++++++++--- .../right_panel/PinnedMessagesCard-test.tsx | 49 ++++++++++--------- .../views/voip/LegacyCallView-test.tsx | 24 ++++++--- 5 files changed, 85 insertions(+), 40 deletions(-) diff --git a/apps/web/test/test-utils/test-utils.ts b/apps/web/test/test-utils/test-utils.ts index e0afa17ee14..cbebf0b5c0a 100644 --- a/apps/web/test/test-utils/test-utils.ts +++ b/apps/web/test/test-utils/test-utils.ts @@ -49,6 +49,7 @@ import { EnhancedMap } from "../../src/utils/maps"; import { type AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient"; import MatrixClientBackedSettingsHandler from "../../src/settings/handlers/MatrixClientBackedSettingsHandler"; import { vi } from "../setup/adapter.ts"; +import { SDKContextClass } from "../../src/contexts/SDKContextClass.ts"; /** * Stub out the MatrixClient, and configure the MatrixClientPeg object to @@ -76,6 +77,7 @@ export function stubClient(): MatrixClient { peg.get = () => client; peg.safeGet = () => client; MatrixClientBackedSettingsHandler.matrixClient = client; + SDKContextClass.instance.client = client; return client; } diff --git a/apps/web/test/unit-tests/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel-test.tsx b/apps/web/test/unit-tests/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel-test.tsx index fb9fc12fa60..75072f2e846 100644 --- a/apps/web/test/unit-tests/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel-test.tsx +++ b/apps/web/test/unit-tests/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel-test.tsx @@ -10,9 +10,10 @@ import { type Mocked } from "jest-mock"; import { UserVerificationStatus, type CryptoApi } from "matrix-js-sdk/src/crypto-api"; import { renderHook, waitFor } from "jest-matrix-react"; -import { createTestClient, withClientContextRenderOptions } from "../../../../../test-utils"; +import { clientAndSDKContextRenderOptions, createTestClient } from "../../../../../test-utils"; import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; import { useUserInfoVerificationViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderVerificationViewModel"; +import { TestSDKContext } from "../../../../TestSDKContext.ts"; describe("useUserInfoVerificationHeaderViewModel", () => { const defaultRoomId = "!fkfk"; @@ -26,6 +27,7 @@ describe("useUserInfoVerificationHeaderViewModel", () => { }; let mockClient: MatrixClient; let mockCrypto: Mocked; + let sdkContext: TestSDKContext; beforeEach(() => { mockCrypto = { @@ -43,6 +45,9 @@ describe("useUserInfoVerificationHeaderViewModel", () => { } as unknown as Mocked; mockClient = createTestClient(); + sdkContext = new TestSDKContext(); + sdkContext.client = mockClient; + jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true); jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true); jest.spyOn(mockClient, "getCrypto").mockReturnValue(mockCrypto); @@ -57,7 +62,7 @@ describe("useUserInfoVerificationHeaderViewModel", () => { const renderUserInfoHeaderVerificationHook = (props = defaultProps) => { return renderHook( () => useUserInfoVerificationViewModel(props.member, props.devices), - withClientContextRenderOptions(mockClient), + clientAndSDKContextRenderOptions(mockClient, sdkContext), ); }; diff --git a/apps/web/test/unit-tests/components/views/right_panel/ExtensionsCard-test.tsx b/apps/web/test/unit-tests/components/views/right_panel/ExtensionsCard-test.tsx index e11b4092d74..c446c176ace 100644 --- a/apps/web/test/unit-tests/components/views/right_panel/ExtensionsCard-test.tsx +++ b/apps/web/test/unit-tests/components/views/right_panel/ExtensionsCard-test.tsx @@ -14,27 +14,34 @@ import { MatrixWidgetType } from "matrix-widget-api"; import userEvent from "@testing-library/user-event"; import ExtensionsCard from "../../../../../src/components/views/right_panel/ExtensionsCard"; -import { stubClient } from "../../../../test-utils"; +import { clientAndSDKContextRenderOptions, stubClient } from "../../../../test-utils"; import { type IApp } from "../../../../../src/stores/WidgetStore"; import WidgetUtils, { useWidgets } from "../../../../../src/utils/WidgetUtils"; import { WidgetLayoutStore } from "../../../../../src/stores/widgets/WidgetLayoutStore"; import { IntegrationManagers } from "../../../../../src/integrations/IntegrationManagers"; +import { TestSDKContext } from "../../../TestSDKContext.ts"; jest.mock("../../../../../src/utils/WidgetUtils"); describe("", () => { let client: Mocked; let room: Room; + let sdkContext: TestSDKContext; beforeEach(() => { client = mocked(stubClient()); + sdkContext = new TestSDKContext(); + sdkContext.client = client; room = new Room("!room:server", client, client.getSafeUserId()); mocked(WidgetUtils.getWidgetName).mockImplementation((app) => app?.name ?? "No Name"); }); it("should render empty state", () => { mocked(useWidgets).mockReturnValue([]); - const { asFragment } = render(); + const { asFragment } = render( + , + clientAndSDKContextRenderOptions(client, sdkContext), + ); expect(screen.getByText("Boost productivity with more tools, widgets and bots")).toBeInTheDocument(); expect(asFragment()).toMatchSnapshot(); }); @@ -61,7 +68,10 @@ describe("", () => { }, ] satisfies IApp[]); - const { asFragment } = render(); + const { asFragment } = render( + , + clientAndSDKContextRenderOptions(client, sdkContext), + ); expect(screen.getByText("Custom Widget")).toBeInTheDocument(); expect(screen.getByText("Jitsi")).toBeInTheDocument(); expect(asFragment()).toMatchSnapshot(); @@ -81,7 +91,10 @@ describe("", () => { }, ] satisfies IApp[]); - const { container } = render(); + const { container } = render( + , + clientAndSDKContextRenderOptions(client, sdkContext), + ); await userEvent.click(container.querySelector(".mx_ExtensionsCard_app_options")!); expect(document.querySelector(".mx_IconizedContextMenu")).toMatchSnapshot(); }); @@ -100,7 +113,10 @@ describe("", () => { }, ] satisfies IApp[]); - render(); + render( + , + clientAndSDKContextRenderOptions(client, sdkContext), + ); expect(screen.getByText("Set layout for everyone")).toBeInTheDocument(); }); @@ -118,7 +134,10 @@ describe("", () => { }, ] satisfies IApp[]); - render(); + render( + , + clientAndSDKContextRenderOptions(client, sdkContext), + ); expect(screen.getByText("Custom Widget").closest(".mx_ExtensionsCard_Button_pinned")).toBeInTheDocument(); }); @@ -137,14 +156,20 @@ describe("", () => { }, ] satisfies IApp[]); - render(); + render( + , + clientAndSDKContextRenderOptions(client, sdkContext), + ); expect(screen.getByLabelText("You can only pin up to 3 widgets")).toBeInTheDocument(); }); it("should should open integration manager on click", async () => { jest.spyOn(IntegrationManagers.sharedInstance(), "hasManager").mockReturnValue(false); const spy = jest.spyOn(IntegrationManagers.sharedInstance(), "openNoManagerDialog"); - render(); + render( + , + clientAndSDKContextRenderOptions(client, sdkContext), + ); await userEvent.click(screen.getByText("Add extensions")); expect(spy).toHaveBeenCalled(); }); diff --git a/apps/web/test/unit-tests/components/views/right_panel/PinnedMessagesCard-test.tsx b/apps/web/test/unit-tests/components/views/right_panel/PinnedMessagesCard-test.tsx index e66930aabe6..ac53b9b6568 100644 --- a/apps/web/test/unit-tests/components/views/right_panel/PinnedMessagesCard-test.tsx +++ b/apps/web/test/unit-tests/components/views/right_panel/PinnedMessagesCard-test.tsx @@ -27,13 +27,19 @@ import { PollEndEvent } from "matrix-js-sdk/src/extensible_events_v1/PollEndEven import { sleep } from "matrix-js-sdk/src/utils"; import userEvent from "@testing-library/user-event"; -import { stubClient, mkEvent, mkMessage, flushPromises } from "../../../../test-utils"; +import { + stubClient, + mkEvent, + mkMessage, + flushPromises, + clientAndSDKContextRenderOptions, +} from "../../../../test-utils"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import { PinnedMessagesCard } from "../../../../../src/components/views/right_panel/PinnedMessagesCard"; -import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; import Modal from "../../../../../src/Modal"; import { UnpinAllDialog } from "../../../../../src/components/views/dialogs/UnpinAllDialog"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; describe("", () => { let cli: MockedObject; @@ -86,13 +92,12 @@ describe("", () => { async function renderMessagePinList(room: Room): Promise { const renderResult = render( - - - , + , + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), ); // Wait a tick for state updates await act(() => sleep(0)); @@ -170,13 +175,12 @@ describe("", () => { it("should show spinner whilst loading", async () => { const room = mkRoom([], [pin1]); render( - - - , + , + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), ); await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar")); @@ -322,13 +326,12 @@ describe("", () => { ).mockReturnValue(false); const { asFragment } = render( - - - , + , + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), ); // Wait a tick for state updates diff --git a/apps/web/test/unit-tests/components/views/voip/LegacyCallView-test.tsx b/apps/web/test/unit-tests/components/views/voip/LegacyCallView-test.tsx index 08f757067d9..5273c07b91e 100644 --- a/apps/web/test/unit-tests/components/views/voip/LegacyCallView-test.tsx +++ b/apps/web/test/unit-tests/components/views/voip/LegacyCallView-test.tsx @@ -12,8 +12,9 @@ import { type CallFeed } from "matrix-js-sdk/src/webrtc/callFeed"; import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes"; import LegacyCallView from "../../../../../src/components/views/voip/LegacyCallView"; -import { stubClient } from "../../../../test-utils"; +import { clientAndSDKContextRenderOptions, stubClient } from "../../../../test-utils"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; describe("LegacyCallView", () => { it("should exit full screen on unmount", () => { @@ -22,7 +23,7 @@ describe("LegacyCallView", () => { document.fullscreenElement = element; document.exitFullscreen = jest.fn(); - stubClient(); + const cli = stubClient(); const call = { on: jest.fn(), @@ -35,14 +36,17 @@ describe("LegacyCallView", () => { isScreensharing: jest.fn().mockReturnValue(false), } as unknown as MatrixCall; - const { unmount } = render(); + const { unmount } = render( + , + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), + ); expect(document.exitFullscreen).not.toHaveBeenCalled(); unmount(); expect(document.exitFullscreen).toHaveBeenCalled(); }); it("should show/hide the sidebar based on the sidebarShown prop", async () => { - stubClient(); + const cli = stubClient(); const call = { roomId: "test-room", @@ -75,14 +79,17 @@ describe("LegacyCallView", () => { getUserIdForRoomId: jest.fn().mockReturnValue("test-user"), } as unknown as DMRoomMap); - const { container, rerender } = render(); + const { container, rerender } = render( + , + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), + ); expect(container.querySelector(".mx_LegacyCallViewSidebar")).toBeTruthy(); rerender(); expect(container.querySelector(".mx_LegacyCallViewSidebar")).toBeTruthy(); }); it("should not show the sidebar button in picture-in-picture mode", async () => { - stubClient(); + const cli = stubClient(); const call = { on: jest.fn(), @@ -98,7 +105,10 @@ describe("LegacyCallView", () => { getUserIdForRoomId: jest.fn().mockReturnValue("test-user"), } as unknown as DMRoomMap); - const { container } = render(); + const { container } = render( + , + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), + ); expect(container.querySelector(".mx_LegacyCallViewButtons_button_sidebar")).toBeFalsy(); }); }); From c1b320f03bcf9bb11d37b0a5db7d3bb0f9839b80 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Mon, 6 Jul 2026 16:02:51 +0100 Subject: [PATCH 5/6] Fix tests --- .../src/toasts/IncomingLegacyCallToast.tsx | 6 ++-- .../components/structures/FilePanel-test.tsx | 9 ++++-- .../structures/SpaceRoomView-test.tsx | 5 ++-- .../ThreadListContextMenu-test.tsx | 8 +++-- .../UserInfoHeaderVerificationView-test.tsx | 17 ++++++----- .../toasts/VerificationRequestToast-test.tsx | 30 ++++++++++++------- .../components/views/voip/VideoFeed-test.tsx | 8 +++-- .../toasts/IncomingLegacyCallToast-test.tsx | 19 ++++++++---- 8 files changed, 66 insertions(+), 36 deletions(-) diff --git a/apps/web/src/toasts/IncomingLegacyCallToast.tsx b/apps/web/src/toasts/IncomingLegacyCallToast.tsx index 577a4e0539a..54f0f2b24cd 100644 --- a/apps/web/src/toasts/IncomingLegacyCallToast.tsx +++ b/apps/web/src/toasts/IncomingLegacyCallToast.tsx @@ -42,10 +42,10 @@ export default class IncomingLegacyCallToast extends React.Component) { + super(props, context); - const roomId = this.context.legacyCallHandler.roomIdForCall(this.props.call); + const roomId = context.legacyCallHandler.roomIdForCall(this.props.call); if (!roomId) { throw new Error("Unable to find room for incoming call"); } diff --git a/apps/web/test/unit-tests/components/structures/FilePanel-test.tsx b/apps/web/test/unit-tests/components/structures/FilePanel-test.tsx index eab67252930..acbf699c420 100644 --- a/apps/web/test/unit-tests/components/structures/FilePanel-test.tsx +++ b/apps/web/test/unit-tests/components/structures/FilePanel-test.tsx @@ -12,8 +12,9 @@ import { screen, render, waitFor } from "jest-matrix-react"; import { mocked } from "jest-mock"; import FilePanel from "../../../../src/components/structures/FilePanel"; -import { mkEvent, stubClient } from "../../../test-utils"; +import { clientAndSDKContextRenderOptions, mkEvent, stubClient } from "../../../test-utils"; import { MatrixClientPeg } from "../../../../src/MatrixClientPeg"; +import { SDKContextClass } from "../../../../src/contexts/SDKContextClass.ts"; jest.mock("matrix-js-sdk/src/matrix", () => ({ ...jest.requireActual("matrix-js-sdk/src/matrix"), @@ -38,7 +39,10 @@ describe("FilePanel", () => { room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet); mocked(cli.getRoom).mockReturnValue(room); - const { asFragment } = render(); + const { asFragment } = render( + , + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), + ); await waitFor(() => { expect(screen.getByText("No files visible in this room")).toBeInTheDocument(); }); @@ -65,6 +69,7 @@ describe("FilePanel", () => { filePanel = ref; }} />, + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), ); await screen.findByText("No files visible in this room"); diff --git a/apps/web/test/unit-tests/components/structures/SpaceRoomView-test.tsx b/apps/web/test/unit-tests/components/structures/SpaceRoomView-test.tsx index 6610587a507..63ef2777e52 100644 --- a/apps/web/test/unit-tests/components/structures/SpaceRoomView-test.tsx +++ b/apps/web/test/unit-tests/components/structures/SpaceRoomView-test.tsx @@ -10,7 +10,7 @@ import { mocked, type MockedObject } from "jest-mock"; import { type MatrixClient, MatrixEvent, Preset, Room } from "matrix-js-sdk/src/matrix"; import { render, cleanup, screen, fireEvent, waitFor, act } from "jest-matrix-react"; -import { stubClient, mockPlatformPeg, unmockPlatformPeg, withClientContextRenderOptions } from "../../../test-utils"; +import { stubClient, mockPlatformPeg, unmockPlatformPeg, clientAndSDKContextRenderOptions } from "../../../test-utils"; import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases"; import SpaceRoomView from "../../../../src/components/structures/SpaceRoomView.tsx"; import ResizeNotifier from "../../../../src/utils/ResizeNotifier.ts"; @@ -19,6 +19,7 @@ import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore. import DMRoomMap from "../../../../src/utils/DMRoomMap.ts"; import { type IOpts } from "../../../../src/createRoom.ts"; import SpaceStore from "../../../../src/stores/spaces/SpaceStore.ts"; +import { SDKContextClass } from "../../../../src/contexts/SDKContextClass.ts"; describe("SpaceRoomView", () => { let cli: MockedObject; @@ -101,7 +102,7 @@ describe("SpaceRoomView", () => { onRejectButtonClicked={jest.fn()} justCreatedOpts={justCreatedOpts} />, - withClientContextRenderOptions(cli), + clientAndSDKContextRenderOptions(cli, SDKContextClass.instance), ); return spaceRoomView; }; diff --git a/apps/web/test/unit-tests/components/views/context_menus/ThreadListContextMenu-test.tsx b/apps/web/test/unit-tests/components/views/context_menus/ThreadListContextMenu-test.tsx index 23dd63e9f20..2181cffd2e1 100644 --- a/apps/web/test/unit-tests/components/views/context_menus/ThreadListContextMenu-test.tsx +++ b/apps/web/test/unit-tests/components/views/context_menus/ThreadListContextMenu-test.tsx @@ -17,8 +17,9 @@ import ThreadListContextMenu, { } from "../../../../../src/components/views/context_menus/ThreadListContextMenu"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks"; -import { stubClient } from "../../../../test-utils/test-utils"; +import { stubClient, clientAndSDKContextRenderOptions } from "../../../../test-utils"; import { mkThread } from "../../../../test-utils/threads"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; describe("ThreadListContextMenu", () => { const ROOM_ID = "!123:matrix.org"; @@ -28,7 +29,10 @@ describe("ThreadListContextMenu", () => { let event: MatrixEvent; function getComponent(props: Partial) { - return render(); + return render( + , + clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance), + ); } beforeEach(() => { diff --git a/apps/web/test/unit-tests/components/views/right_panel/user_info/UserInfoHeaderVerificationView-test.tsx b/apps/web/test/unit-tests/components/views/right_panel/user_info/UserInfoHeaderVerificationView-test.tsx index 7c54d167c3a..e7f3f3856bb 100644 --- a/apps/web/test/unit-tests/components/views/right_panel/user_info/UserInfoHeaderVerificationView-test.tsx +++ b/apps/web/test/unit-tests/components/views/right_panel/user_info/UserInfoHeaderVerificationView-test.tsx @@ -13,9 +13,9 @@ import { render, waitFor, screen } from "jest-matrix-react"; import React from "react"; import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; -import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; import { UserInfoHeaderVerificationView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoHeaderVerificationView"; -import { createTestClient } from "../../../../../test-utils"; +import { clientAndSDKContextRenderOptions, createTestClient } from "../../../../../test-utils"; +import { TestSDKContext } from "../../../../TestSDKContext.ts"; describe("", () => { const defaultRoomId = "!fkfk"; @@ -25,6 +25,7 @@ describe("", () => { let mockClient: MatrixClient; let mockCrypto: Mocked; + let sdkContext: TestSDKContext; beforeEach(() => { mockCrypto = mocked({ @@ -42,6 +43,8 @@ describe("", () => { } as unknown as CryptoApi); mockClient = createTestClient(); + sdkContext = new TestSDKContext(); + sdkContext.client = mockClient; jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true); jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true); jest.spyOn(mockClient, "getCrypto").mockReturnValue(mockCrypto); @@ -62,13 +65,11 @@ describe("", () => { mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true); - const Wrapper = (wrapperProps = {}) => { - return ; - }; - return render(, { - wrapper: Wrapper, - }); + return render( + , + clientAndSDKContextRenderOptions(mockClient, sdkContext), + ); }; it("renders verified badge when user is verified", async () => { diff --git a/apps/web/test/unit-tests/components/views/toasts/VerificationRequestToast-test.tsx b/apps/web/test/unit-tests/components/views/toasts/VerificationRequestToast-test.tsx index 386fcb1d1e7..06e4d5d6a51 100644 --- a/apps/web/test/unit-tests/components/views/toasts/VerificationRequestToast-test.tsx +++ b/apps/web/test/unit-tests/components/views/toasts/VerificationRequestToast-test.tsx @@ -14,26 +14,32 @@ import { type VerificationRequest, VerificationRequestEvent } from "matrix-js-sd import VerificationRequestToast from "../../../../../src/components/views/toasts/VerificationRequestToast"; import { + clientAndSDKContextRenderOptions, flushPromises, getMockClientWithEventEmitter, mockClientMethodsCrypto, mockClientMethodsUser, } from "../../../../test-utils"; import ToastStore from "../../../../../src/stores/ToastStore"; - -function renderComponent( - props: Partial> & { request: VerificationRequest }, -): RenderResult { - const propsWithDefaults = { - toastKey: "test", - ...props, - }; - - return render(); -} +import { TestSDKContext } from "../../../TestSDKContext.ts"; describe("VerificationRequestToast", () => { let client: Mocked; + let sdkContext: TestSDKContext; + + function renderComponent( + props: Partial> & { request: VerificationRequest }, + ): RenderResult { + const propsWithDefaults = { + toastKey: "test", + ...props, + }; + + return render( + , + clientAndSDKContextRenderOptions(client, sdkContext), + ); + } beforeEach(() => { client = getMockClientWithEventEmitter({ @@ -41,6 +47,8 @@ describe("VerificationRequestToast", () => { ...mockClientMethodsCrypto(), getDevice: jest.fn(), }); + sdkContext = new TestSDKContext(); + sdkContext.client = client; }); it("should render a self-verification", async () => { diff --git a/apps/web/test/unit-tests/components/views/voip/VideoFeed-test.tsx b/apps/web/test/unit-tests/components/views/voip/VideoFeed-test.tsx index 29a3aef703c..e4172a76267 100644 --- a/apps/web/test/unit-tests/components/views/voip/VideoFeed-test.tsx +++ b/apps/web/test/unit-tests/components/views/voip/VideoFeed-test.tsx @@ -14,9 +14,10 @@ import { type MatrixClient, Room } from "matrix-js-sdk/src/matrix"; import * as AvatarModule from "../../../../../src/Avatar"; import VideoFeed from "../../../../../src/components/views/voip/VideoFeed"; -import { stubClient, useMockedCalls } from "../../../../test-utils"; +import { clientAndSDKContextRenderOptions, stubClient, useMockedCalls } from "../../../../test-utils"; import LegacyCallHandler from "../../../../../src/LegacyCallHandler"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; const FAKE_AVATAR_URL = "http://fakeurl.dummy/fake.png"; @@ -51,7 +52,10 @@ describe("VideoFeed", () => { addListener: jest.fn(), removeListener: jest.fn(), }; - render(); + render( + , + clientAndSDKContextRenderOptions(client, SDKContextClass.instance), + ); const avatarImg = screen.getByRole("presentation"); expect(avatarImg).toHaveAttribute("src", FAKE_AVATAR_URL); }); diff --git a/apps/web/test/unit-tests/toasts/IncomingLegacyCallToast-test.tsx b/apps/web/test/unit-tests/toasts/IncomingLegacyCallToast-test.tsx index 4b766c6a08b..97aafd2e21d 100644 --- a/apps/web/test/unit-tests/toasts/IncomingLegacyCallToast-test.tsx +++ b/apps/web/test/unit-tests/toasts/IncomingLegacyCallToast-test.tsx @@ -10,10 +10,15 @@ import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixEvent, Room } from "matrix-js import { MatrixCall } from "matrix-js-sdk/src/webrtc/call"; import React from "react"; -import LegacyCallHandler from "../../../src/LegacyCallHandler"; import IncomingLegacyCallToast from "../../../src/toasts/IncomingLegacyCallToast"; import DMRoomMap from "../../../src/utils/DMRoomMap"; -import { getMockClientWithEventEmitter, mockClientMethodsServer, mockClientMethodsUser } from "../../test-utils"; +import { + clientAndSDKContextRenderOptions, + getMockClientWithEventEmitter, + mockClientMethodsServer, + mockClientMethodsUser, +} from "../../test-utils"; +import { TestSDKContext } from "../TestSDKContext.ts"; describe("", () => { const userId = "@alice:server.org"; @@ -28,6 +33,8 @@ describe("", () => { ...mockClientMethodsServer(), getRoom: jest.fn(), }); + const sdkContext = new TestSDKContext(); + sdkContext.client = mockClient; const mockRoom = new Room("!room:server.org", mockClient, userId); mockClient.deviceId = deviceId; @@ -44,13 +51,13 @@ describe("", () => { }); it("renders when silence button when call is not silenced", () => { - const { getByLabelText } = render(getComponent()); + const { getByLabelText } = render(getComponent(), clientAndSDKContextRenderOptions(mockClient, sdkContext)); expect(getByLabelText("Silence call")).toMatchSnapshot(); }); it("renders sound on button when call is silenced", () => { - LegacyCallHandler.instance.silenceCall(call.callId); - const { getByLabelText } = render(getComponent()); + sdkContext.legacyCallHandler.silenceCall(call.callId); + const { getByLabelText } = render(getComponent(), clientAndSDKContextRenderOptions(mockClient, sdkContext)); expect(getByLabelText("Sound on")).toMatchSnapshot(); }); @@ -66,7 +73,7 @@ describe("", () => { }); } }); - const { getByLabelText } = render(getComponent()); + const { getByLabelText } = render(getComponent(), clientAndSDKContextRenderOptions(mockClient, sdkContext)); expect(getByLabelText("Notifications silenced")).toMatchSnapshot(); }); }); From a868d62dd64d246e93dd41ec3d8bd8ea0e0b1e14 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 7 Jul 2026 11:09:07 +0100 Subject: [PATCH 6/6] Iterate --- apps/web/src/Modal.tsx | 65 ++++++++++--------- .../views/right_panel/BaseCard-test.tsx | 3 + .../user_info/UserInfoHeaderView-test.tsx | 15 ++--- .../views/rooms/ThirdPartyMemberInfo-test.tsx | 13 +++- .../views/voip/LegacyCallViewForRoom-test.tsx | 18 +++-- 5 files changed, 66 insertions(+), 48 deletions(-) diff --git a/apps/web/src/Modal.tsx b/apps/web/src/Modal.tsx index d92f83d7f17..adcb23ad8db 100644 --- a/apps/web/src/Modal.tsx +++ b/apps/web/src/Modal.tsx @@ -19,6 +19,7 @@ import AsyncWrapper from "./AsyncWrapper"; import { type Defaultize } from "./@types/common"; import { type ActionPayload } from "./dispatcher/payloads"; import { filterBoolean } from "./utils/arrays.ts"; +import { SDKContext } from "./contexts/SDKContext.ts"; const DIALOG_CONTAINER_ID = "mx_Dialog_Container"; const STATIC_DIALOG_CONTAINER_ID = "mx_Dialog_StaticContainer"; @@ -437,21 +438,23 @@ export class ModalManager extends TypedEventEmitter - {/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */} - - -
- -
{this.staticModal.elem}
-
-
-
- - + + {/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */} + + +
+ +
{this.staticModal.elem}
+
+
+
+ + + ); @@ -469,21 +472,23 @@ export class ModalManager extends TypedEventEmitter - {/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */} - - -
- -
{modal.elem}
-
-
-
- - + + {/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */} + + +
+ +
{modal.elem}
+
+
+
+ + + ); diff --git a/apps/web/test/unit-tests/components/views/right_panel/BaseCard-test.tsx b/apps/web/test/unit-tests/components/views/right_panel/BaseCard-test.tsx index 3764f534929..817c9b44cf5 100644 --- a/apps/web/test/unit-tests/components/views/right_panel/BaseCard-test.tsx +++ b/apps/web/test/unit-tests/components/views/right_panel/BaseCard-test.tsx @@ -10,6 +10,8 @@ import { fireEvent, render, screen } from "jest-matrix-react"; import BaseCard from "../../../../../src/components/views/right_panel/BaseCard.tsx"; import RightPanelStore from "../../../../../src/stores/right-panel/RightPanelStore.ts"; +import { clientAndSDKContextRenderOptions } from "../../../../test-utils"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; jest.mock("../../../../../src/stores/right-panel/RightPanelStore", () => ({ instance: { @@ -24,6 +26,7 @@ describe("", () => {
Content
, + clientAndSDKContextRenderOptions(SDKContextClass.instance.client!, SDKContextClass.instance), ); expect(screen.getByRole("heading")).toHaveTextContent("Heading text"); diff --git a/apps/web/test/unit-tests/components/views/right_panel/user_info/UserInfoHeaderView-test.tsx b/apps/web/test/unit-tests/components/views/right_panel/user_info/UserInfoHeaderView-test.tsx index 31da372b2a3..1792350331e 100644 --- a/apps/web/test/unit-tests/components/views/right_panel/user_info/UserInfoHeaderView-test.tsx +++ b/apps/web/test/unit-tests/components/views/right_panel/user_info/UserInfoHeaderView-test.tsx @@ -13,10 +13,10 @@ import { fireEvent, render, screen } from "jest-matrix-react"; import React from "react"; import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg"; -import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext"; import { UserInfoHeaderView } from "../../../../../../src/components/views/right_panel/user_info/UserInfoHeaderView"; -import { createTestClient } from "../../../../../test-utils"; +import { clientAndSDKContextRenderOptions, createTestClient } from "../../../../../test-utils"; import { useUserfoHeaderViewModel } from "../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel"; +import { TestSDKContext } from "../../../../TestSDKContext.ts"; // Mock the viewmodel hooks jest.mock("../../../../../../src/components/viewmodels/right_panel/user_info/UserInfoHeaderViewModel", () => ({ @@ -45,6 +45,7 @@ describe("", () => { let mockClient: MatrixClient; let mockCrypto: Mocked; + let sdkContext: TestSDKContext; beforeEach(() => { mockCrypto = mocked({ @@ -63,6 +64,8 @@ describe("", () => { mockClient = createTestClient(); mockClient.doesServerSupportExtendedProfiles = () => Promise.resolve(false); + sdkContext = new TestSDKContext(); + sdkContext.client = mockClient; jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true); jest.spyOn(mockClient.secretStorage, "hasKey").mockResolvedValue(true); @@ -90,10 +93,6 @@ describe("", () => { mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap); - const Wrapper = (wrapperProps = {}) => { - return ; - }; - return render( ", () => { devices={[device1]} hideVerificationSection={props.hideVerificationSection} />, - { - wrapper: Wrapper, - }, + clientAndSDKContextRenderOptions(mockClient, sdkContext), ); }; diff --git a/apps/web/test/unit-tests/components/views/rooms/ThirdPartyMemberInfo-test.tsx b/apps/web/test/unit-tests/components/views/rooms/ThirdPartyMemberInfo-test.tsx index 147e412d8fc..bd83665d455 100644 --- a/apps/web/test/unit-tests/components/views/rooms/ThirdPartyMemberInfo-test.tsx +++ b/apps/web/test/unit-tests/components/views/rooms/ThirdPartyMemberInfo-test.tsx @@ -11,7 +11,12 @@ import { render, screen } from "jest-matrix-react"; import { EventType, type IEvent, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix"; import ThirdPartyMemberInfo from "../../../../../src/components/views/rooms/ThirdPartyMemberInfo"; -import { getMockClientWithEventEmitter, mockClientMethodsUser } from "../../../../test-utils"; +import { + clientAndSDKContextRenderOptions, + getMockClientWithEventEmitter, + mockClientMethodsUser, +} from "../../../../test-utils"; +import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass.ts"; describe("", () => { const userId = "@alice:server.org"; @@ -37,7 +42,11 @@ describe("", () => { }); const defaultEvent = makeInviteEvent(); - const getComponent = (event: MatrixEvent = defaultEvent) => render(); + const getComponent = (event: MatrixEvent = defaultEvent) => + render( + , + clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance), + ); const room = new Room(roomId, mockClient, userId); const aliceMember = new RoomMember(roomId, userId); aliceMember.name = "Alice DisplayName"; diff --git a/apps/web/test/unit-tests/components/views/voip/LegacyCallViewForRoom-test.tsx b/apps/web/test/unit-tests/components/views/voip/LegacyCallViewForRoom-test.tsx index 8284c796be9..e5cbcccc124 100644 --- a/apps/web/test/unit-tests/components/views/voip/LegacyCallViewForRoom-test.tsx +++ b/apps/web/test/unit-tests/components/views/voip/LegacyCallViewForRoom-test.tsx @@ -12,11 +12,10 @@ import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler import LegacyCallView from "../../../../../src/components/views/voip/LegacyCallView"; import LegacyCallViewForRoom from "../../../../../src/components/views/voip/LegacyCallViewForRoom"; -import { mkStubRoom, stubClient } from "../../../../test-utils"; +import { clientAndSDKContextRenderOptions, mkStubRoom, stubClient } from "../../../../test-utils"; import DMRoomMap from "../../../../../src/utils/DMRoomMap"; import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg"; import LegacyCallHandler from "../../../../../src/LegacyCallHandler"; -import { SDKContext } from "../../../../../src/contexts/SDKContext"; import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass"; jest.mock("../../../../../src/components/views/voip/LegacyCallView", () => jest.fn(() => "LegacyCallView")); @@ -26,8 +25,9 @@ describe("LegacyCallViewForRoom", () => { let sdkContext: SDKContextClass; beforeEach(() => { - stubClient(); + const cli = stubClient(); sdkContext = new SDKContextClass(); + sdkContext.client = cli; LegacyCallViewMock.mockClear(); }); @@ -49,7 +49,10 @@ describe("LegacyCallViewForRoom", () => { const cli = MatrixClientPeg.safeGet(); cli.emit(CallEventHandlerEvent.Incoming, call); - const { rerender } = render(); + const { rerender } = render( + , + clientAndSDKContextRenderOptions(sdkContext.client!, sdkContext), + ); let props = LegacyCallViewMock.mock.lastCall![0]; expect(props.sidebarShown).toBeTruthy(); // Sidebar defaults to shown @@ -92,9 +95,10 @@ describe("LegacyCallViewForRoom", () => { jest.spyOn(sdkContext.resizeNotifier, "stopResizing"); jest.spyOn(sdkContext.resizeNotifier, "notifyTimelineHeightChanged"); - const { container } = render(, { - wrapper: ({ children }) => {children}, - }); + const { container } = render( + , + clientAndSDKContextRenderOptions(sdkContext.client!, sdkContext), + ); const resizer = container.querySelector(".mx_LegacyCallViewForRoom_ResizeHandle"); await waitFor(() => {