Remove LegacyCallHandler singleton (#34086)

* Remove dead code

* Remove LegacyCallHandler singleton

Route via SDKContext to cut import cycles

* Remove unused setting

* Fix tests

* Fix tests

* Cascade SDKContext through PersistedElement

* Improve coverage

* Iterate

* Improve coverage

* Improve coverage
This commit is contained in:
Michael Telatynski
2026-07-07 12:29:54 +00:00
committed by GitHub
parent 48bd69a8b6
commit 4ec8f1edcd
40 changed files with 639 additions and 348 deletions
-2
View File
@@ -21,7 +21,6 @@ import { type ModalManager } from "../Modal";
import type SettingsStore from "../settings/SettingsStore";
import type RightPanelStore from "../stores/right-panel/RightPanelStore";
import type WidgetStore from "../stores/WidgetStore";
import type LegacyCallHandler from "../LegacyCallHandler";
import type UserActivity from "../UserActivity";
import { type ModalWidgetStore } from "../stores/ModalWidgetStore";
import { type WidgetLayoutStore } from "../stores/widgets/WidgetLayoutStore";
@@ -99,7 +98,6 @@ declare global {
mxRightPanelStore: RightPanelStore;
mxWidgetStore: WidgetStore;
mxWidgetLayoutStore: WidgetLayoutStore;
mxLegacyCallHandler: LegacyCallHandler;
mxUserActivity: UserActivity;
mxModalWidgetStore: ModalWidgetStore;
mxSpaceStore: SpaceStoreClass;
+9 -62
View File
@@ -34,7 +34,6 @@ import { WidgetType } from "./widgets/WidgetType";
import { SettingLevel } from "./settings/SettingLevel";
import QuestionDialog from "./components/views/dialogs/QuestionDialog";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import WidgetStore from "./stores/WidgetStore";
import { WidgetMessagingStore } from "./stores/widgets/WidgetMessagingStore";
import { ElementWidgetActions } from "./stores/widgets/ElementWidgetActions";
import { UIFeature } from "./settings/UIFeature";
@@ -42,7 +41,6 @@ import { Action } from "./dispatcher/actions";
import { addManagedHybridWidget, isManagedHybridWidgetEnabled } from "./widgets/ManagedHybrid";
import SdkConfig from "./SdkConfig";
import { ensureDMExists } from "./createRoom";
import { WidgetLayoutStore } from "./stores/widgets/WidgetLayoutStore";
import IncomingLegacyCallToast, { getIncomingLegacyCallToastKey } from "./toasts/IncomingLegacyCallToast";
import ToastStore from "./stores/ToastStore";
import { type ViewRoomPayload } from "./dispatcher/payloads/ViewRoomPayload";
@@ -54,41 +52,13 @@ import { localNotificationsAreSilenced } from "./utils/notifications";
import { isNotNull } from "./Typeguards";
import { BackgroundAudio } from "./audio/BackgroundAudio";
import { Jitsi } from "./widgets/Jitsi.ts";
import { type SDKContextClass } from "./contexts/SDKContextClass.ts";
export const PROTOCOL_PSTN = "m.protocol.pstn";
export const PROTOCOL_PSTN_PREFIXED = "im.vector.protocol.pstn";
const CHECK_PROTOCOLS_ATTEMPTS = 3;
type MediaEventType = keyof HTMLMediaElementEventMap;
const MEDIA_ERROR_EVENT_TYPES: MediaEventType[] = [
"error",
// The media has become empty; for example, this event is sent if the media has
// already been loaded (or partially loaded), and the HTMLMediaElement.load method
// is called to reload it.
"emptied",
// The user agent is trying to fetch media data, but data is unexpectedly not
// forthcoming.
"stalled",
// Media data loading has been suspended.
"suspend",
// Playback has stopped because of a temporary lack of data
"waiting",
];
const MEDIA_DEBUG_EVENT_TYPES: MediaEventType[] = [
"play",
"pause",
"playing",
"ended",
"loadeddata",
"loadedmetadata",
"canplay",
"canplaythrough",
"volumechange",
];
const MEDIA_EVENT_TYPES = [...MEDIA_ERROR_EVENT_TYPES, ...MEDIA_DEBUG_EVENT_TYPES];
export enum AudioID {
Ring = "ringAudio",
Ringback = "ringbackAudio",
@@ -96,13 +66,6 @@ export enum AudioID {
Busy = "busyAudio",
}
/* istanbul ignore next */
const debuglog = (...args: any[]): void => {
if (SettingsStore.getValue("debug_legacy_call_handler")) {
logger.log.call(console, "LegacyCallHandler debuglog:", ...args);
}
};
interface ThirdpartyLookupResponse {
userid: string;
protocol: string;
@@ -151,12 +114,8 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
private backgroundAudio = new BackgroundAudio();
private playingSources: Record<string, AudioBufferSourceNode> = {}; // Record them for stopping
public static get instance(): LegacyCallHandler {
if (!window.mxLegacyCallHandler) {
window.mxLegacyCallHandler = new LegacyCallHandler();
}
return window.mxLegacyCallHandler;
public constructor(private readonly sdkContext: SDKContextClass) {
super();
}
/*
@@ -193,18 +152,6 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
}
}
/* istanbul ignore next (remove if we start using this function for things other than debug logging) */
public handleEvent(e: Event): void {
const target = e.target as HTMLElement;
const audioId = target?.id;
if (MEDIA_ERROR_EVENT_TYPES.includes(e.type as MediaEventType)) {
logger.error(`LegacyCallHandler: encountered "${e.type}" event with <audio id="${audioId}">`, e);
} else if (MEDIA_EVENT_TYPES.includes(e.type as MediaEventType)) {
debuglog(`encountered "${e.type}" event with <audio id="${audioId}">`, e);
}
}
public isForcedSilent(): boolean {
const cli = MatrixClientPeg.safeGet();
return localNotificationsAreSilenced(cli);
@@ -308,7 +255,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
return;
}
const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call);
const mappedRoomId = this.roomIdForCall(call);
if (!mappedRoomId) return;
if (this.getCallForRoom(mappedRoomId)) {
logger.log(
@@ -365,7 +312,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
public getAllActiveCallsForPip(roomId: string): MatrixCall[] {
const room = MatrixClientPeg.safeGet().getRoom(roomId);
if (room && WidgetLayoutStore.instance.hasMaximisedWidget(room)) {
if (room && this.sdkContext.widgetLayoutStore.hasMaximisedWidget(room)) {
// This checks if there is space for the call view in the aux panel
// If there is no space any call should be displayed in PiP
return this.getAllActiveCalls();
@@ -667,7 +614,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
}
private setCallState(call: MatrixCall, status: CallState): void {
const mappedRoomId = LegacyCallHandler.instance.roomIdForCall(call);
const mappedRoomId = this.roomIdForCall(call);
logger.log(`Call state in ${mappedRoomId} changed to ${status}`);
@@ -1022,12 +969,12 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
dis.dispatch({ action: "appsDrawer", show: true });
// Prevent double clicking the call button
const widget = WidgetStore.instance.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type));
const widget = this.sdkContext.widgetStore.getApps(roomId).find((app) => WidgetType.JITSI.matches(app.type));
if (widget) {
// If there already is a Jitsi widget, pin it
const room = client.getRoom(roomId);
if (isNotNull(room)) {
WidgetLayoutStore.instance.moveToContainer(room, widget, "top");
this.sdkContext.widgetLayoutStore.moveToContainer(room, widget, "top");
}
return;
}
@@ -1049,7 +996,7 @@ export default class LegacyCallHandler extends TypedEventEmitter<LegacyCallHandl
public hangupCallApp(roomId: string): void {
logger.info("Leaving conference call in " + roomId);
const roomInfo = WidgetStore.instance.getRoom(roomId);
const roomInfo = this.sdkContext.widgetStore.getRoom(roomId);
if (!roomInfo) return; // "should never happen" clauses go here
const jitsiWidgets = roomInfo.widgets.filter((w) => WidgetType.JITSI.matches(w.type));
+2 -3
View File
@@ -44,7 +44,6 @@ import { Jitsi } from "./widgets/Jitsi";
import { SSO_HOMESERVER_URL_KEY, SSO_ID_SERVER_URL_KEY, SSO_IDP_ID_KEY } from "./BasePlatform";
import ThreepidInviteStore from "./stores/ThreepidInviteStore";
import { PosthogAnalytics } from "./PosthogAnalytics";
import LegacyCallHandler from "./LegacyCallHandler";
import LifecycleCustomisations from "./customisations/Lifecycle";
import ErrorDialog from "./components/views/dialogs/ErrorDialog";
import { _t } from "./languageHandler";
@@ -1102,7 +1101,7 @@ async function startMatrixClient(
DMRoomMap.makeShared(client).start();
IntegrationManagers.sharedInstance().startWatching();
ActiveWidgetStore.instance.start();
LegacyCallHandler.instance.start();
SDKContextClass.instance.legacyCallHandler.start();
checkBrowserSupport();
// Start Mjolnir even though we haven't checked the feature flag yet. Starting
@@ -1228,8 +1227,8 @@ export async function clearStorage(opts?: { deleteEverything?: boolean }): Promi
* on MatrixClientPeg after stopping.
*/
export function stopMatrixClient(unsetClient = true): void {
SDKContextClass.instance.legacyCallHandler.stop();
SDKContextClass.instance.notifier.stop();
LegacyCallHandler.instance.stop();
UserActivity.sharedInstance().stop();
SDKContextClass.instance.typingStore.reset();
Presence.stop();
@@ -10,8 +10,9 @@ import { EventType, type MatrixEvent } from "matrix-js-sdk/src/matrix";
import { CallEvent, CallState, CallType, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { EventEmitter } from "events";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import { SDKContextClass } from "../../contexts/SDKContextClass.ts";
export enum LegacyCallEventGrouperEvent {
StateChanged = "state_changed",
@@ -65,8 +66,8 @@ export default class LegacyCallEventGrouper extends EventEmitter {
public constructor() {
super();
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallsChanged, this.setCall);
LegacyCallHandler.instance.addListener(
SDKContextClass.instance.legacyCallHandler.addListener(LegacyCallHandlerEvent.CallsChanged, this.setCall);
SDKContextClass.instance.legacyCallHandler.addListener(
LegacyCallHandlerEvent.SilencedCallsChanged,
this.onSilencedCallsChanged,
);
@@ -133,7 +134,7 @@ export default class LegacyCallEventGrouper extends EventEmitter {
}
private onSilencedCallsChanged = (): void => {
const newState = LegacyCallHandler.instance.isCallSilenced(this.callId);
const newState = SDKContextClass.instance.legacyCallHandler.isCallSilenced(this.callId);
this.emit(LegacyCallEventGrouperEvent.SilencedChanged, newState);
};
@@ -144,27 +145,27 @@ export default class LegacyCallEventGrouper extends EventEmitter {
public answerCall = (): void => {
const roomId = this.roomId;
if (!roomId) return;
LegacyCallHandler.instance.answerCall(roomId);
SDKContextClass.instance.legacyCallHandler.answerCall(roomId);
};
public rejectCall = (): void => {
const roomId = this.roomId;
if (!roomId) return;
LegacyCallHandler.instance.hangupOrReject(roomId, true);
SDKContextClass.instance.legacyCallHandler.hangupOrReject(roomId, true);
};
public callBack = (): void => {
const roomId = this.roomId;
if (!roomId) return;
LegacyCallHandler.instance.placeCall(roomId, this.isVoice ? CallType.Voice : CallType.Video);
SDKContextClass.instance.legacyCallHandler.placeCall(roomId, this.isVoice ? CallType.Voice : CallType.Video);
};
public toggleSilenced = (): void => {
const silenced = LegacyCallHandler.instance.isCallSilenced(this.callId);
const silenced = SDKContextClass.instance.legacyCallHandler.isCallSilenced(this.callId);
if (silenced) {
LegacyCallHandler.instance.unSilenceCall(this.callId);
SDKContextClass.instance.legacyCallHandler.unSilenceCall(this.callId);
} else {
LegacyCallHandler.instance.silenceCall(this.callId);
SDKContextClass.instance.legacyCallHandler.silenceCall(this.callId);
}
};
@@ -195,7 +196,7 @@ export default class LegacyCallEventGrouper extends EventEmitter {
const callId = this.callId;
if (!callId || this.call) return;
this.call = LegacyCallHandler.instance.getCallById(callId);
this.call = SDKContextClass.instance.legacyCallHandler.getCallById(callId);
this.setCallListeners();
this.setState();
};
@@ -43,7 +43,7 @@ import Modal from "../../Modal";
import { getKeyBindingsManager } from "../../KeyBindingsManager";
import { type IOpts } from "../../createRoom";
import SpacePanel from "../views/spaces/SpacePanel";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import AudioFeedArrayForLegacyCall from "../views/voip/AudioFeedArrayForLegacyCall";
import { OwnProfileStore } from "../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
@@ -138,7 +138,7 @@ class LoggedInView extends React.Component<IProps, IState> {
// use compact timeline view
useCompactLayout: SettingsStore.getValue("useCompactLayout"),
usageLimitDismissed: false,
activeCalls: LegacyCallHandler.instance.getAllActiveCalls(),
activeCalls: context.legacyCallHandler.getAllActiveCalls(),
};
// stash the MatrixClient in case we log out before we are unmounted
@@ -151,7 +151,7 @@ class LoggedInView extends React.Component<IProps, IState> {
public componentDidMount(): void {
document.addEventListener("keydown", this.onNativeKeyDown, false);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.onCallState);
this.context.legacyCallHandler.addListener(LegacyCallHandlerEvent.CallState, this.onCallState);
this.updateServerNoticeEvents();
@@ -229,7 +229,7 @@ class LoggedInView extends React.Component<IProps, IState> {
public componentWillUnmount(): void {
document.removeEventListener("keydown", this.onNativeKeyDown, false);
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallState, this.onCallState);
this._matrixClient.removeListener(ClientEvent.AccountData, this.onAccountData);
this._matrixClient.removeListener(ClientEvent.Sync, this.onSync);
this._matrixClient.removeListener(RoomStateEvent.Events, this.onRoomStateEvents);
@@ -242,7 +242,7 @@ class LoggedInView extends React.Component<IProps, IState> {
}
private onCallState = (): void => {
const activeCalls = LegacyCallHandler.instance.getAllActiveCalls();
const activeCalls = this.context.legacyCallHandler.getAllActiveCalls();
if (activeCalls === this.state.activeCalls) return;
this.setState({ activeCalls });
};
@@ -95,7 +95,6 @@ import SoftLogout from "./auth/SoftLogout";
import { copyPlaintext } from "../../utils/strings";
import { PosthogAnalytics } from "../../PosthogAnalytics";
import { initSentry } from "../../sentry";
import LegacyCallHandler from "../../LegacyCallHandler";
import { showSpaceInvite } from "../../utils/space";
import { type ButtonEvent } from "../views/elements/AccessibleButton";
import { type ActionPayload } from "../../dispatcher/payloads";
@@ -695,7 +694,7 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
}
break;
case "logout":
LegacyCallHandler.instance.hangupAllCalls();
this.stores.legacyCallHandler.hangupAllCalls();
Promise.all([...[...CallStore.instance.connectedCalls].map((call) => call.disconnect())]).finally(() =>
Lifecycle.logout(this.stores.oidcClientStore),
);
@@ -12,8 +12,8 @@ import { logger } from "matrix-js-sdk/src/logger";
import { useCreateAutoDisposedViewModel, WidgetPipView } from "@element-hq/web-shared-components";
import LegacyCallView from "../views/voip/LegacyCallView";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import { MatrixClientPeg } from "../../MatrixClientPeg";
import type LegacyCallHandler from "../../LegacyCallHandler";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import PictureInPictureDragger, { type CreatePipChildren } from "./PictureInPictureDragger";
import dis from "../../dispatcher/dispatcher";
import { Action } from "../../dispatcher/actions";
@@ -21,9 +21,9 @@ import { WidgetLayoutStore } from "../../stores/widgets/WidgetLayoutStore";
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../stores/ActiveWidgetStore";
import { type ViewRoomPayload } from "../../dispatcher/payloads/ViewRoomPayload";
import { UPDATE_EVENT } from "../../stores/AsyncStore";
import { SDKContextClass } from "../../contexts/SDKContextClass";
import RoomAvatar from "../views/avatars/RoomAvatar";
import { WidgetPipViewModel, type Props as WidgetPipViewModelProps } from "../../viewmodels/room/WidgetPipViewModel";
import { SDKContext } from "../../contexts/SDKContext.ts";
const SHOW_CALL_IN_STATES = [
CallState.Connected,
@@ -58,10 +58,13 @@ interface IState {
// (which should be a single element) of other calls.
// The primary will be the one not on hold, or an arbitrary one
// if they're all on hold)
function getPrimarySecondaryCallsForPip(roomId: string | null): [MatrixCall | null, MatrixCall[]] {
function getPrimarySecondaryCallsForPip(
legacyCallHandler: LegacyCallHandler,
roomId: string | null,
): [MatrixCall | null, MatrixCall[]] {
if (!roomId) return [null, []];
const calls = LegacyCallHandler.instance.getAllActiveCallsForPip(roomId);
const calls = legacyCallHandler.getAllActiveCallsForPip(roomId);
let primary: MatrixCall | null = null;
let secondaries: MatrixCall[] = [];
@@ -96,12 +99,15 @@ function getPrimarySecondaryCallsForPip(roomId: string | null): [MatrixCall | nu
*/
class PipContainerInner extends React.Component<IProps, IState> {
public constructor(props: IProps) {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
super(props);
const roomId = SDKContextClass.instance.roomViewStore.getRoomId();
const roomId = context.roomViewStore.getRoomId();
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(roomId);
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(context.legacyCallHandler, roomId);
this.state = {
viewedRoomId: roomId || undefined,
@@ -114,13 +120,13 @@ class PipContainerInner extends React.Component<IProps, IState> {
}
public componentDidMount(): void {
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
SDKContextClass.instance.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
MatrixClientPeg.safeGet().on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
const room = MatrixClientPeg.safeGet().getRoom(this.state.viewedRoomId);
this.context.legacyCallHandler.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
this.context.legacyCallHandler.addListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
this.context.roomViewStore.addListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
this.context.client?.on(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
const room = this.context.client?.getRoom(this.state.viewedRoomId);
if (room) {
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
}
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
ActiveWidgetStore.instance.on(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
@@ -128,14 +134,13 @@ class PipContainerInner extends React.Component<IProps, IState> {
}
public componentWillUnmount(): void {
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
const cli = MatrixClientPeg.get();
cli?.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
SDKContextClass.instance.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
const room = cli?.getRoom(this.state.viewedRoomId);
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCalls);
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallState, this.updateCalls);
this.context.client?.removeListener(CallEvent.RemoteHoldUnhold, this.onCallRemoteHold);
this.context.roomViewStore.removeListener(UPDATE_EVENT, this.onRoomViewStoreUpdate);
const room = this.context.client?.getRoom(this.state.viewedRoomId);
if (room) {
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
this.context.widgetLayoutStore.off(WidgetLayoutStore.emissionForRoom(room), this.updateCalls);
}
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Persistence, this.onWidgetPersistence);
ActiveWidgetStore.instance.off(ActiveWidgetStoreEvent.Dock, this.onWidgetDockChanges);
@@ -145,22 +150,22 @@ class PipContainerInner extends React.Component<IProps, IState> {
private onMove = (): void => this.props.movePersistedElement.current?.();
private onRoomViewStoreUpdate = (): void => {
const newRoomId = SDKContextClass.instance.roomViewStore.getRoomId();
const newRoomId = this.context.roomViewStore.getRoomId();
const oldRoomId = this.state.viewedRoomId;
if (newRoomId === oldRoomId) return;
// The WidgetLayoutStore observer always tracks the currently viewed Room,
// so we don't end up with multiple observers and know what observer to remove on unmount
const oldRoom = MatrixClientPeg.get()?.getRoom(oldRoomId);
const oldRoom = this.context.client?.getRoom(oldRoomId);
if (oldRoom) {
WidgetLayoutStore.instance.off(WidgetLayoutStore.emissionForRoom(oldRoom), this.updateCalls);
this.context.widgetLayoutStore.off(WidgetLayoutStore.emissionForRoom(oldRoom), this.updateCalls);
}
const newRoom = MatrixClientPeg.get()?.getRoom(newRoomId || undefined);
const newRoom = this.context.client?.getRoom(newRoomId || undefined);
if (newRoom) {
WidgetLayoutStore.instance.on(WidgetLayoutStore.emissionForRoom(newRoom), this.updateCalls);
this.context.widgetLayoutStore.on(WidgetLayoutStore.emissionForRoom(newRoom), this.updateCalls);
}
if (!newRoomId) return;
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(newRoomId);
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.context.legacyCallHandler, newRoomId);
this.setState({
viewedRoomId: newRoomId,
primaryCall: primaryCall,
@@ -179,7 +184,10 @@ class PipContainerInner extends React.Component<IProps, IState> {
private updateCalls = (): void => {
if (!this.state.viewedRoomId) return;
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.state.viewedRoomId);
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(
this.context.legacyCallHandler,
this.state.viewedRoomId,
);
this.setState({
primaryCall: primaryCall,
@@ -190,7 +198,10 @@ class PipContainerInner extends React.Component<IProps, IState> {
private onCallRemoteHold = (): void => {
if (!this.state.viewedRoomId) return;
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(this.state.viewedRoomId);
const [primaryCall, secondaryCalls] = getPrimarySecondaryCallsForPip(
this.context.legacyCallHandler,
this.state.viewedRoomId,
);
this.setState({
primaryCall: primaryCall,
@@ -217,7 +228,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
let notDocked = false;
// Sanity check the room - the widget may have been destroyed between render cycles, and
// thus no room is associated anymore.
if (persistentWidgetId && persistentRoomId && MatrixClientPeg.safeGet().getRoom(persistentRoomId)) {
if (persistentWidgetId && persistentRoomId && this.context.client?.getRoom(persistentRoomId)) {
notDocked = !ActiveWidgetStore.instance.isDocked(persistentWidgetId, persistentRoomId);
fromAnotherRoom = this.state.viewedRoomId !== persistentRoomId;
}
@@ -256,7 +267,7 @@ class PipContainerInner extends React.Component<IProps, IState> {
<WidgetPipWrappedView
key="widget-pip"
widgetId={this.state.persistentWidgetId!}
room={MatrixClientPeg.safeGet().getRoom(this.state.persistentRoomId ?? undefined)!}
room={this.context.client!.getRoom(this.state.persistentRoomId ?? undefined)!}
viewingRoom={this.state.viewedRoomId === this.state.persistentRoomId}
onStartMoving={onStartMoving}
movePersistedElement={this.props.movePersistedElement}
@@ -10,26 +10,29 @@ import { type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import { _t } from "../../../languageHandler";
import ContextMenu, { type IProps as IContextMenuProps, MenuItem } from "../../structures/ContextMenu";
import LegacyCallHandler from "../../../LegacyCallHandler";
import { SDKContext } from "../../../contexts/SDKContext.ts";
interface IProps extends IContextMenuProps {
call: MatrixCall;
}
export default class LegacyCallContextMenu extends React.Component<IProps> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
public onHoldClick = (): void => {
this.props.call.setRemoteOnHold(true);
this.props.onFinished();
};
public onUnholdClick = (): void => {
LegacyCallHandler.instance.setActiveCallRoomId(this.props.call.roomId);
this.context.legacyCallHandler.setActiveCallRoomId(this.props.call.roomId);
this.props.onFinished();
};
public onTransferClick = (): void => {
LegacyCallHandler.instance.showTransferDialog(this.props.call);
this.context.legacyCallHandler.showTransferDialog(this.props.call);
this.props.onFinished();
};
@@ -38,7 +38,6 @@ import Dialpad from "../voip/DialPad";
import QuestionDialog from "./QuestionDialog";
import BaseDialog from "./BaseDialog";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import LegacyCallHandler from "../../../LegacyCallHandler";
import CopyableText from "../elements/CopyableText";
import { type ScreenName } from "../../../PosthogTrackers";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
@@ -483,9 +482,13 @@ export default class InviteDialog extends React.PureComponent<Props, IInviteDial
return;
}
LegacyCallHandler.instance.startTransferToMatrixID(this.props.call, targetIds[0], this.state.consultFirst);
SDKContextClass.instance.legacyCallHandler.startTransferToMatrixID(
this.props.call,
targetIds[0],
this.state.consultFirst,
);
} else {
LegacyCallHandler.instance.startTransferToPhoneNumber(
SDKContextClass.instance.legacyCallHandler.startTransferToPhoneNumber(
this.props.call,
this.state.dialPadValue,
this.state.consultFirst,
@@ -45,25 +45,21 @@ import PersistedElement, { getPersistKey } from "./PersistedElement";
import { WidgetType } from "../../../widgets/WidgetType";
import { ElementWidget, WidgetMessaging, WidgetMessagingEvent } from "../../../stores/widgets/WidgetMessaging";
import WidgetAvatar from "../avatars/WidgetAvatar";
import LegacyCallHandler from "../../../LegacyCallHandler";
import { type IApp, isAppWidget } from "../../../stores/WidgetStore";
import { WidgetLayoutStore } from "../../../stores/widgets/WidgetLayoutStore";
import { OwnProfileStore } from "../../../stores/OwnProfileStore";
import { UPDATE_EVENT } from "../../../stores/AsyncStore";
import WidgetUtils from "../../../utils/WidgetUtils";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { type ActionPayload } from "../../../dispatcher/payloads";
import { Action } from "../../../dispatcher/actions";
import { ElementWidgetCapabilities } from "../../../stores/widgets/ElementWidgetCapabilities";
import { WidgetMessagingStore } from "../../../stores/widgets/WidgetMessagingStore";
import { SDKContextClass } from "../../../contexts/SDKContextClass";
import { ModuleRunner } from "../../../modules/ModuleRunner";
import { ModuleApi } from "../../../modules/Api";
import { toWidgetDescriptor } from "../../../modules/WidgetLifecycleApi";
import { parseUrl } from "../../../utils/UrlUtils";
import RightPanelStore from "../../../stores/right-panel/RightPanelStore.ts";
import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases.ts";
import { WidgetContextMenu } from "../../../viewmodels/room/right-panel/WidgetContextMenuViewModel.tsx";
import { SDKContext } from "../../../contexts/SDKContext.ts";
// Note that there is advice saying allow-scripts shouldn't be used with allow-same-origin
// because that would allow the iframe to programmatically remove the sandbox attribute, but
@@ -138,8 +134,8 @@ interface IState {
}
export default class AppTile extends React.Component<IProps, IState> {
public static contextType = MatrixClientContext;
declare public context: ContextType<typeof MatrixClientContext>;
public static contextType = SDKContext;
declare public context: ContextType<typeof SDKContext>;
public static defaultProps: Partial<IProps> = {
waitForIframeLoad: true,
@@ -162,7 +158,7 @@ export default class AppTile extends React.Component<IProps, IState> {
private dispatcherRef?: string;
private unmounted = false;
public constructor(props: IProps, context: ContextType<typeof MatrixClientContext>) {
public constructor(props: IProps, context: ContextType<typeof SDKContext>) {
super(props, context);
// The key used for PersistedElement
@@ -257,7 +253,7 @@ export default class AppTile extends React.Component<IProps, IState> {
);
if (isActiveWidget) {
// We just left the room that the active widget was from.
if (this.props.room && SDKContextClass.instance.roomViewStore.getRoomId() !== this.props.room.roomId) {
if (this.props.room && this.context.roomViewStore.getRoomId() !== this.props.room.roomId) {
// If we are not actively looking at the room then destroy this widget entirely.
this.endWidgetActions();
} else if (WidgetType.JITSI.matches(this.props.app.type)) {
@@ -393,7 +389,7 @@ export default class AppTile extends React.Component<IProps, IState> {
this.watchUserReady();
if (this.props.room) {
this.context.on(RoomEvent.MyMembership, this.onMyMembership);
this.context.client?.on(RoomEvent.MyMembership, this.onMyMembership);
}
this.allowedWidgetsWatchRef = SettingsStore.watchSetting("allowedWidgets", null, this.onAllowedWidgetsChange);
// Widget action listeners
@@ -426,7 +422,7 @@ export default class AppTile extends React.Component<IProps, IState> {
dis.unregister(this.dispatcherRef);
if (this.props.room) {
this.context.off(RoomEvent.MyMembership, this.onMyMembership);
this.context.client?.off(RoomEvent.MyMembership, this.onMyMembership);
}
SettingsStore.unwatchSetting(this.allowedWidgetsWatchRef);
@@ -545,7 +541,7 @@ export default class AppTile extends React.Component<IProps, IState> {
*/
private endWidgetActions(): void {
if (WidgetType.JITSI.matches(this.props.app.type) && this.props.room) {
LegacyCallHandler.instance.hangupCallApp(this.props.room.roomId);
this.context.legacyCallHandler.hangupCallApp(this.props.room.roomId);
}
// Delete the widget from the persisted store for good measure.
@@ -682,25 +678,27 @@ export default class AppTile extends React.Component<IProps, IState> {
private onToggleMaximisedClick = (): void => {
if (!this.props.room) return; // ignore action - it shouldn't even be visible
const targetContainer = WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center")
const targetContainer = this.context.widgetLayoutStore.isInContainer(this.props.room, this.props.app, "center")
? "top"
: "center";
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, targetContainer);
this.context.widgetLayoutStore.moveToContainer(this.props.room, this.props.app, targetContainer);
if (targetContainer === "top") this.closeChatCardIfNeeded();
};
private onMinimiseClicked = (): void => {
if (!this.props.room) return; // ignore action - it shouldn't even be visible
WidgetLayoutStore.instance.moveToContainer(this.props.room, this.props.app, "right");
this.context.widgetLayoutStore.moveToContainer(this.props.room, this.props.app, "right");
this.closeChatCardIfNeeded();
};
private closeChatCardIfNeeded = (): void => {
if (!this.props.room) return; // ignore action - it shouldn't even be visible
// If the right panel has a timeline, but we're about to show the timeline in the main view, pop the right panel
if (RightPanelStore.instance.currentCardForRoom(this.props.room.roomId).phase === RightPanelPhases.Timeline) {
RightPanelStore.instance.popCard(this.props.room.roomId);
if (
this.context.rightPanelStore.currentCardForRoom(this.props.room.roomId).phase === RightPanelPhases.Timeline
) {
this.context.rightPanelStore.popCard(this.props.room.roomId);
}
};
@@ -742,7 +740,7 @@ export default class AppTile extends React.Component<IProps, IState> {
);
} else if (!this.state.hasPermissionToLoad && this.props.room && this.messaging) {
// only possible for room widgets, can assert this.props.room here
const isEncrypted = this.context.isRoomEncrypted(this.props.room.roomId);
const isEncrypted = this.context.client?.isRoomEncrypted(this.props.room.roomId);
appTileBody = (
<div className={appTileBodyClass} style={appTileBodyStyles}>
<AppPermission
@@ -818,7 +816,8 @@ export default class AppTile extends React.Component<IProps, IState> {
const layoutButtons: ReactNode[] = [];
if (this.props.showLayoutButtons) {
const isMaximised =
this.props.room && WidgetLayoutStore.instance.isInContainer(this.props.room, this.props.app, "center");
this.props.room &&
this.context.widgetLayoutStore.isInContainer(this.props.room, this.props.app, "center");
layoutButtons.push(
<AccessibleButton
@@ -14,6 +14,7 @@ import dis from "../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { type ActionPayload } from "../../../dispatcher/payloads";
import { SDKContext } from "../../../contexts/SDKContext.ts";
export const getPersistKey = (appId: string): string => "widget_" + appId;
@@ -70,6 +71,9 @@ interface IProps {
* bounding rect as the parent of PE.
*/
export default class PersistedElement extends React.Component<IProps> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
private resizeObserver: ResizeObserver;
private dispatcherRef?: string;
private childContainer?: HTMLDivElement;
@@ -165,13 +169,15 @@ export default class PersistedElement extends React.Component<IProps> {
private renderApp(): void {
const content = (
<StrictMode>
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>
<TooltipProvider>
<div ref={this.collectChild} style={this.props.style}>
{this.props.children}
</div>
</TooltipProvider>
</MatrixClientContext.Provider>
<SDKContext.Provider value={this.context}>
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>
<TooltipProvider>
<div ref={this.collectChild} style={this.props.style}>
{this.props.children}
</div>
</TooltipProvider>
</MatrixClientContext.Provider>
</SDKContext.Provider>
</StrictMode>
);
@@ -5,10 +5,11 @@
* Please see LICENSE files in the repository root for full details.
*/
import React, { useEffect, type JSX } from "react";
import React, { useEffect, type JSX, useContext } from "react";
import { RoomListSearchView, useCreateAutoDisposedViewModel } from "@element-hq/web-shared-components";
import { RoomListSearchViewModel } from "../../../../viewmodels/room-list/RoomListSearchViewModel";
import { SDKContext } from "../../../../contexts/SDKContext.ts";
type RoomListSearchProps = {
/**
@@ -23,7 +24,10 @@ type RoomListSearchProps = {
* The `Explore` button is displayed only in the Home meta space and when UIComponent.ExploreRooms is enabled.
*/
export function RoomListSearch({ activeSpace }: RoomListSearchProps): JSX.Element {
const vm = useCreateAutoDisposedViewModel(() => new RoomListSearchViewModel({ activeSpace }));
const sdkContext = useContext(SDKContext);
const vm = useCreateAutoDisposedViewModel(
() => new RoomListSearchViewModel({ activeSpace, legacyCallHandler: sdkContext.legacyCallHandler }),
);
useEffect(() => {
vm.setActiveSpace(activeSpace);
}, [activeSpace, vm]);
@@ -13,7 +13,7 @@ import AccessibleButton, { type ButtonEvent } from "../elements/AccessibleButton
import Field from "../elements/Field";
import DialPad from "./DialPad";
import DialPadBackspaceButton from "../elements/DialPadBackspaceButton";
import LegacyCallHandler from "../../../LegacyCallHandler";
import { SDKContextClass } from "../../../contexts/SDKContextClass.ts";
interface IProps {
onFinished: (dialled: boolean) => void;
@@ -70,7 +70,7 @@ export default class DialpadModal extends React.PureComponent<IProps, IState> {
};
public onDialPress = async (): Promise<void> => {
LegacyCallHandler.instance.dialNumber(this.state.value);
SDKContextClass.instance.legacyCallHandler.dialNumber(this.state.value);
this.props.onFinished(true);
};
@@ -15,8 +15,6 @@ import { type CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
import dis from "../../../dispatcher/dispatcher";
import LegacyCallHandler from "../../../LegacyCallHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { _t, _td } from "../../../languageHandler";
import VideoFeed from "./VideoFeed";
import RoomAvatar from "../avatars/RoomAvatar";
@@ -28,6 +26,7 @@ import LegacyCallViewButtons from "./LegacyCallView/LegacyCallViewButtons";
import { type ActionPayload } from "../../../dispatcher/payloads";
import { getKeyBindingsManager } from "../../../KeyBindingsManager";
import { KeyBindingAction } from "../../../accessibility/KeyboardShortcuts";
import { SDKContext } from "../../../contexts/SDKContext.ts";
interface IProps {
// The call for us to display
@@ -81,6 +80,9 @@ function exitFullscreen(): void {
}
export default class LegacyCallView extends React.Component<IProps, IState> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
private dispatcherRef?: string;
private contentWrapperRef = createRef<HTMLDivElement>();
private buttonsRef = createRef<LegacyCallViewButtons>();
@@ -308,18 +310,18 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
};
private onCallResumeClick = (): void => {
const userFacingRoomId = LegacyCallHandler.instance.roomIdForCall(this.props.call);
if (userFacingRoomId) LegacyCallHandler.instance.setActiveCallRoomId(userFacingRoomId);
const userFacingRoomId = this.context.legacyCallHandler.roomIdForCall(this.props.call);
if (userFacingRoomId) this.context.legacyCallHandler.setActiveCallRoomId(userFacingRoomId);
};
private onTransferClick = (): void => {
const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(this.props.call.callId);
const transfereeCall = this.context.legacyCallHandler.getTransfereeForCallId(this.props.call.callId);
if (transfereeCall) this.props.call.transferToCall(transfereeCall);
};
private onHangupClick = (): void => {
const roomId = LegacyCallHandler.instance.roomIdForCall(this.props.call);
if (roomId) LegacyCallHandler.instance.hangupOrReject(roomId);
const roomId = this.context.legacyCallHandler.roomIdForCall(this.props.call);
if (roomId) this.context.legacyCallHandler.hangupOrReject(roomId);
};
private onToggleSidebar = (): void => {
@@ -400,10 +402,10 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
const { pipMode, call, onResize, sidebarShown } = this.props;
const { isLocalOnHold, isRemoteOnHold, primaryFeed, secondaryFeed, sidebarFeeds } = this.state;
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
const callRoom = (callRoomId ? MatrixClientPeg.safeGet().getRoom(callRoomId) : undefined) ?? undefined;
const callRoomId = this.context.legacyCallHandler.roomIdForCall(call);
const callRoom = (callRoomId ? this.context.client?.getRoom(callRoomId) : undefined) ?? undefined;
const avatarSize = pipMode ? "76px" : "160px";
const transfereeCall = LegacyCallHandler.instance.getTransfereeForCallId(call.callId);
const transfereeCall = this.context.legacyCallHandler.getTransfereeForCallId(call.callId);
const isOnHold = isLocalOnHold || isRemoteOnHold;
let secondaryFeedElement: React.ReactNode;
@@ -421,12 +423,11 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
let holdTransferContent: React.ReactNode;
if (transfereeCall) {
const cli = MatrixClientPeg.safeGet();
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
const transferTargetRoom = callRoomId ? cli.getRoom(callRoomId) : null;
const callRoomId = this.context.legacyCallHandler.roomIdForCall(call);
const transferTargetRoom = callRoomId ? this.context.client?.getRoom(callRoomId) : null;
const transferTargetName = transferTargetRoom ? transferTargetRoom.name : _t("voip|unknown_person");
const transfereeCallRoomId = LegacyCallHandler.instance.roomIdForCall(transfereeCall);
const transfereeRoom = transfereeCallRoomId ? cli.getRoom(transfereeCallRoomId) : null;
const transfereeCallRoomId = this.context.legacyCallHandler.roomIdForCall(transfereeCall);
const transfereeRoom = transfereeCallRoomId ? this.context.client?.getRoom(transfereeCallRoomId) : null;
const transfereeName = transfereeRoom ? transfereeRoom.name : _t("voip|unknown_person");
holdTransferContent = (
@@ -451,7 +452,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
let onHoldText: React.ReactNode;
if (isRemoteOnHold) {
onHoldText = _t(
LegacyCallHandler.instance.hasAnyUnheldCall()
this.context.legacyCallHandler.hasAnyUnheldCall()
? _td("voip|call_held_switch")
: _td("voip|call_held_resume"),
{},
@@ -544,13 +545,12 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
const { call, secondaryCall, pipMode, showApps, onMouseDownOnHeader, sidebarShown } = this.props;
const { sidebarFeeds } = this.state;
const client = MatrixClientPeg.safeGet();
const callRoomId = LegacyCallHandler.instance.roomIdForCall(call);
const secondaryCallRoomId = LegacyCallHandler.instance.roomIdForCall(secondaryCall);
const callRoom = callRoomId ? client.getRoom(callRoomId) : null;
const callRoomId = this.context.legacyCallHandler.roomIdForCall(call);
const secondaryCallRoomId = this.context.legacyCallHandler.roomIdForCall(secondaryCall);
const callRoom = callRoomId ? this.context.client?.getRoom(callRoomId) : null;
if (!callRoom) return null;
const secCallRoom = secondaryCallRoomId ? client.getRoom(secondaryCallRoomId) : null;
const secCallRoom = secondaryCallRoomId ? this.context.client?.getRoom(secondaryCallRoomId) : null;
const callViewClasses = classNames({
mx_LegacyCallView: true,
@@ -565,7 +565,7 @@ export default class LegacyCallView extends React.Component<IProps, IState> {
<LegacyCallViewHeader
onPipMouseDown={onMouseDownOnHeader}
pipMode={pipMode}
callRooms={[callRoom, secCallRoom]}
callRooms={[callRoom, secCallRoom ?? null]}
onMaximize={this.onMaximizeClick}
/>
<div className="mx_LegacyCallView_content_wrapper" ref={this.contentWrapperRef}>
@@ -10,7 +10,7 @@ import { CallState, type MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import React from "react";
import { Resizable } from "re-resizable";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../LegacyCallHandler";
import { LegacyCallHandlerEvent } from "../../../LegacyCallHandler";
import LegacyCallView from "./LegacyCallView";
import { SDKContext } from "../../../contexts/SDKContext";
@@ -39,20 +39,20 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
const call = this.getCall();
this.state = {
call,
sidebarShown: !!call && LegacyCallHandler.instance.isCallSidebarShown(call.callId),
sidebarShown: !!call && context.legacyCallHandler.isCallSidebarShown(call.callId),
};
}
public componentDidMount(): void {
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallState, this.updateCall);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
LegacyCallHandler.instance.addListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
this.context.legacyCallHandler.addListener(LegacyCallHandlerEvent.CallState, this.updateCall);
this.context.legacyCallHandler.addListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
this.context.legacyCallHandler.addListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
}
public componentWillUnmount(): void {
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallState, this.updateCall);
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
LegacyCallHandler.instance.removeListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallState, this.updateCall);
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.CallChangeRoom, this.updateCall);
this.context.legacyCallHandler.removeListener(LegacyCallHandlerEvent.ShownSidebarsChanged, this.updateCall);
}
private updateCall = (): void => {
@@ -60,14 +60,14 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
if (newCall !== this.state.call) {
this.setState({ call: newCall });
}
const newSidebarShown = !!newCall && LegacyCallHandler.instance.isCallSidebarShown(newCall.callId);
const newSidebarShown = !!newCall && this.context.legacyCallHandler.isCallSidebarShown(newCall.callId);
if (newSidebarShown !== this.state.sidebarShown) {
this.setState({ sidebarShown: newSidebarShown });
}
};
private getCall(): MatrixCall | null {
const call = LegacyCallHandler.instance.getCallForRoom(this.props.roomId);
const call = this.context.legacyCallHandler.getCallForRoom(this.props.roomId);
if (call && [CallState.Ended, CallState.Ringing].includes(call.state)) return null;
return call;
@@ -87,7 +87,7 @@ export default class LegacyCallViewForRoom extends React.Component<IProps, IStat
private setSidebarShown = (sidebarShown: boolean): void => {
if (!this.state.call) return;
LegacyCallHandler.instance.setCallSidebarShown(this.state.call.callId, sidebarShown);
this.context.legacyCallHandler.setCallSidebarShown(this.state.call.callId, sidebarShown);
};
public render(): React.ReactNode {
@@ -16,9 +16,8 @@ import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventType
import { MicOffSolidIcon, MicOnSolidIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
import SettingsStore from "../../../settings/SettingsStore";
import LegacyCallHandler from "../../../LegacyCallHandler";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import RoomAvatar from "../avatars/RoomAvatar";
import { SDKContext } from "../../../contexts/SDKContext.ts";
interface IProps {
call: MatrixCall;
@@ -45,6 +44,9 @@ interface IState {
}
export default class VideoFeed extends React.PureComponent<IProps, IState> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
private element?: HTMLVideoElement;
public constructor(props: IProps) {
@@ -192,8 +194,8 @@ export default class VideoFeed extends React.PureComponent<IProps, IState> {
let content;
if (this.state.videoMuted) {
const callRoomId = LegacyCallHandler.instance.roomIdForCall(this.props.call);
const callRoom = (callRoomId ? MatrixClientPeg.safeGet().getRoom(callRoomId) : undefined) ?? undefined;
const callRoomId = this.context.legacyCallHandler.roomIdForCall(this.props.call);
const callRoom = (callRoomId ? this.context.client?.getRoom(callRoomId) : undefined) ?? undefined;
let avatarSize;
if (pipMode && primary) avatarSize = "76px";
+1 -1
View File
@@ -97,7 +97,7 @@ export class SDKContextClass {
public get legacyCallHandler(): LegacyCallHandler {
if (!this._LegacyCallHandler) {
this._LegacyCallHandler = LegacyCallHandler.instance;
this._LegacyCallHandler = new LegacyCallHandler(this);
}
return this._LegacyCallHandler;
}
-5
View File
@@ -349,7 +349,6 @@ export interface Settings {
"debug_timeline_panel": IBaseSetting<boolean>;
"debug_registration": IBaseSetting<boolean>;
"debug_animation": IBaseSetting<boolean>;
"debug_legacy_call_handler": IBaseSetting<boolean>;
"audioInputMuted": IBaseSetting<boolean>;
"videoInputMuted": IBaseSetting<boolean>;
"activeCallRoomIds": IBaseSetting<string[]>;
@@ -1322,10 +1321,6 @@ export const SETTINGS: Settings = {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false,
},
"debug_legacy_call_handler": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false,
},
"audioInputMuted": {
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS,
default: false,
@@ -44,7 +44,6 @@ import SdkConfig from "../SdkConfig";
import SettingsStore from "../settings/SettingsStore";
import { UIComponent, UIFeature } from "../settings/UIFeature";
import { CHAT_EFFECTS } from "../effects";
import LegacyCallHandler from "../LegacyCallHandler";
import { guessAndSetDMRoom } from "../Rooms";
import DevtoolsDialog from "../components/views/dialogs/DevtoolsDialog";
import InfoDialog from "../components/views/dialogs/InfoDialog";
@@ -64,6 +63,7 @@ import { manuallyVerifyDevice } from "../components/views/dialogs/ManualDeviceKe
import upgraderoom from "./upgraderoom/upgraderoom";
import { emoticon } from "./emoticon";
import { statusCommand } from "./status";
import { SDKContextClass } from "../contexts/SDKContextClass.ts";
export { CommandCategories, Command };
@@ -712,7 +712,7 @@ export const Commands = [
return success(
(async (): Promise<void> => {
if (isPhoneNumber) {
const results = await LegacyCallHandler.instance.pstnLookup(userId);
const results = await SDKContextClass.instance.legacyCallHandler.pstnLookup(userId);
if (!results || results.length === 0 || !results[0].userid) {
throw new UserFriendlyError("slash_command|query_not_found_phone_number");
}
@@ -773,7 +773,7 @@ export const Commands = [
category: CommandCategories.other,
isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (cli, roomId, threadId, args) {
const call = LegacyCallHandler.instance.getCallForRoom(roomId);
const call = SDKContextClass.instance.legacyCallHandler.getCallForRoom(roomId);
if (!call) {
return reject(new UserFriendlyError("slash_command|no_active_call"));
}
@@ -788,7 +788,7 @@ export const Commands = [
category: CommandCategories.other,
isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (cli, roomId, threadId, args) {
const call = LegacyCallHandler.instance.getCallForRoom(roomId);
const call = SDKContextClass.instance.legacyCallHandler.getCallForRoom(roomId);
if (!call) {
return reject(new UserFriendlyError("slash_command|no_active_call"));
}
+10 -8
View File
@@ -16,6 +16,7 @@ import React, {
useRef,
useState,
useId,
useContext,
} from "react";
import {
type Room,
@@ -39,7 +40,6 @@ import { AvatarWithDetails } from "@element-hq/web-shared-components";
import { _t } from "../languageHandler";
import RoomAvatar from "../components/views/avatars/RoomAvatar";
import { MatrixClientPeg } from "../MatrixClientPeg";
import defaultDispatcher from "../dispatcher/dispatcher";
import { type ViewRoomPayload } from "../dispatcher/payloads/ViewRoomPayload";
import { Action } from "../dispatcher/actions";
@@ -49,11 +49,12 @@ import AccessibleButton, { type ButtonEvent } from "../components/views/elements
import { useDispatcher } from "../hooks/useDispatcher";
import { type ActionPayload } from "../dispatcher/payloads";
import { type Call, CallEvent } from "../models/Call";
import LegacyCallHandler, { AudioID } from "../LegacyCallHandler";
import { AudioID } from "../LegacyCallHandler";
import { useEventEmitter, useTypedEventEmitter } from "../hooks/useEventEmitter";
import { CallStore, CallStoreEvent } from "../stores/CallStore";
import DMRoomMap from "../utils/DMRoomMap";
import MemberAvatar from "../components/views/avatars/MemberAvatar";
import { SDKContext } from "../contexts/SDKContext.ts";
/**
* Get the key for the incoming call toast. A combination of the call ID and room ID.
@@ -158,10 +159,11 @@ interface Props {
}
export function IncomingCallToast({ notificationEvent, toastKey }: Props): JSX.Element {
const sdkContext = useContext(SDKContext);
const roomId = notificationEvent.getRoomId()!;
// Use a partial type so ts still helps us to not miss any type checks.
const notificationContent = notificationEvent.getContent() as Partial<IRTCNotificationContent>;
const room = MatrixClientPeg.safeGet().getRoom(roomId) ?? undefined;
const room = sdkContext.client?.getRoom(roomId) ?? undefined;
const call = useCall(roomId);
const [connectedCalls, setConnectedCalls] = useState<Call[]>(Array.from(CallStore.instance.connectedCalls));
useEventEmitter(CallStore.instance, CallStoreEvent.ConnectedCalls, () => {
@@ -174,18 +176,18 @@ export function IncomingCallToast({ notificationEvent, toastKey }: Props): JSX.E
// This is because `LegacyCallHandler.play` tries to load the sound and then play it asynchonously
// and `LegacyCallHandler.isPlaying` will not be `true` until the sound starts playing.
const isRingToast = notificationContent.notification_type === "ring";
if (isRingToast && !soundHasStarted.current && !LegacyCallHandler.instance.isPlaying(AudioID.Ring)) {
if (isRingToast && !soundHasStarted.current && !sdkContext.legacyCallHandler.isPlaying(AudioID.Ring)) {
// Start ringing if not already.
soundHasStarted.current = true;
void LegacyCallHandler.instance.play(AudioID.Ring);
void sdkContext.legacyCallHandler.play(AudioID.Ring);
}
}, [notificationContent.notification_type, soundHasStarted]);
}, [notificationContent.notification_type, soundHasStarted, sdkContext.legacyCallHandler]);
// Stop ringing on dismiss.
const dismissToast = useCallback((): void => {
ToastStore.sharedInstance().dismissToast(toastKey);
LegacyCallHandler.instance.pause(AudioID.Ring);
}, [toastKey]);
sdkContext.legacyCallHandler.pause(AudioID.Ring);
}, [toastKey, sdkContext.legacyCallHandler]);
// Dismiss if the notification event or call event is redacted
useTypedEventEmitter(room, MatrixEventEvent.BeforeRedaction, (ev: MatrixEvent) => {
+17 -13
View File
@@ -19,12 +19,13 @@ import {
VolumeOnSolidIcon,
} from "@vector-im/compound-design-tokens/assets/web/icons";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../LegacyCallHandler";
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";
import { getCallStateIcon } from "../components/views/messages/LegacyCallEvent.tsx";
import { SDKContext } from "../contexts/SDKContext.ts";
export const getIncomingLegacyCallToastKey = (callId: string): string => `call_${callId}`;
@@ -37,64 +38,67 @@ interface IState {
}
export default class IncomingLegacyCallToast extends React.Component<IProps, IState> {
public static contextType = SDKContext;
declare public context: React.ContextType<typeof SDKContext>;
private readonly roomId: string;
public constructor(props: IProps) {
super(props);
public constructor(props: IProps, context: React.ContextType<typeof SDKContext>) {
super(props, context);
const roomId = LegacyCallHandler.instance.roomIdForCall(this.props.call);
const roomId = context.legacyCallHandler.roomIdForCall(this.props.call);
if (!roomId) {
throw new Error("Unable to find room for incoming call");
}
this.roomId = roomId;
this.state = {
silenced: LegacyCallHandler.instance.isCallSilenced(this.props.call.callId),
silenced: context.legacyCallHandler.isCallSilenced(this.props.call.callId),
};
}
public componentDidMount = (): void => {
LegacyCallHandler.instance.addListener(
this.context.legacyCallHandler.addListener(
LegacyCallHandlerEvent.SilencedCallsChanged,
this.onSilencedCallsChanged,
);
};
public componentWillUnmount(): void {
LegacyCallHandler.instance.removeListener(
this.context.legacyCallHandler.removeListener(
LegacyCallHandlerEvent.SilencedCallsChanged,
this.onSilencedCallsChanged,
);
}
private onSilencedCallsChanged = (): void => {
this.setState({ silenced: LegacyCallHandler.instance.isCallSilenced(this.props.call.callId) });
this.setState({ silenced: this.context.legacyCallHandler.isCallSilenced(this.props.call.callId) });
};
private onAnswerClick = (e: ButtonEvent): void => {
e.stopPropagation();
LegacyCallHandler.instance.answerCall(this.roomId);
this.context.legacyCallHandler.answerCall(this.roomId);
};
private onRejectClick = (e: ButtonEvent): void => {
e.stopPropagation();
LegacyCallHandler.instance.hangupOrReject(this.roomId, true);
this.context.legacyCallHandler.hangupOrReject(this.roomId, true);
};
private onSilenceClick = (e: ButtonEvent): void => {
e.stopPropagation();
const callId = this.props.call.callId;
if (this.state.silenced) {
LegacyCallHandler.instance.unSilenceCall(callId);
this.context.legacyCallHandler.unSilenceCall(callId);
} else {
LegacyCallHandler.instance.silenceCall(callId);
this.context.legacyCallHandler.silenceCall(callId);
}
};
public render(): React.ReactNode {
const room = MatrixClientPeg.safeGet().getRoom(this.roomId);
const isVoice = this.props.call.type === CallType.Voice;
const callForcedSilent = LegacyCallHandler.instance.isForcedSilent();
const callForcedSilent = this.context.legacyCallHandler.isForcedSilent();
let silenceButtonTooltip = this.state.silenced ? _t("voip|unsilence") : _t("voip|silence");
if (callForcedSilent) {
+3 -2
View File
@@ -26,7 +26,6 @@ import { type ActionPayload } from "../dispatcher/payloads";
import SettingsStore from "../settings/SettingsStore";
import { CallStore } from "../stores/CallStore";
import { type Call } from "../models/Call";
import LegacyCallHandler from "../LegacyCallHandler";
vi.mock("../Modal.tsx");
@@ -84,7 +83,9 @@ describe("leaveRoomBehaviour", () => {
};
it("hangs up legacy calls when leaving a room", async () => {
const hangupSpy = vi.spyOn(LegacyCallHandler.instance, "hangupOrReject").mockImplementation(() => {});
const hangupSpy = vi
.spyOn(SDKContextClass.instance.legacyCallHandler, "hangupOrReject")
.mockImplementation(() => {});
viewRoom(room);
await leaveRoomBehaviour(client, room.roomId);
+1 -2
View File
@@ -27,7 +27,6 @@ import { bulkSpaceBehaviour } from "./space";
import { SDKContextClass } from "../contexts/SDKContextClass";
import SettingsStore from "../settings/SettingsStore";
import { CallStore } from "../stores/CallStore";
import LegacyCallHandler from "../LegacyCallHandler";
export async function leaveRoomBehaviour(
matrixClient: MatrixClient,
@@ -64,7 +63,7 @@ export async function leaveRoomBehaviour(
// attempt to hang up legacy based calls
try {
LegacyCallHandler.instance.hangupOrReject(roomId);
SDKContextClass.instance.legacyCallHandler.hangupOrReject(roomId);
} catch (e) {
logger.warn("Failed to hangup call before leaving room: ", e);
}
+2 -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 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
@@ -34,7 +34,7 @@ export const placeCall = async (
PosthogTrackers.trackInteraction(analyticsName);
if (platformCallType == PlatformCallType.LegacyCall || platformCallType == PlatformCallType.JitsiCall) {
await LegacyCallHandler.instance.placeCall(room.roomId, callType);
await SDKContextClass.instance.legacyCallHandler.placeCall(room.roomId, callType);
} else if (platformCallType == PlatformCallType.ElementCall) {
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
@@ -21,7 +21,8 @@ import { MetaSpace } from "../../stores/spaces";
import { Action } from "../../dispatcher/actions";
import PosthogTrackers from "../../PosthogTrackers";
import defaultDispatcher from "../../dispatcher/dispatcher";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
import type LegacyCallHandler from "../../LegacyCallHandler";
import { LegacyCallHandlerEvent } from "../../LegacyCallHandler";
export interface Props {
/**
@@ -29,6 +30,11 @@ export interface Props {
* The explore button is only displayed in the Home meta space
*/
activeSpace: string;
/**
* Reference to the LegacyCallHandler instance
*/
legacyCallHandler: LegacyCallHandler;
}
/**
@@ -58,13 +64,13 @@ export class RoomListSearchViewModel
};
public constructor(props: Props) {
const supportsPstn = LegacyCallHandler.instance.getSupportsPstnProtocol();
const supportsPstn = props.legacyCallHandler.getSupportsPstnProtocol();
super(props, RoomListSearchViewModel.computeSnapshot(props.activeSpace, supportsPstn));
this.displayDialButton = supportsPstn;
// Listen for changes in PSTN protocol support
this.disposables.trackListener(
LegacyCallHandler.instance,
props.legacyCallHandler,
LegacyCallHandlerEvent.ProtocolSupport,
this.onProtocolSupportChange,
);
@@ -74,7 +80,7 @@ export class RoomListSearchViewModel
* Handles changes in protocol support (PSTN).
*/
private readonly onProtocolSupportChange = (): void => {
const supportsPstn = LegacyCallHandler.instance.getSupportsPstnProtocol();
const supportsPstn = this.props.legacyCallHandler.getSupportsPstnProtocol();
this.displayDialButton = supportsPstn;
this.snapshot.set(RoomListSearchViewModel.computeSnapshot(this.props.activeSpace, supportsPstn));
};
@@ -40,6 +40,7 @@ import SettingsStore from "../../src/settings/SettingsStore";
import { UIFeature } from "../../src/settings/UIFeature";
import { createAudioContext } from "../../src/audio/compat";
import * as ManagedHybrid from "../../src/widgets/ManagedHybrid";
import { TestSDKContext } from "./TestSDKContext.ts";
jest.mock("../../src/Modal");
@@ -165,7 +166,7 @@ describe("LegacyCallHandler", () => {
});
};
callHandler = new LegacyCallHandler();
callHandler = new LegacyCallHandler(new TestSDKContext());
callHandler.start();
mocked(getFunctionalMembers).mockReturnValue([FUNCTIONAL_USER]);
@@ -238,8 +239,6 @@ describe("LegacyCallHandler", () => {
callHandler.stop();
// @ts-ignore
DMRoomMap.setShared(null);
// @ts-ignore
window.mxLegacyCallHandler = null;
MatrixClientPeg.unset();
document.body.removeChild(audioElement);
@@ -373,7 +372,7 @@ describe("LegacyCallHandler without third party protocols", () => {
};
mocked(createAudioContext).mockReturnValue(mockAudioContext as unknown as AudioContext);
callHandler = new LegacyCallHandler();
callHandler = new LegacyCallHandler(new TestSDKContext());
callHandler.start();
const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE);
@@ -423,8 +422,6 @@ describe("LegacyCallHandler without third party protocols", () => {
callHandler.stop();
// @ts-ignore
DMRoomMap.setShared(null);
// @ts-ignore
window.mxLegacyCallHandler = null;
MatrixClientPeg.unset();
document.body.removeChild(audioElement);
@@ -18,6 +18,7 @@ import { type SpaceStoreClass } from "../../src/stores/spaces/SpaceStore";
import { type WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore";
import { type WidgetPermissionStore } from "../../src/stores/widgets/WidgetPermissionStore";
import type WidgetStore from "../../src/stores/WidgetStore";
import type LegacyCallHandler from "../../src/LegacyCallHandler.tsx";
/**
* A class which provides the same API as SDKContextClass but adds additional unsafe setters which can
@@ -34,6 +35,7 @@ export class TestSDKContext extends SDKContextClass {
declare public _PosthogAnalytics?: PosthogAnalytics;
declare public _SlidingSyncManager?: SlidingSyncManager;
declare public _SpaceStore?: SpaceStoreClass;
declare public _LegacyCallHandler?: LegacyCallHandler;
constructor() {
super();
@@ -6,12 +6,13 @@ 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 { type MatrixClient, type MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
import { type MatrixClient, MatrixEvent, EventType } from "matrix-js-sdk/src/matrix";
import { CallState } from "matrix-js-sdk/src/webrtc/call";
import { stubClient } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import LegacyCallEventGrouper from "../../../../src/components/structures/LegacyCallEventGrouper";
import { SDKContextClass } from "../../../../src/contexts/SDKContextClass.ts";
const MY_USER_ID = "@me:here";
const THEIR_USER_ID = "@they:here";
@@ -145,4 +146,83 @@ describe("LegacyCallEventGrouper", () => {
expect(grouper.isVoice).toBe(false);
});
it("should be able to answer call", () => {
const grouper = new LegacyCallEventGrouper();
grouper.add(
new MatrixEvent({
content: {
call_id: "callId",
},
type: EventType.CallInvite,
sender: THEIR_USER_ID,
room_id: "!room:server",
}),
);
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "answerCall");
grouper.answerCall();
expect(SDKContextClass.instance.legacyCallHandler.answerCall).toHaveBeenCalledWith("!room:server");
});
it("should be able to reject call", () => {
const grouper = new LegacyCallEventGrouper();
grouper.add(
new MatrixEvent({
content: {
call_id: "callId",
},
type: EventType.CallInvite,
sender: THEIR_USER_ID,
room_id: "!room:server",
}),
);
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "hangupOrReject");
grouper.rejectCall();
expect(SDKContextClass.instance.legacyCallHandler.hangupOrReject).toHaveBeenCalledWith("!room:server", true);
});
it("should be able to callback call", () => {
const grouper = new LegacyCallEventGrouper();
grouper.add(
new MatrixEvent({
content: {
call_id: "callId",
},
type: EventType.CallHangup,
sender: THEIR_USER_ID,
room_id: "!room:server",
}),
);
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
grouper.callBack();
expect(SDKContextClass.instance.legacyCallHandler.placeCall).toHaveBeenCalledWith("!room:server", "video");
});
it("should be able to toggle call silenced", () => {
const grouper = new LegacyCallEventGrouper();
grouper.add(
new MatrixEvent({
content: {
call_id: "callId",
},
type: EventType.CallHangup,
sender: THEIR_USER_ID,
room_id: "!room:server",
}),
);
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "unSilenceCall");
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "silenceCall");
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "isCallSilenced").mockReturnValue(false);
grouper.toggleSilenced();
expect(SDKContextClass.instance.legacyCallHandler.silenceCall).toHaveBeenCalledWith("callId");
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "isCallSilenced").mockReturnValue(true);
grouper.toggleSilenced();
expect(SDKContextClass.instance.legacyCallHandler.unSilenceCall).toHaveBeenCalledWith("callId");
});
});
@@ -46,7 +46,6 @@ import {
} from "../../../test-utils";
import * as leaveRoomUtils from "../../../../src/utils/leave-behaviour";
import { OidcClientError } from "../../../../src/utils/oidc/error";
import LegacyCallHandler from "../../../../src/LegacyCallHandler";
import { CallStore } from "../../../../src/stores/CallStore";
import { type Call } from "../../../../src/models/Call";
import { PosthogAnalytics } from "../../../../src/PosthogAnalytics";
@@ -1109,7 +1108,7 @@ describe("<MatrixChat />", () => {
beforeEach(() => {
// stub out various cleanup functions
jest.spyOn(LegacyCallHandler.instance, "hangupAllCalls")
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "hangupAllCalls")
.mockClear()
.mockImplementation(() => {});
jest.spyOn(PosthogAnalytics.instance, "logout").mockImplementation(() => {});
@@ -1134,7 +1133,7 @@ describe("<MatrixChat />", () => {
it("should hangup all legacy calls", async () => {
await getComponentAndWaitForReady();
await dispatchLogoutAndWait();
expect(LegacyCallHandler.instance.hangupAllCalls).toHaveBeenCalled();
expect(SDKContextClass.instance.legacyCallHandler.hangupAllCalls).toHaveBeenCalled();
});
it("should disconnect all calls", async () => {
@@ -39,7 +39,7 @@ import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMe
import { ModuleRunner } from "../../../../../src/modules/ModuleRunner";
import { ModuleApi } from "../../../../../src/modules/Api";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass";
import { TestSDKContext } from "../../../TestSDKContext.ts";
jest.mock("../../../../../src/stores/OwnProfileStore", () => ({
OwnProfileStore: {
@@ -56,7 +56,7 @@ const realGetValue = SettingsStore.getValue;
describe("AppTile", () => {
let cli: MatrixClient;
let sdkContext: SDKContextClass;
let sdkContext: TestSDKContext;
let r1: Room;
let r2: Room;
const resizeNotifier = new ResizeNotifier();
@@ -118,18 +118,19 @@ describe("AppTile", () => {
beforeEach(async () => {
// Do not carry across settings from previous tests
SettingsStore.reset();
sdkContext = new SDKContextClass();
sdkContext = new TestSDKContext();
sdkContext._client = cli;
// @ts-ignore
await WidgetMessagingStore.instance.onReady();
// Wake up various stores we rely on
WidgetLayoutStore.instance.useUnitTestClient(cli);
sdkContext.widgetLayoutStore.useUnitTestClient(cli);
// @ts-ignore
await WidgetLayoutStore.instance.onReady();
await sdkContext.widgetLayoutStore.onReady();
RightPanelStore.instance.useUnitTestClient(cli);
sdkContext.rightPanelStore.useUnitTestClient(cli);
// @ts-ignore
await RightPanelStore.instance.onReady();
await sdkContext.rightPanelStore.onReady();
});
afterEach(async () => {
@@ -162,13 +163,12 @@ describe("AppTile", () => {
// Run initial render with room 1, and also running lifecycle methods
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r1}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
/>
</MatrixClientContext.Provider>,
<RightPanel
room={r1}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
/>,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
act(() =>
dis.dispatch({
@@ -232,13 +232,12 @@ describe("AppTile", () => {
// Run initial render with room 1, and also running lifecycle methods
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<RightPanel
room={r1}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
/>
</MatrixClientContext.Provider>,
<RightPanel
room={r1}
resizeNotifier={resizeNotifier}
permalinkCreator={new RoomPermalinkCreator(r1, r1.roomId)}
/>,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
act(() =>
dis.dispatch({
@@ -341,6 +340,47 @@ describe("AppTile", () => {
expect(ActiveWidgetStore.instance.isLive("1", "r1")).toBe(true);
});
it("should hangup Jitsi call when room is left", async () => {
const app: IApp = {
id: "3",
eventId: "jitsi1",
roomId: "r2",
type: MatrixWidgetType.JitsiMeet,
url: "https://jitsi.example.com",
name: "Jitsi Conference",
creatorUserId: cli.getSafeUserId(),
avatar_url: undefined,
};
const { queryByRole, getByText } = render(
<AppTile key={app.id} app={app} room={r2} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => queryByRole("progressbar"));
expect(getByText("Jitsi Conference")).toBeInTheDocument();
// Switch to room 1
dis.dispatch(
{
action: Action.ViewRoom,
room_id: "r1",
},
true,
);
jest.spyOn(ActiveWidgetStore.instance, "getWidgetPersistence").mockReturnValue(true);
jest.spyOn(sdkContext.legacyCallHandler, "hangupCallApp");
dis.dispatch(
{
action: Action.AfterLeaveRoom,
room_id: "r2",
},
true,
);
expect(sdkContext.legacyCallHandler.hangupCallApp).toHaveBeenCalledWith(app.roomId);
});
describe("for a pinned widget", () => {
let moveToContainerSpy: jest.SpyInstance<void, [room: Room, widget: IWidget, toContainer: Container]>;
beforeEach(async () => {
@@ -349,9 +389,8 @@ describe("AppTile", () => {
it("should render", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
const { asFragment } = renderResult;
@@ -361,9 +400,8 @@ describe("AppTile", () => {
it("should not display the »Popout widget« button", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
expect(renderResult.queryByLabelText("Popout widget")).not.toBeInTheDocument();
@@ -371,20 +409,34 @@ describe("AppTile", () => {
it("clicking 'minimise' should send the widget to the right", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
await userEvent.click(renderResult.getByLabelText("Minimise"));
expect(moveToContainerSpy).toHaveBeenCalledWith(r1, app1, "right");
});
it("should close right panel timeline when minimising widget", async () => {
const renderResult = render(
<AppTile key={app1.id} app={app1} room={r1} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
jest.spyOn(sdkContext.rightPanelStore, "currentCardForRoom").mockReturnValue({
phase: RightPanelPhases.Timeline,
});
jest.spyOn(sdkContext.rightPanelStore, "popCard");
await userEvent.click(renderResult.getByLabelText("Minimise"));
expect(sdkContext.rightPanelStore.popCard).toHaveBeenCalledWith(r1.roomId);
});
it("clicking 'maximise' should send the widget to the center", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
await userEvent.click(renderResult.getByLabelText("Maximise"));
@@ -400,9 +452,8 @@ describe("AppTile", () => {
// userId and creatorUserId are different
const { container, asFragment, queryByRole } = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
expect(container.querySelector(".mx_Spinner")).toBeFalsy();
expect(queryByRole("button", { name: "Continue" })).toBeInTheDocument();
@@ -418,9 +469,8 @@ describe("AppTile", () => {
// userId and creatorUserId are different
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
@@ -440,9 +490,8 @@ describe("AppTile", () => {
// userId and creatorUserId are different so legacy path would show "Continue"
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} userId="@user1" creatorUserId="@userAnother" />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
// The new API runs async in componentDidMount, so wait for it to take effect
@@ -466,9 +515,8 @@ describe("AppTile", () => {
it("clicking 'un-maximise' should send the widget to the top", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
await userEvent.click(renderResult.getByLabelText("Un-maximise"));
@@ -496,9 +544,8 @@ describe("AppTile", () => {
it("should display the »Popout widget« button", async () => {
const renderResult = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => renderResult.queryByRole("progressbar"));
expect(renderResult.getByLabelText("Popout widget")).toBeInTheDocument();
@@ -509,9 +556,8 @@ describe("AppTile", () => {
describe("for a persistent app", () => {
it("should render", async () => {
const { asFragment, queryByRole } = render(
<MatrixClientContext.Provider value={cli}>
<AppTile key={app1.id} app={app1} room={r1} fullWidth={true} miniMode={true} showMenubar={false} />
</MatrixClientContext.Provider>,
<AppTile key={app1.id} app={app1} room={r1} fullWidth={true} miniMode={true} showMenubar={false} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
await waitForElementToBeRemoved(() => queryByRole("progressbar"));
expect(asFragment()).toMatchSnapshot();
@@ -46,7 +46,6 @@ import { ScopedRoomContextProvider } from "../../../../../../src/contexts/Scoped
import RoomContext, { type RoomContextType } from "../../../../../../src/contexts/RoomContext";
import RightPanelStore from "../../../../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../../../../src/stores/right-panel/RightPanelStorePhases";
import LegacyCallHandler from "../../../../../../src/LegacyCallHandler";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
import SdkConfig from "../../../../../../src/SdkConfig";
import dispatcher from "../../../../../../src/dispatcher/dispatcher";
@@ -60,6 +59,7 @@ import WidgetStore, { type IApp } from "../../../../../../src/stores/WidgetStore
import { UIFeature } from "../../../../../../src/settings/UIFeature";
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
import { ElementCallMemberEventType } from "../../../../../../src/call-types";
import { SDKContextClass } from "../../../../../../src/contexts/SDKContextClass.ts";
jest.mock("../../../../../../src/utils/ShieldUtils");
jest.mock("../../../../../../src/hooks/right-panel/useCurrentPhase", () => ({
@@ -365,7 +365,7 @@ describe("RoomHeader", () => {
expect(voiceButton).not.toHaveAttribute("aria-disabled", "true");
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
const placeCallSpy = jest.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
await user.click(voiceButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
@@ -376,7 +376,7 @@ describe("RoomHeader", () => {
it("you can't call if there's already a call", () => {
mockRoomMembers(room, 2);
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue(
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "getCallForRoom").mockReturnValue(
// The JS-SDK does not export the class `MatrixCall` only the type
{} as MatrixCall,
);
@@ -508,7 +508,7 @@ describe("RoomHeader", () => {
it("disables calling if there's a jitsi call", () => {
mockRoomMembers(room, 2);
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue(
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "getCallForRoom").mockReturnValue(
// The JS-SDK does not export the class `MatrixCall` only the type
{} as MatrixCall,
);
@@ -532,7 +532,7 @@ describe("RoomHeader", () => {
expect(voiceButton).not.toHaveAttribute("aria-disabled", "true");
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
const placeCallSpy = jest.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
await user.click(voiceButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
@@ -554,7 +554,7 @@ describe("RoomHeader", () => {
const videoButton = screen.getByRole("button", { name: "Video call" });
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
const placeCallSpy = jest.spyOn(SDKContextClass.instance.legacyCallHandler, "placeCall");
await user.click(videoButton);
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
});
@@ -15,6 +15,8 @@ import { shouldShowComponent } from "../../../../../../src/customisations/helper
import { MetaSpace } from "../../../../../../src/stores/spaces";
import { LandmarkNavigation } from "../../../../../../src/accessibility/LandmarkNavigation";
import { ReleaseAnnouncementStore } from "../../../../../../src/stores/ReleaseAnnouncementStore";
import { clientAndSDKContextRenderOptions, createTestClient } from "../../../../../test-utils";
import { TestSDKContext } from "../../../../TestSDKContext.ts";
jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
@@ -34,8 +36,15 @@ jest.mock("../../../../../../src/accessibility/LandmarkNavigation", () => ({
jest.spyOn(ReleaseAnnouncementStore.instance, "getReleaseAnnouncement").mockReturnValue(null);
describe("<RoomListPanel />", () => {
const client = createTestClient();
const sdkContext = new TestSDKContext();
sdkContext._client = client;
function renderComponent() {
return render(<RoomListPanel activeSpace={MetaSpace.Home} />);
return render(
<RoomListPanel activeSpace={MetaSpace.Home} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
}
beforeEach(() => {
@@ -12,7 +12,8 @@ import { mocked } from "jest-mock";
import { RoomListSearch } from "../../../../../../src/components/views/rooms/RoomListPanel/RoomListSearch";
import { MetaSpace } from "../../../../../../src/stores/spaces";
import { shouldShowComponent } from "../../../../../../src/customisations/helpers/UIComponents";
import LegacyCallHandler from "../../../../../../src/LegacyCallHandler";
import { SDKContextClass } from "../../../../../../src/contexts/SDKContextClass.ts";
import { clientAndSDKContextRenderOptions, createTestClient } from "../../../../../test-utils";
jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
@@ -20,13 +21,16 @@ jest.mock("../../../../../../src/customisations/helpers/UIComponents", () => ({
describe("<RoomListSearch />", () => {
function renderComponent(activeSpace = MetaSpace.Home) {
return render(<RoomListSearch activeSpace={activeSpace} />);
return render(
<RoomListSearch activeSpace={activeSpace} />,
clientAndSDKContextRenderOptions(createTestClient(), SDKContextClass.instance),
);
}
beforeEach(() => {
// By default, we consider shouldShowComponent(UIComponent.ExploreRooms) should return true
mocked(shouldShowComponent).mockReturnValue(true);
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
jest.spyOn(SDKContextClass.instance.legacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(false);
});
it("renders", () => {
@@ -26,6 +26,7 @@ import {
MockedCall,
setupAsyncStoreWithClient,
useMockMediaDevices,
clientAndSDKContextRenderOptions,
} from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { CallView as _CallView } from "../../../../../src/components/views/voip/CallView";
@@ -33,6 +34,7 @@ import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMe
import { CallStore } from "../../../../../src/stores/CallStore";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { type WidgetMessaging } from "../../../../../src/stores/widgets/WidgetMessaging";
import { TestSDKContext } from "../../../TestSDKContext.ts";
const CallView = wrapInMatrixClientContext(_CallView);
@@ -41,6 +43,7 @@ describe("CallView", () => {
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
let client: Mocked<MatrixClient>;
let sdkContext: TestSDKContext;
let room: Room;
let alice: RoomMember;
let call: MockedCall;
@@ -51,6 +54,8 @@ describe("CallView", () => {
stubClient();
client = mocked(MatrixClientPeg.safeGet());
sdkContext = new TestSDKContext();
sdkContext._client = client;
DMRoomMap.makeShared(client);
room = new Room("!1:example.org", client, "@alice:example.org", {
@@ -88,7 +93,10 @@ describe("CallView", () => {
});
const renderView = async (role: string | undefined = undefined): Promise<void> => {
render(<CallView room={room} resizing={false} role={role} onClose={() => {}} />);
render(
<CallView room={room} resizing={false} role={role} onClose={() => {}} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
await act(() => Promise.resolve()); // Let effects settle
};
@@ -6,16 +6,21 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { render } from "jest-matrix-react";
import { render, fireEvent } from "jest-matrix-react";
import { type MatrixCall } from "matrix-js-sdk/src/matrix";
import { type CallFeed } from "matrix-js-sdk/src/webrtc/callFeed";
import { SDPStreamMetadataPurpose } from "matrix-js-sdk/src/webrtc/callEventTypes";
import LegacyCallView from "../../../../../src/components/views/voip/LegacyCallView";
import { stubClient } from "../../../../test-utils";
import { clientAndSDKContextRenderOptions, createTestClient, stubClient } from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { TestSDKContext } from "../../../TestSDKContext.ts";
describe("LegacyCallView", () => {
const cli = stubClient();
const sdkContext = new TestSDKContext();
sdkContext._client = cli;
it("should exit full screen on unmount", () => {
const element = document.createElement("div");
// @ts-expect-error
@@ -35,7 +40,10 @@ describe("LegacyCallView", () => {
isScreensharing: jest.fn().mockReturnValue(false),
} as unknown as MatrixCall;
const { unmount } = render(<LegacyCallView call={call} sidebarShown={false} />);
const { unmount } = render(
<LegacyCallView call={call} sidebarShown={false} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
expect(document.exitFullscreen).not.toHaveBeenCalled();
unmount();
expect(document.exitFullscreen).toHaveBeenCalled();
@@ -75,7 +83,10 @@ describe("LegacyCallView", () => {
getUserIdForRoomId: jest.fn().mockReturnValue("test-user"),
} as unknown as DMRoomMap);
const { container, rerender } = render(<LegacyCallView call={call} sidebarShown={true} />);
const { container, rerender } = render(
<LegacyCallView call={call} sidebarShown={true} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
expect(container.querySelector(".mx_LegacyCallViewSidebar")).toBeTruthy();
rerender(<LegacyCallView call={call} sidebarShown={true} />);
expect(container.querySelector(".mx_LegacyCallViewSidebar")).toBeTruthy();
@@ -98,7 +109,102 @@ describe("LegacyCallView", () => {
getUserIdForRoomId: jest.fn().mockReturnValue("test-user"),
} as unknown as DMRoomMap);
const { container } = render(<LegacyCallView call={call} sidebarShown={false} pipMode={true} />);
const { container } = render(
<LegacyCallView call={call} sidebarShown={false} pipMode={true} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
expect(container.querySelector(".mx_LegacyCallViewButtons_button_sidebar")).toBeFalsy();
});
it("should allow user to resume held call", async () => {
const client = createTestClient();
const sdkContext = new TestSDKContext();
sdkContext._client = client;
const call = {
roomId: "test-room",
on: jest.fn(),
removeListener: jest.fn(),
getFeeds: jest.fn().mockReturnValue(
[{ local: true }, { local: false }, { local: true, screenshare: true }].map(
(x, i) =>
({
stream: { id: "test-" + i },
addListener: jest.fn(),
removeListener: jest.fn(),
getMember: jest.fn(),
isAudioMuted: jest.fn().mockReturnValue(true),
isVideoMuted: jest.fn().mockReturnValue(true),
isLocal: jest.fn().mockReturnValue(x.local),
purpose: x.screenshare && SDPStreamMetadataPurpose.Screenshare,
}) as unknown as CallFeed,
),
),
isLocalOnHold: jest.fn().mockReturnValue(false),
isRemoteOnHold: jest.fn().mockReturnValue(true),
isMicrophoneMuted: jest.fn().mockReturnValue(true),
isLocalVideoMuted: jest.fn().mockReturnValue(true),
isScreensharing: jest.fn().mockReturnValue(true),
noIncomingFeeds: jest.fn().mockReturnValue(false),
opponentSupportsSDPStreamMetadata: jest.fn().mockReturnValue(true),
getOpponentMember: jest.fn(),
} as unknown as MatrixCall;
jest.spyOn(sdkContext.legacyCallHandler, "roomIdForCall").mockReturnValue(call.roomId);
jest.spyOn(sdkContext.legacyCallHandler, "setActiveCallRoomId");
const { getByText } = render(
<LegacyCallView call={call} sidebarShown />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
fireEvent.click(getByText("Resume"));
expect(sdkContext.legacyCallHandler.setActiveCallRoomId).toHaveBeenCalledWith(call.roomId);
});
it("should allow user to hangup call", async () => {
const client = createTestClient();
const sdkContext = new TestSDKContext();
sdkContext._client = client;
const call = {
roomId: "test-room",
on: jest.fn(),
removeListener: jest.fn(),
getFeeds: jest.fn().mockReturnValue(
[{ local: true }, { local: false }, { local: true, screenshare: true }].map(
(x, i) =>
({
stream: { id: "test-" + i },
addListener: jest.fn(),
removeListener: jest.fn(),
getMember: jest.fn(),
isAudioMuted: jest.fn().mockReturnValue(true),
isVideoMuted: jest.fn().mockReturnValue(true),
isLocal: jest.fn().mockReturnValue(x.local),
purpose: x.screenshare && SDPStreamMetadataPurpose.Screenshare,
}) as unknown as CallFeed,
),
),
isLocalOnHold: jest.fn().mockReturnValue(false),
isRemoteOnHold: jest.fn().mockReturnValue(false),
isMicrophoneMuted: jest.fn().mockReturnValue(true),
isLocalVideoMuted: jest.fn().mockReturnValue(true),
isScreensharing: jest.fn().mockReturnValue(true),
noIncomingFeeds: jest.fn().mockReturnValue(false),
opponentSupportsSDPStreamMetadata: jest.fn().mockReturnValue(true),
getOpponentMember: jest.fn(),
} as unknown as MatrixCall;
jest.spyOn(sdkContext.legacyCallHandler, "roomIdForCall").mockReturnValue(call.roomId);
jest.spyOn(sdkContext.legacyCallHandler, "hangupOrReject");
const { getByLabelText } = render(
<LegacyCallView call={call} sidebarShown />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
fireEvent.click(getByLabelText("Hangup"));
expect(sdkContext.legacyCallHandler.hangupOrReject).toHaveBeenCalledWith(call.roomId);
});
});
@@ -12,29 +12,29 @@ import { CallEventHandlerEvent } from "matrix-js-sdk/src/webrtc/callEventHandler
import LegacyCallView from "../../../../../src/components/views/voip/LegacyCallView";
import LegacyCallViewForRoom from "../../../../../src/components/views/voip/LegacyCallViewForRoom";
import { mkStubRoom, stubClient } from "../../../../test-utils";
import { clientAndSDKContextRenderOptions, mkStubRoom, stubClient } from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import LegacyCallHandler from "../../../../../src/LegacyCallHandler";
import { SDKContext } from "../../../../../src/contexts/SDKContext";
import { SDKContextClass } from "../../../../../src/contexts/SDKContextClass";
import { TestSDKContext } from "../../../TestSDKContext.ts";
jest.mock("../../../../../src/components/views/voip/LegacyCallView", () => jest.fn(() => "LegacyCallView"));
describe("LegacyCallViewForRoom", () => {
const LegacyCallViewMock = LegacyCallView as unknown as jest.Mock;
let sdkContext: SDKContextClass;
let sdkContext: TestSDKContext;
beforeEach(() => {
stubClient();
sdkContext = new SDKContextClass();
sdkContext = new TestSDKContext();
sdkContext._client = stubClient();
LegacyCallViewMock.mockClear();
});
it("should remember sidebar state, defaulting to shown", async () => {
const callHandler = new LegacyCallHandler();
const callHandler = new LegacyCallHandler(sdkContext);
callHandler.start();
jest.spyOn(LegacyCallHandler, "instance", "get").mockImplementation(() => callHandler);
sdkContext._LegacyCallHandler = callHandler;
const call = new MatrixCall({
client: MatrixClientPeg.safeGet(),
@@ -49,7 +49,10 @@ describe("LegacyCallViewForRoom", () => {
const cli = MatrixClientPeg.safeGet();
cli.emit(CallEventHandlerEvent.Incoming, call);
const { rerender } = render(<LegacyCallViewForRoom roomId={call.roomId} />);
const { rerender } = render(
<LegacyCallViewForRoom roomId={call.roomId} />,
clientAndSDKContextRenderOptions(cli, sdkContext),
);
let props = LegacyCallViewMock.mock.lastCall![0];
expect(props.sidebarShown).toBeTruthy(); // Sidebar defaults to shown
@@ -84,9 +87,7 @@ describe("LegacyCallViewForRoom", () => {
addListener: jest.fn(),
removeListener: jest.fn(),
};
jest.spyOn(LegacyCallHandler, "instance", "get").mockImplementation(
() => callHandler as unknown as LegacyCallHandler,
);
sdkContext._LegacyCallHandler = callHandler as unknown as LegacyCallHandler;
jest.spyOn(sdkContext.resizeNotifier, "startResizing");
jest.spyOn(sdkContext.resizeNotifier, "stopResizing");
@@ -14,9 +14,9 @@ import { type MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import * as AvatarModule from "../../../../../src/Avatar";
import VideoFeed from "../../../../../src/components/views/voip/VideoFeed";
import { stubClient, useMockedCalls } from "../../../../test-utils";
import type LegacyCallHandler from "../../../../../src/LegacyCallHandler";
import { clientAndSDKContextRenderOptions, stubClient, useMockedCalls } from "../../../../test-utils";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { TestSDKContext } from "../../../TestSDKContext.ts";
const FAKE_AVATAR_URL = "http://fakeurl.dummy/fake.png";
@@ -24,9 +24,12 @@ describe("VideoFeed", () => {
useMockedCalls();
let client: MatrixClient;
let sdkContext: TestSDKContext;
beforeAll(() => {
client = stubClient();
sdkContext = new TestSDKContext();
sdkContext._client = client;
(AvatarModule as any).avatarUrlForRoom = jest.fn().mockReturnValue(FAKE_AVATAR_URL);
const dmRoomMap = new DMRoomMap(client);
@@ -39,9 +42,7 @@ describe("VideoFeed", () => {
});
it("Displays the room avatar when no video is available", () => {
window.mxLegacyCallHandler = {
roomIdForCall: jest.fn().mockReturnValue("!this:room.here"),
} as unknown as LegacyCallHandler;
jest.spyOn(sdkContext.legacyCallHandler, "roomIdForCall").mockReturnValue("!this:room.here");
const mockCall = {
room: new Room("!room:example.com", client, client.getSafeUserId()),
@@ -53,7 +54,10 @@ describe("VideoFeed", () => {
addListener: jest.fn(),
removeListener: jest.fn(),
};
render(<VideoFeed feed={feed as unknown as CallFeed} call={mockCall as unknown as MatrixCall} />);
render(
<VideoFeed feed={feed as unknown as CallFeed} call={mockCall as unknown as MatrixCall} />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
const avatarImg = screen.getByRole("presentation");
expect(avatarImg).toHaveAttribute("src", FAKE_AVATAR_URL);
});
@@ -34,6 +34,7 @@ import {
setupAsyncStoreWithClient,
resetAsyncStoreWithClient,
mkEvent,
clientAndSDKContextRenderOptions,
} from "../../test-utils";
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
@@ -47,9 +48,10 @@ import {
getNotificationEventSendTs,
IncomingCallToast,
} from "../../../src/toasts/IncomingCallToast";
import LegacyCallHandler, { AudioID } from "../../../src/LegacyCallHandler";
import { AudioID } from "../../../src/LegacyCallHandler";
import { CallEvent } from "../../../src/models/Call";
import { type WidgetMessaging } from "../../../src/stores/widgets/WidgetMessaging";
import { TestSDKContext } from "../TestSDKContext.ts";
function makeNotificationEvent(room: Room, content: IContent = {}): MatrixEvent {
const ts = Date.now();
@@ -76,6 +78,7 @@ describe("IncomingCallToast", () => {
useMockedCalls();
let client: Mocked<MatrixClient>;
let sdkContext: TestSDKContext;
let room: Room;
let alice: RoomMember;
@@ -92,6 +95,8 @@ describe("IncomingCallToast", () => {
beforeEach(async () => {
stubClient();
client = mocked(MatrixClientPeg.safeGet());
sdkContext = new TestSDKContext();
sdkContext._client = client;
const audio = document.createElement("audio");
audio.id = AudioID.Ring;
@@ -146,6 +151,7 @@ describe("IncomingCallToast", () => {
notificationEvent={notificationEvent}
toastKey={getIncomingCallToastKey(callId, room.roomId)}
/>,
clientAndSDKContextRenderOptions(client, sdkContext),
);
return callId;
};
@@ -196,8 +202,11 @@ describe("IncomingCallToast", () => {
it("start ringing on ring notify event", () => {
const notificationEvent = makeNotificationEvent(room, { notification_type: "ring" });
const playMock = jest.spyOn(LegacyCallHandler.instance, "play");
render(<IncomingCallToast notificationEvent={notificationEvent} toastKey="" />);
const playMock = jest.spyOn(sdkContext.legacyCallHandler, "play");
render(
<IncomingCallToast notificationEvent={notificationEvent} toastKey="" />,
clientAndSDKContextRenderOptions(client, sdkContext),
);
expect(playMock).toHaveBeenCalled();
});
@@ -10,10 +10,15 @@ import { LOCAL_NOTIFICATION_SETTINGS_PREFIX, MatrixEvent, Room } from "matrix-js
import { MatrixCall } from "matrix-js-sdk/src/webrtc/call";
import React from "react";
import LegacyCallHandler from "../../../src/LegacyCallHandler";
import IncomingLegacyCallToast from "../../../src/toasts/IncomingLegacyCallToast";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import { getMockClientWithEventEmitter, mockClientMethodsServer, mockClientMethodsUser } from "../../test-utils";
import {
clientAndSDKContextRenderOptions,
getMockClientWithEventEmitter,
mockClientMethodsServer,
mockClientMethodsUser,
} from "../../test-utils";
import { SDKContextClass } from "../../../src/contexts/SDKContextClass.ts";
describe("<IncomingLegacyCallToast />", () => {
const userId = "@alice:server.org";
@@ -41,16 +46,24 @@ 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());
const { getByLabelText } = render(
getComponent(),
clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance),
);
expect(getByLabelText("Silence call")).toMatchSnapshot();
});
it("renders sound on button when call is silenced", () => {
LegacyCallHandler.instance.silenceCall(call.callId);
const { getByLabelText } = render(getComponent());
SDKContextClass.instance.legacyCallHandler.silenceCall(call.callId);
const { getByLabelText } = render(
getComponent(),
clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance),
);
expect(getByLabelText("Sound on")).toMatchSnapshot();
});
@@ -66,7 +79,10 @@ describe("<IncomingLegacyCallToast />", () => {
});
}
});
const { getByLabelText } = render(getComponent());
const { getByLabelText } = render(
getComponent(),
clientAndSDKContextRenderOptions(mockClient, SDKContextClass.instance),
);
expect(getByLabelText("Notifications silenced")).toMatchSnapshot();
});
});
@@ -13,6 +13,7 @@ import { shouldShowComponent } from "../../../src/customisations/helpers/UICompo
import defaultDispatcher from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
import LegacyCallHandler, { LegacyCallHandlerEvent } from "../../../src/LegacyCallHandler";
import { TestSDKContext } from "../../unit-tests/TestSDKContext.ts";
jest.mock("../../../src/customisations/helpers/UIComponents", () => ({
shouldShowComponent: jest.fn(),
@@ -23,9 +24,12 @@ jest.mock("../../../src/PosthogTrackers", () => ({
}));
describe("RoomListSearchViewModel", () => {
const context = new TestSDKContext();
beforeEach(() => {
mocked(shouldShowComponent).mockReturnValue(true);
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
context._LegacyCallHandler = new LegacyCallHandler(context);
jest.spyOn(context._LegacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(false);
});
afterEach(() => {
@@ -35,35 +39,50 @@ describe("RoomListSearchViewModel", () => {
describe("snapshot", () => {
it("should show explore button in Home space when UIComponent.ExploreRooms is enabled", () => {
mocked(shouldShowComponent).mockReturnValue(true);
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
const vm = new RoomListSearchViewModel({
activeSpace: MetaSpace.Home,
legacyCallHandler: context.legacyCallHandler,
});
expect(vm.getSnapshot().displayExploreButton).toBe(true);
});
it("should hide explore button when not in Home space", () => {
mocked(shouldShowComponent).mockReturnValue(true);
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.VideoRooms });
const vm = new RoomListSearchViewModel({
activeSpace: MetaSpace.VideoRooms,
legacyCallHandler: context.legacyCallHandler,
});
expect(vm.getSnapshot().displayExploreButton).toBe(false);
});
it("should hide explore button when UIComponent.ExploreRooms is disabled", () => {
mocked(shouldShowComponent).mockReturnValue(false);
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
const vm = new RoomListSearchViewModel({
activeSpace: MetaSpace.Home,
legacyCallHandler: context.legacyCallHandler,
});
expect(vm.getSnapshot().displayExploreButton).toBe(false);
});
it("should show dial button when PSTN protocol is supported", () => {
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(true);
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
jest.spyOn(context.legacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(true);
const vm = new RoomListSearchViewModel({
activeSpace: MetaSpace.Home,
legacyCallHandler: context.legacyCallHandler,
});
expect(vm.getSnapshot().displayDialButton).toBe(true);
});
it("should hide dial button when PSTN protocol is not supported", () => {
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
jest.spyOn(context.legacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(false);
const vm = new RoomListSearchViewModel({
activeSpace: MetaSpace.Home,
legacyCallHandler: context.legacyCallHandler,
});
expect(vm.getSnapshot().displayDialButton).toBe(false);
});
@@ -72,7 +91,10 @@ describe("RoomListSearchViewModel", () => {
describe("actions", () => {
it("should fire OpenSpotlight action when onSearchClick is called", () => {
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
const vm = new RoomListSearchViewModel({
activeSpace: MetaSpace.Home,
legacyCallHandler: context.legacyCallHandler,
});
vm.onSearchClick();
expect(fireSpy).toHaveBeenCalledWith(Action.OpenSpotlight);
@@ -80,7 +102,10 @@ describe("RoomListSearchViewModel", () => {
it("should fire OpenDialPad action when onDialPadClick is called", () => {
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
const vm = new RoomListSearchViewModel({
activeSpace: MetaSpace.Home,
legacyCallHandler: context.legacyCallHandler,
});
vm.onDialPadClick();
expect(fireSpy).toHaveBeenCalledWith(Action.OpenDialPad);
@@ -88,7 +113,10 @@ describe("RoomListSearchViewModel", () => {
it("should fire ViewRoomDirectory action and track interaction when onExploreClick is called", () => {
const fireSpy = jest.spyOn(defaultDispatcher, "fire");
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
const vm = new RoomListSearchViewModel({
activeSpace: MetaSpace.Home,
legacyCallHandler: context.legacyCallHandler,
});
const mockEvent = {} as React.MouseEvent<HTMLButtonElement>;
vm.onExploreClick(mockEvent);
@@ -98,14 +126,17 @@ describe("RoomListSearchViewModel", () => {
});
it("should update snapshot when PSTN protocol support changes", () => {
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(false);
const vm = new RoomListSearchViewModel({ activeSpace: MetaSpace.Home });
jest.spyOn(context.legacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(false);
const vm = new RoomListSearchViewModel({
activeSpace: MetaSpace.Home,
legacyCallHandler: context.legacyCallHandler,
});
expect(vm.getSnapshot().displayDialButton).toBe(false);
// Simulate PSTN protocol support change
jest.spyOn(LegacyCallHandler.instance, "getSupportsPstnProtocol").mockReturnValue(true);
LegacyCallHandler.instance.emit(LegacyCallHandlerEvent.ProtocolSupport);
jest.spyOn(context.legacyCallHandler, "getSupportsPstnProtocol").mockReturnValue(true);
context.legacyCallHandler.emit(LegacyCallHandlerEvent.ProtocolSupport);
expect(vm.getSnapshot().displayDialButton).toBe(true);