mirror of
https://github.com/vector-im/element-call.git
synced 2026-10-13 23:35:51 +00:00
Rename GroupCallView to CallView, and fold ElementCallView into it
Three layers stood between a shell and the call: `ElementCall` (the component), `ElementCallView` and `GroupCallView`. The middle one only held the `joined` flag and the mute state, both of which nothing outside the call reads any more, so it is folded into the view it wrapped. That view is now `CallView`, since "group call" is a name from before Element Call handled anything else. Its docstring says what it is: the whole lifecycle of a call — lobby, the call itself, and what comes after — with the lobby and the post-call screen each present or skipped depending on the parameters and the host. Both the standalone RoomPage and the component render it directly. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
8e8bc5ddde
commit
f70fba1dd6
@@ -0,0 +1,602 @@
|
||||
/*
|
||||
Copyright 2022-2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE in the repository root for full details.
|
||||
*/
|
||||
|
||||
import {
|
||||
type FC,
|
||||
type ReactNode,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
type MatrixClient,
|
||||
JoinRule,
|
||||
type Room,
|
||||
UnsupportedStickyEventsEndpointError,
|
||||
} from "matrix-js-sdk";
|
||||
import {
|
||||
Room as LivekitRoom,
|
||||
isE2EESupported as isE2EESupportedBrowser,
|
||||
} from "livekit-client";
|
||||
import { logger } from "matrix-js-sdk/lib/logger";
|
||||
import {
|
||||
MatrixRTCSessionEvent,
|
||||
type MatrixRTCSession,
|
||||
} from "matrix-js-sdk/lib/matrixrtc";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
import { type JoinCallData } from "../widget";
|
||||
import { LobbyView } from "./LobbyView";
|
||||
import { type MatrixInfo } from "./VideoPreview";
|
||||
import { CallEndedView } from "./CallEndedView";
|
||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||
import { useProfile } from "../profile/useProfile";
|
||||
import { findDeviceByName } from "../utils/media";
|
||||
import { ActiveCall } from "./InCallView";
|
||||
import { type MuteStates } from "../state/MuteStates";
|
||||
import { useMediaDevices } from "../MediaDevicesContext";
|
||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||
import {
|
||||
saveKeyForRoom,
|
||||
useRoomEncryptionSystem,
|
||||
} from "../e2ee/sharedKeyManagement";
|
||||
import { useRoomAvatar } from "./useRoomAvatar";
|
||||
import { useRoomName } from "./useRoomName";
|
||||
import { useJoinRule } from "./useJoinRule";
|
||||
import { InviteModal } from "./InviteModal";
|
||||
import { HeaderStyle, type UrlParams, useUrlParams } from "../UrlParams";
|
||||
import { E2eeType } from "../e2ee/e2eeType";
|
||||
import { useAudioContext } from "../useAudioContext";
|
||||
import {
|
||||
callEventAudioSounds,
|
||||
type CallEventSounds,
|
||||
} from "./CallEventAudioRenderer";
|
||||
import { useLatest } from "../useLatest";
|
||||
import {
|
||||
ConnectionLostError,
|
||||
E2EENotSupportedError,
|
||||
ElementCallError,
|
||||
StickyEventsRequiredError,
|
||||
UnknownCallError,
|
||||
} from "../utils/errors.ts";
|
||||
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary.tsx";
|
||||
import { useTypedEventEmitter } from "../useEvents";
|
||||
import { muteAllAudio$ } from "../state/MuteAllAudioModel.ts";
|
||||
import { useAppBarTitle } from "../AppBar.tsx";
|
||||
import { useBehavior } from "../useBehavior.ts";
|
||||
import { useRootElement } from "../RootElementContext.ts";
|
||||
import { useHostBridge } from "../HostBridge.ts";
|
||||
import { useMuteStates } from "../state/useMuteStates.ts";
|
||||
|
||||
/**
|
||||
* If there already are this many participants in the call, we automatically mute
|
||||
* the user.
|
||||
*/
|
||||
export const MUTE_PARTICIPANT_COUNT = 8;
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
rtcSession?: MatrixRTCSession;
|
||||
}
|
||||
}
|
||||
|
||||
interface Props {
|
||||
/** The client to place the call with. */
|
||||
client: MatrixClient;
|
||||
/** 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.
|
||||
*/
|
||||
isPasswordlessUser: boolean;
|
||||
/** Whether to keep the user in this call rather than letting them navigate. */
|
||||
confineToRoom: boolean;
|
||||
/** Whether to wait for the host to ask us to join. */
|
||||
preload: UrlParams["preload"];
|
||||
/** Whether to enter the call directly, without showing the lobby first. */
|
||||
skipLobby: UrlParams["skipLobby"];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
joined: boolean;
|
||||
setJoined: (value: boolean) => void;
|
||||
muteStates: MuteStates;
|
||||
}
|
||||
|
||||
/** {@link CallView}, once it has the mute state everything below needs. */
|
||||
const LoadedCallView: FC<LoadedProps> = ({
|
||||
client,
|
||||
isPasswordlessUser,
|
||||
confineToRoom,
|
||||
preload,
|
||||
skipLobby,
|
||||
rtcSession,
|
||||
joined,
|
||||
setJoined,
|
||||
muteStates,
|
||||
}) => {
|
||||
// Used to thread through any errors that occur outside the error boundary
|
||||
const [externalError, setExternalError] = useState<ElementCallError | null>(
|
||||
null,
|
||||
);
|
||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||
const rootElement = useRootElement();
|
||||
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
|
||||
// finish what we are doing. (Whose account the user's is, by contrast, is
|
||||
// stated outright: see `HostBridge.supportsProfileChanges`.)
|
||||
const hostControlsLifetime = hostBridge.close !== undefined;
|
||||
|
||||
const muteAllAudio = useBehavior(muteAllAudio$);
|
||||
const leaveSoundContext = useLatest(
|
||||
useAudioContext<CallEventSounds>({
|
||||
sounds: callEventAudioSounds,
|
||||
latencyHint: "interactive",
|
||||
muted: muteAllAudio,
|
||||
}),
|
||||
);
|
||||
// This should use `useEffectEvent` (only available in experimental versions)
|
||||
useEffect(() => {
|
||||
if (memberships.length >= MUTE_PARTICIPANT_COUNT)
|
||||
muteStates.audio.setEnabled$.value?.(false);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
logger.info("[Lifecycle] CallView Component mounted");
|
||||
return (): void => {
|
||||
logger.info("[Lifecycle] CallView Component unmounted");
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 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(() => {
|
||||
rootElement.classList.add("no-scroll-body");
|
||||
return (): void => {
|
||||
rootElement.classList.remove("no-scroll-body");
|
||||
};
|
||||
}, [rootElement]);
|
||||
|
||||
useEffect(() => {
|
||||
window.rtcSession = rtcSession;
|
||||
return (): void => {
|
||||
delete window.rtcSession;
|
||||
};
|
||||
}, [rtcSession]);
|
||||
|
||||
// TODO move this into the callViewModel LocalMembership.ts
|
||||
// We might actually not need this at all. Since we get into fatalError on those errors already?
|
||||
useTypedEventEmitter(
|
||||
rtcSession,
|
||||
MatrixRTCSessionEvent.MembershipManagerError,
|
||||
(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());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
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]);
|
||||
|
||||
const room = rtcSession.room as Room;
|
||||
const { displayName, avatarUrl } = useProfile(client);
|
||||
const roomName = useRoomName(room);
|
||||
const roomAvatar = useRoomAvatar(room);
|
||||
const {
|
||||
perParticipantE2EE,
|
||||
returnToLobby,
|
||||
password: passwordFromUrl,
|
||||
header,
|
||||
} = useUrlParams();
|
||||
const e2eeSystem = useRoomEncryptionSystem(room.roomId);
|
||||
|
||||
// Save the password once we start the groupCallView
|
||||
useEffect(() => {
|
||||
if (passwordFromUrl) saveKeyForRoom(room.roomId, passwordFromUrl);
|
||||
}, [passwordFromUrl, room.roomId]);
|
||||
|
||||
useAppBarTitle(roomName);
|
||||
|
||||
const matrixInfo = useMemo((): MatrixInfo => {
|
||||
return {
|
||||
userId: client.getUserId()!,
|
||||
displayName: displayName!,
|
||||
avatarUrl: avatarUrl!,
|
||||
roomId: room.roomId,
|
||||
roomName,
|
||||
roomAlias: room.getCanonicalAlias(),
|
||||
roomAvatar,
|
||||
e2eeSystem,
|
||||
};
|
||||
}, [client, displayName, avatarUrl, roomName, room, roomAvatar, e2eeSystem]);
|
||||
|
||||
// Count each member only once, regardless of how many devices they use
|
||||
const participantCount = useMemo(
|
||||
() => new Set<string>(memberships.map((m) => m.userId!)).size,
|
||||
[memberships],
|
||||
);
|
||||
|
||||
const mediaDevices = useMediaDevices();
|
||||
const latestMuteStates = useLatest(muteStates);
|
||||
|
||||
const enterRTCSessionOrError = useCallback(
|
||||
async (rtcSession: MatrixRTCSession): Promise<void> => {
|
||||
try {
|
||||
setJoined(true);
|
||||
// TODO-MULTI-SFU what to do with error handling now that we don't use this function?
|
||||
// @BillCarsonFr
|
||||
} catch (e) {
|
||||
if (e instanceof ElementCallError) {
|
||||
setExternalError(e);
|
||||
} else {
|
||||
logger.error(`Unknown Error while entering RTC session`, e);
|
||||
const error = new UnknownCallError(
|
||||
e instanceof Error ? e : new Error("Unknown error", { cause: e }),
|
||||
);
|
||||
setExternalError(error);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
},
|
||||
[setJoined],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const defaultDeviceSetup = async ({
|
||||
audioInput,
|
||||
videoInput,
|
||||
}: JoinCallData): Promise<void> => {
|
||||
// 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.
|
||||
const devices = await LivekitRoom.getLocalDevices(undefined, true);
|
||||
|
||||
if (audioInput) {
|
||||
const deviceId = findDeviceByName(audioInput, "audioinput", devices);
|
||||
if (!deviceId) {
|
||||
logger.warn("Unknown audio input: " + audioInput);
|
||||
// override the default mute state
|
||||
latestMuteStates.current!.audio.setEnabled$.value?.(false);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Found audio input ID ${deviceId} for name ${audioInput}`,
|
||||
);
|
||||
mediaDevices.audioInput.select(deviceId);
|
||||
}
|
||||
}
|
||||
|
||||
if (videoInput) {
|
||||
const deviceId = findDeviceByName(videoInput, "videoinput", devices);
|
||||
if (!deviceId) {
|
||||
logger.warn("Unknown video input: " + videoInput);
|
||||
// override the default mute state
|
||||
latestMuteStates.current!.video.setEnabled$.value?.(false);
|
||||
} else {
|
||||
logger.debug(
|
||||
`Found video input ID ${deviceId} for name ${videoInput}`,
|
||||
);
|
||||
mediaDevices.videoInput.select(deviceId);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (skipLobby) {
|
||||
// `preload` is only ever set when we have a host to be preloaded by.
|
||||
if (preload) {
|
||||
// In preload mode without lobby we wait for a join action before entering
|
||||
const subscription = hostBridge.join$.subscribe(({ data, reply }) => {
|
||||
(async (): Promise<void> => {
|
||||
await defaultDeviceSetup(data);
|
||||
setJoined(true);
|
||||
reply();
|
||||
})().catch((e) => {
|
||||
logger.error("Error joining RTC session on preload", e);
|
||||
});
|
||||
});
|
||||
return (): void => subscription.unsubscribe();
|
||||
} else {
|
||||
// No lobby and no preload: we enter the rtc session right away
|
||||
setJoined(true);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
hostBridge,
|
||||
rtcSession,
|
||||
preload,
|
||||
skipLobby,
|
||||
perParticipantE2EE,
|
||||
mediaDevices,
|
||||
latestMuteStates,
|
||||
setJoined,
|
||||
]);
|
||||
|
||||
// TODO refactor this + "joined" to just one callState
|
||||
const [left, setLeft] = useState(false);
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
// TODO split this into leave and onDisconnect
|
||||
const onLeft = useCallback(
|
||||
(
|
||||
reason: "timeout" | "user" | "allOthersLeft" | "decline" | "error",
|
||||
): void => {
|
||||
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
|
||||
// disappears. We play it here silenced (volumeOverwrite = 0) so we have the right duration in the audioPromise.
|
||||
// (which is what delays asking the host to close us)
|
||||
audioPromise = leaveSoundContext.current?.playSound("left", 0);
|
||||
break;
|
||||
case "timeout":
|
||||
case "decline":
|
||||
audioPromise = leaveSoundContext.current?.playSound(reason);
|
||||
break;
|
||||
default:
|
||||
audioPromise = leaveSoundContext.current?.playSound("left");
|
||||
}
|
||||
|
||||
setJoined(false);
|
||||
setLeft(true);
|
||||
|
||||
// We need to wait until the callEnded event is tracked on PostHog,
|
||||
// otherwise we may be torn down first.
|
||||
const posthogRequest = new Promise((resolve) => {
|
||||
// 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;
|
||||
PosthogAnalytics.instance.eventCallEnded.track(
|
||||
room.roomId,
|
||||
rtcSession.memberships.length,
|
||||
sendInstantly,
|
||||
rtcSession,
|
||||
);
|
||||
// Unfortunately the PostHog library provides no way to await the
|
||||
// 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.
|
||||
window.setTimeout(resolve, 10);
|
||||
});
|
||||
|
||||
void Promise.all([audioPromise, posthogRequest])
|
||||
.catch((e) =>
|
||||
logger.error(
|
||||
"Failed to play leave audio and/or send PostHog leave event",
|
||||
e,
|
||||
),
|
||||
)
|
||||
.then(async () => {
|
||||
if (
|
||||
!isPasswordlessUser &&
|
||||
!confineToRoom &&
|
||||
!PosthogAnalytics.instance.isEnabled()
|
||||
)
|
||||
void navigate("/");
|
||||
|
||||
// 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) {
|
||||
try {
|
||||
await hostBridge.close?.();
|
||||
} catch (e) {
|
||||
logger.error("Failed to ask the host to close Element Call", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
[
|
||||
setJoined,
|
||||
leaveSoundContext,
|
||||
hostBridge,
|
||||
hostControlsLifetime,
|
||||
room.roomId,
|
||||
rtcSession,
|
||||
isPasswordlessUser,
|
||||
confineToRoom,
|
||||
returnToLobby,
|
||||
navigate,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (joined)
|
||||
// ask to be kept on screen once joined.
|
||||
hostBridge.setAlwaysOnScreen(true).catch((e) => {
|
||||
logger.error("Error calling setAlwaysOnScreen(true)", e);
|
||||
});
|
||||
}, [hostBridge, joined, rtcSession]);
|
||||
|
||||
const joinRule = useJoinRule(room);
|
||||
|
||||
const [shareModalOpen, setInviteModalOpen] = useState(false);
|
||||
const onDismissInviteModal = useCallback(
|
||||
() => setInviteModalOpen(false),
|
||||
[setInviteModalOpen],
|
||||
);
|
||||
|
||||
const onShareClickFn = useCallback(
|
||||
() => setInviteModalOpen(true),
|
||||
[setInviteModalOpen],
|
||||
);
|
||||
const onShareClick = joinRule === JoinRule.Public ? onShareClickFn : null;
|
||||
|
||||
if (!isE2EESupportedBrowser() && e2eeSystem.kind !== E2eeType.NONE) {
|
||||
// If we have a encryption system but the browser does not support it.
|
||||
throw new E2EENotSupportedError();
|
||||
}
|
||||
|
||||
const shareModal = (
|
||||
<InviteModal
|
||||
room={room}
|
||||
open={shareModalOpen}
|
||||
onDismiss={onDismissInviteModal}
|
||||
/>
|
||||
);
|
||||
const lobbyView = (
|
||||
<>
|
||||
{shareModal}
|
||||
<LobbyView
|
||||
client={client}
|
||||
matrixInfo={matrixInfo}
|
||||
muteStates={muteStates}
|
||||
onEnter={() => setJoined(true)}
|
||||
confineToRoom={confineToRoom}
|
||||
hideHeader={header !== HeaderStyle.Standard}
|
||||
participantCount={participantCount}
|
||||
onShareClick={onShareClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
let body: ReactNode;
|
||||
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
|
||||
const ErrorComponent = (): ReactNode => {
|
||||
throw externalError;
|
||||
};
|
||||
body = <ErrorComponent />;
|
||||
} else if (joined) {
|
||||
body = (
|
||||
<>
|
||||
{shareModal}
|
||||
<ActiveCall
|
||||
client={client}
|
||||
matrixInfo={matrixInfo}
|
||||
rtcSession={rtcSession as MatrixRTCSession}
|
||||
matrixRoom={room}
|
||||
onLeft={onLeft}
|
||||
muteStates={muteStates}
|
||||
e2eeSystem={e2eeSystem}
|
||||
//otelGroupCallMembership={otelGroupCallMembership}
|
||||
onShareClick={onShareClick}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
} else if (left && !hostControlsLifetime) {
|
||||
// Left, and it is up to us what to show next:
|
||||
|
||||
// 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
|
||||
// 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()) {
|
||||
body = (
|
||||
<CallEndedView
|
||||
endedCallId={rtcSession.room.roomId}
|
||||
client={client}
|
||||
isPasswordlessUser={isPasswordlessUser}
|
||||
hideHeader={header === HeaderStyle.None}
|
||||
confineToRoom={confineToRoom}
|
||||
/>
|
||||
);
|
||||
} 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.
|
||||
body = null;
|
||||
}
|
||||
} else if (left && hostControlsLifetime) {
|
||||
// Left, and the host decides what happens next:
|
||||
body = returnToLobby ? lobbyView : null;
|
||||
} else if (preload || skipLobby) {
|
||||
// The RTC session is not joined to yet (`isJoined`), but enterRTCSessionOrError should have been called.
|
||||
body = null;
|
||||
} else {
|
||||
body = lobbyView;
|
||||
}
|
||||
|
||||
return (
|
||||
<GroupCallErrorBoundary
|
||||
recoveryActionHandler={async (action) => {
|
||||
setExternalError(null);
|
||||
if (action == "reconnect") {
|
||||
setLeft(false);
|
||||
await enterRTCSessionOrError(rtcSession).catch((e) => {
|
||||
logger.error("Error re-entering RTC session", e);
|
||||
});
|
||||
}
|
||||
}}
|
||||
onError={(_error) => {
|
||||
if (rtcSession.isJoined()) onLeft("error");
|
||||
// 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);
|
||||
}}
|
||||
>
|
||||
{body}
|
||||
</GroupCallErrorBoundary>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user