mirror of
https://github.com/vector-im/element-web.git
synced 2025-11-29 14:31:22 +01:00
Show error screens in group calls (#29254)
* Avoid destroying calls until they are hidden from the UI We often want calls to exist even when no more participants are left in the MatrixRTC session. So, we should avoid destroying calls as long as they're being presented in the UI; this means that the user has an intent to either join the call or continue looking at an error screen, and we shouldn't interrupt that interaction. The RoomViewStore is now what takes care of creating and destroying calls, rather than the CallView. In general it seems kinda impossible to safely create and destroy model objects from React lifecycle hooks, so moving this responsibility to a store seemed appropriate and resolves existing issues with calls in React strict mode. * Wait for a close action before closing a call This creates a distinction between the user hanging up and the widget being ready to close, which is useful for allowing Element Call to show error screens when disconnected from the call, for example. * Don't expect a 'close' action in video rooms These use the returnToLobby option and are expected to remain visible when the user leaves the call.
This commit is contained in:
parent
e9a3625bd6
commit
aa996010b4
@ -118,8 +118,6 @@ import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages";
|
|||||||
import { LargeLoader } from "./LargeLoader";
|
import { LargeLoader } from "./LargeLoader";
|
||||||
import { isVideoRoom } from "../../utils/video-rooms";
|
import { isVideoRoom } from "../../utils/video-rooms";
|
||||||
import { SDKContext } from "../../contexts/SDKContext";
|
import { SDKContext } from "../../contexts/SDKContext";
|
||||||
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
|
||||||
import { type Call } from "../../models/Call";
|
|
||||||
import { RoomSearchView } from "./RoomSearchView";
|
import { RoomSearchView } from "./RoomSearchView";
|
||||||
import eventSearch, { type SearchInfo, SearchScope } from "../../Searching";
|
import eventSearch, { type SearchInfo, SearchScope } from "../../Searching";
|
||||||
import VoipUserMapper from "../../VoipUserMapper";
|
import VoipUserMapper from "../../VoipUserMapper";
|
||||||
@ -190,7 +188,6 @@ export interface IRoomState {
|
|||||||
*/
|
*/
|
||||||
search?: SearchInfo;
|
search?: SearchInfo;
|
||||||
callState?: CallState;
|
callState?: CallState;
|
||||||
activeCall: Call | null;
|
|
||||||
canPeek: boolean;
|
canPeek: boolean;
|
||||||
canSelfRedact: boolean;
|
canSelfRedact: boolean;
|
||||||
showApps: boolean;
|
showApps: boolean;
|
||||||
@ -401,7 +398,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
membersLoaded: !llMembers,
|
membersLoaded: !llMembers,
|
||||||
numUnreadMessages: 0,
|
numUnreadMessages: 0,
|
||||||
callState: undefined,
|
callState: undefined,
|
||||||
activeCall: null,
|
|
||||||
canPeek: false,
|
canPeek: false,
|
||||||
canSelfRedact: false,
|
canSelfRedact: false,
|
||||||
showApps: false,
|
showApps: false,
|
||||||
@ -577,7 +573,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
mainSplitContentType: room ? this.getMainSplitContentType(room) : undefined,
|
mainSplitContentType: room ? this.getMainSplitContentType(room) : undefined,
|
||||||
initialEventId: undefined, // default to clearing this, will get set later in the method if needed
|
initialEventId: undefined, // default to clearing this, will get set later in the method if needed
|
||||||
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
|
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
|
||||||
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
|
|
||||||
promptAskToJoin: this.context.roomViewStore.promptAskToJoin(),
|
promptAskToJoin: this.context.roomViewStore.promptAskToJoin(),
|
||||||
viewRoomOpts: this.context.roomViewStore.getViewRoomOpts(),
|
viewRoomOpts: this.context.roomViewStore.getViewRoomOpts(),
|
||||||
};
|
};
|
||||||
@ -727,11 +722,8 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onConnectedCalls = (): void => {
|
private onCallClose = (): void => {
|
||||||
if (this.state.roomId === undefined) return;
|
// Stop viewing the call
|
||||||
const activeCall = CallStore.instance.getActiveCall(this.state.roomId);
|
|
||||||
if (activeCall === null) {
|
|
||||||
// We disconnected from the call, so stop viewing it
|
|
||||||
defaultDispatcher.dispatch<ViewRoomPayload>(
|
defaultDispatcher.dispatch<ViewRoomPayload>(
|
||||||
{
|
{
|
||||||
action: Action.ViewRoom,
|
action: Action.ViewRoom,
|
||||||
@ -741,9 +733,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
},
|
},
|
||||||
true,
|
true,
|
||||||
); // Synchronous so that CallView disappears immediately
|
); // Synchronous so that CallView disappears immediately
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ activeCall });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private getRoomId = (): string | undefined => {
|
private getRoomId = (): string | undefined => {
|
||||||
@ -900,8 +889,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||||
this.context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
this.context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||||
|
|
||||||
CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
|
|
||||||
|
|
||||||
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
||||||
|
|
||||||
this.settingWatchers = [
|
this.settingWatchers = [
|
||||||
@ -1027,7 +1014,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
CallStore.instance.off(CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
|
|
||||||
this.context.legacyCallHandler.off(LegacyCallHandlerEvent.CallState, this.onCallState);
|
this.context.legacyCallHandler.off(LegacyCallHandlerEvent.CallState, this.onCallState);
|
||||||
|
|
||||||
// cancel any pending calls to the throttled updated
|
// cancel any pending calls to the throttled updated
|
||||||
@ -2562,9 +2548,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
|||||||
<CallView
|
<CallView
|
||||||
room={this.state.room}
|
room={this.state.room}
|
||||||
resizing={this.state.resizing}
|
resizing={this.state.resizing}
|
||||||
waitForCall={isVideoRoom(this.state.room)}
|
|
||||||
skipLobby={this.context.roomViewStore.skipCallLobby() ?? false}
|
skipLobby={this.context.roomViewStore.skipCallLobby() ?? false}
|
||||||
role="main"
|
role="main"
|
||||||
|
onClose={this.onCallClose}
|
||||||
/>
|
/>
|
||||||
{previewBar}
|
{previewBar}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@ -126,10 +126,6 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
|
|||||||
return [_t("action|leave"), "danger", disconnect];
|
return [_t("action|leave"), "danger", disconnect];
|
||||||
case ConnectionState.Disconnecting:
|
case ConnectionState.Disconnecting:
|
||||||
return [_t("action|leave"), "danger", null];
|
return [_t("action|leave"), "danger", null];
|
||||||
case ConnectionState.Connecting:
|
|
||||||
case ConnectionState.Lobby:
|
|
||||||
case ConnectionState.WidgetLoading:
|
|
||||||
return [_t("action|join"), "primary", null];
|
|
||||||
}
|
}
|
||||||
}, [connectionState, connect, disconnect]);
|
}, [connectionState, connect, disconnect]);
|
||||||
|
|
||||||
|
|||||||
@ -27,18 +27,6 @@ export const RoomTileCallSummary: FC<Props> = ({ call }) => {
|
|||||||
text = _t("common|video");
|
text = _t("common|video");
|
||||||
active = false;
|
active = false;
|
||||||
break;
|
break;
|
||||||
case ConnectionState.WidgetLoading:
|
|
||||||
text = _t("common|loading");
|
|
||||||
active = false;
|
|
||||||
break;
|
|
||||||
case ConnectionState.Lobby:
|
|
||||||
text = _t("common|lobby");
|
|
||||||
active = false;
|
|
||||||
break;
|
|
||||||
case ConnectionState.Connecting:
|
|
||||||
text = _t("room|joining");
|
|
||||||
active = true;
|
|
||||||
break;
|
|
||||||
case ConnectionState.Connected:
|
case ConnectionState.Connected:
|
||||||
case ConnectionState.Disconnecting:
|
case ConnectionState.Disconnecting:
|
||||||
text = _t("common|joined");
|
text = _t("common|joined");
|
||||||
|
|||||||
@ -9,12 +9,13 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
import React, { type FC, useContext, useEffect, type AriaRole, useCallback } from "react";
|
import React, { type FC, useContext, useEffect, type AriaRole, useCallback } from "react";
|
||||||
|
|
||||||
import type { Room } from "matrix-js-sdk/src/matrix";
|
import type { Room } from "matrix-js-sdk/src/matrix";
|
||||||
import { type Call, ConnectionState, ElementCall } from "../../../models/Call";
|
import { type Call, CallEvent } from "../../../models/Call";
|
||||||
import { useCall } from "../../../hooks/useCall";
|
|
||||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||||
import AppTile from "../elements/AppTile";
|
import AppTile from "../elements/AppTile";
|
||||||
import { CallStore } from "../../../stores/CallStore";
|
import { CallStore } from "../../../stores/CallStore";
|
||||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||||
|
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||||
|
import { useCall } from "../../../hooks/useCall";
|
||||||
|
|
||||||
interface JoinCallViewProps {
|
interface JoinCallViewProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
@ -22,10 +23,12 @@ interface JoinCallViewProps {
|
|||||||
call: Call;
|
call: Call;
|
||||||
skipLobby?: boolean;
|
skipLobby?: boolean;
|
||||||
role?: AriaRole;
|
role?: AriaRole;
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby, role }) => {
|
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby, role, onClose }) => {
|
||||||
const cli = useContext(MatrixClientContext);
|
const cli = useContext(MatrixClientContext);
|
||||||
|
useTypedEventEmitter(call, CallEvent.Close, onClose);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// We'll take this opportunity to tidy up our room state
|
// We'll take this opportunity to tidy up our room state
|
||||||
@ -38,17 +41,6 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
|
|||||||
call.widget.data.skipLobby = skipLobby;
|
call.widget.data.skipLobby = skipLobby;
|
||||||
}, [call.widget, skipLobby]);
|
}, [call.widget, skipLobby]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (call.connectionState === ConnectionState.Disconnected) {
|
|
||||||
// immediately start the call
|
|
||||||
// (this will start the lobby view in the widget and connect to all required widget events)
|
|
||||||
call.start();
|
|
||||||
}
|
|
||||||
return (): void => {
|
|
||||||
// If we are connected the widget is sticky and we do not want to destroy the call.
|
|
||||||
if (!call.connected) call.destroy();
|
|
||||||
};
|
|
||||||
}, [call]);
|
|
||||||
const disconnectAllOtherCalls: () => Promise<void> = useCallback(async () => {
|
const disconnectAllOtherCalls: () => Promise<void> = useCallback(async () => {
|
||||||
// The stickyPromise has to resolve before the widget actually becomes sticky.
|
// The stickyPromise has to resolve before the widget actually becomes sticky.
|
||||||
// We only let the widget become sticky after disconnecting all other active calls.
|
// We only let the widget become sticky after disconnecting all other active calls.
|
||||||
@ -57,6 +49,7 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
|
|||||||
);
|
);
|
||||||
await Promise.all(calls.map(async (call) => await call.disconnect()));
|
await Promise.all(calls.map(async (call) => await call.disconnect()));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx_CallView" role={role}>
|
<div className="mx_CallView" role={role}>
|
||||||
<AppTile
|
<AppTile
|
||||||
@ -76,26 +69,27 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
|
|||||||
interface CallViewProps {
|
interface CallViewProps {
|
||||||
room: Room;
|
room: Room;
|
||||||
resizing: boolean;
|
resizing: boolean;
|
||||||
/**
|
|
||||||
* If true, the view will be blank until a call appears. Otherwise, the join
|
|
||||||
* button will create a call if there isn't already one.
|
|
||||||
*/
|
|
||||||
waitForCall: boolean;
|
|
||||||
skipLobby?: boolean;
|
skipLobby?: boolean;
|
||||||
role?: AriaRole;
|
role?: AriaRole;
|
||||||
|
/**
|
||||||
|
* Callback for when the user closes the call.
|
||||||
|
*/
|
||||||
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CallView: FC<CallViewProps> = ({ room, resizing, waitForCall, skipLobby, role }) => {
|
export const CallView: FC<CallViewProps> = ({ room, resizing, skipLobby, role, onClose }) => {
|
||||||
const call = useCall(room.roomId);
|
const call = useCall(room.roomId);
|
||||||
|
|
||||||
useEffect(() => {
|
return (
|
||||||
if (call === null && !waitForCall) {
|
call && (
|
||||||
ElementCall.create(room, skipLobby);
|
<JoinCallView
|
||||||
}
|
room={room}
|
||||||
}, [call, room, skipLobby, waitForCall]);
|
resizing={resizing}
|
||||||
if (call === null) {
|
call={call}
|
||||||
return null;
|
skipLobby={skipLobby}
|
||||||
} else {
|
role={role}
|
||||||
return <JoinCallView room={room} resizing={resizing} call={call} skipLobby={skipLobby} role={role} />;
|
onClose={onClose}
|
||||||
}
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -70,7 +70,6 @@ const RoomContext = createContext<
|
|||||||
threadId: undefined,
|
threadId: undefined,
|
||||||
liveTimeline: undefined,
|
liveTimeline: undefined,
|
||||||
narrow: false,
|
narrow: false,
|
||||||
activeCall: null,
|
|
||||||
msc3946ProcessDynamicPredecessor: false,
|
msc3946ProcessDynamicPredecessor: false,
|
||||||
canAskToJoin: false,
|
canAskToJoin: false,
|
||||||
promptAskToJoin: false,
|
promptAskToJoin: false,
|
||||||
|
|||||||
@ -343,7 +343,7 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
|
|||||||
await client.setPowerLevel(roomId, client.getUserId()!, 100);
|
await client.setPowerLevel(roomId, client.getUserId()!, 100);
|
||||||
} else if (opts.roomType === RoomType.UnstableCall) {
|
} else if (opts.roomType === RoomType.UnstableCall) {
|
||||||
// Set up this video room with an Element call
|
// Set up this video room with an Element call
|
||||||
await ElementCall.create(await room);
|
ElementCall.create(await room);
|
||||||
|
|
||||||
// Reset our power level back to admin so that the call becomes immutable
|
// Reset our power level back to admin so that the call becomes immutable
|
||||||
await client.setPowerLevel(roomId, client.getUserId()!, 100);
|
await client.setPowerLevel(roomId, client.getUserId()!, 100);
|
||||||
|
|||||||
@ -75,9 +75,5 @@ export const useFull = (call: Call | null): boolean => {
|
|||||||
|
|
||||||
export const useJoinCallButtonDisabledTooltip = (call: Call | null): string | null => {
|
export const useJoinCallButtonDisabledTooltip = (call: Call | null): string | null => {
|
||||||
const isFull = useFull(call);
|
const isFull = useFull(call);
|
||||||
const state = useConnectionState(call);
|
return isFull ? _t("voip|join_button_tooltip_call_full") : null;
|
||||||
|
|
||||||
if (state === ConnectionState.Connecting) return _t("voip|join_button_tooltip_connecting");
|
|
||||||
if (isFull) return _t("voip|join_button_tooltip_call_full");
|
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -497,7 +497,6 @@
|
|||||||
"legal": "Legal",
|
"legal": "Legal",
|
||||||
"light": "Light",
|
"light": "Light",
|
||||||
"loading": "Loading…",
|
"loading": "Loading…",
|
||||||
"lobby": "Lobby",
|
|
||||||
"location": "Location",
|
"location": "Location",
|
||||||
"low_priority": "Low priority",
|
"low_priority": "Low priority",
|
||||||
"matrix": "Matrix",
|
"matrix": "Matrix",
|
||||||
@ -3909,7 +3908,6 @@
|
|||||||
"input_devices": "Input devices",
|
"input_devices": "Input devices",
|
||||||
"jitsi_call": "Jitsi Conference",
|
"jitsi_call": "Jitsi Conference",
|
||||||
"join_button_tooltip_call_full": "Sorry — this call is currently full",
|
"join_button_tooltip_call_full": "Sorry — this call is currently full",
|
||||||
"join_button_tooltip_connecting": "Connecting",
|
|
||||||
"legacy_call": "Legacy Call",
|
"legacy_call": "Legacy Call",
|
||||||
"maximise": "Fill screen",
|
"maximise": "Fill screen",
|
||||||
"maximise_call": "Maximise call",
|
"maximise_call": "Maximise call",
|
||||||
|
|||||||
@ -77,13 +77,7 @@ const waitForEvent = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export enum ConnectionState {
|
export enum ConnectionState {
|
||||||
// Widget related states that are equivalent to disconnected,
|
|
||||||
// but hold additional information about the state of the widget.
|
|
||||||
Lobby = "lobby",
|
|
||||||
WidgetLoading = "widget_loading",
|
|
||||||
Disconnected = "disconnected",
|
Disconnected = "disconnected",
|
||||||
|
|
||||||
Connecting = "connecting",
|
|
||||||
Connected = "connected",
|
Connected = "connected",
|
||||||
Disconnecting = "disconnecting",
|
Disconnecting = "disconnecting",
|
||||||
}
|
}
|
||||||
@ -100,6 +94,7 @@ export enum CallEvent {
|
|||||||
ConnectionState = "connection_state",
|
ConnectionState = "connection_state",
|
||||||
Participants = "participants",
|
Participants = "participants",
|
||||||
Layout = "layout",
|
Layout = "layout",
|
||||||
|
Close = "close",
|
||||||
Destroy = "destroy",
|
Destroy = "destroy",
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +105,7 @@ interface CallEventHandlerMap {
|
|||||||
prevParticipants: Map<RoomMember, Set<string>>,
|
prevParticipants: Map<RoomMember, Set<string>>,
|
||||||
) => void;
|
) => void;
|
||||||
[CallEvent.Layout]: (layout: Layout) => void;
|
[CallEvent.Layout]: (layout: Layout) => void;
|
||||||
|
[CallEvent.Close]: () => void;
|
||||||
[CallEvent.Destroy]: () => void;
|
[CallEvent.Destroy]: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -167,6 +163,17 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
|||||||
this.emit(CallEvent.Participants, value, prevValue);
|
this.emit(CallEvent.Participants, value, prevValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _presented = false;
|
||||||
|
/**
|
||||||
|
* Whether the call widget is currently being presented in the user interface.
|
||||||
|
*/
|
||||||
|
public get presented(): boolean {
|
||||||
|
return this._presented;
|
||||||
|
}
|
||||||
|
public set presented(value: boolean) {
|
||||||
|
this._presented = value;
|
||||||
|
}
|
||||||
|
|
||||||
protected constructor(
|
protected constructor(
|
||||||
/**
|
/**
|
||||||
* The widget used to access this call.
|
* The widget used to access this call.
|
||||||
@ -177,6 +184,7 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
|||||||
super();
|
super();
|
||||||
this.widgetUid = WidgetUtils.getWidgetUid(this.widget);
|
this.widgetUid = WidgetUtils.getWidgetUid(this.widget);
|
||||||
this.room = this.client.getRoom(this.roomId)!;
|
this.room = this.client.getRoom(this.roomId)!;
|
||||||
|
WidgetMessagingStore.instance.on(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -221,8 +229,6 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
|||||||
* Only call this if the call state is: ConnectionState.Disconnected.
|
* Only call this if the call state is: ConnectionState.Disconnected.
|
||||||
*/
|
*/
|
||||||
public async start(): Promise<void> {
|
public async start(): Promise<void> {
|
||||||
this.connectionState = ConnectionState.WidgetLoading;
|
|
||||||
|
|
||||||
const { [MediaDeviceKindEnum.AudioInput]: audioInputs, [MediaDeviceKindEnum.VideoInput]: videoInputs } =
|
const { [MediaDeviceKindEnum.AudioInput]: audioInputs, [MediaDeviceKindEnum.VideoInput]: videoInputs } =
|
||||||
(await MediaDeviceHandler.getDevices())!;
|
(await MediaDeviceHandler.getDevices())!;
|
||||||
|
|
||||||
@ -257,16 +263,9 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
|||||||
throw new Error(`Failed to bind call widget in room ${this.roomId}: ${e}`);
|
throw new Error(`Failed to bind call widget in room ${this.roomId}: ${e}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.connectionState = ConnectionState.Connecting;
|
|
||||||
try {
|
|
||||||
await this.performConnection(audioInput, videoInput);
|
await this.performConnection(audioInput, videoInput);
|
||||||
} catch (e) {
|
|
||||||
this.connectionState = ConnectionState.Disconnected;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
|
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
|
||||||
WidgetMessagingStore.instance.on(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
|
|
||||||
window.addEventListener("beforeunload", this.beforeUnload);
|
window.addEventListener("beforeunload", this.beforeUnload);
|
||||||
this.connectionState = ConnectionState.Connected;
|
this.connectionState = ConnectionState.Connected;
|
||||||
}
|
}
|
||||||
@ -280,39 +279,54 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
|||||||
this.connectionState = ConnectionState.Disconnecting;
|
this.connectionState = ConnectionState.Disconnecting;
|
||||||
await this.performDisconnection();
|
await this.performDisconnection();
|
||||||
this.setDisconnected();
|
this.setDisconnected();
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manually marks the call as disconnected and cleans up.
|
* Manually marks the call as disconnected.
|
||||||
*/
|
*/
|
||||||
public setDisconnected(): void {
|
public setDisconnected(): void {
|
||||||
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
|
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
|
||||||
WidgetMessagingStore.instance.off(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
|
|
||||||
window.removeEventListener("beforeunload", this.beforeUnload);
|
window.removeEventListener("beforeunload", this.beforeUnload);
|
||||||
this.messaging = null;
|
|
||||||
this.connectionState = ConnectionState.Disconnected;
|
this.connectionState = ConnectionState.Disconnected;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops further communication with the widget and tells the UI to close.
|
||||||
|
*/
|
||||||
|
protected close(): void {
|
||||||
|
this.messaging = null;
|
||||||
|
this.emit(CallEvent.Close);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stops all internal timers and tasks to prepare for garbage collection.
|
* Stops all internal timers and tasks to prepare for garbage collection.
|
||||||
*/
|
*/
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
if (this.connected) this.setDisconnected();
|
if (this.connected) {
|
||||||
|
this.setDisconnected();
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
WidgetMessagingStore.instance.off(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
|
||||||
this.emit(CallEvent.Destroy);
|
this.emit(CallEvent.Destroy);
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMyMembership = async (_room: Room, membership: Membership): Promise<void> => {
|
private readonly onMyMembership = async (_room: Room, membership: Membership): Promise<void> => {
|
||||||
if (membership !== KnownMembership.Join) this.setDisconnected();
|
if (membership !== KnownMembership.Join) this.setDisconnected();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onStopMessaging = (uid: string): void => {
|
private readonly onStopMessaging = (uid: string): void => {
|
||||||
if (uid === this.widgetUid) {
|
if (uid === this.widgetUid && this.connected) {
|
||||||
logger.log("The widget died; treating this as a user hangup");
|
logger.log("The widget died; treating this as a user hangup");
|
||||||
this.setDisconnected();
|
this.setDisconnected();
|
||||||
|
this.close();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private beforeUnload = (): void => this.setDisconnected();
|
private beforeUnload = (): void => {
|
||||||
|
this.setDisconnected();
|
||||||
|
this.close();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type { JitsiCallMemberContent };
|
export type { JitsiCallMemberContent };
|
||||||
@ -466,7 +480,6 @@ export class JitsiCall extends Call {
|
|||||||
audioInput: MediaDeviceInfo | null,
|
audioInput: MediaDeviceInfo | null,
|
||||||
videoInput: MediaDeviceInfo | null,
|
videoInput: MediaDeviceInfo | null,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
this.connectionState = ConnectionState.Lobby;
|
|
||||||
// Ensure that the messaging doesn't get stopped while we're waiting for responses
|
// Ensure that the messaging doesn't get stopped while we're waiting for responses
|
||||||
const dontStopMessaging = new Promise<void>((resolve, reject) => {
|
const dontStopMessaging = new Promise<void>((resolve, reject) => {
|
||||||
const messagingStore = WidgetMessagingStore.instance;
|
const messagingStore = WidgetMessagingStore.instance;
|
||||||
@ -569,9 +582,9 @@ export class JitsiCall extends Call {
|
|||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRoomState = (): void => this.updateParticipants();
|
private readonly onRoomState = (): void => this.updateParticipants();
|
||||||
|
|
||||||
private onConnectionState = async (state: ConnectionState, prevState: ConnectionState): Promise<void> => {
|
private readonly onConnectionState = async (state: ConnectionState, prevState: ConnectionState): Promise<void> => {
|
||||||
if (state === ConnectionState.Connected && !isConnected(prevState)) {
|
if (state === ConnectionState.Connected && !isConnected(prevState)) {
|
||||||
this.updateParticipants(); // Local echo
|
this.updateParticipants(); // Local echo
|
||||||
|
|
||||||
@ -597,18 +610,18 @@ export class JitsiCall extends Call {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onDock = async (): Promise<void> => {
|
private readonly onDock = async (): Promise<void> => {
|
||||||
// The widget is no longer a PiP, so let's restore the default layout
|
// The widget is no longer a PiP, so let's restore the default layout
|
||||||
await this.messaging!.transport.send(ElementWidgetActions.TileLayout, {});
|
await this.messaging!.transport.send(ElementWidgetActions.TileLayout, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onUndock = async (): Promise<void> => {
|
private readonly onUndock = async (): Promise<void> => {
|
||||||
// The widget has become a PiP, so let's switch Jitsi to spotlight mode
|
// The widget has become a PiP, so let's switch Jitsi to spotlight mode
|
||||||
// to only show the active speaker and economize on space
|
// to only show the active speaker and economize on space
|
||||||
await this.messaging!.transport.send(ElementWidgetActions.SpotlightLayout, {});
|
await this.messaging!.transport.send(ElementWidgetActions.SpotlightLayout, {});
|
||||||
};
|
};
|
||||||
|
|
||||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||||
// If we're already in the middle of a client-initiated disconnection,
|
// If we're already in the middle of a client-initiated disconnection,
|
||||||
// ignore the event
|
// ignore the event
|
||||||
if (this.connectionState === ConnectionState.Disconnecting) return;
|
if (this.connectionState === ConnectionState.Disconnecting) return;
|
||||||
@ -617,14 +630,15 @@ export class JitsiCall extends Call {
|
|||||||
|
|
||||||
// In case this hangup is caused by Jitsi Meet crashing at startup,
|
// In case this hangup is caused by Jitsi Meet crashing at startup,
|
||||||
// wait for the connection event in order to avoid racing
|
// wait for the connection event in order to avoid racing
|
||||||
if (this.connectionState === ConnectionState.Connecting) {
|
if (this.connectionState === ConnectionState.Disconnected) {
|
||||||
await waitForEvent(this, CallEvent.ConnectionState);
|
await waitForEvent(this, CallEvent.ConnectionState);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
this.setDisconnected();
|
this.setDisconnected();
|
||||||
|
this.close();
|
||||||
// In video rooms we immediately want to restart the call after hangup
|
// In video rooms we immediately want to restart the call after hangup
|
||||||
// The lobby will be shown again and it connects to all signals from EC and Jitsi.
|
// The lobby will be shown again and it connects to all signals from Jitsi.
|
||||||
if (isVideoRoom(this.room)) {
|
if (isVideoRoom(this.room)) {
|
||||||
this.start();
|
this.start();
|
||||||
}
|
}
|
||||||
@ -653,6 +667,14 @@ export class ElementCall extends Call {
|
|||||||
this.emit(CallEvent.Layout, value);
|
this.emit(CallEvent.Layout, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get presented(): boolean {
|
||||||
|
return super.presented;
|
||||||
|
}
|
||||||
|
public set presented(value: boolean) {
|
||||||
|
super.presented = value;
|
||||||
|
this.checkDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
private static generateWidgetUrl(client: MatrixClient, roomId: string): URL {
|
private static generateWidgetUrl(client: MatrixClient, roomId: string): URL {
|
||||||
const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE);
|
const accountAnalyticsData = client.getAccountData(PosthogAnalytics.ANALYTICS_EVENT_TYPE);
|
||||||
// The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget.
|
// The analyticsID is passed directly to element call (EC) since this codepath is only for EC and no other widget.
|
||||||
@ -740,7 +762,7 @@ export class ElementCall extends Call {
|
|||||||
// To use Element Call without touching room state, we create a virtual
|
// To use Element Call without touching room state, we create a virtual
|
||||||
// widget (one that doesn't have a corresponding state event)
|
// widget (one that doesn't have a corresponding state event)
|
||||||
const url = ElementCall.generateWidgetUrl(client, roomId);
|
const url = ElementCall.generateWidgetUrl(client, roomId);
|
||||||
return WidgetStore.instance.addVirtualWidget(
|
const createdWidget = WidgetStore.instance.addVirtualWidget(
|
||||||
{
|
{
|
||||||
id: secureRandomString(24), // So that it's globally unique
|
id: secureRandomString(24), // So that it's globally unique
|
||||||
creatorUserId: client.getUserId()!,
|
creatorUserId: client.getUserId()!,
|
||||||
@ -761,6 +783,8 @@ export class ElementCall extends Call {
|
|||||||
},
|
},
|
||||||
roomId,
|
roomId,
|
||||||
);
|
);
|
||||||
|
WidgetStore.instance.emit(UPDATE_EVENT, null);
|
||||||
|
return createdWidget;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static getWidgetData(
|
private static getWidgetData(
|
||||||
@ -794,7 +818,7 @@ export class ElementCall extends Call {
|
|||||||
super(widget, client);
|
super(widget, client);
|
||||||
|
|
||||||
this.session.on(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
|
this.session.on(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
|
||||||
this.client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
|
this.client.matrixRTC.on(MatrixRTCSessionManagerEvents.SessionEnded, this.checkDestroy);
|
||||||
SettingsStore.watchSetting(
|
SettingsStore.watchSetting(
|
||||||
"feature_disable_call_per_sender_encryption",
|
"feature_disable_call_per_sender_encryption",
|
||||||
null,
|
null,
|
||||||
@ -827,9 +851,8 @@ export class ElementCall extends Call {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async create(room: Room, skipLobby = false): Promise<void> {
|
public static create(room: Room, skipLobby = false): void {
|
||||||
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, false, isVideoRoom(room));
|
ElementCall.createOrGetCallWidget(room.roomId, room.client, skipLobby, false, isVideoRoom(room));
|
||||||
WidgetStore.instance.emit(UPDATE_EVENT, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async sendCallNotify(): Promise<void> {
|
protected async sendCallNotify(): Promise<void> {
|
||||||
@ -875,17 +898,9 @@ export class ElementCall extends Call {
|
|||||||
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.on(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||||
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, async (ev) => {
|
this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose);
|
||||||
ev.preventDefault();
|
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
|
||||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.widget.data?.skipLobby) {
|
|
||||||
// If we do not skip the lobby we need to wait until the widget has
|
|
||||||
// connected to matrixRTC. This is either observed through the session state
|
|
||||||
// or the MatrixRTCSessionManager session started event.
|
|
||||||
this.connectionState = ConnectionState.Lobby;
|
|
||||||
}
|
|
||||||
// TODO: if the widget informs us when the join button is clicked (widget action), so we can
|
// TODO: if the widget informs us when the join button is clicked (widget action), so we can
|
||||||
// - set state to connecting
|
// - set state to connecting
|
||||||
// - send call notify
|
// - send call notify
|
||||||
@ -927,15 +942,16 @@ export class ElementCall extends Call {
|
|||||||
public setDisconnected(): void {
|
public setDisconnected(): void {
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||||
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
this.messaging!.off(`action:${ElementWidgetActions.SpotlightLayout}`, this.onSpotlightLayout);
|
||||||
|
this.messaging!.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||||
|
this.messaging!.off(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
|
||||||
super.setDisconnected();
|
super.setDisconnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
public destroy(): void {
|
public destroy(): void {
|
||||||
ActiveWidgetStore.instance.destroyPersistentWidget(this.widget.id, this.widget.roomId);
|
ActiveWidgetStore.instance.destroyPersistentWidget(this.widget.id, this.widget.roomId);
|
||||||
WidgetStore.instance.removeVirtualWidget(this.widget.id, this.widget.roomId);
|
WidgetStore.instance.removeVirtualWidget(this.widget.id, this.widget.roomId);
|
||||||
this.messaging?.off(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
|
||||||
this.session.off(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
|
this.session.off(MatrixRTCSessionEvent.MembershipsChanged, this.onMembershipChanged);
|
||||||
this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
|
this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.checkDestroy);
|
||||||
|
|
||||||
SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher);
|
SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher);
|
||||||
clearTimeout(this.terminationTimer);
|
clearTimeout(this.terminationTimer);
|
||||||
@ -944,11 +960,10 @@ export class ElementCall extends Call {
|
|||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
private onRTCSessionEnded = (roomId: string, session: MatrixRTCSession): void => {
|
private checkDestroy = (): void => {
|
||||||
// Don't destroy the call on hangup for video call rooms.
|
// A call ceases to exist as soon as all participants leave and also the
|
||||||
if (roomId === this.roomId && !this.room.isCallRoom()) {
|
// user isn't looking at it (for example, waiting in an empty lobby)
|
||||||
this.destroy();
|
if (this.session.memberships.length === 0 && !this.presented && !this.room.isCallRoom()) this.destroy();
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -960,7 +975,7 @@ export class ElementCall extends Call {
|
|||||||
await this.messaging!.transport.send(action, {});
|
await this.messaging!.transport.send(action, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
private onMembershipChanged = (): void => this.updateParticipants();
|
private readonly onMembershipChanged = (): void => this.updateParticipants();
|
||||||
|
|
||||||
private updateParticipants(): void {
|
private updateParticipants(): void {
|
||||||
const participants = new Map<RoomMember, Set<string>>();
|
const participants = new Map<RoomMember, Set<string>>();
|
||||||
@ -980,9 +995,14 @@ export class ElementCall extends Call {
|
|||||||
this.participants = participants;
|
this.participants = participants;
|
||||||
}
|
}
|
||||||
|
|
||||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
private readonly onDeviceMute = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
this.setDisconnected();
|
this.setDisconnected();
|
||||||
// In video rooms we immediately want to reconnect after hangup
|
// In video rooms we immediately want to reconnect after hangup
|
||||||
// This starts the lobby again and connects to all signals from EC.
|
// This starts the lobby again and connects to all signals from EC.
|
||||||
@ -991,16 +1011,23 @@ export class ElementCall extends Call {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
private onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
private readonly onClose = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.layout = Layout.Tile;
|
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
// User is done with the call; tell the UI to close it
|
||||||
|
this.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
private readonly onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.layout = Layout.Tile;
|
||||||
|
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
|
};
|
||||||
|
|
||||||
|
private readonly onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.layout = Layout.Spotlight;
|
this.layout = Layout.Spotlight;
|
||||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||||
};
|
};
|
||||||
|
|
||||||
public clean(): Promise<void> {
|
public clean(): Promise<void> {
|
||||||
|
|||||||
@ -50,6 +50,8 @@ import { type CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJ
|
|||||||
import { type SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
|
import { type SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||||
import { ModuleRunner } from "../modules/ModuleRunner";
|
import { ModuleRunner } from "../modules/ModuleRunner";
|
||||||
import { setMarkedUnreadState } from "../utils/notifications";
|
import { setMarkedUnreadState } from "../utils/notifications";
|
||||||
|
import { ConnectionState, ElementCall } from "../models/Call";
|
||||||
|
import { isVideoRoom } from "../utils/video-rooms";
|
||||||
|
|
||||||
const NUM_JOIN_RETRY = 5;
|
const NUM_JOIN_RETRY = 5;
|
||||||
|
|
||||||
@ -353,6 +355,23 @@ export class RoomViewStore extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (room && (payload.view_call || isVideoRoom(room))) {
|
||||||
|
let call = CallStore.instance.getCall(payload.room_id);
|
||||||
|
// Start a call if not already there
|
||||||
|
if (call === null) {
|
||||||
|
ElementCall.create(room, false);
|
||||||
|
call = CallStore.instance.getCall(payload.room_id)!;
|
||||||
|
}
|
||||||
|
call.presented = true;
|
||||||
|
// Immediately start the call. This will connect to all required widget events
|
||||||
|
// and allow the widget to show the lobby.
|
||||||
|
if (call.connectionState === ConnectionState.Disconnected) call.start();
|
||||||
|
}
|
||||||
|
// If we switch to a different room from the call, we are no longer presenting it
|
||||||
|
const prevRoomCall = this.state.roomId ? CallStore.instance.getCall(this.state.roomId) : null;
|
||||||
|
if (prevRoomCall !== null && (!payload.view_call || payload.room_id !== this.state.roomId))
|
||||||
|
prevRoomCall.presented = false;
|
||||||
|
|
||||||
if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) {
|
if (SettingsStore.getValue("feature_sliding_sync") && this.state.roomId !== payload.room_id) {
|
||||||
if (this.state.subscribingRoomId && this.state.subscribingRoomId !== payload.room_id) {
|
if (this.state.subscribingRoomId && this.state.subscribingRoomId !== payload.room_id) {
|
||||||
// unsubscribe from this room, but don't await it as we don't care when this gets done.
|
// unsubscribe from this room, but don't await it as we don't care when this gets done.
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export enum ElementWidgetActions {
|
|||||||
// All of these actions are currently specific to Jitsi and Element Call
|
// All of these actions are currently specific to Jitsi and Element Call
|
||||||
JoinCall = "io.element.join",
|
JoinCall = "io.element.join",
|
||||||
HangupCall = "im.vector.hangup",
|
HangupCall = "im.vector.hangup",
|
||||||
|
Close = "io.element.close",
|
||||||
CallParticipants = "io.element.participants",
|
CallParticipants = "io.element.participants",
|
||||||
StartLiveStream = "im.vector.start_live_stream",
|
StartLiveStream = "im.vector.start_live_stream",
|
||||||
|
|
||||||
|
|||||||
@ -72,9 +72,12 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<EmptyObject> {
|
|||||||
* @param {string} widgetUid The widget UID.
|
* @param {string} widgetUid The widget UID.
|
||||||
*/
|
*/
|
||||||
public stopMessagingByUid(widgetUid: string): void {
|
public stopMessagingByUid(widgetUid: string): void {
|
||||||
this.widgetMap.remove(widgetUid)?.stop();
|
const messaging = this.widgetMap.remove(widgetUid);
|
||||||
|
if (messaging !== undefined) {
|
||||||
|
messaging.stop();
|
||||||
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
|
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the widget messaging class for a given widget UID.
|
* Gets the widget messaging class for a given widget UID.
|
||||||
|
|||||||
@ -109,5 +109,5 @@ export class MockedCall extends Call {
|
|||||||
export const useMockedCalls = () => {
|
export const useMockedCalls = () => {
|
||||||
Call.get = (room) => MockedCall.get(room);
|
Call.get = (room) => MockedCall.get(room);
|
||||||
JitsiCall.create = async (room) => MockedCall.create(room, "1");
|
JitsiCall.create = async (room) => MockedCall.create(room, "1");
|
||||||
ElementCall.create = async (room) => MockedCall.create(room, "1");
|
ElementCall.create = (room) => MockedCall.create(room, "1");
|
||||||
};
|
};
|
||||||
|
|||||||
@ -80,7 +80,6 @@ export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoom
|
|||||||
canSelfRedact: false,
|
canSelfRedact: false,
|
||||||
resizing: false,
|
resizing: false,
|
||||||
narrow: false,
|
narrow: false,
|
||||||
activeCall: null,
|
|
||||||
msc3946ProcessDynamicPredecessor: false,
|
msc3946ProcessDynamicPredecessor: false,
|
||||||
canAskToJoin: false,
|
canAskToJoin: false,
|
||||||
promptAskToJoin: false,
|
promptAskToJoin: false,
|
||||||
|
|||||||
@ -76,6 +76,15 @@ import { SearchScope } from "../../../../src/Searching";
|
|||||||
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto";
|
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto";
|
||||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||||
import { type ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload.ts";
|
import { type ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload.ts";
|
||||||
|
import { CallStore } from "../../../../src/stores/CallStore.ts";
|
||||||
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler.ts";
|
||||||
|
|
||||||
|
// Used by group calls
|
||||||
|
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||||
|
[MediaDeviceKindEnum.AudioInput]: [],
|
||||||
|
[MediaDeviceKindEnum.VideoInput]: [],
|
||||||
|
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||||
|
});
|
||||||
|
|
||||||
describe("RoomView", () => {
|
describe("RoomView", () => {
|
||||||
let cli: MockedObject<MatrixClient>;
|
let cli: MockedObject<MatrixClient>;
|
||||||
@ -98,6 +107,7 @@ describe("RoomView", () => {
|
|||||||
rooms = new Map();
|
rooms = new Map();
|
||||||
rooms.set(room.roomId, room);
|
rooms.set(room.roomId, room);
|
||||||
cli.getRoom.mockImplementation((roomId: string | undefined) => rooms.get(roomId || "") || null);
|
cli.getRoom.mockImplementation((roomId: string | undefined) => rooms.get(roomId || "") || null);
|
||||||
|
cli.getRooms.mockImplementation(() => [...rooms.values()]);
|
||||||
// Re-emit certain events on the mocked client
|
// Re-emit certain events on the mocked client
|
||||||
room.on(RoomEvent.Timeline, (...args) => cli.emit(RoomEvent.Timeline, ...args));
|
room.on(RoomEvent.Timeline, (...args) => cli.emit(RoomEvent.Timeline, ...args));
|
||||||
room.on(RoomEvent.TimelineReset, (...args) => cli.emit(RoomEvent.TimelineReset, ...args));
|
room.on(RoomEvent.TimelineReset, (...args) => cli.emit(RoomEvent.TimelineReset, ...args));
|
||||||
@ -371,6 +381,7 @@ describe("RoomView", () => {
|
|||||||
|
|
||||||
describe("video rooms", () => {
|
describe("video rooms", () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
|
await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet());
|
||||||
// Make it a video room
|
// Make it a video room
|
||||||
room.isElementVideoRoom = () => true;
|
room.isElementVideoRoom = () => true;
|
||||||
await SettingsStore.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
|
await SettingsStore.setValue("feature_video_rooms", null, SettingLevel.DEVICE, true);
|
||||||
|
|||||||
@ -2006,6 +2006,41 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<div
|
||||||
|
class="mx_CallView"
|
||||||
|
role="main"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AppTile"
|
||||||
|
id="vY7Q4uEh9K38QgU2PomxwKpa"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AppTileBody mx_AppTileBody--large mx_AppTileBody--loading mx_AppTileBody--call"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_AppTileBody_fadeInSpinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Spinner"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_Spinner_Msg"
|
||||||
|
>
|
||||||
|
Loading…
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
aria-label="Loading…"
|
||||||
|
class="mx_Spinner_icon"
|
||||||
|
data-testid="spinner"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 32px; height: 32px;"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mx_RightPanel_ResizeWrapper"
|
class="mx_RightPanel_ResizeWrapper"
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
} from "jest-matrix-react";
|
} from "jest-matrix-react";
|
||||||
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||||
import { mocked } from "jest-mock";
|
import { mocked } from "jest-mock";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
import { filterConsole, stubClient } from "../../../../../test-utils";
|
import { filterConsole, stubClient } from "../../../../../test-utils";
|
||||||
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
||||||
@ -106,13 +107,15 @@ describe("RoomHeader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("opens the room summary", async () => {
|
it("opens the room summary", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
const { container } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
|
||||||
fireEvent.click(getByText(container, ROOM_ID));
|
await user.click(getByText(container, ROOM_ID));
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("shows a face pile for rooms", async () => {
|
it("shows a face pile for rooms", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
const members = [
|
const members = [
|
||||||
{
|
{
|
||||||
userId: "@me:example.org",
|
userId: "@me:example.org",
|
||||||
@ -161,33 +164,36 @@ describe("RoomHeader", () => {
|
|||||||
const facePile = getByLabelText(document.body, "4 members");
|
const facePile = getByLabelText(document.body, "4 members");
|
||||||
expect(facePile).toHaveTextContent("4");
|
expect(facePile).toHaveTextContent("4");
|
||||||
|
|
||||||
fireEvent.click(facePile);
|
await user.click(facePile);
|
||||||
|
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("has room info icon that opens the room info panel", async () => {
|
it("has room info icon that opens the room info panel", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
const { getAllByRole } = render(<RoomHeader room={room} />, getWrapper());
|
const { getAllByRole } = render(<RoomHeader room={room} />, getWrapper());
|
||||||
const infoButton = getAllByRole("button", { name: "Room info" })[1];
|
const infoButton = getAllByRole("button", { name: "Room info" })[1];
|
||||||
fireEvent.click(infoButton);
|
await user.click(infoButton);
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens the thread panel", async () => {
|
it("opens the thread panel", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
render(<RoomHeader room={room} />, getWrapper());
|
render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
|
||||||
fireEvent.click(getByLabelText(document.body, "Threads"));
|
await user.click(getByLabelText(document.body, "Threads"));
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("opens the notifications panel", async () => {
|
it("opens the notifications panel", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
|
||||||
if (name === "feature_notifications") return true;
|
if (name === "feature_notifications") return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
render(<RoomHeader room={room} />, getWrapper());
|
render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
|
||||||
fireEvent.click(getByLabelText(document.body, "Notifications"));
|
await user.click(getByLabelText(document.body, "Notifications"));
|
||||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
|
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -274,6 +280,7 @@ describe("RoomHeader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("you can call when you're two in the room", async () => {
|
it("you can call when you're two in the room", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
mockRoomMembers(room, 2);
|
mockRoomMembers(room, 2);
|
||||||
render(<RoomHeader room={room} />, getWrapper());
|
render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
|
||||||
@ -284,10 +291,10 @@ describe("RoomHeader", () => {
|
|||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
||||||
|
|
||||||
fireEvent.click(voiceButton);
|
await user.click(voiceButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
fireEvent.click(videoButton);
|
await user.click(videoButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -332,6 +339,7 @@ describe("RoomHeader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders only the video call element", async () => {
|
it("renders only the video call element", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
mockRoomMembers(room, 3);
|
mockRoomMembers(room, 3);
|
||||||
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
||||||
// allow element calls
|
// allow element calls
|
||||||
@ -344,9 +352,9 @@ describe("RoomHeader", () => {
|
|||||||
const videoCallButton = screen.getByRole("button", { name: "Video call" });
|
const videoCallButton = screen.getByRole("button", { name: "Video call" });
|
||||||
expect(videoCallButton).not.toHaveAttribute("aria-disabled", "true");
|
expect(videoCallButton).not.toHaveAttribute("aria-disabled", "true");
|
||||||
|
|
||||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
|
||||||
|
|
||||||
fireEvent.click(videoCallButton);
|
await user.click(videoCallButton);
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -366,7 +374,8 @@ describe("RoomHeader", () => {
|
|||||||
expect(screen.getByRole("button", { name: "Ongoing call" })).toHaveAttribute("aria-disabled", "true");
|
expect(screen.getByRole("button", { name: "Ongoing call" })).toHaveAttribute("aria-disabled", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("clicking on ongoing (unpinned) call re-pins it", () => {
|
it("clicking on ongoing (unpinned) call re-pins it", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
mockRoomMembers(room, 3);
|
mockRoomMembers(room, 3);
|
||||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature == UIFeature.Widgets);
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature == UIFeature.Widgets);
|
||||||
// allow calls
|
// allow calls
|
||||||
@ -386,7 +395,7 @@ describe("RoomHeader", () => {
|
|||||||
|
|
||||||
const videoButton = screen.getByRole("button", { name: "Video call" });
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
||||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||||
fireEvent.click(videoButton);
|
await user.click(videoButton);
|
||||||
expect(spy).toHaveBeenCalledWith(room, widget, Container.Top);
|
expect(spy).toHaveBeenCalledWith(room, widget, Container.Top);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -463,6 +472,7 @@ describe("RoomHeader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("calls using legacy or jitsi", async () => {
|
it("calls using legacy or jitsi", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
mockRoomMembers(room, 2);
|
mockRoomMembers(room, 2);
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||||
if (key === "im.vector.modular.widgets") return true;
|
if (key === "im.vector.modular.widgets") return true;
|
||||||
@ -476,14 +486,15 @@ describe("RoomHeader", () => {
|
|||||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
||||||
fireEvent.click(voiceButton);
|
await user.click(voiceButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
||||||
|
|
||||||
fireEvent.click(videoButton);
|
await user.click(videoButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls using legacy or jitsi for large rooms", async () => {
|
it("calls using legacy or jitsi for large rooms", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
mockRoomMembers(room, 3);
|
mockRoomMembers(room, 3);
|
||||||
|
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||||
@ -497,11 +508,12 @@ describe("RoomHeader", () => {
|
|||||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||||
|
|
||||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
||||||
fireEvent.click(videoButton);
|
await user.click(videoButton);
|
||||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls using element call for large rooms", async () => {
|
it("calls using element call for large rooms", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
mockRoomMembers(room, 3);
|
mockRoomMembers(room, 3);
|
||||||
|
|
||||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||||
@ -514,8 +526,8 @@ describe("RoomHeader", () => {
|
|||||||
const videoButton = screen.getByRole("button", { name: "Video call" });
|
const videoButton = screen.getByRole("button", { name: "Video call" });
|
||||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||||
|
|
||||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
|
||||||
fireEvent.click(videoButton);
|
await user.click(videoButton);
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true }));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -750,10 +762,11 @@ describe("RoomHeader", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should open room settings when clicking the room avatar", async () => {
|
it("should open room settings when clicking the room avatar", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
render(<RoomHeader room={room} />, getWrapper());
|
render(<RoomHeader room={room} />, getWrapper());
|
||||||
|
|
||||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
||||||
fireEvent.click(getByLabelText(document.body, "Open room settings"));
|
await user.click(getByLabelText(document.body, "Open room settings"));
|
||||||
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ action: "open_room_settings" }));
|
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ action: "open_room_settings" }));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -43,7 +43,8 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-labelledby=":r16c:"
|
aria-disabled="true"
|
||||||
|
aria-label="There's no one here to call"
|
||||||
class="_icon-button_m2erp_8"
|
class="_icon-button_m2erp_8"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 32px;"
|
style="--cpd-icon-button-size: 32px;"
|
||||||
@ -51,9 +52,10 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="_indicator-icon_zr2a0_17"
|
class="_indicator-icon_zr2a0_17"
|
||||||
style="--cpd-icon-button-size: 100%;"
|
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
aria-labelledby=":r166:"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
height="1em"
|
height="1em"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@ -61,7 +63,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
|||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
>
|
>
|
||||||
<path
|
<path
|
||||||
d="M6.293 6.293a1 1 0 0 1 1.414 0L12 10.586l4.293-4.293a1 1 0 1 1 1.414 1.414L13.414 12l4.293 4.293a1 1 0 0 1-1.414 1.414L12 13.414l-4.293 4.293a1 1 0 0 1-1.414-1.414L10.586 12 6.293 7.707a1 1 0 0 1 0-1.414"
|
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4"
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
@ -69,7 +71,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
|||||||
<button
|
<button
|
||||||
aria-disabled="true"
|
aria-disabled="true"
|
||||||
aria-label="There's no one here to call"
|
aria-label="There's no one here to call"
|
||||||
aria-labelledby=":r16h:"
|
aria-labelledby=":r16b:"
|
||||||
class="_icon-button_m2erp_8"
|
class="_icon-button_m2erp_8"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 32px;"
|
style="--cpd-icon-button-size: 32px;"
|
||||||
@ -94,7 +96,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-label="Threads"
|
aria-label="Threads"
|
||||||
aria-labelledby=":r16m:"
|
aria-labelledby=":r16g:"
|
||||||
class="_icon-button_m2erp_8"
|
class="_icon-button_m2erp_8"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 32px;"
|
style="--cpd-icon-button-size: 32px;"
|
||||||
@ -120,7 +122,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
aria-label="Room info"
|
aria-label="Room info"
|
||||||
aria-labelledby=":r16r:"
|
aria-labelledby=":r16l:"
|
||||||
class="_icon-button_m2erp_8"
|
class="_icon-button_m2erp_8"
|
||||||
role="button"
|
role="button"
|
||||||
style="--cpd-icon-button-size: 32px;"
|
style="--cpd-icon-button-size: 32px;"
|
||||||
|
|||||||
@ -46,7 +46,6 @@ import { UIComponent } from "../../../../../src/settings/UIFeature";
|
|||||||
import { MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore";
|
import { MessagePreviewStore } from "../../../../../src/stores/room-list/MessagePreviewStore";
|
||||||
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||||
import { ConnectionState } from "../../../../../src/models/Call";
|
|
||||||
|
|
||||||
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||||
shouldShowComponent: jest.fn(),
|
shouldShowComponent: jest.fn(),
|
||||||
@ -216,41 +215,10 @@ describe("RoomTile", () => {
|
|||||||
it("tracks connection state", async () => {
|
it("tracks connection state", async () => {
|
||||||
renderRoomTile();
|
renderRoomTile();
|
||||||
screen.getByText("Video");
|
screen.getByText("Video");
|
||||||
|
await act(() => call.start());
|
||||||
let completeWidgetLoading: () => void = () => {};
|
screen.getByText("Joined");
|
||||||
const widgetLoadingCompleted = new Promise<void>((resolve) => (completeWidgetLoading = resolve));
|
await act(() => call.disconnect());
|
||||||
|
screen.getByText("Video");
|
||||||
// Insert an await point in the connection method so we can inspect
|
|
||||||
// the intermediate connecting state
|
|
||||||
let completeConnection: () => void = () => {};
|
|
||||||
const connectionCompleted = new Promise<void>((resolve) => (completeConnection = resolve));
|
|
||||||
|
|
||||||
let completeLobby: () => void = () => {};
|
|
||||||
const lobbyCompleted = new Promise<void>((resolve) => (completeLobby = resolve));
|
|
||||||
|
|
||||||
jest.spyOn(call, "performConnection").mockImplementation(async () => {
|
|
||||||
call.setConnectionState(ConnectionState.WidgetLoading);
|
|
||||||
await widgetLoadingCompleted;
|
|
||||||
call.setConnectionState(ConnectionState.Lobby);
|
|
||||||
await lobbyCompleted;
|
|
||||||
call.setConnectionState(ConnectionState.Connecting);
|
|
||||||
await connectionCompleted;
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all([
|
|
||||||
(async () => {
|
|
||||||
await screen.findByText("Loading…");
|
|
||||||
completeWidgetLoading();
|
|
||||||
await screen.findByText("Lobby");
|
|
||||||
completeLobby();
|
|
||||||
await screen.findByText("Joining…");
|
|
||||||
completeConnection();
|
|
||||||
await screen.findByText("Joined");
|
|
||||||
})(),
|
|
||||||
call.start(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await Promise.all([screen.findByText("Video"), call.disconnect()]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("tracks participants", () => {
|
it("tracks participants", () => {
|
||||||
|
|||||||
@ -73,7 +73,6 @@ describe("<SendMessageComposer/>", () => {
|
|||||||
canSelfRedact: false,
|
canSelfRedact: false,
|
||||||
resizing: false,
|
resizing: false,
|
||||||
narrow: false,
|
narrow: false,
|
||||||
activeCall: null,
|
|
||||||
msc3946ProcessDynamicPredecessor: false,
|
msc3946ProcessDynamicPredecessor: false,
|
||||||
canAskToJoin: false,
|
canAskToJoin: false,
|
||||||
promptAskToJoin: false,
|
promptAskToJoin: false,
|
||||||
|
|||||||
@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { zip } from "lodash";
|
import { render, screen, act, cleanup } from "jest-matrix-react";
|
||||||
import { render, screen, act, fireEvent, waitFor, cleanup } from "jest-matrix-react";
|
|
||||||
import { mocked, type Mocked } from "jest-mock";
|
import { mocked, type Mocked } from "jest-mock";
|
||||||
import {
|
import {
|
||||||
type MatrixClient,
|
type MatrixClient,
|
||||||
@ -33,7 +32,6 @@ import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
|||||||
import { CallView as _CallView } from "../../../../../src/components/views/voip/CallView";
|
import { CallView as _CallView } from "../../../../../src/components/views/voip/CallView";
|
||||||
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
||||||
import { CallStore } from "../../../../../src/stores/CallStore";
|
import { CallStore } from "../../../../../src/stores/CallStore";
|
||||||
import { Call, ConnectionState } from "../../../../../src/models/Call";
|
|
||||||
|
|
||||||
const CallView = wrapInMatrixClientContext(_CallView);
|
const CallView = wrapInMatrixClientContext(_CallView);
|
||||||
|
|
||||||
@ -44,6 +42,8 @@ describe("CallView", () => {
|
|||||||
let client: Mocked<MatrixClient>;
|
let client: Mocked<MatrixClient>;
|
||||||
let room: Room;
|
let room: Room;
|
||||||
let alice: RoomMember;
|
let alice: RoomMember;
|
||||||
|
let call: MockedCall;
|
||||||
|
let widget: Widget;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
useMockMediaDevices();
|
useMockMediaDevices();
|
||||||
@ -63,22 +63,7 @@ describe("CallView", () => {
|
|||||||
|
|
||||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
|
||||||
});
|
|
||||||
|
|
||||||
const renderView = async (skipLobby = false, role: string | undefined = undefined): Promise<void> => {
|
|
||||||
render(<CallView room={room} resizing={false} waitForCall={false} skipLobby={skipLobby} role={role} />);
|
|
||||||
await act(() => Promise.resolve()); // Let effects settle
|
|
||||||
};
|
|
||||||
|
|
||||||
describe("with an existing call", () => {
|
|
||||||
let call: MockedCall;
|
|
||||||
let widget: Widget;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
MockedCall.create(room, "1");
|
MockedCall.create(room, "1");
|
||||||
const maybeCall = CallStore.instance.getCall(room.roomId);
|
const maybeCall = CallStore.instance.getCall(room.roomId);
|
||||||
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
|
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
|
||||||
@ -94,8 +79,14 @@ describe("CallView", () => {
|
|||||||
cleanup(); // Unmount before we do any cleanup that might update the component
|
cleanup(); // Unmount before we do any cleanup that might update the component
|
||||||
call.destroy();
|
call.destroy();
|
||||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||||
|
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const renderView = async (skipLobby = false, role: string | undefined = undefined): Promise<void> => {
|
||||||
|
render(<CallView room={room} resizing={false} skipLobby={skipLobby} role={role} onClose={() => {}} />);
|
||||||
|
await act(() => Promise.resolve()); // Let effects settle
|
||||||
|
};
|
||||||
|
|
||||||
it("accepts an accessibility role", async () => {
|
it("accepts an accessibility role", async () => {
|
||||||
await renderView(undefined, "main");
|
await renderView(undefined, "main");
|
||||||
screen.getByRole("main");
|
screen.getByRole("main");
|
||||||
@ -107,73 +98,8 @@ describe("CallView", () => {
|
|||||||
expect(cleanSpy).toHaveBeenCalled();
|
expect(cleanSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
it("updates the call's skipLobby parameter", async () => {
|
||||||
* TODO: Fix I do not understand this test
|
|
||||||
*/
|
|
||||||
it.skip("tracks participants", async () => {
|
|
||||||
const bob = mkRoomMember(room.roomId, "@bob:example.org");
|
|
||||||
const carol = mkRoomMember(room.roomId, "@carol:example.org");
|
|
||||||
|
|
||||||
const expectAvatars = (userIds: string[]) => {
|
|
||||||
const avatars = screen.queryAllByRole("button", { name: "Profile picture" });
|
|
||||||
expect(userIds.length).toBe(avatars.length);
|
|
||||||
|
|
||||||
for (const [userId, avatar] of zip(userIds, avatars)) {
|
|
||||||
fireEvent.focus(avatar!);
|
|
||||||
screen.getAllByRole("tooltip", { name: userId });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
await renderView();
|
|
||||||
expect(screen.queryByLabelText(/joined/)).toBe(null);
|
|
||||||
expectAvatars([]);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
call.participants = new Map([[alice, new Set(["a"])]]);
|
|
||||||
});
|
|
||||||
screen.getByText("1 person joined");
|
|
||||||
expectAvatars([alice.userId]);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
call.participants = new Map([
|
|
||||||
[alice, new Set(["a"])],
|
|
||||||
[bob, new Set(["b1", "b2"])],
|
|
||||||
[carol, new Set(["c"])],
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
screen.getByText("4 people joined");
|
|
||||||
expectAvatars([alice.userId, bob.userId, bob.userId, carol.userId]);
|
|
||||||
|
|
||||||
act(() => {
|
|
||||||
call.participants = new Map();
|
|
||||||
});
|
|
||||||
expect(screen.queryByLabelText(/joined/)).toBe(null);
|
|
||||||
expectAvatars([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("automatically connects to the call when skipLobby is true", async () => {
|
|
||||||
const connectSpy = jest.spyOn(call, "start");
|
|
||||||
await renderView(true);
|
await renderView(true);
|
||||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled(), { interval: 1 });
|
expect(call.widget.data?.skipLobby).toBe(true);
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("without an existing call", () => {
|
|
||||||
it("creates and connects to a new call when the join button is pressed", async () => {
|
|
||||||
expect(Call.get(room)).toBeNull();
|
|
||||||
await renderView(true);
|
|
||||||
await waitFor(() => expect(CallStore.instance.getCall(room.roomId)).not.toBeNull());
|
|
||||||
const call = CallStore.instance.getCall(room.roomId)!;
|
|
||||||
|
|
||||||
const widget = new Widget(call.widget);
|
|
||||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
|
||||||
stop: () => {},
|
|
||||||
} as unknown as ClientWidgetApi);
|
|
||||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected));
|
|
||||||
|
|
||||||
cleanup(); // Unmount before we do any cleanup that might update the component
|
|
||||||
call.destroy();
|
|
||||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -235,16 +235,16 @@ describe("JitsiCall", () => {
|
|||||||
|
|
||||||
({ widget, messaging, audioMutedSpy, videoMutedSpy } = setUpWidget(call));
|
({ widget, messaging, audioMutedSpy, videoMutedSpy } = setUpWidget(call));
|
||||||
|
|
||||||
mocked(messaging.transport).send.mockImplementation(async (action: string): Promise<any> => {
|
mocked(messaging.transport).send.mockImplementation(async (action, data): Promise<any> => {
|
||||||
if (action === ElementWidgetActions.JoinCall) {
|
if (action === ElementWidgetActions.JoinCall) {
|
||||||
messaging.emit(
|
messaging.emit(
|
||||||
`action:${ElementWidgetActions.JoinCall}`,
|
`action:${ElementWidgetActions.JoinCall}`,
|
||||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
new CustomEvent("widgetapirequest", { detail: { data } }),
|
||||||
);
|
);
|
||||||
} else if (action === ElementWidgetActions.HangupCall) {
|
} else if (action === ElementWidgetActions.HangupCall) {
|
||||||
messaging.emit(
|
messaging.emit(
|
||||||
`action:${ElementWidgetActions.HangupCall}`,
|
`action:${ElementWidgetActions.HangupCall}`,
|
||||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
new CustomEvent("widgetapirequest", { detail: { data } }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
@ -286,8 +286,6 @@ describe("JitsiCall", () => {
|
|||||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||||
|
|
||||||
const connect = call.start();
|
const connect = call.start();
|
||||||
expect(call.connectionState).toBe(ConnectionState.WidgetLoading);
|
|
||||||
|
|
||||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||||
await connect;
|
await connect;
|
||||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||||
@ -310,7 +308,6 @@ describe("JitsiCall", () => {
|
|||||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||||
|
|
||||||
const connect = call.start();
|
const connect = call.start();
|
||||||
expect(call.connectionState).toBe(ConnectionState.WidgetLoading);
|
|
||||||
async function runTimers() {
|
async function runTimers() {
|
||||||
jest.advanceTimersByTime(500);
|
jest.advanceTimersByTime(500);
|
||||||
jest.advanceTimersByTime(1000);
|
jest.advanceTimersByTime(1000);
|
||||||
@ -356,18 +353,10 @@ describe("JitsiCall", () => {
|
|||||||
|
|
||||||
call.on(CallEvent.ConnectionState, callback);
|
call.on(CallEvent.ConnectionState, callback);
|
||||||
|
|
||||||
messaging.emit(
|
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||||
`action:${ElementWidgetActions.HangupCall}`,
|
messaging.emit(`action:${ElementWidgetActions.Close}`, new CustomEvent("widgetapirequest", {}));
|
||||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
|
||||||
);
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(callback).toHaveBeenNthCalledWith(1, ConnectionState.Disconnected, ConnectionState.Connected);
|
expect(callback).toHaveBeenNthCalledWith(1, ConnectionState.Disconnected, ConnectionState.Connected);
|
||||||
expect(callback).toHaveBeenNthCalledWith(
|
|
||||||
2,
|
|
||||||
ConnectionState.WidgetLoading,
|
|
||||||
ConnectionState.Disconnected,
|
|
||||||
);
|
|
||||||
expect(callback).toHaveBeenNthCalledWith(3, ConnectionState.Connecting, ConnectionState.WidgetLoading);
|
|
||||||
});
|
});
|
||||||
// in video rooms we expect the call to immediately reconnect
|
// in video rooms we expect the call to immediately reconnect
|
||||||
call.off(CallEvent.ConnectionState, callback);
|
call.off(CallEvent.ConnectionState, callback);
|
||||||
@ -497,10 +486,7 @@ describe("JitsiCall", () => {
|
|||||||
await call.start();
|
await call.start();
|
||||||
await call.disconnect();
|
await call.disconnect();
|
||||||
expect(onConnectionState.mock.calls).toEqual([
|
expect(onConnectionState.mock.calls).toEqual([
|
||||||
[ConnectionState.WidgetLoading, ConnectionState.Disconnected],
|
[ConnectionState.Connected, ConnectionState.Disconnected],
|
||||||
[ConnectionState.Connecting, ConnectionState.WidgetLoading],
|
|
||||||
[ConnectionState.Lobby, ConnectionState.Connecting],
|
|
||||||
[ConnectionState.Connected, ConnectionState.Lobby],
|
|
||||||
[ConnectionState.Disconnecting, ConnectionState.Connected],
|
[ConnectionState.Disconnecting, ConnectionState.Connected],
|
||||||
[ConnectionState.Disconnected, ConnectionState.Disconnecting],
|
[ConnectionState.Disconnected, ConnectionState.Disconnecting],
|
||||||
]);
|
]);
|
||||||
@ -634,7 +620,7 @@ describe("ElementCall", () => {
|
|||||||
jest.spyOn(room, "getJoinedMembers").mockReturnValue(memberIds.map((id) => ({ userId: id }) as RoomMember));
|
jest.spyOn(room, "getJoinedMembers").mockReturnValue(memberIds.map((id) => ({ userId: id }) as RoomMember));
|
||||||
}
|
}
|
||||||
|
|
||||||
const callConnectProcedure: (call: ElementCall) => Promise<void> = async (call) => {
|
const callConnectProcedure = async (call: ElementCall, startWidget = true): Promise<void> => {
|
||||||
async function sessionConnect() {
|
async function sessionConnect() {
|
||||||
await new Promise<void>((r) => {
|
await new Promise<void>((r) => {
|
||||||
setTimeout(() => r(), 400);
|
setTimeout(() => r(), 400);
|
||||||
@ -653,9 +639,7 @@ describe("ElementCall", () => {
|
|||||||
jest.advanceTimersByTime(500);
|
jest.advanceTimersByTime(500);
|
||||||
}
|
}
|
||||||
sessionConnect();
|
sessionConnect();
|
||||||
const promise = call.start();
|
await Promise.all([...(startWidget ? [call.start()] : []), runTimers()]);
|
||||||
runTimers();
|
|
||||||
await promise;
|
|
||||||
};
|
};
|
||||||
const callDisconnectionProcedure: (call: ElementCall) => Promise<void> = async (call) => {
|
const callDisconnectionProcedure: (call: ElementCall) => Promise<void> = async (call) => {
|
||||||
async function sessionDisconnect() {
|
async function sessionDisconnect() {
|
||||||
@ -683,6 +667,7 @@ describe("ElementCall", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
jest.runOnlyPendingTimers();
|
||||||
jest.useRealTimers();
|
jest.useRealTimers();
|
||||||
cleanUpClientRoomAndStores(client, room);
|
cleanUpClientRoomAndStores(client, room);
|
||||||
});
|
});
|
||||||
@ -693,7 +678,7 @@ describe("ElementCall", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("finds calls", async () => {
|
it("finds calls", async () => {
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
expect(Call.get(room)).toBeInstanceOf(ElementCall);
|
expect(Call.get(room)).toBeInstanceOf(ElementCall);
|
||||||
Call.get(room)?.destroy();
|
Call.get(room)?.destroy();
|
||||||
});
|
});
|
||||||
@ -728,7 +713,7 @@ describe("ElementCall", () => {
|
|||||||
};
|
};
|
||||||
document.documentElement.style.fontSize = "12px";
|
document.documentElement.style.fontSize = "12px";
|
||||||
|
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
const call = Call.get(room);
|
const call = Call.get(room);
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
@ -741,7 +726,7 @@ describe("ElementCall", () => {
|
|||||||
|
|
||||||
it("passes ICE fallback preference through widget URL", async () => {
|
it("passes ICE fallback preference through widget URL", async () => {
|
||||||
// Test with the preference set to false
|
// Test with the preference set to false
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
const call1 = Call.get(room);
|
const call1 = Call.get(room);
|
||||||
if (!(call1 instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call1 instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
@ -780,7 +765,7 @@ describe("ElementCall", () => {
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
const call = Call.get(room);
|
const call = Call.get(room);
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
@ -798,7 +783,7 @@ describe("ElementCall", () => {
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
const call = Call.get(room);
|
const call = Call.get(room);
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
@ -820,7 +805,7 @@ describe("ElementCall", () => {
|
|||||||
: originalGetValue(name, roomId, excludeDefault);
|
: originalGetValue(name, roomId, excludeDefault);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
const call = Call.get(room);
|
const call = Call.get(room);
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
@ -837,7 +822,7 @@ describe("ElementCall", () => {
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
});
|
});
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
const call = Call.get(room);
|
const call = Call.get(room);
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
|
|
||||||
@ -857,7 +842,7 @@ describe("ElementCall", () => {
|
|||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
jest.setSystemTime(0);
|
jest.setSystemTime(0);
|
||||||
|
|
||||||
await ElementCall.create(room, true);
|
ElementCall.create(room, true);
|
||||||
const maybeCall = ElementCall.get(room);
|
const maybeCall = ElementCall.get(room);
|
||||||
if (maybeCall === null) throw new Error("Failed to create call");
|
if (maybeCall === null) throw new Error("Failed to create call");
|
||||||
call = maybeCall;
|
call = maybeCall;
|
||||||
@ -876,9 +861,6 @@ describe("ElementCall", () => {
|
|||||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||||
|
|
||||||
const connect = callConnectProcedure(call);
|
const connect = callConnectProcedure(call);
|
||||||
|
|
||||||
expect(call.connectionState).toBe(ConnectionState.WidgetLoading);
|
|
||||||
|
|
||||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||||
await connect;
|
await connect;
|
||||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||||
@ -903,10 +885,8 @@ describe("ElementCall", () => {
|
|||||||
await callConnectProcedure(call);
|
await callConnectProcedure(call);
|
||||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||||
|
|
||||||
messaging.emit(
|
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||||
`action:${ElementWidgetActions.HangupCall}`,
|
messaging.emit(`action:${ElementWidgetActions.Close}`, new CustomEvent("widgetapirequest", {}));
|
||||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
|
||||||
);
|
|
||||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Disconnected), { interval: 5 });
|
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Disconnected), { interval: 5 });
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -986,9 +966,7 @@ describe("ElementCall", () => {
|
|||||||
await callConnectProcedure(call);
|
await callConnectProcedure(call);
|
||||||
await callDisconnectionProcedure(call);
|
await callDisconnectionProcedure(call);
|
||||||
expect(onConnectionState.mock.calls).toEqual([
|
expect(onConnectionState.mock.calls).toEqual([
|
||||||
[ConnectionState.WidgetLoading, ConnectionState.Disconnected],
|
[ConnectionState.Connected, ConnectionState.Disconnected],
|
||||||
[ConnectionState.Connecting, ConnectionState.WidgetLoading],
|
|
||||||
[ConnectionState.Connected, ConnectionState.Connecting],
|
|
||||||
[ConnectionState.Disconnecting, ConnectionState.Connected],
|
[ConnectionState.Disconnecting, ConnectionState.Connected],
|
||||||
[ConnectionState.Disconnected, ConnectionState.Disconnecting],
|
[ConnectionState.Disconnected, ConnectionState.Disconnecting],
|
||||||
]);
|
]);
|
||||||
@ -1068,7 +1046,7 @@ describe("ElementCall", () => {
|
|||||||
|
|
||||||
it("sends notify event on connect in a room with more than two members", async () => {
|
it("sends notify event on connect in a room with more than two members", async () => {
|
||||||
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
await callConnectProcedure(Call.get(room) as ElementCall);
|
await callConnectProcedure(Call.get(room) as ElementCall);
|
||||||
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
|
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
|
||||||
"application": "m.call",
|
"application": "m.call",
|
||||||
@ -1081,7 +1059,7 @@ describe("ElementCall", () => {
|
|||||||
setRoomMembers(["@user:example.com", "@user2:example.com"]);
|
setRoomMembers(["@user:example.com", "@user2:example.com"]);
|
||||||
|
|
||||||
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
await callConnectProcedure(Call.get(room) as ElementCall);
|
await callConnectProcedure(Call.get(room) as ElementCall);
|
||||||
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
|
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
|
||||||
"application": "m.call",
|
"application": "m.call",
|
||||||
@ -1105,7 +1083,7 @@ describe("ElementCall", () => {
|
|||||||
|
|
||||||
jest.spyOn(room, "getType").mockReturnValue(RoomType.UnstableCall);
|
jest.spyOn(room, "getType").mockReturnValue(RoomType.UnstableCall);
|
||||||
|
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
const maybeCall = ElementCall.get(room);
|
const maybeCall = ElementCall.get(room);
|
||||||
if (maybeCall === null) throw new Error("Failed to create call");
|
if (maybeCall === null) throw new Error("Failed to create call");
|
||||||
call = maybeCall;
|
call = maybeCall;
|
||||||
@ -1144,7 +1122,7 @@ describe("ElementCall", () => {
|
|||||||
return roomSession;
|
return roomSession;
|
||||||
});
|
});
|
||||||
|
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
const call = Call.get(room);
|
const call = Call.get(room);
|
||||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||||
expect(call.session).toBe(roomSession);
|
expect(call.session).toBe(roomSession);
|
||||||
@ -1163,12 +1141,12 @@ describe("ElementCall", () => {
|
|||||||
await callConnectProcedure(call);
|
await callConnectProcedure(call);
|
||||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||||
|
|
||||||
messaging.emit(
|
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||||
`action:${ElementWidgetActions.HangupCall}`,
|
messaging.emit(`action:${ElementWidgetActions.Close}`, new CustomEvent("widgetapirequest", {}));
|
||||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
// We should now be able to reconnect without manually starting the widget
|
||||||
);
|
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||||
// We want the call to be connecting after the hangup.
|
await callConnectProcedure(call, false);
|
||||||
waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connecting), { interval: 5 });
|
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected), { interval: 5 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe("create call", () => {
|
describe("create call", () => {
|
||||||
@ -1180,7 +1158,7 @@ describe("ElementCall", () => {
|
|||||||
{ application: "m.call", callId: "" } as unknown as CallMembership,
|
{ application: "m.call", callId: "" } as unknown as CallMembership,
|
||||||
]);
|
]);
|
||||||
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
||||||
await ElementCall.create(room);
|
ElementCall.create(room);
|
||||||
expect(sendEventSpy).not.toHaveBeenCalled();
|
expect(sendEventSpy).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -13,10 +13,16 @@ import {
|
|||||||
RoomViewLifecycle,
|
RoomViewLifecycle,
|
||||||
type ViewRoomOpts,
|
type ViewRoomOpts,
|
||||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||||
|
import EventEmitter from "events";
|
||||||
|
|
||||||
import { RoomViewStore } from "../../../src/stores/RoomViewStore";
|
import { RoomViewStore } from "../../../src/stores/RoomViewStore";
|
||||||
import { Action } from "../../../src/dispatcher/actions";
|
import { Action } from "../../../src/dispatcher/actions";
|
||||||
import { getMockClientWithEventEmitter, untilDispatch, untilEmission } from "../../test-utils";
|
import {
|
||||||
|
getMockClientWithEventEmitter,
|
||||||
|
setupAsyncStoreWithClient,
|
||||||
|
untilDispatch,
|
||||||
|
untilEmission,
|
||||||
|
} from "../../test-utils";
|
||||||
import SettingsStore from "../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../src/settings/SettingsStore";
|
||||||
import { SlidingSyncManager } from "../../../src/SlidingSyncManager";
|
import { SlidingSyncManager } from "../../../src/SlidingSyncManager";
|
||||||
import { PosthogAnalytics } from "../../../src/PosthogAnalytics";
|
import { PosthogAnalytics } from "../../../src/PosthogAnalytics";
|
||||||
@ -33,6 +39,10 @@ import { type CancelAskToJoinPayload } from "../../../src/dispatcher/payloads/Ca
|
|||||||
import { type JoinRoomErrorPayload } from "../../../src/dispatcher/payloads/JoinRoomErrorPayload";
|
import { type JoinRoomErrorPayload } from "../../../src/dispatcher/payloads/JoinRoomErrorPayload";
|
||||||
import { type SubmitAskToJoinPayload } from "../../../src/dispatcher/payloads/SubmitAskToJoinPayload";
|
import { type SubmitAskToJoinPayload } from "../../../src/dispatcher/payloads/SubmitAskToJoinPayload";
|
||||||
import { ModuleRunner } from "../../../src/modules/ModuleRunner";
|
import { ModuleRunner } from "../../../src/modules/ModuleRunner";
|
||||||
|
import { type IApp } from "../../../src/utils/WidgetUtils-types";
|
||||||
|
import { CallStore } from "../../../src/stores/CallStore";
|
||||||
|
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||||
|
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../src/MediaDeviceHandler";
|
||||||
|
|
||||||
jest.mock("../../../src/Modal");
|
jest.mock("../../../src/Modal");
|
||||||
|
|
||||||
@ -60,6 +70,12 @@ jest.mock("../../../src/audio/VoiceRecording", () => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||||
|
[MediaDeviceKindEnum.AudioInput]: [],
|
||||||
|
[MediaDeviceKindEnum.VideoInput]: [],
|
||||||
|
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||||
|
});
|
||||||
|
|
||||||
jest.mock("../../../src/utils/DMRoomMap", () => {
|
jest.mock("../../../src/utils/DMRoomMap", () => {
|
||||||
const mock = {
|
const mock = {
|
||||||
getUserIdForRoomId: jest.fn(),
|
getUserIdForRoomId: jest.fn(),
|
||||||
@ -72,7 +88,21 @@ jest.mock("../../../src/utils/DMRoomMap", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
jest.mock("../../../src/stores/WidgetStore");
|
jest.mock("../../../src/stores/WidgetStore", () => {
|
||||||
|
// This mock needs to use a real EventEmitter; require is the only way to import that in a hoisted block
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||||
|
const EventEmitter = require("events");
|
||||||
|
const apps: IApp[] = [];
|
||||||
|
const instance = new (class extends EventEmitter {
|
||||||
|
getApps() {
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
addVirtualWidget(app: IApp) {
|
||||||
|
apps.push(app);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return { instance };
|
||||||
|
});
|
||||||
jest.mock("../../../src/stores/widgets/WidgetLayoutStore");
|
jest.mock("../../../src/stores/widgets/WidgetLayoutStore");
|
||||||
|
|
||||||
describe("RoomViewStore", function () {
|
describe("RoomViewStore", function () {
|
||||||
@ -82,10 +112,12 @@ describe("RoomViewStore", function () {
|
|||||||
// we need to change the alias to ensure cache misses as the cache exists
|
// we need to change the alias to ensure cache misses as the cache exists
|
||||||
// through all tests.
|
// through all tests.
|
||||||
let alias = "#somealias2:aser.ver";
|
let alias = "#somealias2:aser.ver";
|
||||||
|
const getRooms = jest.fn();
|
||||||
const mockClient = getMockClientWithEventEmitter({
|
const mockClient = getMockClientWithEventEmitter({
|
||||||
joinRoom: jest.fn(),
|
joinRoom: jest.fn(),
|
||||||
getRoom: jest.fn(),
|
getRoom: jest.fn(),
|
||||||
getRoomIdForAlias: jest.fn(),
|
getRoomIdForAlias: jest.fn(),
|
||||||
|
getRooms,
|
||||||
isGuest: jest.fn(),
|
isGuest: jest.fn(),
|
||||||
getUserId: jest.fn().mockReturnValue(userId),
|
getUserId: jest.fn().mockReturnValue(userId),
|
||||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||||
@ -97,9 +129,18 @@ describe("RoomViewStore", function () {
|
|||||||
knockRoom: jest.fn(),
|
knockRoom: jest.fn(),
|
||||||
leave: jest.fn(),
|
leave: jest.fn(),
|
||||||
setRoomAccountData: jest.fn(),
|
setRoomAccountData: jest.fn(),
|
||||||
|
getAccountData: jest.fn(),
|
||||||
|
matrixRTC: new (class extends EventEmitter {
|
||||||
|
getRoomSession() {
|
||||||
|
return new (class extends EventEmitter {
|
||||||
|
memberships = [];
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
})(),
|
||||||
});
|
});
|
||||||
const room = new Room(roomId, mockClient, userId);
|
const room = new Room(roomId, mockClient, userId);
|
||||||
const room2 = new Room(roomId2, mockClient, userId);
|
const room2 = new Room(roomId2, mockClient, userId);
|
||||||
|
getRooms.mockReturnValue([room, room2]);
|
||||||
|
|
||||||
const viewCall = async (): Promise<void> => {
|
const viewCall = async (): Promise<void> => {
|
||||||
dis.dispatch<ViewRoomPayload>({
|
dis.dispatch<ViewRoomPayload>({
|
||||||
@ -301,6 +342,7 @@ describe("RoomViewStore", function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("when viewing a call without a broadcast, it should not raise an error", async () => {
|
it("when viewing a call without a broadcast, it should not raise an error", async () => {
|
||||||
|
await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet());
|
||||||
await viewCall();
|
await viewCall();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user