2022-05-04 17:09:48 +01:00
/*
2024-09-06 10:22:13 +02:00
Copyright 2022-2024 New Vector Ltd.
2022-05-04 17:09:48 +01:00
2025-02-18 17:59:58 +00:00
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
2024-09-06 10:22:13 +02:00
Please see LICENSE in the repository root for full details.
2022-05-04 17:09:48 +01:00
*/
2025-02-26 17:20:30 +07:00
import {
type FC ,
type ReactNode ,
useCallback ,
useEffect ,
useMemo ,
useState ,
} from "react" ;
2026-06-10 18:48:16 +02:00
import {
type MatrixClient ,
JoinRule ,
type Room ,
UnsupportedStickyEventsEndpointError ,
} from "matrix-js-sdk" ;
2024-04-23 15:15:13 +02:00
import {
2025-03-07 17:27:04 +01:00
Room as LivekitRoom ,
2024-04-23 15:15:13 +02:00
isE2EESupported as isE2EESupportedBrowser ,
} from "livekit-client" ;
2025-03-13 13:58:43 +01:00
import { logger } from "matrix-js-sdk/lib/logger" ;
2025-03-07 17:27:04 +01:00
import {
MatrixRTCSessionEvent ,
type MatrixRTCSession ,
2025-03-13 13:58:43 +01:00
} from "matrix-js-sdk/lib/matrixrtc" ;
2025-01-06 18:00:20 +01:00
import { useNavigate } from "react-router-dom" ;
2022-08-02 00:46:16 +02:00
2026-09-02 13:03:23 +02:00
import { type JoinCallData } from "../widget" ;
2022-01-05 15:35:12 -08:00
import { LobbyView } from "./LobbyView" ;
2024-12-11 09:27:55 +00:00
import { type MatrixInfo } from "./VideoPreview" ;
2022-01-05 15:35:12 -08:00
import { CallEndedView } from "./CallEndedView" ;
2023-03-01 13:47:36 +01:00
import { PosthogAnalytics } from "../analytics/PosthogAnalytics" ;
2023-05-26 20:41:32 +02:00
import { useProfile } from "../profile/useProfile" ;
2024-08-27 09:45:39 -04:00
import { findDeviceByName } from "../utils/media" ;
2025-02-26 14:53:15 +01:00
import { ActiveCall } from "./InCallView" ;
2025-08-28 17:40:35 +02:00
import { type MuteStates } from "../state/MuteStates" ;
2025-06-20 12:37:25 -04:00
import { useMediaDevices } from "../MediaDevicesContext" ;
2023-08-16 18:41:27 +01:00
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships" ;
2025-06-04 22:51:13 +02:00
import {
saveKeyForRoom ,
useRoomEncryptionSystem ,
} from "../e2ee/sharedKeyManagement" ;
2023-09-08 15:39:10 -04:00
import { useRoomAvatar } from "./useRoomAvatar" ;
import { useRoomName } from "./useRoomName" ;
import { useJoinRule } from "./useJoinRule" ;
2023-09-27 15:17:04 -04:00
import { InviteModal } from "./InviteModal" ;
2026-09-02 10:31:48 +02:00
import { HeaderStyle , type UrlParams , useUrlParams } from "../UrlParams" ;
2023-10-30 17:06:59 +00:00
import { E2eeType } from "../e2ee/e2eeType" ;
2024-12-12 07:33:47 +00:00
import { useAudioContext } from "../useAudioContext" ;
2026-06-02 13:56:16 +02:00
import {
callEventAudioSounds ,
type CallEventSounds ,
} from "./CallEventAudioRenderer" ;
2024-12-12 07:33:47 +00:00
import { useLatest } from "../useLatest" ;
2025-02-28 12:17:28 +01:00
import {
2025-06-13 00:04:13 -04:00
ConnectionLostError ,
2025-03-05 20:50:19 +01:00
E2EENotSupportedError ,
2025-02-28 12:17:28 +01:00
ElementCallError ,
2026-06-10 18:48:16 +02:00
StickyEventsRequiredError ,
2025-03-05 20:50:19 +01:00
UnknownCallError ,
2025-02-28 12:17:28 +01:00
} from "../utils/errors.ts" ;
2025-03-05 20:50:19 +01:00
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx" ;
2025-03-07 17:27:04 +01:00
import { useTypedEventEmitter } from "../useEvents" ;
2025-05-16 11:32:32 +02:00
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts" ;
2025-06-26 05:08:57 -04:00
import { useAppBarTitle } from "../AppBar.tsx" ;
2025-06-18 18:33:35 -04:00
import { useBehavior } from "../useBehavior.ts" ;
2026-09-01 18:54:27 +02:00
import { useRootElement } from "../RootElementContext.ts" ;
2026-09-02 13:03:23 +02:00
import { useHostBridge } from "../HostBridge.ts" ;
2026-09-08 15:07:34 +02:00
import { useMuteStates } from "../state/useMuteStates.ts" ;
2022-09-09 02:10:45 -04:00
2025-08-28 17:40:35 +02:00
/**
* If there already are this many participants in the call, we automatically mute
* the user.
*/
export const MUTE_PARTICIPANT_COUNT = 8 ;
2022-08-02 00:46:16 +02:00
declare global {
interface Window {
2023-08-16 18:41:27 +01:00
rtcSession? : MatrixRTCSession ;
2022-08-02 00:46:16 +02:00
}
}
2022-09-09 02:04:53 -04:00
2022-08-02 00:46:16 +02:00
interface Props {
2026-09-08 15:07:34 +02:00
/** The client to place the call with. */
2022-08-02 00:46:16 +02:00
client : MatrixClient ;
2026-09-08 15:07:34 +02:00
/** The call to join. */
rtcSession : MatrixRTCSession ;
/**
* Whether the user is signed in as a guest, and so should be offered the
* chance to create an account when the call ends.
*/
2022-08-02 00:46:16 +02:00
isPasswordlessUser : boolean ;
2026-09-08 15:07:34 +02:00
/** Whether to keep the user in this call rather than letting them navigate. */
2023-09-18 20:47:47 -04:00
confineToRoom : boolean ;
2026-09-08 15:07:34 +02:00
/** Whether to wait for the host to ask us to join. */
2025-09-11 14:57:26 +01:00
preload : UrlParams [ "preload" ];
2026-09-08 15:07:34 +02:00
/** Whether to enter the call directly, without showing the lobby first. */
2025-09-11 14:57:26 +01:00
skipLobby : UrlParams [ "skipLobby" ];
2026-09-08 15:07:34 +02:00
}
/**
* A call, from start to finish.
*
* This owns the whole lifecycle of being in a call: the lobby, where the user
* checks their camera and microphone before joining; the call itself; and the
* screen shown once it has ended. Not every call has every stage — the lobby
* is skipped when the user is put straight into the call, or when the host
* wants to say when to join; and after the call there may be a post-call
* screen, a return to the lobby, or nothing, depending on whether the host
* decides what comes next. The view decides which stages apply from the
* parameters it was started with and from what the host bridge says.
*
* It owns nothing about how Element Call came to be showing a call: no
* routing, no authentication, no resolving of room aliases. Those belong to
* whatever is hosting it — the standalone app's own shell, or an application
* embedding Element Call as a component. Both render this.
*/
export const CallView : FC < Props > = ( props ) : ReactNode => {
// Whether the user is in the call is the call's own business, not its host's.
// Held here rather than below so that it survives the mute state being
// rebuilt.
const [ joined , setJoined ] = useState ( false );
const muteStates = useMuteStates ();
if ( muteStates === null ) return null ;
return (
< LoadedCallView
{ ...props }
joined = { joined }
setJoined = { setJoined }
muteStates = { muteStates }
/>
);
};
interface LoadedProps extends Props {
2025-08-27 15:06:14 +02:00
joined : boolean ;
setJoined : ( value : boolean ) => void ;
2024-04-23 15:15:13 +02:00
muteStates : MuteStates ;
2022-08-02 00:46:16 +02:00
}
2022-09-09 02:04:53 -04:00
2026-09-08 15:07:34 +02:00
/** {@link CallView}, once it has the mute state everything below needs. */
const LoadedCallView : FC < LoadedProps > = ({
2022-01-05 15:35:12 -08:00
client ,
isPasswordlessUser ,
2023-09-18 20:47:47 -04:00
confineToRoom ,
2022-09-09 02:10:45 -04:00
preload ,
2023-10-25 13:49:18 +02:00
skipLobby ,
2023-08-16 18:41:27 +01:00
rtcSession ,
2025-08-27 15:06:14 +02:00
joined ,
setJoined ,
2024-04-23 15:15:13 +02:00
muteStates ,
2023-09-22 18:05:13 -04:00
}) => {
2025-03-21 14:59:27 -04:00
// Used to thread through any errors that occur outside the error boundary
const [ externalError , setExternalError ] = useState < ElementCallError | null >(
null ,
);
2023-08-16 18:41:27 +01:00
const memberships = useMatrixRTCSessionMemberships ( rtcSession );
2026-09-01 18:54:27 +02:00
const rootElement = useRootElement ();
2026-09-02 13:03:23 +02:00
const hostBridge = useHostBridge ();
// A host that can close us is a host that decides when we stop existing, so
// we neither show our own post-call screens nor assume we have time to
2026-09-08 15:38:59 +02:00
// finish what we are doing. (Whose account the user's is, by contrast, is
// stated outright: see `HostBridge.supportsProfileChanges`.)
2026-09-02 13:03:23 +02:00
const hostControlsLifetime = hostBridge . close !== undefined ;
2025-05-16 11:32:32 +02:00
2025-06-18 18:33:35 -04:00
const muteAllAudio = useBehavior ( muteAllAudio$ );
2024-12-12 07:33:47 +00:00
const leaveSoundContext = useLatest (
2026-06-02 13:56:16 +02:00
useAudioContext < CallEventSounds >({
2024-12-12 07:33:47 +00:00
sounds : callEventAudioSounds ,
latencyHint : "interactive" ,
2025-05-13 21:11:12 +02:00
muted : muteAllAudio ,
2024-12-12 07:33:47 +00:00
}),
);
2024-07-18 18:14:29 +02:00
// This should use `useEffectEvent` (only available in experimental versions)
2024-04-23 15:15:13 +02:00
useEffect (() => {
2024-07-18 18:14:29 +02:00
if ( memberships . length >= MUTE_PARTICIPANT_COUNT )
2025-08-28 18:41:13 +02:00
muteStates . audio . setEnabled$ . value ? .( false );
2024-07-18 18:14:29 +02:00
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
2024-04-23 15:15:13 +02:00
2025-05-14 18:41:22 +02:00
useEffect (() => {
2026-09-08 15:07:34 +02:00
logger . info ( "[Lifecycle] CallView Component mounted" );
2025-05-14 18:41:22 +02:00
return () : void => {
2026-09-08 15:07:34 +02:00
logger . info ( "[Lifecycle] CallView Component unmounted" );
2025-05-14 18:41:22 +02:00
};
}, []);
2025-06-25 19:38:51 +02:00
// This CSS is the only way we could find to not make element call scroll for
// viewport sizes smaller than 122px width. (It is actually this exact number: 122px
// tested on different devices...)
useEffect (() => {
2026-09-01 18:54:27 +02:00
rootElement . classList . add ( "no-scroll-body" );
2025-06-25 19:38:51 +02:00
return () : void => {
2026-09-01 18:54:27 +02:00
rootElement . classList . remove ( "no-scroll-body" );
2025-06-25 19:38:51 +02:00
};
2026-09-01 18:54:27 +02:00
}, [ rootElement ]);
2025-06-25 19:38:51 +02:00
2022-01-05 15:35:12 -08:00
useEffect (() => {
2023-08-16 18:41:27 +01:00
window . rtcSession = rtcSession ;
2024-06-04 11:20:25 -04:00
return () : void => {
2023-08-16 18:41:27 +01:00
delete window . rtcSession ;
2022-09-09 02:10:45 -04:00
};
2023-08-16 18:41:27 +01:00
}, [ rtcSession ]);
2022-07-08 20:55:18 +01:00
2025-11-20 14:42:12 +01:00
// TODO move this into the callViewModel LocalMembership.ts
2025-12-02 19:40:08 +01:00
// We might actually not need this at all. Since we get into fatalError on those errors already?
2025-03-07 17:27:04 +01:00
useTypedEventEmitter (
rtcSession ,
MatrixRTCSessionEvent . MembershipManagerError ,
2026-06-10 18:48:16 +02:00
( error ) => {
// When matrix_rtc_mode=matrix_2_0 is in effect but the homeserver does
// not advertise MSC4354 (sticky events), the SDK throws an
// `UnsupportedStickyEventsEndpointError`. The MembershipManager
// scheduler wraps it and exposes the original via `.cause`.
if (
error instanceof Error &&
error . cause instanceof UnsupportedStickyEventsEndpointError
) {
setExternalError ( new StickyEventsRequiredError ());
} else {
setExternalError ( new ConnectionLostError ());
}
},
2025-03-07 17:27:04 +01:00
);
2026-06-10 18:48:16 +02:00
2024-09-11 01:27:02 -04:00
useEffect (() => {
// Sanity check the room object
if ( client . getRoom ( rtcSession . room . roomId ) !== rtcSession . room )
logger . warn (
`We've ended up with multiple rooms for the same ID ( ${ rtcSession . room . roomId } ). This indicates a bug in the group call loading code, and may lead to incomplete room state.` ,
);
}, [ client , rtcSession . room ]);
2025-03-07 17:27:04 +01:00
const room = rtcSession . room as Room ;
2023-05-26 20:41:32 +02:00
const { displayName , avatarUrl } = useProfile ( client );
2025-03-07 17:27:04 +01:00
const roomName = useRoomName ( room );
const roomAvatar = useRoomAvatar ( room );
2025-08-08 17:15:47 +02:00
const {
perParticipantE2EE ,
returnToLobby ,
password : passwordFromUrl ,
2026-04-10 17:01:56 +02:00
header ,
2025-08-08 17:15:47 +02:00
} = useUrlParams ();
2025-03-07 17:27:04 +01:00
const e2eeSystem = useRoomEncryptionSystem ( room . roomId );
2025-06-04 22:51:13 +02:00
// Save the password once we start the groupCallView
useEffect (() => {
if ( passwordFromUrl ) saveKeyForRoom ( room . roomId , passwordFromUrl );
}, [ passwordFromUrl , room . roomId ]);
2025-06-26 05:08:57 -04:00
useAppBarTitle ( roomName );
2023-09-08 15:39:10 -04:00
2023-07-04 18:46:27 +01:00
const matrixInfo = useMemo (() : MatrixInfo => {
return {
2023-08-31 15:46:09 +02:00
userId : client.getUserId () ! ,
2023-07-04 18:46:27 +01:00
displayName : displayName ! ,
avatarUrl : avatarUrl ! ,
2025-03-07 17:27:04 +01:00
roomId : room.roomId ,
2023-09-08 15:39:10 -04:00
roomName ,
2025-03-07 17:27:04 +01:00
roomAlias : room.getCanonicalAlias (),
2023-09-08 15:39:10 -04:00
roomAvatar ,
2024-04-23 15:15:13 +02:00
e2eeSystem ,
2023-07-04 18:46:27 +01:00
};
2025-03-07 17:27:04 +01:00
}, [ client , displayName , avatarUrl , roomName , room , roomAvatar , e2eeSystem ]);
2023-09-08 15:39:10 -04:00
2023-09-27 18:12:04 -04:00
// Count each member only once, regardless of how many devices they use
const participantCount = useMemo (
2025-10-14 12:07:51 -04:00
() => new Set < string >( memberships . map (( m ) => m . userId ! )). size ,
2023-10-11 10:42:04 -04:00
[ memberships ],
2023-09-27 18:12:04 -04:00
);
2023-05-26 20:41:32 +02:00
2025-06-20 12:37:25 -04:00
const mediaDevices = useMediaDevices ();
2025-02-24 17:45:40 +07:00
const latestMuteStates = useLatest ( muteStates );
2023-08-02 15:29:37 -04:00
2025-03-10 15:20:51 +01:00
const enterRTCSessionOrError = useCallback (
2025-04-11 10:07:50 +02:00
async ( rtcSession : MatrixRTCSession ) : Promise < void > => {
2025-03-10 15:20:51 +01:00
try {
2025-08-27 15:06:14 +02:00
setJoined ( true );
// TODO-MULTI-SFU what to do with error handling now that we don't use this function?
2025-09-22 14:17:38 +02:00
// @BillCarsonFr
2025-03-10 15:20:51 +01:00
} catch ( e ) {
if ( e instanceof ElementCallError ) {
2025-03-21 14:59:27 -04:00
setExternalError ( e );
2025-03-10 15:20:51 +01:00
} else {
logger . error ( `Unknown Error while entering RTC session` , e );
const error = new UnknownCallError (
e instanceof Error ? e : new Error ( "Unknown error" , { cause : e }),
);
2025-03-21 14:59:27 -04:00
setExternalError ( error );
2025-03-10 15:20:51 +01:00
}
2025-02-26 14:55:03 +01:00
}
2025-08-27 15:06:14 +02:00
return Promise . resolve ();
2025-03-10 15:20:51 +01:00
},
2025-08-27 15:06:14 +02:00
[ setJoined ],
2025-03-10 15:20:51 +01:00
);
2025-02-26 14:55:03 +01:00
2022-09-09 02:10:45 -04:00
useEffect (() => {
2024-11-28 18:05:12 +01:00
const defaultDeviceSetup = async ({
audioInput ,
videoInput ,
} : JoinCallData ) : Promise < void > => {
2023-10-25 13:49:18 +02:00
// XXX: I think this is broken currently - LiveKit *won't* request
// permissions and give you device names unless you specify a kind, but
// here we want all kinds of devices. This needs a fix in livekit-client
// for the following name-matching logic to do anything useful.
2025-03-07 17:27:04 +01:00
const devices = await LivekitRoom . getLocalDevices ( undefined , true );
2024-11-28 18:05:12 +01:00
if ( audioInput ) {
2024-09-10 09:49:35 +02:00
const deviceId = findDeviceByName ( audioInput , "audioinput" , devices );
2023-10-25 13:49:18 +02:00
if ( ! deviceId ) {
logger . warn ( "Unknown audio input: " + audioInput );
2024-11-28 18:05:12 +01:00
// override the default mute state
2025-08-28 18:41:13 +02:00
latestMuteStates . current ! . audio . setEnabled$ . value ? .( false );
2023-08-02 15:29:37 -04:00
} else {
2023-10-25 13:49:18 +02:00
logger . debug (
`Found audio input ID ${ deviceId } for name ${ audioInput } ` ,
2023-06-23 14:17:51 -04:00
);
2025-06-20 12:37:25 -04:00
mediaDevices . audioInput . select ( deviceId );
2023-06-23 14:17:51 -04:00
}
2023-10-25 13:49:18 +02:00
}
2024-11-28 18:05:12 +01:00
if ( videoInput ) {
2024-09-10 09:49:35 +02:00
const deviceId = findDeviceByName ( videoInput , "videoinput" , devices );
2023-10-25 13:49:18 +02:00
if ( ! deviceId ) {
logger . warn ( "Unknown video input: " + videoInput );
2024-11-28 18:05:12 +01:00
// override the default mute state
2025-08-28 18:41:13 +02:00
latestMuteStates . current ! . video . setEnabled$ . value ? .( false );
2023-08-02 15:29:37 -04:00
} else {
2023-10-25 13:49:18 +02:00
logger . debug (
`Found video input ID ${ deviceId } for name ${ videoInput } ` ,
2023-06-23 14:17:51 -04:00
);
2025-06-20 12:37:25 -04:00
mediaDevices . videoInput . select ( deviceId );
2023-06-23 14:17:51 -04:00
}
2023-10-25 13:49:18 +02:00
}
};
2024-01-26 10:03:08 +01:00
2024-11-11 18:30:15 +01:00
if ( skipLobby ) {
2026-09-02 13:03:23 +02:00
// `preload` is only ever set when we have a host to be preloaded by.
if ( preload ) {
2025-09-11 14:57:26 +01:00
// In preload mode without lobby we wait for a join action before entering
2026-09-02 13:03:23 +02:00
const subscription = hostBridge . join$ . subscribe (({ data , reply }) => {
2024-11-11 18:30:15 +01:00
( async () : Promise < void > => {
2026-09-02 13:03:23 +02:00
await defaultDeviceSetup ( data );
2025-08-27 15:06:14 +02:00
setJoined ( true );
2026-09-02 13:03:23 +02:00
reply ();
2024-11-11 18:30:15 +01:00
})(). catch (( e ) => {
2025-09-11 14:57:26 +01:00
logger . error ( "Error joining RTC session on preload" , e );
2024-11-11 18:30:15 +01:00
});
2026-09-02 13:03:23 +02:00
});
return () : void => subscription . unsubscribe ();
2024-11-11 18:30:15 +01:00
} else {
2025-09-11 14:57:26 +01:00
// No lobby and no preload: we enter the rtc session right away
2025-08-27 15:06:14 +02:00
setJoined ( true );
2024-11-11 18:30:15 +01:00
}
2022-09-09 02:10:45 -04:00
}
2025-02-24 17:45:40 +07:00
}, [
2026-09-02 13:03:23 +02:00
hostBridge ,
2025-02-24 17:45:40 +07:00
rtcSession ,
preload ,
skipLobby ,
perParticipantE2EE ,
2025-06-20 12:37:25 -04:00
mediaDevices ,
2025-02-24 17:45:40 +07:00
latestMuteStates ,
2025-08-27 15:06:14 +02:00
setJoined ,
2025-02-24 17:45:40 +07:00
]);
2022-09-09 02:10:45 -04:00
2025-09-23 12:25:05 +02:00
// TODO refactor this + "joined" to just one callState
2022-01-05 15:35:12 -08:00
const [ left , setLeft ] = useState ( false );
2025-03-10 15:20:51 +01:00
2025-01-06 18:00:20 +01:00
const navigate = useNavigate ();
2022-01-05 15:35:12 -08:00
2025-12-03 10:04:22 +01:00
// TODO split this into leave and onDisconnect
2025-09-16 16:52:17 +02:00
const onLeft = useCallback (
2025-10-27 14:31:23 +01:00
(
reason : "timeout" | "user" | "allOthersLeft" | "decline" | "error" ,
) : void => {
2026-05-28 12:04:42 +02:00
let audioPromise : Promise < void > | undefined = undefined ;
switch ( reason ) {
case "allOthersLeft" :
// When "allOthersLeft", the leaveSoundEffect$ in CallEventAudioRenderer
// already plays the "left" sound when the remote participant's media
2026-06-02 11:27:59 +02:00
// disappears. We play it here silenced (volumeOverwrite = 0) so we have the right duration in the audioPromise.
2026-09-02 13:03:23 +02:00
// (which is what delays asking the host to close us)
2026-06-02 11:27:59 +02:00
audioPromise = leaveSoundContext . current ? . playSound ( "left" , 0 );
2026-05-28 12:04:42 +02:00
break ;
case "timeout" :
case "decline" :
audioPromise = leaveSoundContext . current ? . playSound ( reason );
break ;
default :
audioPromise = leaveSoundContext . current ? . playSound ( "left" );
}
2025-09-16 16:52:17 +02:00
2025-09-23 12:25:05 +02:00
setJoined ( false );
2025-09-16 16:52:17 +02:00
setLeft ( true );
2026-05-28 12:04:42 +02:00
2025-09-24 21:26:16 -04:00
// We need to wait until the callEnded event is tracked on PostHog,
2026-09-02 13:03:23 +02:00
// otherwise we may be torn down first.
2025-02-17 19:19:31 +07:00
const posthogRequest = new Promise (( resolve ) => {
2026-09-02 13:03:23 +02:00
// To increase the likelihood of the PostHog event being sent out
// before the host disposes of us, we ask it to skip the usual
// queuing/batching of requests.
const sendInstantly = hostControlsLifetime ;
2025-02-17 19:19:31 +07:00
PosthogAnalytics . instance . eventCallEnded . track (
2025-03-07 17:27:04 +01:00
room . roomId ,
2025-02-17 19:19:31 +07:00
rtcSession . memberships . length ,
sendInstantly ,
rtcSession ,
);
2025-09-24 21:26:16 -04:00
// Unfortunately the PostHog library provides no way to await the
2026-09-02 13:03:23 +02:00
// tracking of an event, but we don't really want it to hold up our
// disposal that long anyway, so giving it 10 ms will do.
2025-02-17 19:19:31 +07:00
window . setTimeout ( resolve , 10 );
});
2022-11-04 13:07:14 +01:00
2025-09-16 16:52:17 +02:00
void Promise . all ([ audioPromise , posthogRequest ])
2025-09-24 21:26:16 -04:00
. catch (( e ) =>
logger . error (
"Failed to play leave audio and/or send PostHog leave event" ,
e ,
),
)
. then ( async () => {
2024-09-10 09:49:35 +02:00
if (
! isPasswordlessUser &&
! confineToRoom &&
! PosthogAnalytics . instance . isEnabled ()
2025-09-24 21:26:16 -04:00
)
2025-09-16 16:52:17 +02:00
void navigate ( "/" );
2025-09-24 21:26:16 -04:00
2026-09-02 13:03:23 +02:00
// After this point the host could dispose of us at any moment!
try {
await hostBridge . setAlwaysOnScreen ( false );
} catch ( e ) {
logger . error ( "Failed to set `alwaysOnScreen` to false" , e );
}
// On a normal user hangup we can shut down and ask to be closed. But
// if an error occurs we should stay open until the user reads it.
if ( reason != "error" && ! returnToLobby ) {
2025-09-24 21:26:16 -04:00
try {
2026-09-02 13:03:23 +02:00
await hostBridge . close ? .();
2025-09-24 21:26:16 -04:00
} catch ( e ) {
2026-09-02 13:03:23 +02:00
logger . error ( "Failed to ask the host to close Element Call" , e );
2025-09-24 21:26:16 -04:00
}
2024-09-10 09:49:35 +02:00
}
2025-09-24 21:26:16 -04:00
});
2023-07-20 17:55:50 +01:00
},
2024-12-12 07:33:47 +00:00
[
2025-09-23 12:25:05 +02:00
setJoined ,
2025-03-07 17:27:04 +01:00
leaveSoundContext ,
2026-09-02 13:03:23 +02:00
hostBridge ,
hostControlsLifetime ,
2025-03-07 17:27:04 +01:00
room . roomId ,
2025-09-16 16:52:17 +02:00
rtcSession ,
2024-12-12 07:33:47 +00:00
isPasswordlessUser ,
confineToRoom ,
2026-09-02 10:31:48 +02:00
returnToLobby ,
2025-01-06 18:00:20 +01:00
navigate ,
2024-12-12 07:33:47 +00:00
],
2023-07-20 17:55:50 +01:00
);
2022-09-09 02:10:45 -04:00
useEffect (() => {
2026-09-02 13:03:23 +02:00
if ( joined )
// ask to be kept on screen once joined.
hostBridge . setAlwaysOnScreen ( true ). catch (( e ) => {
2024-09-10 09:49:35 +02:00
logger . error ( "Error calling setAlwaysOnScreen(true)" , e );
});
2026-09-02 13:03:23 +02:00
}, [ hostBridge , joined , rtcSession ]);
2023-06-16 18:07:13 +02:00
2025-03-07 17:27:04 +01:00
const joinRule = useJoinRule ( room );
2023-09-08 15:39:10 -04:00
2023-09-27 15:17:04 -04:00
const [ shareModalOpen , setInviteModalOpen ] = useState ( false );
const onDismissInviteModal = useCallback (
() => setInviteModalOpen ( false ),
2023-10-11 10:42:04 -04:00
[ setInviteModalOpen ],
2023-09-17 14:35:35 -04:00
);
2023-09-08 15:39:10 -04:00
const onShareClickFn = useCallback (
2023-09-27 15:17:04 -04:00
() => setInviteModalOpen ( true ),
2023-10-11 10:42:04 -04:00
[ setInviteModalOpen ],
2023-09-08 15:39:10 -04:00
);
const onShareClick = joinRule === JoinRule . Public ? onShareClickFn : null ;
2024-04-23 15:15:13 +02:00
if ( ! isE2EESupportedBrowser () && e2eeSystem . kind !== E2eeType . NONE ) {
// If we have a encryption system but the browser does not support it.
2025-03-05 20:50:19 +01:00
throw new E2EENotSupportedError ();
2023-08-11 16:59:26 +02:00
}
2023-09-17 14:35:35 -04:00
const shareModal = (
2023-09-27 15:17:04 -04:00
< InviteModal
2025-03-07 17:27:04 +01:00
room = { room }
2023-09-17 14:35:35 -04:00
open = { shareModalOpen }
2023-09-27 15:17:04 -04:00
onDismiss = { onDismissInviteModal }
2023-09-17 14:35:35 -04:00
/>
2023-09-08 15:39:10 -04:00
);
2024-01-26 10:03:08 +01:00
const lobbyView = (
<>
{ shareModal }
< LobbyView
client = { client }
matrixInfo = { matrixInfo }
muteStates = { muteStates }
2025-08-27 15:06:14 +02:00
onEnter = {() => setJoined ( true )}
2024-01-26 10:03:08 +01:00
confineToRoom = { confineToRoom }
2026-04-15 11:46:08 +02:00
hideHeader = { header !== HeaderStyle . Standard }
2024-01-26 10:03:08 +01:00
participantCount = { participantCount }
onShareClick = { onShareClick }
/>
</>
);
2023-09-08 15:39:10 -04:00
2025-02-26 17:20:30 +07:00
let body : ReactNode ;
2025-03-21 14:59:27 -04:00
if ( externalError ) {
// If an error was recorded within this component but outside
// GroupCallErrorBoundary, create a component that rethrows the error from
// within the error boundary, so it can be handled uniformly
2025-02-26 14:55:03 +01:00
const ErrorComponent = () : ReactNode => {
2025-03-21 14:59:27 -04:00
throw externalError ;
2025-02-26 14:55:03 +01:00
};
body = < ErrorComponent />;
2025-08-27 15:06:14 +02:00
} else if ( joined ) {
2025-02-26 17:20:30 +07:00
body = (
2023-09-12 11:30:46 +01:00
<>
2023-09-08 15:39:10 -04:00
{ shareModal }
2023-06-30 18:12:58 +01:00
< ActiveCall
client = { client }
2023-09-08 15:39:10 -04:00
matrixInfo = { matrixInfo }
2024-12-19 15:54:28 +00:00
rtcSession = { rtcSession as MatrixRTCSession }
2025-08-15 18:32:37 +02:00
matrixRoom = { room }
2025-09-16 16:52:17 +02:00
onLeft = { onLeft }
2023-08-02 15:29:37 -04:00
muteStates = { muteStates }
2024-04-23 15:15:13 +02:00
e2eeSystem = { e2eeSystem }
2023-09-12 11:30:46 +01:00
//otelGroupCallMembership={otelGroupCallMembership}
2023-09-08 15:39:10 -04:00
onShareClick = { onShareClick }
2023-06-30 18:12:58 +01:00
/>
2023-09-12 11:30:46 +01:00
</>
2023-05-26 20:41:32 +02:00
);
2026-09-02 13:03:23 +02:00
} else if ( left && ! hostControlsLifetime ) {
// Left, and it is up to us what to show next:
2024-01-26 10:03:08 +01:00
2023-06-07 14:19:53 -04:00
// The call ended view is shown for two reasons: prompting guests to create
// an account, and prompting users that have opted into analytics to provide
2026-09-02 13:03:23 +02:00
// feedback. We don't show a feedback prompt when a host owns our lifetime
// however (at least for now), because we don't yet have designs that would
// allow those users to dismiss the feedback prompt and close the call
// window without submitting anything.
if ( isPasswordlessUser || PosthogAnalytics . instance . isEnabled ()) {
2025-02-26 17:20:30 +07:00
body = (
< CallEndedView
endedCallId = { rtcSession . room . roomId }
client = { client }
isPasswordlessUser = { isPasswordlessUser }
2025-06-26 05:08:57 -04:00
hideHeader = { header === HeaderStyle . None }
2025-02-26 17:20:30 +07:00
confineToRoom = { confineToRoom }
/>
2023-06-07 16:22:44 +02:00
);
2022-09-23 15:35:05 +01:00
} else {
// If the user is a regular user, we'll have sent them back to the homepage,
// so just sit here & do nothing: otherwise we would (briefly) mount the
// LobbyView again which would open capture devices again.
2025-02-26 17:20:30 +07:00
body = null ;
2022-09-23 15:35:05 +01:00
}
2026-09-02 13:03:23 +02:00
} else if ( left && hostControlsLifetime ) {
// Left, and the host decides what happens next:
2025-03-04 15:06:47 -05:00
body = returnToLobby ? lobbyView : null ;
2024-01-26 10:03:08 +01:00
} else if ( preload || skipLobby ) {
2025-09-11 14:57:26 +01:00
// The RTC session is not joined to yet (`isJoined`), but enterRTCSessionOrError should have been called.
2025-02-26 17:20:30 +07:00
body = null ;
} else {
body = lobbyView ;
2022-01-05 15:35:12 -08:00
}
2024-01-26 10:03:08 +01:00
2025-03-05 20:50:19 +01:00
return (
< GroupCallErrorBoundary
2025-09-08 14:21:38 +02:00
recoveryActionHandler = { async ( action ) => {
setExternalError ( null );
2025-03-05 20:50:19 +01:00
if ( action == "reconnect" ) {
setLeft ( false );
2025-09-08 14:21:38 +02:00
await enterRTCSessionOrError ( rtcSession ). catch (( e ) => {
2025-03-05 20:50:19 +01:00
logger . error ( "Error re-entering RTC session" , e );
});
}
}}
2026-06-17 12:13:30 +02:00
onError = {( _error ) => {
if ( rtcSession . isJoined ()) onLeft ( "error" );
2026-09-02 13:03:23 +02:00
// If there is an error we need to be dismissible again. This is done in
// `onLeft` as well; we need it here explicitly in case
// rtcSession.isJoined is false.
void hostBridge . setAlwaysOnScreen ( false );
2026-06-17 12:13:30 +02:00
}}
2025-03-05 20:50:19 +01:00
>
{ body }
</ GroupCallErrorBoundary >
);
2023-09-22 18:05:13 -04:00
};