Remove more usages of singleton store getter in favour of contexts (#34099)

* Expose SDKContextClass via window for debugging

* Remove stores from window if they are exposed via sdkContext

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

* Remove more usages of singleton store getter in favour of contexts

* Remove more usages of singleton store getter in favour of contexts

* Fix tests by adding SDKContext.Provider

* Fix tests by adding SDKContext.Provider

* Fix tests by adding SDKContext.Provider

* Fix tests by adding SDKContext.Provider

* Fix tests

* Fix tests

* Fix tests

* Iterate

* Fix bad merge

* Iterate

* Fix tests

* Iterate

* Iterate

* Iterate

* Iterate

* Iterate

* Improve coverage

* Improve coverage
This commit is contained in:
Michael Telatynski
2026-07-14 12:58:34 +00:00
committed by GitHub
parent 9575b236c0
commit b5fd249e38
52 changed files with 666 additions and 331 deletions
+35 -30
View File
@@ -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<ModalManagerEvent, HandlerMa
const staticDialog = (
<StrictMode>
{/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */}
<I18nContext.Provider value={window.mxModuleApi.i18n}>
<TooltipProvider>
<div className={classes}>
<Glass className="mx_Dialog_border">
<div className="mx_Dialog">{this.staticModal.elem}</div>
</Glass>
<div
data-testid="dialog-background"
className="mx_Dialog_background mx_Dialog_staticBackground"
onClick={this.onBackgroundClick}
/>
</div>
</TooltipProvider>
</I18nContext.Provider>
<SDKContext.Provider value={window.mxSdkContext}>
{/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */}
<I18nContext.Provider value={window.mxModuleApi.i18n}>
<TooltipProvider>
<div className={classes}>
<Glass className="mx_Dialog_border">
<div className="mx_Dialog">{this.staticModal.elem}</div>
</Glass>
<div
data-testid="dialog-background"
className="mx_Dialog_background mx_Dialog_staticBackground"
onClick={this.onBackgroundClick}
/>
</div>
</TooltipProvider>
</I18nContext.Provider>
</SDKContext.Provider>
</StrictMode>
);
@@ -469,21 +472,23 @@ export class ModalManager extends TypedEventEmitter<ModalManagerEvent, HandlerMa
const dialog = (
<StrictMode>
{/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */}
<I18nContext.Provider value={window.mxModuleApi.i18n}>
<TooltipProvider>
<div className={classes}>
<Glass className="mx_Dialog_border">
<div className="mx_Dialog">{modal.elem}</div>
</Glass>
<div
data-testid="dialog-background"
className="mx_Dialog_background"
onClick={this.onBackgroundClick}
/>
</div>
</TooltipProvider>
</I18nContext.Provider>
<SDKContext.Provider value={window.mxSdkContext}>
{/* Provide I18nContext for shared-components used inside dialogs rendered in a separate root. */}
<I18nContext.Provider value={window.mxModuleApi.i18n}>
<TooltipProvider>
<div className={classes}>
<Glass className="mx_Dialog_border">
<div className="mx_Dialog">{modal.elem}</div>
</Glass>
<div
data-testid="dialog-background"
className="mx_Dialog_background"
onClick={this.onBackgroundClick}
/>
</div>
</TooltipProvider>
</I18nContext.Provider>
</SDKContext.Provider>
</StrictMode>
);
@@ -53,7 +53,6 @@ import UserView from "./UserView";
import { mediaFromMxc } from "../../customisations/Media";
import { UserTab } from "../views/dialogs/UserTab";
import { type OpenToTabPayload } from "../../dispatcher/payloads/OpenToTabPayload";
import RightPanelStore from "../../stores/right-panel/RightPanelStore";
import { TimelineRenderingType } from "../../contexts/RoomContext";
import { KeyBindingAction } from "../../accessibility/KeyboardShortcuts";
import { type SwitchSpacePayload } from "../../dispatcher/payloads/SwitchSpacePayload";
@@ -494,7 +493,7 @@ class LoggedInView extends React.Component<IProps, IState> {
break;
case KeyBindingAction.ToggleRoomSidePanel:
if (this.props.page_type === "room_view") {
RightPanelStore.instance.togglePanel(null);
this.context.rightPanelStore.togglePanel(null);
handled = true;
}
break;
@@ -129,7 +129,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";
@@ -1332,23 +1331,23 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
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) {
@@ -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<IProps, IState> {
public static contextType = MatrixClientContext;
declare public context: React.ContextType<typeof MatrixClientContext>;
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
private dispatcherRef?: string;
public constructor(props: IProps, context: React.ContextType<typeof MatrixClientContext>) {
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
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<IProps, IState> {
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<IProps, IState> {
private onRightPanelStoreUpdate = (): void => {
this.setState({
showRightPanel: RightPanelStore.instance.isOpenForRoom(this.props.space.roomId),
showRightPanel: this.context.rightPanelStore.isOpenForRoom(this.props.space.roomId),
});
};
@@ -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 {
/**
@@ -26,7 +26,7 @@ export interface UserInfoVerificationSectionState {
/**
* callback function when verifyUser button is clicked
*/
verifySelectedUser: () => Promise<void>;
verifySelectedUser: () => void;
}
const useHasCrossSigningKeys = (cli: MatrixClient, member: User, canVerify: boolean): boolean | undefined => {
@@ -44,21 +44,21 @@ export const useUserInfoVerificationViewModel = (
member: User | RoomMember,
devices: IDevice[],
): UserInfoVerificationSectionState => {
const cli = useContext(MatrixClientContext);
const sdkContext = useContext(SDKContext);
const userTrust = useAsyncMemo<UserVerificationStatus | undefined>(
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<void> => verifyUser(cli, member as User);
const hasCrossSigningKeys = useHasCrossSigningKeys(sdkContext.client!, member as User, canVerify);
const verifySelectedUser = (): void => verifyUser(sdkContext.rightPanelStore, sdkContext.client!, member as User);
return {
canVerify,
@@ -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<ThreadListContextMenuProps> = ({
onMenuToggle,
...props
}) => {
const sdkContext = useContext(SDKContext);
const [menuDisplayed, button, openMenu, closeThreadOptions] = useContextMenu();
const viewInRoom = useCallback(
@@ -77,8 +77,8 @@ const ThreadListContextMenu: React.FC<ThreadListContextMenuProps> = ({
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 (
<React.Fragment>
<ContextMenuTooltipButton
@@ -135,7 +135,7 @@ interface IState {
export default class AppTile extends React.Component<IProps, IState> {
public static contextType = SDKContext;
declare public context: ContextType<typeof SDKContext>;
declare public context: React.ContextType<typeof SDKContext>;
public static defaultProps: Partial<IProps> = {
waitForIframeLoad: true,
@@ -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, useMemo } from "react";
import React, { type ReactNode, type KeyboardEvent, type Ref, type MouseEvent, useMemo, 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<HTMLButtonElement>): void {
ev.preventDefault();
ev.stopPropagation();
RightPanelStore.instance.popCard();
}
const BaseCard: React.FC<IProps> = ({
closeLabel,
onClose,
@@ -60,13 +54,15 @@ const BaseCard: React.FC<IProps> = ({
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<HTMLButtonElement>): 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<IProps> = ({
let closeButton;
if (!hideHeaderButtons) {
// eslint-disable-next-line no-inner-declarations
function closeRightPanel(ev: MouseEvent<HTMLButtonElement>): void {
ev.preventDefault();
ev.stopPropagation();
sdkContext.rightPanelStore.popCard();
}
closeButton = (
<IconButton
size="28px"
@@ -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, useRef, useState } from "react";
import React, { useCallback, useContext, useEffect, useRef, useState } from "react";
import { VerificationPhase, type VerificationRequest, VerificationRequestEvent } from "matrix-js-sdk/src/crypto-api";
import { type RoomMember, type User } from "matrix-js-sdk/src/matrix";
import { logger } from "matrix-js-sdk/src/logger";
@@ -21,9 +21,8 @@ import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
import Modal from "../../../Modal";
import { _t } from "../../../languageHandler";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import ErrorDialog from "../dialogs/ErrorDialog";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext";
import { SDKContext } from "../../../contexts/SDKContext.ts";
// cancellation codes which constitute a key mismatch
const MISMATCHES = ["m.key_mismatch", "m.user_error", "m.mismatched_sas"];
@@ -38,7 +37,7 @@ interface IProps {
}
const EncryptionPanel: React.FC<IProps> = (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<IProps> = (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<IProps> = (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);
@@ -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<IAppRowProps> = ({ app, room }) => {
const sdkContext = useContext(SDKContext);
const name = WidgetUtils.getWidgetName(app);
const [canModifyWidget, setCanModifyWidget] = useState<boolean>();
@@ -52,24 +53,24 @@ const AppRow: React.FC<IAppRowProps> = ({ 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<HTMLDivElement>();
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<IAppRowProps> = ({ 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<IAppRowProps> = ({ app, room }) => {
* @param onClose callback when the card is closed
*/
const ExtensionsCard: React.FC<Props> = ({ 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<Props> = ({ room, onClose }) => {
);
} else {
let copyLayoutBtn: JSX.Element | null = null;
if (WidgetLayoutStore.instance.canCopyLayoutToRoom(room)) {
if (sdkContext.widgetLayoutStore.canCopyLayoutToRoom(room)) {
copyLayoutBtn = (
<Link onClick={() => WidgetLayoutStore.instance.copyLayoutToRoom(room)}>
<Link onClick={() => sdkContext.widgetLayoutStore.copyLayoutToRoom(room)}>
{_t("widget|set_room_layout")}
</Link>
);
@@ -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<IProps> = ({ 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<IProps> = ({ 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);
}
}}
@@ -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<IProps> = ({ 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<IProps> = ({ 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}
@@ -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 AriaRole } from "react";
import React, { type AriaRole, useContext } from "react";
import classNames from "classnames";
import { Resizable, type Size } from "re-resizable";
import { type Room } from "matrix-js-sdk/src/matrix";
@@ -79,14 +79,14 @@ export default class AppsDrawer extends React.Component<IProps, IState> {
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<IProps, IState> {
},
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<IProps, IState> {
};
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<IProps, IState> {
};
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<IPersistentResizerProps> = ({
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<IPersistentResizerProps> = ({
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();
}}
@@ -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<RightPanelPhases | null>(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
@@ -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";
@@ -58,6 +56,7 @@ import { CurrentRightPanelPhaseContextProvider } from "../../../../contexts/Curr
import { LocalRoom } from "../../../../models/LocalRoom.ts";
import { useIsEncrypted } from "../../../../hooks/useIsEncrypted.ts";
import { useUserStatus } from "../../../../hooks/useUserStatus.ts";
import { SDKContext } from "../../../../contexts/SDKContext.ts";
function RoomHeaderButtons({
room,
@@ -68,6 +67,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 });
@@ -338,7 +338,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")}
@@ -352,7 +352,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")}
>
@@ -365,7 +365,7 @@ function RoomHeaderButtons({
<IconButton
onClick={(evt) => {
evt.stopPropagation();
RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary);
sdkContext.rightPanelStore.showOrHidePhase(RightPanelPhases.RoomSummary);
}}
aria-label={_t("right_panel|room_summary_card|title")}
>
@@ -383,7 +383,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 })}
@@ -443,15 +443,15 @@ 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 dmUserStatus = useUserStatus(dmMember?.userId);
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({
@@ -482,7 +482,7 @@ export default function RoomHeader({
onClick={
room instanceof LocalRoom
? undefined
: () => RightPanelStore.instance.showOrHidePhase(RightPanelPhases.RoomSummary)
: () => sdkContext.rightPanelStore.showOrHidePhase(RightPanelPhases.RoomSummary)
}
className="mx_RoomHeader_infoWrapper"
>
@@ -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<IProps> = ({ 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<Awaited<ReturnType<MatrixClient["getRoomSummary"]>> | null> => {
if (room.getMyMembership() !== KnownMembership.Invite) return null;
@@ -64,7 +66,7 @@ const RoomInfoLine: FC<IProps> = ({ room }) => {
} else if (memberCount && summary !== undefined) {
// summary is not still loading
const viewMembers = (): void =>
RightPanelStore.instance.setCard({
sdkContext.rightPanelStore.setCard({
phase: RightPanelPhases.MemberList,
});
@@ -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<IProps, IState> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
public static defaultProps: Partial<IProps> = {
threadId: null,
};
@@ -130,17 +132,16 @@ export default class Stickerpicker extends React.PureComponent<IProps, IState> {
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<IProps, IState> {
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}
@@ -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 (
<MenuItem
className="mx_ThreadsActivityCentreRow"
@@ -125,7 +127,7 @@ function ThreadsActivityCentreRow({ room, onClick, notificationLevel }: ThreadsA
// Set the right panel card for that room so the threads panel is open before we dispatch,
// so it will open once the room appears.
RightPanelStore.instance.setCard({ phase: RightPanelPhases.ThreadPanel }, true, room.roomId);
sdkContext.rightPanelStore.setCard({ phase: RightPanelPhases.ThreadPanel }, true, room.roomId);
// Track the click on the room
PosthogTrackers.trackInteraction("WebThreadsActivityCentreRoomItem", event);
@@ -16,7 +16,6 @@ import { logger } from "matrix-js-sdk/src/logger";
import { type Device } from "matrix-js-sdk/src/matrix";
import { _t } from "../../../languageHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases";
import { userLabelForEventRoom } from "../../../utils/KeyVerificationStateObserver";
import dis from "../../../dispatcher/dispatcher";
@@ -25,9 +24,9 @@ import Modal from "../../../Modal";
import GenericToast from "./GenericToast";
import { Action } from "../../../dispatcher/actions";
import VerificationRequestDialog from "../dialogs/VerificationRequestDialog";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore";
import { type ViewRoomPayload } from "../../../dispatcher/payloads/ViewRoomPayload";
import { getDeviceCryptoInfo } from "../../../utils/crypto/deviceInfo";
import { SDKContext } from "../../../contexts/SDKContext.ts";
interface IProps {
toastKey: string;
@@ -42,6 +41,9 @@ interface IState {
}
export default class VerificationRequestToast extends React.PureComponent<IProps, IState> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
private intervalHandle?: number;
public constructor(props: IProps) {
@@ -69,7 +71,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
const otherDeviceId = request.otherDeviceId;
if (request.isSelfVerification && !!otherDeviceId) {
const cli = MatrixClientPeg.safeGet();
const cli = this.context.client!;
const device = await cli.getDevice(otherDeviceId);
this.setState({
ip: device.last_seen_ip,
@@ -104,7 +106,6 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
ToastStore.sharedInstance().dismissToast(this.props.toastKey);
const { request } = this.props;
// no room id for to_device requests
const cli = MatrixClientPeg.safeGet();
try {
if (request.roomId) {
dis.dispatch<ViewRoomPayload>({
@@ -113,8 +114,8 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
should_peek: false,
metricsTrigger: "VerificationRequest",
});
const member = cli.getUser(request.otherUserId) ?? undefined;
RightPanelStore.instance.setCards(
const member = this.context.client?.getUser(request.otherUserId) ?? undefined;
this.context.rightPanelStore.setCards(
[
{ phase: RightPanelPhases.RoomSummary },
{ phase: RightPanelPhases.MemberInfo, state: { member } },
@@ -154,7 +155,7 @@ export default class VerificationRequestToast extends React.PureComponent<IProps
});
}
} else {
const client = MatrixClientPeg.safeGet();
const client = this.context.client!;
const userId = request.otherUserId;
const roomId = request.roomId;
description = roomId ? userLabelForEventRoom(client, userId, roomId) : userId;
+1
View File
@@ -199,6 +199,7 @@ function CallStartedTileViewWrapped({ mxEvent, getRelationsForEvent }: IBodyProp
cli,
callStore: sdkContext.callStore,
latestRtcNotificationEventStore: sdkContext.latestRtcNotificationEventStore,
legacyCallHandler: sdkContext.legacyCallHandler,
}),
);
return <RootCallTileView vm={vm} />;
+24 -10
View File
@@ -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;
@@ -20,7 +20,6 @@ import {
} from "@vector-im/compound-design-tokens/assets/web/icons";
import { LegacyCallHandlerEvent } from "../LegacyCallHandler";
import { MatrixClientPeg } from "../MatrixClientPeg";
import { _t } from "../languageHandler";
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import AccessibleButton, { type ButtonEvent } from "../components/views/elements/AccessibleButton";
@@ -96,7 +95,7 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
};
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 = this.context.legacyCallHandler.isForcedSilent();
@@ -109,7 +108,7 @@ export default class IncomingLegacyCallToast extends React.Component<IProps, ISt
<React.Fragment>
<RoomAvatar room={room ?? undefined} size="32px" />
<div className="mx_IncomingLegacyCallToast_content">
<span className="mx_LegacyCallEvent_caller">{room ? room.name : _t("voip|unknown_caller")}</span>
<span className="mx_LegacyCallEvent_caller">{room?.name ?? _t("voip|unknown_caller")}</span>
<div className="mx_LegacyCallEvent_type">
{getCallStateIcon(isVoice, undefined)}
{isVoice ? _t("voip|voice_call") : _t("voip|video_call")}
+3 -2
View File
@@ -9,12 +9,12 @@ 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 type LegacyCallHandler from "../../LegacyCallHandler";
import { getPlatformCallTypeProps, PlatformCallType } from "../../hooks/room/useRoomCall";
import defaultDispatcher from "../../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../../dispatcher/actions";
import PosthogTrackers from "../../PosthogTrackers";
import { SDKContextClass } from "../../contexts/SDKContextClass.ts";
/**
* Helper to place a call in a room that works with all the legacy modes
@@ -24,6 +24,7 @@ import { SDKContextClass } from "../../contexts/SDKContextClass.ts";
* @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 SDKContextClass.instance.legacyCallHandler.placeCall(room.roomId, callType);
await legacyCallHandler.placeCall(room.roomId, callType);
} else if (platformCallType == PlatformCallType.ElementCall) {
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
+48
View File
@@ -0,0 +1,48 @@
/*
Copyright 2026 Element Creations Ltd.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
Please see LICENSE files in the repository root for full details.
*/
// @vitest-environment happy-dom
import { vi, describe, it, expect } from "vitest";
import { createTestClient, TestSDKContext } from "test-utils";
import { verifyUser } from "./verification";
import defaultDispatcher from "./dispatcher/dispatcher";
import DMRoomMap from "./utils/DMRoomMap.ts";
import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases.ts";
describe("verifyUser", () => {
const sdkContext = new TestSDKContext();
sdkContext._client = createTestClient();
DMRoomMap.makeShared(sdkContext._client);
it("should require registration if user is a guest", () => {
vi.spyOn(defaultDispatcher, "dispatch");
vi.spyOn(sdkContext._client!, "isGuest").mockReturnValue(true);
verifyUser(
sdkContext.rightPanelStore,
sdkContext.client!,
sdkContext.client!.getUser(sdkContext.client!.getUserId()!)!,
);
expect(defaultDispatcher.dispatch).toHaveBeenCalledWith({ action: "require_registration" });
});
it("should open verification in right panel", () => {
vi.spyOn(sdkContext.rightPanelStore, "setCards");
vi.spyOn(sdkContext._client!, "isGuest").mockReturnValue(false);
verifyUser(
sdkContext.rightPanelStore,
sdkContext.client!,
sdkContext.client!.getUser(sdkContext.client!.getUserId()!)!,
);
expect(sdkContext.rightPanelStore.setCards).toHaveBeenCalledWith([
{ phase: RightPanelPhases.RoomSummary },
expect.objectContaining({ phase: RightPanelPhases.MemberInfo }),
expect.objectContaining({ phase: RightPanelPhases.EncryptionPanel }),
]);
});
});
+7 -7
View File
@@ -11,7 +11,7 @@ import { type VerificationRequest } from "matrix-js-sdk/src/crypto-api";
import dis from "./dispatcher/dispatcher";
import { RightPanelPhases } from "./stores/right-panel/RightPanelStorePhases";
import RightPanelStore from "./stores/right-panel/RightPanelStore";
import type RightPanelStore from "./stores/right-panel/RightPanelStore";
import { type IRightPanelCardState } from "./stores/right-panel/RightPanelStoreIPanelState";
import { findDMForUser } from "./utils/dm/findDMForUser";
@@ -20,20 +20,20 @@ import { findDMForUser } from "./utils/dm/findDMForUser";
*
* Note: cross-signing must be set up before calling this function.
*/
export async function verifyUser(matrixClient: MatrixClient, user: User): Promise<void> {
export function verifyUser(rightPanelStore: RightPanelStore, matrixClient: MatrixClient, user: User): void {
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 },
@@ -11,7 +11,14 @@ import { it, describe, expect, vi } from "vitest";
import { type EventTimeline, EventType, type MatrixEvent, type RoomState } from "matrix-js-sdk/src/matrix";
import { EventEmitter } from "node:stream";
import { mkEvent, mkMessage, mkRoomMember, mkStubRoom, stubClient } from "../../../../../../test/test-utils";
import {
mkEvent,
mkMessage,
mkRoomMember,
mkStubRoom,
stubClient,
TestSDKContext,
} from "../../../../../../test/test-utils";
import { getMockedRtcNotificationEvent, MockedCall, MockedCallStore } from "./call-mocks";
import { RootCallTileViewModel } from "./RootCallTileViewModel";
import {
@@ -92,10 +99,19 @@ function getMocked(userIds: string[]) {
}
describe("RootCallTileViewModel", () => {
const sdkContext = new TestSDKContext();
const legacyCallHandler = sdkContext.legacyCallHandler;
it("computes correct tileType for ongoing call in DM", () => {
const { callStore, cli, mxEvent, latestRtcNotificationEventStore } = getMocked(["@alice:m.org", "@bob:m.org"]);
latestRtcNotificationEventStore.getLatestEventId = () => "new-event";
const vm = new RootCallTileViewModel({ latestRtcNotificationEventStore, callStore, cli, mxEvent });
const vm = new RootCallTileViewModel({
latestRtcNotificationEventStore,
callStore,
cli,
mxEvent,
legacyCallHandler,
});
expect(vm.getSnapshot().tileType).toStrictEqual("ongoing-call-dm");
});
@@ -107,7 +123,13 @@ describe("RootCallTileViewModel", () => {
"@jack:m.org",
]);
latestRtcNotificationEventStore.getLatestEventId = () => "new-event";
const vm = new RootCallTileViewModel({ latestRtcNotificationEventStore, callStore, cli, mxEvent });
const vm = new RootCallTileViewModel({
latestRtcNotificationEventStore,
callStore,
cli,
mxEvent,
legacyCallHandler,
});
expect(vm.getSnapshot().tileType).toStrictEqual("ongoing-call-room");
});
@@ -115,7 +137,13 @@ describe("RootCallTileViewModel", () => {
it("computes correct tileType for tombstone call in DM", () => {
// When there's an ongoing call
const { callStore, cli, mxEvent, latestRtcNotificationEventStore } = getMocked(["@alice:m.org", "@bob:m.org"]);
const vm = new RootCallTileViewModel({ latestRtcNotificationEventStore, callStore, cli, mxEvent });
const vm = new RootCallTileViewModel({
latestRtcNotificationEventStore,
callStore,
cli,
mxEvent,
legacyCallHandler,
});
expect(vm.getSnapshot().tileType).toStrictEqual("tombstone-call-dm");
});
@@ -127,7 +155,13 @@ describe("RootCallTileViewModel", () => {
"@bob:m.org",
"@jack:m.org",
]);
const vm = new RootCallTileViewModel({ latestRtcNotificationEventStore, callStore, cli, mxEvent });
const vm = new RootCallTileViewModel({
latestRtcNotificationEventStore,
callStore,
cli,
mxEvent,
legacyCallHandler,
});
expect(vm.getSnapshot().tileType).toStrictEqual("tombstone-call-room");
});
@@ -138,7 +172,13 @@ describe("RootCallTileViewModel", () => {
"@bob:m.org",
"@jack:m.org",
]);
const vm = new RootCallTileViewModel({ latestRtcNotificationEventStore, callStore, cli, mxEvent });
const vm = new RootCallTileViewModel({
latestRtcNotificationEventStore,
callStore,
cli,
mxEvent,
legacyCallHandler,
});
expect(vm.getSnapshot().tileType).toStrictEqual("tombstone-call-room");
// Tile type should update on event
@@ -19,6 +19,7 @@ import {
type LatestRtcNotificationEventStore,
} from "../../../../../stores/LatestRtcNotificationEventStore";
import { JitsiCall } from "../../../../../models/Call";
import type LegacyCallHandler from "../../../../../LegacyCallHandler.tsx";
interface Props {
/**
@@ -41,6 +42,11 @@ interface Props {
*/
callStore: CallStore;
/**
* {@link LegacyCallHandler} to handle calls in a room.
*/
legacyCallHandler: LegacyCallHandler;
/**
* {@link LatestRtcNotificationEventStore} to track the latest notification event id.
*/
@@ -83,6 +89,7 @@ function computeSnapshot(props: Props): RootCallTileViewSnapshot {
getRelationsForEvent: props.getRelationsForEvent,
callStore: props.callStore,
cli: props.cli,
legacyCallHandler: props.legacyCallHandler,
}),
};
}
@@ -94,6 +101,7 @@ function computeSnapshot(props: Props): RootCallTileViewSnapshot {
getRelationsForEvent: props.getRelationsForEvent,
callStore: props.callStore,
cli: props.cli,
legacyCallHandler: props.legacyCallHandler,
}),
};
}
@@ -18,6 +18,7 @@ import { getMockedMember, getMockedRtcNotificationEvent, MockedCall, MockedCallS
import type { EventTimeline, RoomState } from "matrix-js-sdk/src/matrix";
import { placeCall } from "../../../../../../../utils/room/placeCall";
import { PlatformCallType } from "../../../../../../../hooks/room/useRoomCall";
import { SDKContextClass } from "../../../../../../../contexts/SDKContextClass.ts";
/**
* There's a nasty circular dependency in useRoomCall so that we end up with:
@@ -40,6 +41,8 @@ vi.mock(import("../../../../../../../utils/room/placeCall"), () => {
const roomId = "!my-room:m.org";
describe("BaseOngoingCallViewModel", () => {
const legacyCallHandler = SDKContextClass.instance.legacyCallHandler;
describe("should compute the correct snapshot", () => {
it("startedByDisplayName", () => {
const cli = stubClient();
@@ -50,7 +53,7 @@ describe("BaseOngoingCallViewModel", () => {
const call = MockedCall.create();
const callStore = MockedCallStore.create(call);
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId });
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
expect(vm.getSnapshot().startedByDisplayName).toStrictEqual("Alice");
});
@@ -65,7 +68,7 @@ describe("BaseOngoingCallViewModel", () => {
const callStore = MockedCallStore.create(call);
// Alice hasn't joined the call yet
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId });
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
expect(vm.getSnapshot().isJoined).toStrictEqual(false);
// Alice has joined call
@@ -85,11 +88,11 @@ describe("BaseOngoingCallViewModel", () => {
const call = MockedCall.create();
const callStore = MockedCallStore.create(call);
const vm1 = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId });
const vm1 = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
expect(vm1.getSnapshot().callDirection).toStrictEqual(CallDirection.Incoming);
vi.spyOn(cli, "getUserId").mockReturnValue("@alice:m.org");
const vm2 = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId });
const vm2 = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
expect(vm2.getSnapshot().callDirection).toStrictEqual(CallDirection.Outgoing);
});
@@ -103,7 +106,7 @@ describe("BaseOngoingCallViewModel", () => {
const callStore = MockedCallStore.create(call);
// Call has no other participants other than alice
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId });
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
expect(vm.getSnapshot().callHasOtherParticipants).toStrictEqual(false);
// Let's say others join
@@ -135,7 +138,7 @@ describe("BaseOngoingCallViewModel", () => {
const call = MockedCall.create();
const callStore = MockedCallStore.create(call);
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId });
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
expect(vm.getSnapshot().isJoinable).toStrictEqual(true);
});
});
@@ -148,10 +151,10 @@ describe("BaseOngoingCallViewModel", () => {
const call = MockedCall.create().withParticipants([mxEvent.sender]);
const callStore = MockedCallStore.create(call);
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId });
const vm = new BaseOngoingCallViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
vm.join();
const [room, callType, platformCallType] = vi.mocked(placeCall).mock.calls[0];
const [_, room, callType, platformCallType] = vi.mocked(placeCall).mock.calls[0];
expect(room.roomId).toStrictEqual(roomId);
expect(callType).toStrictEqual(CallType.Video);
expect(platformCallType).toStrictEqual(PlatformCallType.ElementCall);
@@ -29,6 +29,7 @@ import { PlatformCallType } from "../../../../../../../hooks/room/useRoomCall";
import { type GetRelationsForEvent } from "../../../../../../../components/views/rooms/EventTile";
import { getIntentFromEvent } from "../../common";
import { DurationViewModel } from "./components/DurationViewModel";
import type LegacyCallHandler from "../../../../../../../LegacyCallHandler.tsx";
export interface Props {
/**
@@ -51,6 +52,10 @@ export interface Props {
* {@link CallStore} to access calls in a room.
*/
callStore: CallStore;
/**
* {@link LegacyCallHandler} to handle calls in a room.
*/
legacyCallHandler: LegacyCallHandler;
}
function getCallOrThrow(store: CallStore, roomId: string): ElementCall {
@@ -167,7 +172,14 @@ export class BaseOngoingCallViewModel<
}
const callType = getIntentFromEvent(this.props.mxEvent);
const type = callType === SharedComponentsCallType.Voice ? CallType.Voice : CallType.Video;
placeCall(room, type, PlatformCallType.ElementCall, event?.shiftKey || undefined, type === CallType.Voice);
placeCall(
this.props.legacyCallHandler,
room,
type,
PlatformCallType.ElementCall,
event?.shiftKey || undefined,
type === CallType.Voice,
);
}
/**
@@ -10,13 +10,16 @@
import { it, describe, expect } from "vitest";
import { CallType } from "@element-hq/web-shared-components";
import { stubClient } from "../../../../../../../../test/test-utils";
import { stubClient, TestSDKContext } from "../../../../../../../../test/test-utils";
import { getMockedMember, getMockedRtcNotificationEvent, MockedCall, MockedCallStore } from "../../call-mocks";
import { DmOngoingCallTileViewModel } from "./DmOngoingCallTileViewModel";
const roomId = "!my-room:m.org";
describe("DmOngoingCallTileViewModel", () => {
const sdkContext = new TestSDKContext();
const legacyCallHandler = sdkContext.legacyCallHandler;
describe("should compute the correct snapshot", () => {
describe("callType", () => {
it("voice", () => {
@@ -27,7 +30,7 @@ describe("DmOngoingCallTileViewModel", () => {
const call = MockedCall.create().withParticipants([mxEvent.sender]);
const callStore = MockedCallStore.create(call);
const vm = new DmOngoingCallTileViewModel({ mxEvent, cli, callStore, roomId });
const vm = new DmOngoingCallTileViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
expect(vm.getSnapshot().callType).toStrictEqual(CallType.Voice);
});
@@ -40,7 +43,7 @@ describe("DmOngoingCallTileViewModel", () => {
const call = MockedCall.create().withParticipants([mxEvent.sender]);
const callStore = MockedCallStore.create(call);
const vm = new DmOngoingCallTileViewModel({ mxEvent, cli, callStore, roomId });
const vm = new DmOngoingCallTileViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
expect(vm.getSnapshot().callType).toStrictEqual(CallType.Video);
});
@@ -10,7 +10,7 @@
import { it, describe, expect, vi } from "vitest";
import { EventType, MatrixEventEvent } from "matrix-js-sdk/src/matrix";
import { stubClient } from "../../../../../../../../test/test-utils";
import { stubClient, TestSDKContext } from "../../../../../../../../test/test-utils";
import {
getMockedMember,
getMockedRtcDeclineEvent,
@@ -24,6 +24,9 @@ import { CallEvent } from "../../../../../../../models/Call";
const roomId = "!my-room:m.org";
describe("RoomOngoingCallTileViewModel", () => {
const sdkContext = new TestSDKContext();
const legacyCallHandler = sdkContext.legacyCallHandler;
describe("should compute the correct snapshot", () => {
it("totalParticipants", () => {
const cli = stubClient();
@@ -35,7 +38,7 @@ describe("RoomOngoingCallTileViewModel", () => {
const james = getMockedMember(roomId, "@james:m.org", "James");
const call = MockedCall.create().withParticipants([mxEvent.sender, bob, james]);
const callStore = MockedCallStore.create(call);
const vm = new RoomOngoingCallTileViewModel({ mxEvent, cli, callStore, roomId });
const vm = new RoomOngoingCallTileViewModel({ mxEvent, cli, callStore, roomId, legacyCallHandler });
// Call has 3 participants now
expect(vm.getSnapshot().totalParticipants).toStrictEqual(3);
@@ -59,7 +62,14 @@ describe("RoomOngoingCallTileViewModel", () => {
const call = MockedCall.create().withParticipants([mxEvent.sender]);
const callStore = MockedCallStore.create(call);
const vm = new RoomOngoingCallTileViewModel({ mxEvent, cli, callStore, roomId, getRelationsForEvent });
const vm = new RoomOngoingCallTileViewModel({
mxEvent,
cli,
callStore,
roomId,
getRelationsForEvent,
legacyCallHandler,
});
// Call hasn't been ignored yet
expect(vm.getSnapshot().isCallIgnored).toStrictEqual(false);
@@ -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(<FilePanel roomId={room.roomId} onClose={jest.fn()} />);
const { asFragment } = render(
<FilePanel roomId={room.roomId} onClose={jest.fn()} />,
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");
@@ -46,6 +46,7 @@ import Modal from "../../../../src/Modal";
import { SETTINGS } from "../../../../src/settings/Settings";
import ToastStore from "../../../../src/stores/ToastStore";
import { ModuleApi } from "../../../../src/modules/Api";
import { fireEvent } from "@testing-library/dom";
describe("<LoggedInView />", () => {
const userId = "@alice:domain.org";
@@ -67,6 +68,8 @@ describe("<LoggedInView />", () => {
on: jest.fn(),
},
getAuthMetadata: jest.fn().mockRejectedValue(new Error("Legacy auth")),
hasLazyLoadMembersEnabled: jest.fn(),
isInitialSyncComplete: jest.fn(),
});
const mediaHandler = new MediaHandler(mockClient);
const mockSdkContext = new TestSDKContext();
@@ -555,4 +558,11 @@ describe("<LoggedInView />", () => {
expect(container.querySelector(".mx_SpacePanel")).toBeInTheDocument();
});
});
it("should handle KeyBindingAction.ToggleRoomSidePanel", async () => {
getComponent({ page_type: "room_view" });
jest.spyOn(mockSdkContext.rightPanelStore, "togglePanel");
fireEvent.keyDown(document.body, { key: ".", code: "Period", ctrlKey: true, keyCode: 190 });
expect(mockSdkContext.rightPanelStore.togglePanel).toHaveBeenCalledWith(null);
});
});
@@ -21,6 +21,7 @@ import {
RoomMember,
RoomStateEvent,
SearchResult,
User,
} from "matrix-js-sdk/src/matrix";
import { type CryptoApi, CryptoEvent, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { KnownMembership } from "matrix-js-sdk/src/types";
@@ -1132,6 +1133,54 @@ describe("RoomView", () => {
});
});
it("should handle Action.ViewUser", async () => {
await mountRoomView();
jest.spyOn(stores.rightPanelStore, "setCards");
const member = new User("@user:server");
defaultDispatcher.dispatch(
{
action: Action.ViewUser,
member,
},
true,
);
expect(stores.rightPanelStore.setCards).toHaveBeenCalledWith([
{ phase: RightPanelPhases.RoomSummary },
{ phase: RightPanelPhases.MemberList },
{ phase: RightPanelPhases.MemberInfo, state: { member } },
]);
});
it("should handle Action.ViewUser with push", async () => {
await mountRoomView();
jest.spyOn(stores.rightPanelStore, "pushCard");
const member = new User("@user:server");
defaultDispatcher.dispatch(
{
action: Action.ViewUser,
member,
push: true,
},
true,
);
expect(stores.rightPanelStore.pushCard).toHaveBeenCalledWith({
phase: RightPanelPhases.MemberInfo,
state: { member },
});
});
it("should handle Action.View3pidInvite", async () => {
await mountRoomView();
jest.spyOn(stores.rightPanelStore, "showOrHidePhase");
defaultDispatcher.dispatch(
{
action: Action.View3pidInvite,
},
true,
);
expect(stores.rightPanelStore.showOrHidePhase).toHaveBeenCalledWith("MemberList");
});
describe("when there is a RoomView", () => {
const widget1Id = "widget1";
const widget2Id = "widget2";
@@ -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";
@@ -101,7 +101,7 @@ describe("SpaceRoomView", () => {
onRejectButtonClicked={jest.fn()}
justCreatedOpts={justCreatedOpts}
/>,
withClientContextRenderOptions(cli),
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
return spaceRoomView;
};
@@ -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(<TestThreadPanel />);
const renderResult = render(
<TestThreadPanel />,
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(<TestThreadPanel />);
const renderResult = render(
<TestThreadPanel />,
clientAndSDKContextRenderOptions(createTestClient(), SDKContextClass.instance),
);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
await waitFor(() => {
const events = findEvents(renderResult.container);
@@ -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<CryptoApi>;
let sdkContext: TestSDKContext;
beforeEach(() => {
mockCrypto = {
@@ -43,6 +45,9 @@ describe("useUserInfoVerificationHeaderViewModel", () => {
} as unknown as Mocked<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);
@@ -57,7 +62,7 @@ describe("useUserInfoVerificationHeaderViewModel", () => {
const renderUserInfoHeaderVerificationHook = (props = defaultProps) => {
return renderHook(
() => useUserInfoVerificationViewModel(props.member, props.devices),
withClientContextRenderOptions(mockClient),
clientAndSDKContextRenderOptions(mockClient, sdkContext),
);
};
@@ -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<ThreadListContextMenuProps>) {
return render(<ThreadListContextMenu mxEvent={event} {...props} />);
return render(
<ThreadListContextMenu mxEvent={event} {...props} />,
clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance),
);
}
beforeEach(() => {
@@ -19,8 +19,9 @@ import {
} from "matrix-js-sdk/src/crypto-api";
import { VerificationMethod } from "matrix-js-sdk/src/types";
import { stubClient } from "../../../../test-utils";
import { stubClient, withContexts } from "../../../../test-utils";
import VerificationRequestDialog from "../../../../../src/components/views/dialogs/VerificationRequestDialog";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass";
describe("VerificationRequestDialog", () => {
function renderComponent(phase: VerificationPhase, method?: "emoji" | "qr"): ReturnType<typeof render> {
@@ -29,6 +30,7 @@ describe("VerificationRequestDialog", () => {
return render(
<VerificationRequestDialog onFinished={jest.fn()} member={member} verificationRequest={request} />,
withContexts({ sdkContext: SDKContextClass.instance }),
);
}
@@ -124,6 +126,7 @@ describe("VerificationRequestDialog", () => {
member={member}
verificationRequestPromise={requestPromise}
/>,
withContexts({ sdkContext: SDKContextClass.instance }),
);
// And wait for the component to mount, the promise to resolve and the component state to update
@@ -153,6 +156,7 @@ describe("VerificationRequestDialog", () => {
verificationRequest={request}
verificationRequestPromise={requestPromise}
/>,
withContexts({ sdkContext: SDKContextClass.instance }),
);
// And wait for the component to mount, the promise to resolve and the component state to update
@@ -173,7 +177,10 @@ describe("VerificationRequestDialog", () => {
const member = User.createUser("@alice:example.org", stubClient());
const request = createRequest(VerificationPhase.Unsent);
render(<VerificationRequestDialog onFinished={jest.fn()} member={member} verificationRequest={request} />);
render(
<VerificationRequestDialog onFinished={jest.fn()} member={member} verificationRequest={request} />,
withContexts({ sdkContext: SDKContextClass.instance }),
);
// When I cancel the request (which changes phase and emits a Changed event)
await act(async () => await request.cancel());
@@ -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("<BaseCard />", () => {
<BaseCard header="Heading text">
<div>Content</div>
</BaseCard>,
clientAndSDKContextRenderOptions(SDKContextClass.instance.client!, SDKContextClass.instance),
);
expect(screen.getByRole("heading")).toHaveTextContent("Heading text");
@@ -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("<ExtensionsCard />", () => {
let client: Mocked<MatrixClient>;
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(<ExtensionsCard room={room} onClose={jest.fn()} />);
const { asFragment } = render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(screen.getByText("Boost productivity with more tools, widgets and bots")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
});
@@ -61,7 +68,10 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
const { asFragment } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
const { asFragment } = render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(screen.getByText("Custom Widget")).toBeInTheDocument();
expect(screen.getByText("Jitsi")).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
@@ -81,7 +91,10 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
const { container } = render(<ExtensionsCard room={room} onClose={jest.fn()} />);
const { container } = render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
await userEvent.click(container.querySelector(".mx_ExtensionsCard_app_options")!);
expect(document.querySelector(".mx_IconizedContextMenu")).toMatchSnapshot();
});
@@ -100,7 +113,10 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(screen.getByText("Set layout for everyone")).toBeInTheDocument();
});
@@ -118,7 +134,10 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(screen.getByText("Custom Widget").closest(".mx_ExtensionsCard_Button_pinned")).toBeInTheDocument();
});
@@ -137,15 +156,52 @@ describe("<ExtensionsCard />", () => {
},
] satisfies IApp[]);
render(<ExtensionsCard room={room} onClose={jest.fn()} />);
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
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(<ExtensionsCard room={room} onClose={jest.fn()} />);
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
await userEvent.click(screen.getByText("Add extensions"));
expect(spy).toHaveBeenCalled();
});
it("should set room layout on click", async () => {
mocked(useWidgets).mockReturnValue([
{
id: "id",
roomId: room.roomId,
eventId: "$event1",
creatorUserId: client.getSafeUserId(),
type: MatrixWidgetType.Custom,
name: "Custom Widget",
url: "http://url1",
},
{
id: "jitsi",
roomId: room.roomId,
eventId: "$event2",
creatorUserId: client.getSafeUserId(),
type: MatrixWidgetType.JitsiMeet,
name: "Jitsi",
url: "http://jitsi",
},
] satisfies IApp[]);
jest.spyOn(sdkContext.widgetLayoutStore, "copyLayoutToRoom");
render(
<ExtensionsCard room={room} onClose={jest.fn()} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
await userEvent.click(screen.getByText("Set layout for everyone"));
expect(sdkContext.widgetLayoutStore.copyLayoutToRoom).toHaveBeenCalledWith(room);
});
});
@@ -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("<PinnedMessagesCard />", () => {
let cli: MockedObject<MatrixClient>;
@@ -86,13 +92,12 @@ describe("<PinnedMessagesCard />", () => {
async function renderMessagePinList(room: Room): Promise<RenderResult> {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>
</MatrixClientContext.Provider>,
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
// Wait a tick for state updates
await act(() => sleep(0));
@@ -170,13 +175,12 @@ describe("<PinnedMessagesCard />", () => {
it("should show spinner whilst loading", async () => {
const room = mkRoom([], [pin1]);
render(
<MatrixClientContext.Provider value={cli}>
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>
</MatrixClientContext.Provider>,
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
await waitForElementToBeRemoved(() => screen.queryAllByRole("progressbar"));
@@ -322,13 +326,12 @@ describe("<PinnedMessagesCard />", () => {
).mockReturnValue(false);
const { asFragment } = render(
<MatrixClientContext.Provider value={cli}>
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>
</MatrixClientContext.Provider>,
<PinnedMessagesCard
room={room}
onClose={jest.fn()}
permalinkCreator={new RoomPermalinkCreator(room, room.roomId)}
/>,
clientAndSDKContextRenderOptions(cli, SDKContextClass.instance),
);
// Wait a tick for state updates
@@ -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("<RoomSummaryCard />", () => {
return render(<RoomSummaryCardView {...defaultProps} {...props} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>
<LinkedTextContext.Provider value={{}}>{children}</LinkedTextContext.Provider>
</MatrixClientContext.Provider>
<SDKContext.Provider value={SDKContextClass.instance}>
<MatrixClientContext.Provider value={mockClient}>
<LinkedTextContext.Provider value={{}}>{children}</LinkedTextContext.Provider>
</MatrixClientContext.Provider>
</SDKContext.Provider>
),
});
};
@@ -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<Room>;
let mockClient: Mocked<MatrixClient>;
let mockCrypto: Mocked<CryptoApi>;
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("<UserInfo />", () => {
};
const renderComponent = (props = {}) => {
const Wrapper = (wrapperProps = {}) => {
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
};
return render(<UserInfo {...defaultProps} {...props} />, {
wrapper: Wrapper,
});
return render(
<UserInfo {...defaultProps} {...props} />,
clientAndSDKContextRenderOptions(mockClient, sdkContext),
);
};
beforeEach(() => {
@@ -307,11 +307,7 @@ describe("<UserInfo />", () => {
});
it("renders the message button", () => {
render(
<MatrixClientContext.Provider value={mockClient}>
<UserInfo {...defaultProps} />
</MatrixClientContext.Provider>,
);
render(<UserInfo {...defaultProps} />, clientAndSDKContextRenderOptions(mockClient, sdkContext));
screen.getByRole("button", { name: "Send message" });
});
@@ -322,11 +318,7 @@ describe("<UserInfo />", () => {
return component !== UIComponent.CreateRooms;
},
() => {
render(
<MatrixClientContext.Provider value={mockClient}>
<UserInfo {...defaultProps} />
</MatrixClientContext.Provider>,
);
render(<UserInfo {...defaultProps} />, clientAndSDKContextRenderOptions(mockClient, sdkContext));
expect(screen.queryByRole("button", { name: "Message" })).toBeNull();
},
@@ -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("<UserInfoHeaderVerificationView />", () => {
const defaultRoomId = "!fkfk";
@@ -25,6 +25,7 @@ describe("<UserInfoHeaderVerificationView />", () => {
let mockClient: MatrixClient;
let mockCrypto: Mocked<CryptoApi>;
let sdkContext: TestSDKContext;
beforeEach(() => {
mockCrypto = mocked({
@@ -42,6 +43,8 @@ describe("<UserInfoHeaderVerificationView />", () => {
} 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("<UserInfoHeaderVerificationView />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
jest.spyOn(mockClient, "doesServerSupportUnstableFeature").mockResolvedValue(true);
const Wrapper = (wrapperProps = {}) => {
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
};
return render(<UserInfoHeaderVerificationView member={defaultMember} devices={[device1]} />, {
wrapper: Wrapper,
});
return render(
<UserInfoHeaderVerificationView member={defaultMember} devices={[device1]} />,
clientAndSDKContextRenderOptions(mockClient, sdkContext),
);
};
it("renders verified badge when user is verified", async () => {
@@ -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("<UserInfoHeaderView />", () => {
let mockClient: MatrixClient;
let mockCrypto: Mocked<CryptoApi>;
let sdkContext: TestSDKContext;
beforeEach(() => {
mockCrypto = mocked({
@@ -63,6 +64,8 @@ describe("<UserInfoHeaderView />", () => {
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("<UserInfoHeaderView />", () => {
mockCrypto.getUserDeviceInfo.mockResolvedValue(userDeviceMap);
const Wrapper = (wrapperProps = {}) => {
return <MatrixClientContext.Provider value={mockClient} {...wrapperProps} />;
};
return render(
<UserInfoHeaderView
{...defaultProps}
@@ -101,9 +100,7 @@ describe("<UserInfoHeaderView />", () => {
devices={[device1]}
hideVerificationSection={props.hideVerificationSection}
/>,
{
wrapper: Wrapper,
},
clientAndSDKContextRenderOptions(mockClient, sdkContext),
);
};
@@ -10,7 +10,9 @@ import React from "react";
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import RoomInfoLine from "../../../../../src/components/views/rooms/RoomInfoLine.tsx";
import { stubClient } from "../../../../test-utils";
import { stubClient, TestSDKContext, withContexts } from "../../../../test-utils";
import { fireEvent } from "@testing-library/dom";
import { RightPanelPhases } from "../../../../../src/stores/right-panel/RightPanelStorePhases.ts";
describe("RoomInfoLine", () => {
it("renders for public room", () => {
@@ -33,4 +35,29 @@ describe("RoomInfoLine", () => {
expect(getByText("Public room")).toBeVisible();
expect(asFragment()).toMatchSnapshot();
});
it("should render members button which opens right panel", async () => {
const sdkContext = new TestSDKContext();
sdkContext._client = stubClient();
const room = new Room("!roomId", sdkContext.client!, sdkContext.client!.getUserId()!);
room.currentState.setStateEvents([
new MatrixEvent({
sender: sdkContext.client!.getUserId()!,
room_id: room.roomId,
state_key: "",
event_id: "$eventId",
type: "m.room.join_rules",
content: {
join_rule: "public",
},
}),
]);
jest.spyOn(room, "getJoinedMemberCount").mockReturnValue(50);
jest.spyOn(sdkContext.rightPanelStore, "setCard");
const { findByText } = render(<RoomInfoLine room={room} />, withContexts({ sdkContext }));
fireEvent.click(await findByText("50 members"));
expect(sdkContext.rightPanelStore.setCard).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
});
});
@@ -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("<ThirdPartyMemberInfo />", () => {
const userId = "@alice:server.org";
@@ -37,7 +42,11 @@ describe("<ThirdPartyMemberInfo />", () => {
});
const defaultEvent = makeInviteEvent();
const getComponent = (event: MatrixEvent = defaultEvent) => render(<ThirdPartyMemberInfo event={event} />);
const getComponent = (event: MatrixEvent = defaultEvent) =>
render(
<ThirdPartyMemberInfo event={event} />,
clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance),
);
const room = new Room(roomId, mockClient, userId);
const aliceMember = new RoomMember(roomId, userId);
aliceMember.name = "Alice DisplayName";
@@ -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<ComponentProps<typeof VerificationRequestToast>> & { request: VerificationRequest },
): RenderResult {
const propsWithDefaults = {
toastKey: "test",
...props,
};
return render(<VerificationRequestToast {...propsWithDefaults} />);
}
import { TestSDKContext } from "../../../TestSDKContext.ts";
describe("VerificationRequestToast", () => {
let client: Mocked<MatrixClient>;
let sdkContext: TestSDKContext;
function renderComponent(
props: Partial<ComponentProps<typeof VerificationRequestToast>> & { request: VerificationRequest },
): RenderResult {
const propsWithDefaults = {
toastKey: "test",
...props,
};
return render(
<VerificationRequestToast {...propsWithDefaults} />,
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 () => {
@@ -27,8 +27,6 @@ describe("LegacyCallView", () => {
document.fullscreenElement = element;
document.exitFullscreen = jest.fn();
stubClient();
const call = {
on: jest.fn(),
removeListener: jest.fn(),
@@ -50,8 +48,7 @@ describe("LegacyCallView", () => {
});
it("should show/hide the sidebar based on the sidebarShown prop", async () => {
stubClient();
const cli = stubClient();
const call = {
roomId: "test-room",
on: jest.fn(),
@@ -93,8 +90,7 @@ describe("LegacyCallView", () => {
});
it("should not show the sidebar button in picture-in-picture mode", async () => {
stubClient();
const cli = stubClient();
const call = {
on: jest.fn(),
removeListener: jest.fn(),
@@ -16,7 +16,6 @@ import { clientAndSDKContextRenderOptions, mkStubRoom, stubClient } from "../../
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import LegacyCallHandler from "../../../../../src/LegacyCallHandler";
import { SDKContext } from "../../../../../src/contexts/SDKContext";
import { TestSDKContext } from "../../../TestSDKContext.ts";
jest.mock("../../../../../src/components/views/voip/LegacyCallView", () => jest.fn(() => "LegacyCallView"));
@@ -93,9 +92,10 @@ describe("LegacyCallViewForRoom", () => {
jest.spyOn(sdkContext.resizeNotifier, "stopResizing");
jest.spyOn(sdkContext.resizeNotifier, "notifyTimelineHeightChanged");
const { container } = render(<LegacyCallViewForRoom roomId={call.roomId} />, {
wrapper: ({ children }) => <SDKContext.Provider value={sdkContext}>{children}</SDKContext.Provider>,
});
const { container } = render(
<LegacyCallViewForRoom roomId={call.roomId} />,
clientAndSDKContextRenderOptions(sdkContext.client!, sdkContext),
);
const resizer = container.querySelector(".mx_LegacyCallViewForRoom_ResizeHandle");
await waitFor(() => {
@@ -18,7 +18,7 @@ import {
mockClientMethodsServer,
mockClientMethodsUser,
} from "../../test-utils";
import { SDKContextClass } from "../../../src/contexts/SDKContextClass.ts";
import { TestSDKContext } from "../TestSDKContext.ts";
describe("<IncomingLegacyCallToast />", () => {
const userId = "@alice:server.org";
@@ -33,6 +33,8 @@ describe("<IncomingLegacyCallToast />", () => {
...mockClientMethodsServer(),
getRoom: jest.fn(),
});
const sdkContext = new TestSDKContext();
sdkContext._client = mockClient;
const mockRoom = new Room("!room:server.org", mockClient, userId);
mockClient.deviceId = deviceId;
@@ -46,24 +48,16 @@ describe("<IncomingLegacyCallToast />", () => {
jest.clearAllMocks();
mockClient.getAccountData.mockReturnValue(undefined);
mockClient.getRoom.mockReturnValue(mockRoom);
// @ts-ignore
SDKContextClass.instance._client = mockClient;
});
it("renders when silence button when call is not silenced", () => {
const { getByLabelText } = render(
getComponent(),
clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance),
);
const { getByLabelText } = render(getComponent(), clientAndSDKContextRenderOptions(mockClient, sdkContext));
expect(getByLabelText("Silence call")).toMatchSnapshot();
});
it("renders sound on button when call is silenced", () => {
SDKContextClass.instance.legacyCallHandler.silenceCall(call.callId);
const { getByLabelText } = render(
getComponent(),
clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance),
);
sdkContext.legacyCallHandler.silenceCall(call.callId);
const { getByLabelText } = render(getComponent(), clientAndSDKContextRenderOptions(mockClient, sdkContext));
expect(getByLabelText("Sound on")).toMatchSnapshot();
});
@@ -79,10 +73,7 @@ describe("<IncomingLegacyCallToast />", () => {
});
}
});
const { getByLabelText } = render(
getComponent(),
clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance),
);
const { getByLabelText } = render(getComponent(), clientAndSDKContextRenderOptions(mockClient, sdkContext));
expect(getByLabelText("Notifications silenced")).toMatchSnapshot();
});
});