mirror of
https://github.com/element-hq/element-web.git
synced 2025-12-05 01:10:40 +00:00
Compare commits
2 Commits
t3chguy/mo
...
robin/call
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
569d525c6e | ||
|
|
7799cb2ec5 |
@@ -118,8 +118,6 @@ import { RoomStatusBarUnsentMessages } from "./RoomStatusBarUnsentMessages";
|
||||
import { LargeLoader } from "./LargeLoader";
|
||||
import { isVideoRoom } from "../../utils/video-rooms";
|
||||
import { SDKContext } from "../../contexts/SDKContext";
|
||||
import { CallStore, CallStoreEvent } from "../../stores/CallStore";
|
||||
import { type Call } from "../../models/Call";
|
||||
import { RoomSearchView } from "./RoomSearchView";
|
||||
import eventSearch, { type SearchInfo, SearchScope } from "../../Searching";
|
||||
import VoipUserMapper from "../../VoipUserMapper";
|
||||
@@ -190,7 +188,6 @@ export interface IRoomState {
|
||||
*/
|
||||
search?: SearchInfo;
|
||||
callState?: CallState;
|
||||
activeCall: Call | null;
|
||||
canPeek: boolean;
|
||||
canSelfRedact: boolean;
|
||||
showApps: boolean;
|
||||
@@ -401,7 +398,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
membersLoaded: !llMembers,
|
||||
numUnreadMessages: 0,
|
||||
callState: undefined,
|
||||
activeCall: null,
|
||||
canPeek: false,
|
||||
canSelfRedact: false,
|
||||
showApps: false,
|
||||
@@ -577,7 +573,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
mainSplitContentType: room ? this.getMainSplitContentType(room) : undefined,
|
||||
initialEventId: undefined, // default to clearing this, will get set later in the method if needed
|
||||
showRightPanel: roomId ? this.context.rightPanelStore.isOpenForRoom(roomId) : false,
|
||||
activeCall: roomId ? CallStore.instance.getActiveCall(roomId) : null,
|
||||
promptAskToJoin: this.context.roomViewStore.promptAskToJoin(),
|
||||
viewRoomOpts: this.context.roomViewStore.getViewRoomOpts(),
|
||||
};
|
||||
@@ -727,23 +722,17 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
});
|
||||
};
|
||||
|
||||
private onConnectedCalls = (): void => {
|
||||
if (this.state.roomId === undefined) return;
|
||||
const activeCall = CallStore.instance.getActiveCall(this.state.roomId);
|
||||
if (activeCall === null) {
|
||||
// We disconnected from the call, so stop viewing it
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>(
|
||||
{
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.roomId,
|
||||
view_call: false,
|
||||
metricsTrigger: undefined,
|
||||
},
|
||||
true,
|
||||
); // Synchronous so that CallView disappears immediately
|
||||
}
|
||||
|
||||
this.setState({ activeCall });
|
||||
private onCallClose = (): void => {
|
||||
// Stop viewing the call
|
||||
defaultDispatcher.dispatch<ViewRoomPayload>(
|
||||
{
|
||||
action: Action.ViewRoom,
|
||||
room_id: this.state.roomId,
|
||||
view_call: false,
|
||||
metricsTrigger: undefined,
|
||||
},
|
||||
true,
|
||||
); // Synchronous so that CallView disappears immediately
|
||||
};
|
||||
|
||||
private getRoomId = (): string | undefined => {
|
||||
@@ -900,8 +889,6 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
WidgetEchoStore.on(UPDATE_EVENT, this.onWidgetEchoStoreUpdate);
|
||||
this.context.widgetStore.on(UPDATE_EVENT, this.onWidgetStoreUpdate);
|
||||
|
||||
CallStore.instance.on(CallStoreEvent.ConnectedCalls, this.onConnectedCalls);
|
||||
|
||||
this.props.resizeNotifier.on("isResizing", this.onIsResizing);
|
||||
|
||||
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);
|
||||
|
||||
// cancel any pending calls to the throttled updated
|
||||
@@ -2562,9 +2548,9 @@ export class RoomView extends React.Component<IRoomProps, IRoomState> {
|
||||
<CallView
|
||||
room={this.state.room}
|
||||
resizing={this.state.resizing}
|
||||
waitForCall={isVideoRoom(this.state.room)}
|
||||
skipLobby={this.context.roomViewStore.skipCallLobby() ?? false}
|
||||
role="main"
|
||||
onClose={this.onCallClose}
|
||||
/>
|
||||
{previewBar}
|
||||
</>
|
||||
|
||||
@@ -126,10 +126,6 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
|
||||
return [_t("action|leave"), "danger", disconnect];
|
||||
case ConnectionState.Disconnecting:
|
||||
return [_t("action|leave"), "danger", null];
|
||||
case ConnectionState.Connecting:
|
||||
case ConnectionState.Lobby:
|
||||
case ConnectionState.WidgetLoading:
|
||||
return [_t("action|join"), "primary", null];
|
||||
}
|
||||
}, [connectionState, connect, disconnect]);
|
||||
|
||||
|
||||
@@ -27,18 +27,6 @@ export const RoomTileCallSummary: FC<Props> = ({ call }) => {
|
||||
text = _t("common|video");
|
||||
active = false;
|
||||
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.Disconnecting:
|
||||
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 type { Room } from "matrix-js-sdk/src/matrix";
|
||||
import { type Call, ConnectionState, ElementCall } from "../../../models/Call";
|
||||
import { useCall } from "../../../hooks/useCall";
|
||||
import { type Call, CallEvent } from "../../../models/Call";
|
||||
import MatrixClientContext from "../../../contexts/MatrixClientContext";
|
||||
import AppTile from "../elements/AppTile";
|
||||
import { CallStore } from "../../../stores/CallStore";
|
||||
import { SdkContextClass } from "../../../contexts/SDKContext";
|
||||
import { useTypedEventEmitter } from "../../../hooks/useEventEmitter";
|
||||
import { useCall } from "../../../hooks/useCall";
|
||||
|
||||
interface JoinCallViewProps {
|
||||
room: Room;
|
||||
@@ -22,10 +23,12 @@ interface JoinCallViewProps {
|
||||
call: Call;
|
||||
skipLobby?: boolean;
|
||||
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);
|
||||
useTypedEventEmitter(call, CallEvent.Close, onClose);
|
||||
|
||||
useEffect(() => {
|
||||
// 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, 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 () => {
|
||||
// The stickyPromise has to resolve before the widget actually becomes sticky.
|
||||
// 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()));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="mx_CallView" role={role}>
|
||||
<AppTile
|
||||
@@ -76,26 +69,27 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby,
|
||||
interface CallViewProps {
|
||||
room: Room;
|
||||
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;
|
||||
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);
|
||||
|
||||
useEffect(() => {
|
||||
if (call === null && !waitForCall) {
|
||||
ElementCall.create(room, skipLobby);
|
||||
}
|
||||
}, [call, room, skipLobby, waitForCall]);
|
||||
if (call === null) {
|
||||
return null;
|
||||
} else {
|
||||
return <JoinCallView room={room} resizing={resizing} call={call} skipLobby={skipLobby} role={role} />;
|
||||
}
|
||||
return (
|
||||
call && (
|
||||
<JoinCallView
|
||||
room={room}
|
||||
resizing={resizing}
|
||||
call={call}
|
||||
skipLobby={skipLobby}
|
||||
role={role}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
@@ -70,7 +70,6 @@ const RoomContext = createContext<
|
||||
threadId: undefined,
|
||||
liveTimeline: undefined,
|
||||
narrow: false,
|
||||
activeCall: null,
|
||||
msc3946ProcessDynamicPredecessor: false,
|
||||
canAskToJoin: false,
|
||||
promptAskToJoin: false,
|
||||
|
||||
@@ -343,7 +343,7 @@ export default async function createRoom(client: MatrixClient, opts: IOpts): Pro
|
||||
await client.setPowerLevel(roomId, client.getUserId()!, 100);
|
||||
} else if (opts.roomType === RoomType.UnstableCall) {
|
||||
// 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
|
||||
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 => {
|
||||
const isFull = useFull(call);
|
||||
const state = useConnectionState(call);
|
||||
|
||||
if (state === ConnectionState.Connecting) return _t("voip|join_button_tooltip_connecting");
|
||||
if (isFull) return _t("voip|join_button_tooltip_call_full");
|
||||
return null;
|
||||
return isFull ? _t("voip|join_button_tooltip_call_full") : null;
|
||||
};
|
||||
|
||||
@@ -495,7 +495,6 @@
|
||||
"legal": "Legal",
|
||||
"light": "Light",
|
||||
"loading": "Loading…",
|
||||
"lobby": "Lobby",
|
||||
"location": "Location",
|
||||
"low_priority": "Low priority",
|
||||
"matrix": "Matrix",
|
||||
@@ -3899,7 +3898,6 @@
|
||||
"input_devices": "Input devices",
|
||||
"jitsi_call": "Jitsi Conference",
|
||||
"join_button_tooltip_call_full": "Sorry — this call is currently full",
|
||||
"join_button_tooltip_connecting": "Connecting",
|
||||
"legacy_call": "Legacy Call",
|
||||
"maximise": "Fill screen",
|
||||
"maximise_call": "Maximise call",
|
||||
|
||||
@@ -77,13 +77,7 @@ const waitForEvent = async (
|
||||
};
|
||||
|
||||
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",
|
||||
|
||||
Connecting = "connecting",
|
||||
Connected = "connected",
|
||||
Disconnecting = "disconnecting",
|
||||
}
|
||||
@@ -100,6 +94,7 @@ export enum CallEvent {
|
||||
ConnectionState = "connection_state",
|
||||
Participants = "participants",
|
||||
Layout = "layout",
|
||||
Close = "close",
|
||||
Destroy = "destroy",
|
||||
}
|
||||
|
||||
@@ -110,6 +105,7 @@ interface CallEventHandlerMap {
|
||||
prevParticipants: Map<RoomMember, Set<string>>,
|
||||
) => void;
|
||||
[CallEvent.Layout]: (layout: Layout) => void;
|
||||
[CallEvent.Close]: () => void;
|
||||
[CallEvent.Destroy]: () => void;
|
||||
}
|
||||
|
||||
@@ -167,6 +163,17 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
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(
|
||||
/**
|
||||
* The widget used to access this call.
|
||||
@@ -177,6 +184,7 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
super();
|
||||
this.widgetUid = WidgetUtils.getWidgetUid(this.widget);
|
||||
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.
|
||||
*/
|
||||
public async start(): Promise<void> {
|
||||
this.connectionState = ConnectionState.WidgetLoading;
|
||||
|
||||
const { [MediaDeviceKindEnum.AudioInput]: audioInputs, [MediaDeviceKindEnum.VideoInput]: videoInputs } =
|
||||
(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}`);
|
||||
}
|
||||
}
|
||||
this.connectionState = ConnectionState.Connecting;
|
||||
try {
|
||||
await this.performConnection(audioInput, videoInput);
|
||||
} catch (e) {
|
||||
this.connectionState = ConnectionState.Disconnected;
|
||||
throw e;
|
||||
}
|
||||
await this.performConnection(audioInput, videoInput);
|
||||
|
||||
this.room.on(RoomEvent.MyMembership, this.onMyMembership);
|
||||
WidgetMessagingStore.instance.on(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
|
||||
window.addEventListener("beforeunload", this.beforeUnload);
|
||||
this.connectionState = ConnectionState.Connected;
|
||||
}
|
||||
@@ -280,39 +279,54 @@ export abstract class Call extends TypedEventEmitter<CallEvent, CallEventHandler
|
||||
this.connectionState = ConnectionState.Disconnecting;
|
||||
await this.performDisconnection();
|
||||
this.setDisconnected();
|
||||
this.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually marks the call as disconnected and cleans up.
|
||||
* Manually marks the call as disconnected.
|
||||
*/
|
||||
public setDisconnected(): void {
|
||||
this.room.off(RoomEvent.MyMembership, this.onMyMembership);
|
||||
WidgetMessagingStore.instance.off(WidgetMessagingStoreEvent.StopMessaging, this.onStopMessaging);
|
||||
window.removeEventListener("beforeunload", this.beforeUnload);
|
||||
this.messaging = null;
|
||||
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.
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
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();
|
||||
};
|
||||
|
||||
private onStopMessaging = (uid: string): void => {
|
||||
if (uid === this.widgetUid) {
|
||||
private readonly onStopMessaging = (uid: string): void => {
|
||||
if (uid === this.widgetUid && this.connected) {
|
||||
logger.log("The widget died; treating this as a user hangup");
|
||||
this.setDisconnected();
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
private beforeUnload = (): void => this.setDisconnected();
|
||||
private beforeUnload = (): void => {
|
||||
this.setDisconnected();
|
||||
this.close();
|
||||
};
|
||||
}
|
||||
|
||||
export type { JitsiCallMemberContent };
|
||||
@@ -466,7 +480,6 @@ export class JitsiCall extends Call {
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void> {
|
||||
this.connectionState = ConnectionState.Lobby;
|
||||
// Ensure that the messaging doesn't get stopped while we're waiting for responses
|
||||
const dontStopMessaging = new Promise<void>((resolve, reject) => {
|
||||
const messagingStore = WidgetMessagingStore.instance;
|
||||
@@ -569,9 +582,9 @@ export class JitsiCall extends Call {
|
||||
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)) {
|
||||
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
|
||||
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
|
||||
// to only show the active speaker and economize on space
|
||||
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,
|
||||
// ignore the event
|
||||
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,
|
||||
// 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 this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.setDisconnected();
|
||||
this.close();
|
||||
// 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)) {
|
||||
this.start();
|
||||
}
|
||||
@@ -653,6 +667,14 @@ export class ElementCall extends Call {
|
||||
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 {
|
||||
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.
|
||||
@@ -740,7 +762,7 @@ export class ElementCall extends Call {
|
||||
// To use Element Call without touching room state, we create a virtual
|
||||
// widget (one that doesn't have a corresponding state event)
|
||||
const url = ElementCall.generateWidgetUrl(client, roomId);
|
||||
return WidgetStore.instance.addVirtualWidget(
|
||||
const createdWidget = WidgetStore.instance.addVirtualWidget(
|
||||
{
|
||||
id: secureRandomString(24), // So that it's globally unique
|
||||
creatorUserId: client.getUserId()!,
|
||||
@@ -761,6 +783,8 @@ export class ElementCall extends Call {
|
||||
},
|
||||
roomId,
|
||||
);
|
||||
WidgetStore.instance.emit(UPDATE_EVENT, null);
|
||||
return createdWidget;
|
||||
}
|
||||
|
||||
private static getWidgetData(
|
||||
@@ -794,7 +818,7 @@ export class ElementCall extends Call {
|
||||
super(widget, client);
|
||||
|
||||
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(
|
||||
"feature_disable_call_per_sender_encryption",
|
||||
null,
|
||||
@@ -827,9 +851,8 @@ export class ElementCall extends Call {
|
||||
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));
|
||||
WidgetStore.instance.emit(UPDATE_EVENT, null);
|
||||
}
|
||||
|
||||
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.SpotlightLayout}`, this.onSpotlightLayout);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.HangupCall}`, this.onHangup);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, async (ev) => {
|
||||
ev.preventDefault();
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
});
|
||||
this.messaging!.once(`action:${ElementWidgetActions.Close}`, this.onClose);
|
||||
this.messaging!.on(`action:${ElementWidgetActions.DeviceMute}`, this.onDeviceMute);
|
||||
|
||||
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
|
||||
// - set state to connecting
|
||||
// - send call notify
|
||||
@@ -927,15 +942,16 @@ export class ElementCall extends Call {
|
||||
public setDisconnected(): void {
|
||||
this.messaging!.off(`action:${ElementWidgetActions.TileLayout}`, this.onTileLayout);
|
||||
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();
|
||||
}
|
||||
|
||||
public destroy(): void {
|
||||
ActiveWidgetStore.instance.destroyPersistentWidget(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.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.onRTCSessionEnded);
|
||||
this.client.matrixRTC.off(MatrixRTCSessionManagerEvents.SessionEnded, this.checkDestroy);
|
||||
|
||||
SettingsStore.unwatchSetting(this.settingsStoreCallEncryptionWatcher);
|
||||
clearTimeout(this.terminationTimer);
|
||||
@@ -944,11 +960,10 @@ export class ElementCall extends Call {
|
||||
super.destroy();
|
||||
}
|
||||
|
||||
private onRTCSessionEnded = (roomId: string, session: MatrixRTCSession): void => {
|
||||
// Don't destroy the call on hangup for video call rooms.
|
||||
if (roomId === this.roomId && !this.room.isCallRoom()) {
|
||||
this.destroy();
|
||||
}
|
||||
private checkDestroy = (): void => {
|
||||
// A call ceases to exist as soon as all participants leave and also the
|
||||
// user isn't looking at it (for example, waiting in an empty lobby)
|
||||
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, {});
|
||||
}
|
||||
|
||||
private onMembershipChanged = (): void => this.updateParticipants();
|
||||
private readonly onMembershipChanged = (): void => this.updateParticipants();
|
||||
|
||||
private updateParticipants(): void {
|
||||
const participants = new Map<RoomMember, Set<string>>();
|
||||
@@ -980,27 +995,40 @@ export class ElementCall extends Call {
|
||||
this.participants = participants;
|
||||
}
|
||||
|
||||
private onHangup = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
private readonly onDeviceMute = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||
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();
|
||||
};
|
||||
|
||||
private readonly onClose = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
// In video rooms we immediately want to reconnect after hangup
|
||||
// This starts the lobby again and connects to all signals from EC.
|
||||
if (isVideoRoom(this.room)) {
|
||||
this.start();
|
||||
} else {
|
||||
// User is done with the call; tell the UI to close it
|
||||
this.close();
|
||||
}
|
||||
};
|
||||
|
||||
private onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
private readonly onTileLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
this.layout = Layout.Tile;
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
|
||||
private onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
private readonly onSpotlightLayout = async (ev: CustomEvent<IWidgetApiRequest>): Promise<void> => {
|
||||
ev.preventDefault();
|
||||
this.layout = Layout.Spotlight;
|
||||
await this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
this.messaging!.transport.reply(ev.detail, {}); // ack
|
||||
};
|
||||
|
||||
public clean(): Promise<void> {
|
||||
|
||||
@@ -50,6 +50,8 @@ import { type CancelAskToJoinPayload } from "../dispatcher/payloads/CancelAskToJ
|
||||
import { type SubmitAskToJoinPayload } from "../dispatcher/payloads/SubmitAskToJoinPayload";
|
||||
import { ModuleRunner } from "../modules/ModuleRunner";
|
||||
import { setMarkedUnreadState } from "../utils/notifications";
|
||||
import { ConnectionState, ElementCall } from "../models/Call";
|
||||
import { isVideoRoom } from "../utils/video-rooms";
|
||||
|
||||
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 (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.
|
||||
|
||||
@@ -12,6 +12,7 @@ export enum ElementWidgetActions {
|
||||
// All of these actions are currently specific to Jitsi and Element Call
|
||||
JoinCall = "io.element.join",
|
||||
HangupCall = "im.vector.hangup",
|
||||
Close = "io.element.close",
|
||||
CallParticipants = "io.element.participants",
|
||||
StartLiveStream = "im.vector.start_live_stream",
|
||||
|
||||
|
||||
@@ -72,8 +72,11 @@ export class WidgetMessagingStore extends AsyncStoreWithClient<EmptyObject> {
|
||||
* @param {string} widgetUid The widget UID.
|
||||
*/
|
||||
public stopMessagingByUid(widgetUid: string): void {
|
||||
this.widgetMap.remove(widgetUid)?.stop();
|
||||
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
|
||||
const messaging = this.widgetMap.remove(widgetUid);
|
||||
if (messaging !== undefined) {
|
||||
messaging.stop();
|
||||
this.emit(WidgetMessagingStoreEvent.StopMessaging, widgetUid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -109,5 +109,5 @@ export class MockedCall extends Call {
|
||||
export const useMockedCalls = () => {
|
||||
Call.get = (room) => MockedCall.get(room);
|
||||
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,
|
||||
resizing: false,
|
||||
narrow: false,
|
||||
activeCall: null,
|
||||
msc3946ProcessDynamicPredecessor: false,
|
||||
canAskToJoin: false,
|
||||
promptAskToJoin: false,
|
||||
|
||||
@@ -76,6 +76,15 @@ import { SearchScope } from "../../../../src/Searching";
|
||||
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto";
|
||||
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
|
||||
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", () => {
|
||||
let cli: MockedObject<MatrixClient>;
|
||||
@@ -98,6 +107,7 @@ describe("RoomView", () => {
|
||||
rooms = new Map();
|
||||
rooms.set(room.roomId, room);
|
||||
cli.getRoom.mockImplementation((roomId: string | undefined) => rooms.get(roomId || "") || null);
|
||||
cli.getRooms.mockImplementation(() => [...rooms.values()]);
|
||||
// Re-emit certain events on the mocked client
|
||||
room.on(RoomEvent.Timeline, (...args) => cli.emit(RoomEvent.Timeline, ...args));
|
||||
room.on(RoomEvent.TimelineReset, (...args) => cli.emit(RoomEvent.TimelineReset, ...args));
|
||||
@@ -371,6 +381,7 @@ describe("RoomView", () => {
|
||||
|
||||
describe("video rooms", () => {
|
||||
beforeEach(async () => {
|
||||
await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet());
|
||||
// Make it a video room
|
||||
room.isElementVideoRoom = () => 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>
|
||||
</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
|
||||
class="mx_RightPanel_ResizeWrapper"
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
} from "jest-matrix-react";
|
||||
import { type ViewRoomOpts } from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import { mocked } from "jest-mock";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import { filterConsole, stubClient } from "../../../../../test-utils";
|
||||
import RoomHeader from "../../../../../../src/components/views/rooms/RoomHeader/RoomHeader";
|
||||
@@ -106,13 +107,15 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("opens the room summary", async () => {
|
||||
const user = userEvent.setup();
|
||||
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 });
|
||||
});
|
||||
|
||||
it("shows a face pile for rooms", async () => {
|
||||
const user = userEvent.setup();
|
||||
const members = [
|
||||
{
|
||||
userId: "@me:example.org",
|
||||
@@ -161,33 +164,36 @@ describe("RoomHeader", () => {
|
||||
const facePile = getByLabelText(document.body, "4 members");
|
||||
expect(facePile).toHaveTextContent("4");
|
||||
|
||||
fireEvent.click(facePile);
|
||||
await user.click(facePile);
|
||||
|
||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
|
||||
});
|
||||
|
||||
it("has room info icon that opens the room info panel", async () => {
|
||||
const user = userEvent.setup();
|
||||
const { getAllByRole } = render(<RoomHeader room={room} />, getWrapper());
|
||||
const infoButton = getAllByRole("button", { name: "Room info" })[1];
|
||||
fireEvent.click(infoButton);
|
||||
await user.click(infoButton);
|
||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomSummary });
|
||||
});
|
||||
|
||||
it("opens the thread panel", async () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
fireEvent.click(getByLabelText(document.body, "Threads"));
|
||||
await user.click(getByLabelText(document.body, "Threads"));
|
||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.ThreadPanel });
|
||||
});
|
||||
|
||||
it("opens the notifications panel", async () => {
|
||||
const user = userEvent.setup();
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string): any => {
|
||||
if (name === "feature_notifications") return true;
|
||||
});
|
||||
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
fireEvent.click(getByLabelText(document.body, "Notifications"));
|
||||
await user.click(getByLabelText(document.body, "Notifications"));
|
||||
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.NotificationPanel });
|
||||
});
|
||||
|
||||
@@ -274,6 +280,7 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("you can call when you're two in the room", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 2);
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
@@ -284,10 +291,10 @@ describe("RoomHeader", () => {
|
||||
|
||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
||||
|
||||
fireEvent.click(voiceButton);
|
||||
await user.click(voiceButton);
|
||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
||||
|
||||
fireEvent.click(videoButton);
|
||||
await user.click(videoButton);
|
||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||
});
|
||||
|
||||
@@ -332,6 +339,7 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("renders only the video call element", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 3);
|
||||
jest.spyOn(SdkConfig, "get").mockReturnValue({ use_exclusively: true });
|
||||
// allow element calls
|
||||
@@ -344,9 +352,9 @@ describe("RoomHeader", () => {
|
||||
const videoCallButton = screen.getByRole("button", { name: "Video call" });
|
||||
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 }));
|
||||
});
|
||||
|
||||
@@ -366,7 +374,8 @@ describe("RoomHeader", () => {
|
||||
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);
|
||||
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => feature == UIFeature.Widgets);
|
||||
// allow calls
|
||||
@@ -386,7 +395,7 @@ describe("RoomHeader", () => {
|
||||
|
||||
const videoButton = screen.getByRole("button", { name: "Video call" });
|
||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
fireEvent.click(videoButton);
|
||||
await user.click(videoButton);
|
||||
expect(spy).toHaveBeenCalledWith(room, widget, Container.Top);
|
||||
});
|
||||
|
||||
@@ -463,6 +472,7 @@ describe("RoomHeader", () => {
|
||||
});
|
||||
|
||||
it("calls using legacy or jitsi", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 2);
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
if (key === "im.vector.modular.widgets") return true;
|
||||
@@ -476,14 +486,15 @@ describe("RoomHeader", () => {
|
||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
||||
fireEvent.click(voiceButton);
|
||||
await user.click(voiceButton);
|
||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Voice);
|
||||
|
||||
fireEvent.click(videoButton);
|
||||
await user.click(videoButton);
|
||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||
});
|
||||
|
||||
it("calls using legacy or jitsi for large rooms", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 3);
|
||||
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
@@ -497,11 +508,12 @@ describe("RoomHeader", () => {
|
||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const placeCallSpy = jest.spyOn(LegacyCallHandler.instance, "placeCall");
|
||||
fireEvent.click(videoButton);
|
||||
await user.click(videoButton);
|
||||
expect(placeCallSpy).toHaveBeenLastCalledWith(room.roomId, CallType.Video);
|
||||
});
|
||||
|
||||
it("calls using element call for large rooms", async () => {
|
||||
const user = userEvent.setup();
|
||||
mockRoomMembers(room, 3);
|
||||
|
||||
jest.spyOn(room.currentState, "mayClientSendStateEvent").mockImplementation((key) => {
|
||||
@@ -514,8 +526,8 @@ describe("RoomHeader", () => {
|
||||
const videoButton = screen.getByRole("button", { name: "Video call" });
|
||||
expect(videoButton).not.toHaveAttribute("aria-disabled", "true");
|
||||
|
||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch");
|
||||
fireEvent.click(videoButton);
|
||||
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
|
||||
await user.click(videoButton);
|
||||
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 () => {
|
||||
const user = userEvent.setup();
|
||||
render(<RoomHeader room={room} />, getWrapper());
|
||||
|
||||
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" }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -43,7 +43,8 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
</div>
|
||||
</button>
|
||||
<button
|
||||
aria-labelledby=":r16c:"
|
||||
aria-disabled="true"
|
||||
aria-label="There's no one here to call"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
@@ -51,9 +52,10 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
>
|
||||
<div
|
||||
class="_indicator-icon_133tf_26"
|
||||
style="--cpd-icon-button-size: 100%;"
|
||||
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
|
||||
>
|
||||
<svg
|
||||
aria-labelledby=":r166:"
|
||||
fill="currentColor"
|
||||
height="1em"
|
||||
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"
|
||||
>
|
||||
<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.414Z"
|
||||
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-4Z"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -69,7 +71,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
<button
|
||||
aria-disabled="true"
|
||||
aria-label="There's no one here to call"
|
||||
aria-labelledby=":r16h:"
|
||||
aria-labelledby=":r16b:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
@@ -94,7 +96,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Threads"
|
||||
aria-labelledby=":r16m:"
|
||||
aria-labelledby=":r16g:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
style="--cpd-icon-button-size: 32px;"
|
||||
@@ -120,7 +122,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
|
||||
</button>
|
||||
<button
|
||||
aria-label="Room info"
|
||||
aria-labelledby=":r16r:"
|
||||
aria-labelledby=":r16l:"
|
||||
class="_icon-button_bh2qc_17"
|
||||
role="button"
|
||||
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 { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import SettingsStore from "../../../../../src/settings/SettingsStore";
|
||||
import { ConnectionState } from "../../../../../src/models/Call";
|
||||
|
||||
jest.mock("../../../../../src/customisations/helpers/UIComponents", () => ({
|
||||
shouldShowComponent: jest.fn(),
|
||||
@@ -216,41 +215,10 @@ describe("RoomTile", () => {
|
||||
it("tracks connection state", async () => {
|
||||
renderRoomTile();
|
||||
screen.getByText("Video");
|
||||
|
||||
let completeWidgetLoading: () => void = () => {};
|
||||
const widgetLoadingCompleted = new Promise<void>((resolve) => (completeWidgetLoading = resolve));
|
||||
|
||||
// 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()]);
|
||||
await act(() => call.start());
|
||||
screen.getByText("Joined");
|
||||
await act(() => call.disconnect());
|
||||
screen.getByText("Video");
|
||||
});
|
||||
|
||||
it("tracks participants", () => {
|
||||
|
||||
@@ -73,7 +73,6 @@ describe("<SendMessageComposer/>", () => {
|
||||
canSelfRedact: false,
|
||||
resizing: false,
|
||||
narrow: false,
|
||||
activeCall: null,
|
||||
msc3946ProcessDynamicPredecessor: false,
|
||||
canAskToJoin: false,
|
||||
promptAskToJoin: false,
|
||||
|
||||
@@ -7,8 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React from "react";
|
||||
import { zip } from "lodash";
|
||||
import { render, screen, act, fireEvent, waitFor, cleanup } from "jest-matrix-react";
|
||||
import { render, screen, act, cleanup } from "jest-matrix-react";
|
||||
import { mocked, type Mocked } from "jest-mock";
|
||||
import {
|
||||
type MatrixClient,
|
||||
@@ -33,7 +32,6 @@ import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
|
||||
import { CallView as _CallView } from "../../../../../src/components/views/voip/CallView";
|
||||
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { CallStore } from "../../../../../src/stores/CallStore";
|
||||
import { Call, ConnectionState } from "../../../../../src/models/Call";
|
||||
|
||||
const CallView = wrapInMatrixClientContext(_CallView);
|
||||
|
||||
@@ -44,6 +42,8 @@ describe("CallView", () => {
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let alice: RoomMember;
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
|
||||
beforeEach(() => {
|
||||
useMockMediaDevices();
|
||||
@@ -63,117 +63,43 @@ describe("CallView", () => {
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
MockedCall.create(room, "1");
|
||||
const maybeCall = CallStore.instance.getCall(room.roomId);
|
||||
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(); // Unmount before we do any cleanup that might update the component
|
||||
call.destroy();
|
||||
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} waitForCall={false} skipLobby={skipLobby} role={role} />);
|
||||
render(<CallView room={room} resizing={false} skipLobby={skipLobby} role={role} onClose={() => {}} />);
|
||||
await act(() => Promise.resolve()); // Let effects settle
|
||||
};
|
||||
|
||||
describe("with an existing call", () => {
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
|
||||
beforeEach(() => {
|
||||
MockedCall.create(room, "1");
|
||||
const maybeCall = CallStore.instance.getCall(room.roomId);
|
||||
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup(); // Unmount before we do any cleanup that might update the component
|
||||
call.destroy();
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
|
||||
it("accepts an accessibility role", async () => {
|
||||
await renderView(undefined, "main");
|
||||
screen.getByRole("main");
|
||||
});
|
||||
|
||||
it("calls clean on mount", async () => {
|
||||
const cleanSpy = jest.spyOn(call, "clean");
|
||||
await renderView();
|
||||
expect(cleanSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
/**
|
||||
* 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 waitFor(() => expect(connectSpy).toHaveBeenCalled(), { interval: 1 });
|
||||
});
|
||||
it("accepts an accessibility role", async () => {
|
||||
await renderView(undefined, "main");
|
||||
screen.getByRole("main");
|
||||
});
|
||||
|
||||
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)!;
|
||||
it("calls clean on mount", async () => {
|
||||
const cleanSpy = jest.spyOn(call, "clean");
|
||||
await renderView();
|
||||
expect(cleanSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
it("updates the call's skipLobby parameter", async () => {
|
||||
await renderView(true);
|
||||
expect(call.widget.data?.skipLobby).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -235,16 +235,16 @@ describe("JitsiCall", () => {
|
||||
|
||||
({ 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) {
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
new CustomEvent("widgetapirequest", { detail: { data } }),
|
||||
);
|
||||
} else if (action === ElementWidgetActions.HangupCall) {
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
new CustomEvent("widgetapirequest", { detail: { data } }),
|
||||
);
|
||||
}
|
||||
return {};
|
||||
@@ -286,8 +286,6 @@ describe("JitsiCall", () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
const connect = call.start();
|
||||
expect(call.connectionState).toBe(ConnectionState.WidgetLoading);
|
||||
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||
await connect;
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
@@ -310,7 +308,6 @@ describe("JitsiCall", () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
const connect = call.start();
|
||||
expect(call.connectionState).toBe(ConnectionState.WidgetLoading);
|
||||
async function runTimers() {
|
||||
jest.advanceTimersByTime(500);
|
||||
jest.advanceTimersByTime(1000);
|
||||
@@ -356,18 +353,10 @@ describe("JitsiCall", () => {
|
||||
|
||||
call.on(CallEvent.ConnectionState, callback);
|
||||
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||
messaging.emit(`action:${ElementWidgetActions.Close}`, new CustomEvent("widgetapirequest", {}));
|
||||
await waitFor(() => {
|
||||
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
|
||||
call.off(CallEvent.ConnectionState, callback);
|
||||
@@ -497,10 +486,7 @@ describe("JitsiCall", () => {
|
||||
await call.start();
|
||||
await call.disconnect();
|
||||
expect(onConnectionState.mock.calls).toEqual([
|
||||
[ConnectionState.WidgetLoading, ConnectionState.Disconnected],
|
||||
[ConnectionState.Connecting, ConnectionState.WidgetLoading],
|
||||
[ConnectionState.Lobby, ConnectionState.Connecting],
|
||||
[ConnectionState.Connected, ConnectionState.Lobby],
|
||||
[ConnectionState.Connected, ConnectionState.Disconnected],
|
||||
[ConnectionState.Disconnecting, ConnectionState.Connected],
|
||||
[ConnectionState.Disconnected, ConnectionState.Disconnecting],
|
||||
]);
|
||||
@@ -634,7 +620,7 @@ describe("ElementCall", () => {
|
||||
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() {
|
||||
await new Promise<void>((r) => {
|
||||
setTimeout(() => r(), 400);
|
||||
@@ -653,9 +639,7 @@ describe("ElementCall", () => {
|
||||
jest.advanceTimersByTime(500);
|
||||
}
|
||||
sessionConnect();
|
||||
const promise = call.start();
|
||||
runTimers();
|
||||
await promise;
|
||||
await Promise.all([...(startWidget ? [call.start()] : []), runTimers()]);
|
||||
};
|
||||
const callDisconnectionProcedure: (call: ElementCall) => Promise<void> = async (call) => {
|
||||
async function sessionDisconnect() {
|
||||
@@ -683,6 +667,7 @@ describe("ElementCall", () => {
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.runOnlyPendingTimers();
|
||||
jest.useRealTimers();
|
||||
cleanUpClientRoomAndStores(client, room);
|
||||
});
|
||||
@@ -693,7 +678,7 @@ describe("ElementCall", () => {
|
||||
});
|
||||
|
||||
it("finds calls", async () => {
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
expect(Call.get(room)).toBeInstanceOf(ElementCall);
|
||||
Call.get(room)?.destroy();
|
||||
});
|
||||
@@ -728,7 +713,7 @@ describe("ElementCall", () => {
|
||||
};
|
||||
document.documentElement.style.fontSize = "12px";
|
||||
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
const call = Call.get(room);
|
||||
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 () => {
|
||||
// Test with the preference set to false
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
const call1 = Call.get(room);
|
||||
if (!(call1 instanceof ElementCall)) throw new Error("Failed to create call");
|
||||
|
||||
@@ -780,7 +765,7 @@ describe("ElementCall", () => {
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
const call = Call.get(room);
|
||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||
|
||||
@@ -798,7 +783,7 @@ describe("ElementCall", () => {
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
const call = Call.get(room);
|
||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||
|
||||
@@ -820,7 +805,7 @@ describe("ElementCall", () => {
|
||||
: originalGetValue(name, roomId, excludeDefault);
|
||||
}
|
||||
};
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
const call = Call.get(room);
|
||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||
|
||||
@@ -837,7 +822,7 @@ describe("ElementCall", () => {
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
const call = Call.get(room);
|
||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||
|
||||
@@ -857,7 +842,7 @@ describe("ElementCall", () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
|
||||
await ElementCall.create(room, true);
|
||||
ElementCall.create(room, true);
|
||||
const maybeCall = ElementCall.get(room);
|
||||
if (maybeCall === null) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
@@ -876,9 +861,6 @@ describe("ElementCall", () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
const connect = callConnectProcedure(call);
|
||||
|
||||
expect(call.connectionState).toBe(ConnectionState.WidgetLoading);
|
||||
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||
await connect;
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
@@ -903,10 +885,8 @@ describe("ElementCall", () => {
|
||||
await callConnectProcedure(call);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||
messaging.emit(`action:${ElementWidgetActions.Close}`, new CustomEvent("widgetapirequest", {}));
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Disconnected), { interval: 5 });
|
||||
});
|
||||
|
||||
@@ -986,9 +966,7 @@ describe("ElementCall", () => {
|
||||
await callConnectProcedure(call);
|
||||
await callDisconnectionProcedure(call);
|
||||
expect(onConnectionState.mock.calls).toEqual([
|
||||
[ConnectionState.WidgetLoading, ConnectionState.Disconnected],
|
||||
[ConnectionState.Connecting, ConnectionState.WidgetLoading],
|
||||
[ConnectionState.Connected, ConnectionState.Connecting],
|
||||
[ConnectionState.Connected, ConnectionState.Disconnected],
|
||||
[ConnectionState.Disconnecting, ConnectionState.Connected],
|
||||
[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 () => {
|
||||
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
await callConnectProcedure(Call.get(room) as ElementCall);
|
||||
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
|
||||
"application": "m.call",
|
||||
@@ -1081,7 +1059,7 @@ describe("ElementCall", () => {
|
||||
setRoomMembers(["@user:example.com", "@user2:example.com"]);
|
||||
|
||||
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
await callConnectProcedure(Call.get(room) as ElementCall);
|
||||
expect(sendEventSpy).toHaveBeenCalledWith("!1:example.org", "org.matrix.msc4075.call.notify", {
|
||||
"application": "m.call",
|
||||
@@ -1105,7 +1083,7 @@ describe("ElementCall", () => {
|
||||
|
||||
jest.spyOn(room, "getType").mockReturnValue(RoomType.UnstableCall);
|
||||
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
const maybeCall = ElementCall.get(room);
|
||||
if (maybeCall === null) throw new Error("Failed to create call");
|
||||
call = maybeCall;
|
||||
@@ -1144,7 +1122,7 @@ describe("ElementCall", () => {
|
||||
return roomSession;
|
||||
});
|
||||
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
const call = Call.get(room);
|
||||
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
|
||||
expect(call.session).toBe(roomSession);
|
||||
@@ -1163,12 +1141,12 @@ describe("ElementCall", () => {
|
||||
await callConnectProcedure(call);
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
// We want the call to be connecting after the hangup.
|
||||
waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connecting), { interval: 5 });
|
||||
messaging.emit(`action:${ElementWidgetActions.HangupCall}`, new CustomEvent("widgetapirequest", {}));
|
||||
messaging.emit(`action:${ElementWidgetActions.Close}`, new CustomEvent("widgetapirequest", {}));
|
||||
// We should now be able to reconnect without manually starting the widget
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await callConnectProcedure(call, false);
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected), { interval: 5 });
|
||||
});
|
||||
});
|
||||
describe("create call", () => {
|
||||
@@ -1180,7 +1158,7 @@ describe("ElementCall", () => {
|
||||
{ application: "m.call", callId: "" } as unknown as CallMembership,
|
||||
]);
|
||||
const sendEventSpy = jest.spyOn(room.client, "sendEvent");
|
||||
await ElementCall.create(room);
|
||||
ElementCall.create(room);
|
||||
expect(sendEventSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,10 +13,16 @@ import {
|
||||
RoomViewLifecycle,
|
||||
type ViewRoomOpts,
|
||||
} from "@matrix-org/react-sdk-module-api/lib/lifecycles/RoomViewLifecycle";
|
||||
import EventEmitter from "events";
|
||||
|
||||
import { RoomViewStore } from "../../../src/stores/RoomViewStore";
|
||||
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 { SlidingSyncManager } from "../../../src/SlidingSyncManager";
|
||||
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 SubmitAskToJoinPayload } from "../../../src/dispatcher/payloads/SubmitAskToJoinPayload";
|
||||
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");
|
||||
|
||||
@@ -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", () => {
|
||||
const mock = {
|
||||
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");
|
||||
|
||||
describe("RoomViewStore", function () {
|
||||
@@ -82,10 +112,12 @@ describe("RoomViewStore", function () {
|
||||
// we need to change the alias to ensure cache misses as the cache exists
|
||||
// through all tests.
|
||||
let alias = "#somealias2:aser.ver";
|
||||
const getRooms = jest.fn();
|
||||
const mockClient = getMockClientWithEventEmitter({
|
||||
joinRoom: jest.fn(),
|
||||
getRoom: jest.fn(),
|
||||
getRoomIdForAlias: jest.fn(),
|
||||
getRooms,
|
||||
isGuest: jest.fn(),
|
||||
getUserId: jest.fn().mockReturnValue(userId),
|
||||
getSafeUserId: jest.fn().mockReturnValue(userId),
|
||||
@@ -97,9 +129,18 @@ describe("RoomViewStore", function () {
|
||||
knockRoom: jest.fn(),
|
||||
leave: 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 room2 = new Room(roomId2, mockClient, userId);
|
||||
getRooms.mockReturnValue([room, room2]);
|
||||
|
||||
const viewCall = async (): Promise<void> => {
|
||||
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 () => {
|
||||
await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet());
|
||||
await viewCall();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user