Refactor element call lobby + skip lobby (#12057)

* Refactor ElementCall to use the widget lobby.
 - expose skip lobby
 - use the widget.data to build the widget url

Signed-off-by: Timo K <toger5@hotmail.de>

* Use shiftKey click to skip the lobby

Signed-off-by: Timo K <toger5@hotmail.de>

* remove Lobby component

Signed-off-by: Timo K <toger5@hotmail.de>

* update tests + remove EW lobby related tests

Signed-off-by: Timo K <toger5@hotmail.de>

* remove lobby device button tests

Signed-off-by: Timo K <toger5@hotmail.de>

* i18n

Signed-off-by: Timo K <toger5@hotmail.de>

* use voip participant label

Signed-off-by: Timo K <toger5@hotmail.de>

* update tests
Signed-off-by: Timo K <toger5@hotmail.de>

* fix rounded corners in pip

Signed-off-by: Timo K <toger5@hotmail.de>

* allow joining call in legacy room header (without banner)

Signed-off-by: Timo K <toger5@hotmail.de>

* Introduce new connection states for calls.
And use them for integrated lobby.

Signed-off-by: Timo K <toger5@hotmail.de>

* New room header call join
Fix broken top container element call.

Signed-off-by: Timo K <toger5@hotmail.de>

* i18n

Signed-off-by: Timo K <toger5@hotmail.de>

* Fix closing element call in lobby view.
(should destroy call if there the user never managed to connect (not clicked join in lobby)

Signed-off-by: Timo K <toger5@hotmail.de>

* all cases for connection state

Signed-off-by: Timo K <toger5@hotmail.de>

* add correct LiveContentSummary labels

Signed-off-by: Timo K <toger5@hotmail.de>

* Theme widget loading (no rounded corner)
destroy call when switching room while a call is loading.

Signed-off-by: Timo K <toger5@hotmail.de>

* temp

Signed-off-by: Timo K <toger5@hotmail.de>

* usei view room dispatcher instead of emitter

Signed-off-by: Timo K <toger5@hotmail.de>

* tidy up

Signed-off-by: Timo K <toger5@hotmail.de>

* returnToLobby + remove StartCallView

Signed-off-by: Timo K <toger5@hotmail.de>

* comment cleanup

Signed-off-by: Timo K <toger5@hotmail.de>

* disconnect ongoing calls before making widget sticky.

Signed-off-by: Timo K <toger5@hotmail.de>

* linter + jitsi as videoChannel

Signed-off-by: Timo K <toger5@hotmail.de>

* stickyPromise type

Signed-off-by: Timo K <toger5@hotmail.de>

* fix legacy call (jistsi, cisco, bbb) reopen
when clicking call button

Signed-off-by: Timo K <toger5@hotmail.de>

* fix tests and connect resolves

Signed-off-by: Timo K <toger5@hotmail.de>

* fix "waits for messaging when connecting" test

Signed-off-by: Timo K <toger5@hotmail.de>

* Allow to skip awaiting Call session events.
This option is used in tests to spare mocking the
events emitted when EC updates the room state

Signed-off-by: Timo K <toger5@hotmail.de>

* add sticky test

Signed-off-by: Timo K <toger5@hotmail.de>

* add test for looby tile rendering

Signed-off-by: Timo K <toger5@hotmail.de>

* fix flaky test

Signed-off-by: Timo K <toger5@hotmail.de>

* add reconnect after disconnect test (video room)

Signed-off-by: Timo K <toger5@hotmail.de>

* add shift click test to call toast

Signed-off-by: Timo K <toger5@hotmail.de>

* test for allowVoipWithNoMedia in widget url

Signed-off-by: Timo K <toger5@hotmail.de>

* fix e2e tests to search for the right element

Signed-off-by: Timo K <toger5@hotmail.de>

* destroy call after test so next test does not fail

Signed-off-by: Timo K <toger5@hotmail.de>

* new call test (connection failed)

Signed-off-by: Timo K <toger5@hotmail.de>

* reset to real timers

Signed-off-by: Timo K <toger5@hotmail.de>

* dont use skipSessionAwait for tests

Signed-off-by: Timo K <toger5@hotmail.de>

* code quality (sonar)

Signed-off-by: Timo K <toger5@hotmail.de>

* refactor call.disconnect tests (dont use skipSessionAwait)

Signed-off-by: Timo K <toger5@hotmail.de>

* miscellaneous cleanup

Signed-off-by: Timo K <toger5@hotmail.de>

* only send call notify after the call has been joined (not when just opening the lobby)

Signed-off-by: Timo K <toger5@hotmail.de>

* update call notify tests to expect notify on connect.
Not on widget creation.

Signed-off-by: Timo K <toger5@hotmail.de>

* Update playwright/e2e/room/room-header.spec.ts

Co-authored-by: Robin <robin@robin.town>

* Update src/components/views/voip/CallView.tsx

Co-authored-by: Robin <robin@robin.town>

* review
rename connect -> start
isVideoRoom not dependant on feature flags
rename allOtherCallsDisconnected -> disconnectAllOtherCalls

Signed-off-by: Timo K <toger5@hotmail.de>

* check for EC widget

Signed-off-by: Timo K <toger5@hotmail.de>

* dep array

Signed-off-by: Timo K <toger5@hotmail.de>

* rename in spyOn

Signed-off-by: Timo K <toger5@hotmail.de>

---------

Signed-off-by: Timo K <toger5@hotmail.de>
Co-authored-by: Robin <robin@robin.town>
This commit is contained in:
Timo 2024-01-29 17:06:12 +01:00 committed by GitHub
parent 3f7e21e08d
commit a370a5cfa4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 693 additions and 767 deletions

View file

@ -43,6 +43,7 @@ const RoomCallBannerInner: React.FC<RoomCallBannerProps> = ({ roomId, call }) =>
action: Action.ViewRoom,
room_id: roomId,
view_call: true,
skipLobby: "shiftKey" in ev ? ev.shiftKey : false,
metricsTrigger: undefined,
});
},

