Leave session when error occurs and show error screens in widget mode (#3021)
Co-authored-by: Hugh Nimmo-Smith <hughns@users.noreply.github.com>
This commit is contained in:
@@ -5,17 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import { type FC, type FormEventHandler, useCallback, useState } from "react";
|
||||||
type FC,
|
|
||||||
type FormEventHandler,
|
|
||||||
type ReactNode,
|
|
||||||
useCallback,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { Trans, useTranslation } from "react-i18next";
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Button, Heading, Text } from "@vector-im/compound-web";
|
import { Button, Heading, Text } from "@vector-im/compound-web";
|
||||||
import { OfflineIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
@@ -28,15 +21,12 @@ import { FieldRow, InputField } from "../input/Input";
|
|||||||
import { StarRatingInput } from "../input/StarRatingInput";
|
import { StarRatingInput } from "../input/StarRatingInput";
|
||||||
import { Link } from "../button/Link";
|
import { Link } from "../button/Link";
|
||||||
import { LinkButton } from "../button";
|
import { LinkButton } from "../button";
|
||||||
import { ErrorView } from "../ErrorView";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
isPasswordlessUser: boolean;
|
isPasswordlessUser: boolean;
|
||||||
confineToRoom: boolean;
|
confineToRoom: boolean;
|
||||||
endedCallId: string;
|
endedCallId: string;
|
||||||
leaveError?: Error;
|
|
||||||
reconnect: () => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CallEndedView: FC<Props> = ({
|
export const CallEndedView: FC<Props> = ({
|
||||||
@@ -44,8 +34,6 @@ export const CallEndedView: FC<Props> = ({
|
|||||||
isPasswordlessUser,
|
isPasswordlessUser,
|
||||||
confineToRoom,
|
confineToRoom,
|
||||||
endedCallId,
|
endedCallId,
|
||||||
leaveError,
|
|
||||||
reconnect,
|
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -143,61 +131,32 @@ export const CallEndedView: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderBody = (): ReactNode => {
|
|
||||||
if (leaveError) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className={styles.main}>
|
|
||||||
<ErrorView
|
|
||||||
Icon={OfflineIcon}
|
|
||||||
title={t("error.connection_lost")}
|
|
||||||
rageshake
|
|
||||||
>
|
|
||||||
<p>{t("error.connection_lost_description")}</p>
|
|
||||||
<Button onClick={reconnect}>
|
|
||||||
{t("call_ended_view.reconnect_button")}
|
|
||||||
</Button>
|
|
||||||
</ErrorView>
|
|
||||||
</main>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<main className={styles.main}>
|
|
||||||
<Heading size="xl" weight="semibold" className={styles.headline}>
|
|
||||||
{surveySubmitted
|
|
||||||
? t("call_ended_view.headline", {
|
|
||||||
displayName,
|
|
||||||
})
|
|
||||||
: t("call_ended_view.headline", {
|
|
||||||
displayName,
|
|
||||||
}) +
|
|
||||||
"\n" +
|
|
||||||
t("call_ended_view.survey_prompt")}
|
|
||||||
</Heading>
|
|
||||||
{(!surveySubmitted || confineToRoom) &&
|
|
||||||
PosthogAnalytics.instance.isEnabled()
|
|
||||||
? qualitySurveyDialog
|
|
||||||
: createAccountDialog}
|
|
||||||
</main>
|
|
||||||
{!confineToRoom && (
|
|
||||||
<Text className={styles.footer}>
|
|
||||||
<Link to="/"> {t("call_ended_view.not_now_button")} </Link>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header>
|
<Header>
|
||||||
<LeftNav>{!confineToRoom && <HeaderLogo />}</LeftNav>
|
<LeftNav>{!confineToRoom && <HeaderLogo />}</LeftNav>
|
||||||
<RightNav />
|
<RightNav />
|
||||||
</Header>
|
</Header>
|
||||||
<div className={styles.container}>{renderBody()}</div>
|
<div className={styles.container}>
|
||||||
|
<main className={styles.main}>
|
||||||
|
<Heading size="xl" weight="semibold" className={styles.headline}>
|
||||||
|
{surveySubmitted
|
||||||
|
? t("call_ended_view.headline", { displayName })
|
||||||
|
: t("call_ended_view.headline", { displayName }) +
|
||||||
|
"\n" +
|
||||||
|
t("call_ended_view.survey_prompt")}
|
||||||
|
</Heading>
|
||||||
|
{(!surveySubmitted || confineToRoom) &&
|
||||||
|
PosthogAnalytics.instance.isEnabled()
|
||||||
|
? qualitySurveyDialog
|
||||||
|
: createAccountDialog}
|
||||||
|
</main>
|
||||||
|
{!confineToRoom && (
|
||||||
|
<Text className={styles.footer}>
|
||||||
|
<Link to="/"> {t("call_ended_view.not_now_button")} </Link>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { beforeEach, expect, type MockedFunction, test, vitest } from "vitest";
|
import { beforeEach, expect, type MockedFunction, test, vitest } from "vitest";
|
||||||
import { render, waitFor } from "@testing-library/react";
|
import { render, waitFor, screen } from "@testing-library/react";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
|
||||||
import { of } from "rxjs";
|
import { of } from "rxjs";
|
||||||
@@ -14,6 +14,7 @@ import { JoinRule, type RoomState } from "matrix-js-sdk/src/matrix";
|
|||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
import { type RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
import { type MuteStates } from "./MuteStates";
|
import { type MuteStates } from "./MuteStates";
|
||||||
import { prefetchSounds } from "../soundUtils";
|
import { prefetchSounds } from "../soundUtils";
|
||||||
@@ -184,3 +185,28 @@ test("will play a leave sound synchronously in widget mode", async () => {
|
|||||||
);
|
);
|
||||||
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
|
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("GroupCallView leaves the session when an error occurs", async () => {
|
||||||
|
(ActiveCall as MockedFunction<typeof ActiveCall>).mockImplementation(() => {
|
||||||
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
if (error !== null) throw error;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<button onClick={() => setError(new Error())}>Panic!</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const { rtcSession } = createGroupCallView(null);
|
||||||
|
await user.click(screen.getByRole("button", { name: "Panic!" }));
|
||||||
|
screen.getByText("error.generic");
|
||||||
|
expect(leaveRTCSession).toHaveBeenCalledWith(
|
||||||
|
rtcSession,
|
||||||
|
"error",
|
||||||
|
expect.any(Promise),
|
||||||
|
);
|
||||||
|
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
|
||||||
|
// Ensure that the playSound promise resolves within this test to avoid
|
||||||
|
// impacting the results of other tests
|
||||||
|
await waitFor(() => expect(leaveRTCSession).toHaveResolved());
|
||||||
|
});
|
||||||
|
|||||||
@@ -5,7 +5,15 @@ SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|||||||
Please see LICENSE in the repository root for full details.
|
Please see LICENSE in the repository root for full details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { type FC, useCallback, useEffect, useMemo, useState } from "react";
|
import {
|
||||||
|
type FC,
|
||||||
|
type ReactElement,
|
||||||
|
type ReactNode,
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from "react";
|
||||||
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
import { type MatrixClient } from "matrix-js-sdk/src/client";
|
||||||
import {
|
import {
|
||||||
Room,
|
Room,
|
||||||
@@ -14,9 +22,14 @@ import {
|
|||||||
import { logger } from "matrix-js-sdk/src/logger";
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc/MatrixRTCSession";
|
||||||
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
import { JoinRule } from "matrix-js-sdk/src/matrix";
|
||||||
import { WebBrowserIcon } from "@vector-im/compound-design-tokens/assets/web/icons";
|
import {
|
||||||
|
OfflineIcon,
|
||||||
|
WebBrowserIcon,
|
||||||
|
} from "@vector-im/compound-design-tokens/assets/web/icons";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { ErrorBoundary } from "@sentry/react";
|
||||||
|
import { Button } from "@vector-im/compound-web";
|
||||||
|
|
||||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import {
|
import {
|
||||||
@@ -24,14 +37,14 @@ import {
|
|||||||
type JoinCallData,
|
type JoinCallData,
|
||||||
type WidgetHelpers,
|
type WidgetHelpers,
|
||||||
} from "../widget";
|
} from "../widget";
|
||||||
import { FullScreenView } from "../FullScreenView";
|
import { ErrorPage, FullScreenView } from "../FullScreenView";
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
import { type MatrixInfo } from "./VideoPreview";
|
import { type MatrixInfo } from "./VideoPreview";
|
||||||
import { CallEndedView } from "./CallEndedView";
|
import { CallEndedView } from "./CallEndedView";
|
||||||
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
import { PosthogAnalytics } from "../analytics/PosthogAnalytics";
|
||||||
import { useProfile } from "../profile/useProfile";
|
import { useProfile } from "../profile/useProfile";
|
||||||
import { findDeviceByName } from "../utils/media";
|
import { findDeviceByName } from "../utils/media";
|
||||||
import { ActiveCall } from "./InCallView";
|
import { ActiveCall, ConnectionLostError } from "./InCallView";
|
||||||
import { MUTE_PARTICIPANT_COUNT, type MuteStates } from "./MuteStates";
|
import { MUTE_PARTICIPANT_COUNT, type MuteStates } from "./MuteStates";
|
||||||
import { useMediaDevices } from "../livekit/MediaDevicesContext";
|
import { useMediaDevices } from "../livekit/MediaDevicesContext";
|
||||||
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
import { useMatrixRTCSessionMemberships } from "../useMatrixRTCSessionMemberships";
|
||||||
@@ -55,6 +68,11 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GroupCallErrorPageProps {
|
||||||
|
error: Error | unknown;
|
||||||
|
resetError: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
client: MatrixClient;
|
client: MatrixClient;
|
||||||
isPasswordlessUser: boolean;
|
isPasswordlessUser: boolean;
|
||||||
@@ -229,16 +247,14 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const [left, setLeft] = useState(false);
|
const [left, setLeft] = useState(false);
|
||||||
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const onLeave = useCallback(
|
const onLeave = useCallback(
|
||||||
(leaveError?: Error): void => {
|
(cause: "user" | "error" = "user"): void => {
|
||||||
const audioPromise = leaveSoundContext.current?.playSound("left");
|
const audioPromise = leaveSoundContext.current?.playSound("left");
|
||||||
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
|
// In embedded/widget mode the iFrame will be killed right after the call ended prohibiting the posthog event from getting sent,
|
||||||
// therefore we want the event to be sent instantly without getting queued/batched.
|
// therefore we want the event to be sent instantly without getting queued/batched.
|
||||||
const sendInstantly = !!widget;
|
const sendInstantly = !!widget;
|
||||||
setLeaveError(leaveError);
|
|
||||||
setLeft(true);
|
setLeft(true);
|
||||||
// we need to wait until the callEnded event is tracked on posthog.
|
// we need to wait until the callEnded event is tracked on posthog.
|
||||||
// Otherwise the iFrame gets killed before the callEnded event got tracked.
|
// Otherwise the iFrame gets killed before the callEnded event got tracked.
|
||||||
@@ -254,7 +270,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
|
|
||||||
leaveRTCSession(
|
leaveRTCSession(
|
||||||
rtcSession,
|
rtcSession,
|
||||||
leaveError === undefined ? "user" : "error",
|
cause,
|
||||||
// Wait for the sound in widget mode (it's not long)
|
// Wait for the sound in widget mode (it's not long)
|
||||||
Promise.all([audioPromise, posthogRequest]),
|
Promise.all([audioPromise, posthogRequest]),
|
||||||
)
|
)
|
||||||
@@ -303,14 +319,6 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
}, [widget, isJoined, rtcSession]);
|
}, [widget, isJoined, rtcSession]);
|
||||||
|
|
||||||
const onReconnect = useCallback(() => {
|
|
||||||
setLeft(false);
|
|
||||||
setLeaveError(undefined);
|
|
||||||
enterRTCSession(rtcSession, perParticipantE2EE).catch((e) => {
|
|
||||||
logger.error("Error re-entering RTC session on reconnect", e);
|
|
||||||
});
|
|
||||||
}, [rtcSession, perParticipantE2EE]);
|
|
||||||
|
|
||||||
const joinRule = useJoinRule(rtcSession.room);
|
const joinRule = useJoinRule(rtcSession.room);
|
||||||
|
|
||||||
const [shareModalOpen, setInviteModalOpen] = useState(false);
|
const [shareModalOpen, setInviteModalOpen] = useState(false);
|
||||||
@@ -327,6 +335,43 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const errorPage = useMemo(() => {
|
||||||
|
function GroupCallErrorPage({
|
||||||
|
error,
|
||||||
|
resetError,
|
||||||
|
}: GroupCallErrorPageProps): ReactElement {
|
||||||
|
useEffect(() => {
|
||||||
|
if (rtcSession.isJoined()) onLeave("error");
|
||||||
|
}, [error]);
|
||||||
|
|
||||||
|
const onReconnect = useCallback(() => {
|
||||||
|
setLeft(false);
|
||||||
|
resetError();
|
||||||
|
enterRTCSession(rtcSession, perParticipantE2EE).catch((e) => {
|
||||||
|
logger.error("Error re-entering RTC session on reconnect", e);
|
||||||
|
});
|
||||||
|
}, [resetError]);
|
||||||
|
|
||||||
|
return error instanceof ConnectionLostError ? (
|
||||||
|
<FullScreenView>
|
||||||
|
<ErrorView
|
||||||
|
Icon={OfflineIcon}
|
||||||
|
title={t("error.connection_lost")}
|
||||||
|
rageshake
|
||||||
|
>
|
||||||
|
<p>{t("error.connection_lost_description")}</p>
|
||||||
|
<Button onClick={onReconnect}>
|
||||||
|
{t("call_ended_view.reconnect_button")}
|
||||||
|
</Button>
|
||||||
|
</ErrorView>
|
||||||
|
</FullScreenView>
|
||||||
|
) : (
|
||||||
|
<ErrorPage error={error} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return GroupCallErrorPage;
|
||||||
|
}, [onLeave, rtcSession, perParticipantE2EE, t]);
|
||||||
|
|
||||||
if (!isE2EESupportedBrowser() && e2eeSystem.kind !== E2eeType.NONE) {
|
if (!isE2EESupportedBrowser() && e2eeSystem.kind !== E2eeType.NONE) {
|
||||||
// If we have a encryption system but the browser does not support it.
|
// If we have a encryption system but the browser does not support it.
|
||||||
return (
|
return (
|
||||||
@@ -361,8 +406,9 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let body: ReactNode;
|
||||||
if (isJoined) {
|
if (isJoined) {
|
||||||
return (
|
body = (
|
||||||
<>
|
<>
|
||||||
{shareModal}
|
{shareModal}
|
||||||
<ActiveCall
|
<ActiveCall
|
||||||
@@ -390,36 +436,32 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
// submitting anything.
|
// submitting anything.
|
||||||
if (
|
if (
|
||||||
isPasswordlessUser ||
|
isPasswordlessUser ||
|
||||||
(PosthogAnalytics.instance.isEnabled() && widget === null) ||
|
(PosthogAnalytics.instance.isEnabled() && widget === null)
|
||||||
leaveError
|
|
||||||
) {
|
) {
|
||||||
return (
|
body = (
|
||||||
<>
|
<CallEndedView
|
||||||
<CallEndedView
|
endedCallId={rtcSession.room.roomId}
|
||||||
endedCallId={rtcSession.room.roomId}
|
client={client}
|
||||||
client={client}
|
isPasswordlessUser={isPasswordlessUser}
|
||||||
isPasswordlessUser={isPasswordlessUser}
|
confineToRoom={confineToRoom}
|
||||||
confineToRoom={confineToRoom}
|
/>
|
||||||
leaveError={leaveError}
|
|
||||||
reconnect={onReconnect}
|
|
||||||
/>
|
|
||||||
;
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// If the user is a regular user, we'll have sent them back to the homepage,
|
// 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
|
// so just sit here & do nothing: otherwise we would (briefly) mount the
|
||||||
// LobbyView again which would open capture devices again.
|
// LobbyView again which would open capture devices again.
|
||||||
return null;
|
body = null;
|
||||||
}
|
}
|
||||||
} else if (left && widget !== null) {
|
} else if (left && widget !== null) {
|
||||||
// Left in widget mode:
|
// Left in widget mode:
|
||||||
if (!returnToLobby) {
|
if (!returnToLobby) {
|
||||||
return null;
|
body = null;
|
||||||
}
|
}
|
||||||
} else if (preload || skipLobby) {
|
} else if (preload || skipLobby) {
|
||||||
return null;
|
body = null;
|
||||||
|
} else {
|
||||||
|
body = lobbyView;
|
||||||
}
|
}
|
||||||
|
|
||||||
return lobbyView;
|
return <ErrorBoundary fallback={errorPage}>{body}</ErrorBoundary>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,6 +102,8 @@ const canScreenshare = "getDisplayMedia" in (navigator.mediaDevices ?? {});
|
|||||||
|
|
||||||
const maxTapDurationMs = 400;
|
const maxTapDurationMs = 400;
|
||||||
|
|
||||||
|
export class ConnectionLostError extends Error {}
|
||||||
|
|
||||||
export interface ActiveCallProps
|
export interface ActiveCallProps
|
||||||
extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
|
extends Omit<InCallViewProps, "vm" | "livekitRoom" | "connState"> {
|
||||||
e2eeSystem: EncryptionSystem;
|
e2eeSystem: EncryptionSystem;
|
||||||
@@ -173,7 +175,8 @@ export interface InCallViewProps {
|
|||||||
livekitRoom: Room;
|
livekitRoom: Room;
|
||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
participantCount: number;
|
participantCount: number;
|
||||||
onLeave: (error?: Error) => void;
|
/** Function to call when the user explicitly ends the call */
|
||||||
|
onLeave: () => void;
|
||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
otelGroupCallMembership?: OTelGroupCallMembership;
|
otelGroupCallMembership?: OTelGroupCallMembership;
|
||||||
connState: ECConnectionState;
|
connState: ECConnectionState;
|
||||||
@@ -198,13 +201,10 @@ export const InCallView: FC<InCallViewProps> = ({
|
|||||||
|
|
||||||
useWakeLock();
|
useWakeLock();
|
||||||
|
|
||||||
useEffect(() => {
|
// annoyingly we don't get the disconnection reason this way,
|
||||||
if (connState === ConnectionState.Disconnected) {
|
// only by listening for the emitted event
|
||||||
// annoyingly we don't get the disconnection reason this way,
|
if (connState === ConnectionState.Disconnected)
|
||||||
// only by listening for the emitted event
|
throw new ConnectionLostError();
|
||||||
onLeave(new Error("Disconnected from call server"));
|
|
||||||
}
|
|
||||||
}, [connState, onLeave]);
|
|
||||||
|
|
||||||
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
const containerRef1 = useRef<HTMLDivElement | null>(null);
|
||||||
const [containerRef2, bounds] = useMeasure();
|
const [containerRef2, bounds] = useMeasure();
|
||||||
|
|||||||
Reference in New Issue
Block a user