View file

@ -95,6 +95,11 @@ interface IProps {
movePersistedElement?: MutableRefObject<(() => void) | undefined>;
// An element to render after the iframe as an overlay
overlay?: ReactNode;
// If defined this async method will be called when the widget requests to become sticky.
// It will only become sticky once the returned promise resolves.
// This is useful because: Widget B is sticky. Making widget A sticky will kill widget B immediately.
// This promise allows to do Widget B related cleanup before Widget A becomes sticky. (e.g. hangup a Voip call)
stickyPromise?: () => Promise<void>;
}
interface IState {
@ -610,11 +615,11 @@ export default class AppTile extends React.Component<IProps, IState> {
"microphone; camera; encrypted-media; autoplay; display-capture; clipboard-write; " + "clipboard-read;";
const appTileBodyClass = classNames({
// We don't want mx_AppTileBody (rounded corners) for call widgets
"mx_AppTileBody": true,
"mx_AppTileBody--large": !this.props.miniMode,
"mx_AppTileBody--mini": this.props.miniMode,
"mx_AppTileBody--loading": this.state.loading,
// We don't want mx_AppTileBody (rounded corners) for call widgets
"mx_AppTileBody--call": this.props.app.type === WidgetType.CALL.preferred,
});
const appTileBodyStyles: CSSProperties = {};

View file

@ -131,12 +131,14 @@ const ActiveLoadedCallEvent = forwardRef<any, ActiveLoadedCallEventProps>(({ mxE
switch (connectionState) {
case ConnectionState.Disconnected:
return [_t("action|join"), "primary", connect];
case ConnectionState.Connecting:
return [_t("action|join"), "primary", null];
case ConnectionState.Connected:
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]);

View file

@ -143,16 +143,20 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
setBusy(false);
}, [setBusy, room]);
const startElementCall = useCallback(() => {
setBusy(true);
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: true,
metricsTrigger: undefined,
});
setBusy(false);
}, [setBusy, room]);
const startElementCall = useCallback(
(skipLobby: boolean) => {
setBusy(true);
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: true,
skipLobby: skipLobby,
metricsTrigger: undefined,
});
setBusy(false);
},
[setBusy, room],
);
const { onClick, tooltip, disabled } = useMemo(() => {
if (behavior instanceof DisabledWithReason) {
@ -173,7 +177,7 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
return {
onClick: async (ev: ButtonEvent): Promise<void> => {
ev.preventDefault();
startElementCall();
startElementCall("shiftKey" in ev ? ev.shiftKey : false);
},
disabled: false,
};
@ -202,7 +206,7 @@ const VideoCallButton: FC<VideoCallButtonProps> = ({ room, busy, setBusy, behavi
(ev: ButtonEvent) => {
ev.preventDefault();
closeMenu();
startElementCall();
startElementCall("shiftKey" in ev ? ev.shiftKey : false);
},
[closeMenu, startElementCall],
);
@ -305,7 +309,7 @@ const CallButtons: FC<CallButtonsProps> = ({ room }) => {
} else {
return makeVideoCallButton(new DisabledWithReason(_t("voip|disabled_no_perms_start_video_call")));
}
} else if (hasLegacyCall || hasJitsiWidget || hasGroupCall) {
} else if (hasLegacyCall || hasJitsiWidget) {
return (
<>
{makeVoiceCallButton(new DisabledWithReason(_t("voip|disabled_ongoing_call")))}

View file

@ -51,7 +51,7 @@ export const LiveContentSummary: FC<Props> = ({ type, text, active, participantC
{" • "}
<span
className="mx_LiveContentSummary_participants"
aria-label={_t("common|n_participants", { count: participantCount })}
aria-label={_t("voip|n_people_joined", { count: participantCount })}
>
{participantCount}
</span>

View file

@ -35,6 +35,14 @@ 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;

View file

@ -14,412 +14,59 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import React, { FC, ReactNode, useState, useContext, useEffect, useMemo, useRef, useCallback, AriaRole } from "react";
import classNames from "classnames";
import { logger } from "matrix-js-sdk/src/logger";
import { defer, IDeferred } from "matrix-js-sdk/src/utils";
import React, { FC, useContext, useEffect, AriaRole, useCallback } from "react";
import type { Room } from "matrix-js-sdk/src/matrix";
import type { ConnectionState } from "../../../models/Call";
import { Call, CallEvent, ElementCall, isConnected } from "../../../models/Call";
import {
useCall,
useConnectionState,
useJoinCallButtonDisabledTooltip,
useParticipatingMembers,
} from "../../../hooks/useCall";
import { Call, ConnectionState, ElementCall } from "../../../models/Call";
import { useCall } from "../../../hooks/useCall";
import MatrixClientContext from "../../../contexts/MatrixClientContext";
import AppTile from "../elements/AppTile";
import { _t } from "../../../languageHandler";
import { useAsyncMemo } from "../../../hooks/useAsyncMemo";
import MediaDeviceHandler, { IMediaDevices } from "../../../MediaDeviceHandler";
import { CallStore } from "../../../stores/CallStore";
import IconizedContextMenu, {
IconizedContextMenuOption,
IconizedContextMenuOptionList,
} from "../context_menus/IconizedContextMenu";
import { aboveRightOf, ContextMenuButton, useContextMenu } from "../../structures/ContextMenu";
import { Alignment } from "../elements/Tooltip";
import { ButtonEvent } from "../elements/AccessibleButton";
import AccessibleTooltipButton from "../elements/AccessibleTooltipButton";
import FacePile from "../elements/FacePile";
import MemberAvatar from "../avatars/MemberAvatar";
interface DeviceButtonProps {
kind: string;
devices: MediaDeviceInfo[];
setDevice: (device: MediaDeviceInfo) => void;
deviceListLabel: string;
muted: boolean;
disabled: boolean;
toggle: () => void;
unmutedTitle: string;
mutedTitle: string;
}
const DeviceButton: FC<DeviceButtonProps> = ({
kind,
devices,
setDevice,
deviceListLabel,
muted,
disabled,
toggle,
unmutedTitle,
mutedTitle,
}) => {
const [showMenu, buttonRef, openMenu, closeMenu] = useContextMenu();
const selectDevice = useCallback(
(device: MediaDeviceInfo) => {
setDevice(device);
closeMenu();
},
[setDevice, closeMenu],
);
let contextMenu: JSX.Element | null = null;
if (showMenu) {
const buttonRect = buttonRef.current!.getBoundingClientRect();
contextMenu = (
<IconizedContextMenu {...aboveRightOf(buttonRect, undefined, 10)} onFinished={closeMenu}>
<IconizedContextMenuOptionList>
{devices.map((d) => (
<IconizedContextMenuOption key={d.deviceId} label={d.label} onClick={() => selectDevice(d)} />
))}
</IconizedContextMenuOptionList>
</IconizedContextMenu>
);
}
if (!devices.length) return null;
return (
<div
className={classNames("mx_CallView_deviceButtonWrapper", {
mx_CallView_deviceButtonWrapper_muted: muted,
})}
>
<AccessibleTooltipButton
className={`mx_CallView_deviceButton mx_CallView_deviceButton_${kind}`}
ref={buttonRef}
title={muted ? mutedTitle : unmutedTitle}
alignment={Alignment.Top}
onClick={toggle}
disabled={disabled}
/>
{devices.length > 1 ? (
<ContextMenuButton
className="mx_CallView_deviceListButton"
onClick={openMenu}
isExpanded={showMenu}
label={deviceListLabel}
disabled={disabled}
/>
) : null}
{contextMenu}
</div>
);
};
const MAX_FACES = 8;
interface LobbyProps {
room: Room;
connect: () => Promise<void>;
joinCallButtonDisabledTooltip?: string;
children?: ReactNode;
}
export const Lobby: FC<LobbyProps> = ({ room, joinCallButtonDisabledTooltip, connect, children }) => {
const [connecting, setConnecting] = useState(false);
const me = useMemo(() => room.getMember(room.myUserId)!, [room]);
const videoRef = useRef<HTMLVideoElement>(null);
const [videoInputId, setVideoInputId] = useState<string>(() => MediaDeviceHandler.getVideoInput());
const [audioMuted, setAudioMuted] = useState(() => MediaDeviceHandler.startWithAudioMuted);
const [videoMuted, setVideoMuted] = useState(() => MediaDeviceHandler.startWithVideoMuted);
const toggleAudio = useCallback(() => {
MediaDeviceHandler.startWithAudioMuted = !audioMuted;
setAudioMuted(!audioMuted);
}, [audioMuted, setAudioMuted]);
const toggleVideo = useCallback(() => {
MediaDeviceHandler.startWithVideoMuted = !videoMuted;
setVideoMuted(!videoMuted);
}, [videoMuted, setVideoMuted]);
// In case we can not fetch media devices we should mute the devices
const handleMediaDeviceFailing = (message: string): void => {
MediaDeviceHandler.startWithAudioMuted = true;
MediaDeviceHandler.startWithVideoMuted = true;
logger.warn(message);
};
const [videoStream, audioInputs, videoInputs] = useAsyncMemo(
async (): Promise<[MediaStream | null, MediaDeviceInfo[], MediaDeviceInfo[]]> => {
let devices: IMediaDevices | undefined;
try {
devices = await MediaDeviceHandler.getDevices();
if (devices === undefined) {
handleMediaDeviceFailing("Could not access devices!");
return [null, [], []];
}
} catch (error) {
handleMediaDeviceFailing(`Unable to get Media Devices: ${error}`);
return [null, [], []];
}
// We get the preview stream before requesting devices: this is because
// we need (in some browsers) an active media stream in order to get
// non-blank labels for the devices.
let stream: MediaStream | null = null;
try {
if (devices!.audioinput.length > 0) {
// Holding just an audio stream will be enough to get us all device labels, so
// if video is muted, don't bother requesting video.
stream = await navigator.mediaDevices.getUserMedia({
audio: true,
video: !videoMuted && devices!.videoinput.length > 0 && { deviceId: videoInputId },
});
} else if (devices!.videoinput.length > 0) {
// We have to resort to a video stream, even if video is supposed to be muted.
stream = await navigator.mediaDevices.getUserMedia({ video: { deviceId: videoInputId } });
}
} catch (e) {
logger.warn(`Failed to get stream for device ${videoInputId}`, e);
handleMediaDeviceFailing(`Have access to Device list but unable to read from Media Devices`);
}
// Refresh the devices now that we hold a stream
if (stream !== null) devices = await MediaDeviceHandler.getDevices();
// If video is muted, we don't actually want the stream, so we can get rid of it now.
if (videoMuted) {
stream?.getTracks().forEach((t) => t.stop());
stream = null;
}
return [stream, devices?.audioinput ?? [], devices?.videoinput ?? []];
},
[videoInputId, videoMuted],
[null, [], []],
);
const setAudioInput = useCallback((device: MediaDeviceInfo) => {
MediaDeviceHandler.instance.setAudioInput(device.deviceId);
}, []);
const setVideoInput = useCallback((device: MediaDeviceInfo) => {
MediaDeviceHandler.instance.setVideoInput(device.deviceId);
setVideoInputId(device.deviceId);
}, []);
useEffect(() => {
if (videoStream) {
const videoElement = videoRef.current!;
videoElement.srcObject = videoStream;
videoElement.play();
return () => {
videoStream.getTracks().forEach((track) => track.stop());
videoElement.srcObject = null;
};
}
}, [videoStream]);
const onConnectClick = useCallback(
async (ev: ButtonEvent): Promise<void> => {
ev.preventDefault();
setConnecting(true);
try {
await connect();
} catch (e) {
logger.error(e);
setConnecting(false);
}
},
[connect, setConnecting],
);
return (
<div className="mx_CallView_lobby">
{children}
<div className="mx_CallView_preview">
<MemberAvatar key={me.userId} member={me} size="200px" resizeMethod="scale" />
<video
ref={videoRef}
style={{ visibility: videoMuted ? "hidden" : undefined }}
muted
playsInline
disablePictureInPicture
/>
<div className="mx_CallView_controls">
<DeviceButton
kind="audio"
devices={audioInputs}
setDevice={setAudioInput}
deviceListLabel={_t("voip|audio_devices")}
muted={audioMuted}
disabled={connecting}
toggle={toggleAudio}
unmutedTitle={_t("voip|disable_microphone")}
mutedTitle={_t("voip|enable_microphone")}
/>
<DeviceButton
kind="video"
devices={videoInputs}
setDevice={setVideoInput}
deviceListLabel={_t("voip|video_devices")}
muted={videoMuted}
disabled={connecting}
toggle={toggleVideo}
unmutedTitle={_t("voip|disable_camera")}
mutedTitle={_t("voip|enable_camera")}
/>
</div>
</div>
<AccessibleTooltipButton
className="mx_CallView_connectButton"
kind="primary"
disabled={connecting || joinCallButtonDisabledTooltip !== undefined}
onClick={onConnectClick}
label={_t("action|join")}
tooltip={connecting ? _t("voip|connecting") : joinCallButtonDisabledTooltip}
alignment={Alignment.Bottom}
/>
</div>
);
};
interface StartCallViewProps {
room: Room;
resizing: boolean;
call: Call | null;
setStartingCall: (value: boolean) => void;
role?: AriaRole;
}
const StartCallView: FC<StartCallViewProps> = ({ room, resizing, call, setStartingCall, role }) => {
const cli = useContext(MatrixClientContext);
// Since connection has to be split across two different callbacks, we
// create a promise to communicate the results back to the caller
const connectDeferredRef = useRef<IDeferred<void>>();
if (connectDeferredRef.current === undefined) {
connectDeferredRef.current = defer();
}
const connectDeferred = connectDeferredRef.current!;
// Since the call might be null, we have to track connection state by hand.
// The alternative would be to split this component in two depending on
// whether we've received the call, so we could use the useConnectionState
// hook, but then React would remount the lobby when the call arrives.
const [connected, setConnected] = useState(() => call !== null && isConnected(call.connectionState));
useEffect(() => {
if (call !== null) {
const onConnectionState = (state: ConnectionState): void => setConnected(isConnected(state));
call.on(CallEvent.ConnectionState, onConnectionState);
return () => {
call.off(CallEvent.ConnectionState, onConnectionState);
};
}
}, [call]);
const connect = useCallback(async (): Promise<void> => {
setStartingCall(true);
await ElementCall.create(room);
await connectDeferred.promise;
}, [room, setStartingCall, connectDeferred]);
useEffect(() => {
(async (): Promise<void> => {
// If the call was successfully started, connect automatically
if (call !== null) {
try {
// Disconnect from any other active calls first, since we don't yet support holding
await Promise.all([...CallStore.instance.activeCalls].map((call) => call.disconnect()));
await call.connect();
connectDeferred.resolve();
} catch (e) {
connectDeferred.reject(e);
}
}
})();
}, [call, connectDeferred]);
return (
<div className="mx_CallView" role={role}>
{connected ? null : <Lobby room={room} connect={connect} />}
{call !== null && (
<AppTile
app={call.widget}
room={room}
userId={cli.credentials.userId!}
creatorUserId={call.widget.creatorUserId}
waitForIframeLoad={call.widget.waitForIframeLoad}
showMenubar={false}
pointerEvents={resizing ? "none" : undefined}
/>
)}
</div>
);
};
import { SdkContextClass } from "../../../contexts/SDKContext";
interface JoinCallViewProps {
room: Room;
resizing: boolean;
call: Call;
skipLobby?: boolean;
role?: AriaRole;
}
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, role }) => {
const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, skipLobby, role }) => {
const cli = useContext(MatrixClientContext);
const connected = isConnected(useConnectionState(call));
const members = useParticipatingMembers(call);
const joinCallButtonDisabledTooltip = useJoinCallButtonDisabledTooltip(call);
const connect = useCallback(async (): Promise<void> => {
// Disconnect from any other active calls first, since we don't yet support holding
await Promise.all([...CallStore.instance.activeCalls].map((call) => call.disconnect()));
await call.connect();
}, [call]);
// We'll take this opportunity to tidy up our room state
useEffect(() => {
// We'll take this opportunity to tidy up our room state
call.clean();
}, [call]);
let lobby: JSX.Element | null = null;
if (!connected) {
let facePile: JSX.Element | null = null;
if (members.length) {
const shownMembers = members.slice(0, MAX_FACES);
const overflow = members.length > shownMembers.length;
useEffect(() => {
// Always update the widget data so that we don't ignore "skipLobby" accidentally.
call.widget.data ??= {};
call.widget.data.skipLobby = skipLobby;
}, [call.widget, skipLobby]);
facePile = (
<div className="mx_CallView_participants">
{_t("voip|n_people_joined", { count: members.length })}
<FacePile members={shownMembers} size="24px" overflow={overflow} />
</div>
);
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();
}
lobby = (
<Lobby
room={room}
connect={connect}
joinCallButtonDisabledTooltip={joinCallButtonDisabledTooltip ?? undefined}
>
{facePile}
</Lobby>
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.
const calls = [...CallStore.instance.activeCalls].filter(
(call) => SdkContextClass.instance.roomViewStore.getRoomId() !== call.roomId,
);
}
await Promise.all(calls.map(async (call) => await call.disconnect()));
}, []);
return (
<div className="mx_CallView" role={role}>
{lobby}
{/* We render the widget even if we're disconnected, so it stays loaded */}
<div className="mx_CallView">
<AppTile
app={call.widget}
room={room}
@ -428,6 +75,7 @@ const JoinCallView: FC<JoinCallViewProps> = ({ room, resizing, call, role }) =>
waitForIframeLoad={call.widget.waitForIframeLoad}
showMenubar={false}
pointerEvents={resizing ? "none" : undefined}
stickyPromise={disconnectAllOtherCalls}
/>
</div>
);
@ -441,19 +89,21 @@ interface CallViewProps {
* button will create a call if there isn't already one.
*/
waitForCall: boolean;
skipLobby?: boolean;
role?: AriaRole;
}
export const CallView: FC<CallViewProps> = ({ room, resizing, waitForCall, role }) => {
export const CallView: FC<CallViewProps> = ({ room, resizing, waitForCall, skipLobby, role }) => {
const call = useCall(room.roomId);
const [startingCall, setStartingCall] = useState(false);
if (call === null || startingCall) {
if (waitForCall) return null;
return (
<StartCallView room={room} resizing={resizing} call={call} setStartingCall={setStartingCall} role={role} />
);
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} role={role} />;
return <JoinCallView room={room} resizing={resizing} call={call} skipLobby={skipLobby} role={role} />;
}
};