From 2374a3fd333a58b274e3e03fb04a06b8e01e2361 Mon Sep 17 00:00:00 2001 From: Will Hunt Date: Tue, 16 Sep 2025 14:16:11 +0100 Subject: [PATCH 1/9] Prevent showing calling view when disconnected from Livekit. (#3491) * Refactor disconnection handling * Use "unknown" * Update signature * Add tests * Expose livekitConnectionState directly * fix whoopsie --- src/room/InCallView.test.tsx | 3 +-- src/room/InCallView.tsx | 19 ++++++-------- src/state/CallViewModel.test.ts | 45 +++++++++++++++++++++++++++++++++ src/state/CallViewModel.ts | 24 ++++++++++++------ 4 files changed, 70 insertions(+), 21 deletions(-) diff --git a/src/room/InCallView.test.tsx b/src/room/InCallView.test.tsx index eb192f66..f20ffada 100644 --- a/src/room/InCallView.test.tsx +++ b/src/room/InCallView.test.tsx @@ -17,7 +17,7 @@ import { act, render, type RenderResult } from "@testing-library/react"; import { type MatrixClient, JoinRule, type RoomState } from "matrix-js-sdk"; import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container"; -import { ConnectionState, type LocalParticipant } from "livekit-client"; +import { type LocalParticipant } from "livekit-client"; import { of } from "rxjs"; import { BrowserRouter } from "react-router-dom"; import { TooltipProvider } from "@vector-im/compound-web"; @@ -180,7 +180,6 @@ function createInCallView(): RenderResult & { onLeave={function (): void { throw new Error("Function not implemented."); }} - connState={ConnectionState.Connected} onShareClick={null} /> diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 3d7044be..350af973 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -25,7 +25,11 @@ import useMeasure from "react-use-measure"; import { type MatrixRTCSession } from "matrix-js-sdk/lib/matrixrtc"; import classNames from "classnames"; import { BehaviorSubject, map } from "rxjs"; -import { useObservable, useSubscription } from "observable-hooks"; +import { + useObservable, + useObservableEagerState, + useSubscription, +} from "observable-hooks"; import { logger } from "matrix-js-sdk/lib/logger"; import { RoomAndToDeviceEvents } from "matrix-js-sdk/lib/matrixrtc/RoomAndToDeviceKeyTransport"; import { @@ -63,7 +67,6 @@ import { type MuteStates } from "./MuteStates"; import { type MatrixInfo } from "./VideoPreview"; import { InviteButton } from "../button/InviteButton"; import { LayoutToggle } from "./LayoutToggle"; -import { type ECConnectionState } from "../livekit/useECConnectionState"; import { useOpenIDSFU } from "../livekit/openIDSFU"; import { CallViewModel, @@ -212,12 +215,7 @@ export const ActiveCall: FC = (props) => { return ( - + ); @@ -235,7 +233,6 @@ export interface InCallViewProps { onLeave: (cause: "user", soundFile?: CallEventSounds) => void; header: HeaderStyle; otelGroupCallMembership?: OTelGroupCallMembership; - connState: ECConnectionState; onShareClick: (() => void) | null; } @@ -249,7 +246,6 @@ export const InCallView: FC = ({ muteStates, onLeave, header: headerStyle, - connState, onShareClick, }) => { const { t } = useTranslation(); @@ -257,10 +253,11 @@ export const InCallView: FC = ({ useReactionsSender(); useWakeLock(); + const connectionState = useObservableEagerState(vm.livekitConnectionState$); // annoyingly we don't get the disconnection reason this way, // only by listening for the emitted event - if (connState === ConnectionState.Disconnected) + if (connectionState === ConnectionState.Disconnected) throw new ConnectionLostError(); const containerRef1 = useRef(null); diff --git a/src/state/CallViewModel.test.ts b/src/state/CallViewModel.test.ts index 4b5e603f..b6935a9b 100644 --- a/src/state/CallViewModel.test.ts +++ b/src/state/CallViewModel.test.ts @@ -1291,6 +1291,51 @@ describe("waitForCallPickup$", () => { }); }); + test("ringing -> unknown if we get disconnected", () => { + withTestScheduler(({ behavior, schedule, expectObservable }) => { + const connectionState$ = new BehaviorSubject(ConnectionState.Connected); + // Someone joins at 20ms (both LiveKit participant and MatrixRTC member) + withCallViewModel( + { + remoteParticipants$: behavior("a 19ms b", { + a: [], + b: [aliceParticipant], + }), + rtcMembers$: behavior("a 19ms b", { + a: [localRtcMember], + b: [localRtcMember, aliceRtcMember], + }), + connectionState$, + }, + (vm, rtcSession) => { + // Notify at 5ms so we enter ringing, then get disconnected 5ms later + schedule(" 5ms r 5ms d", { + r: () => { + rtcSession.emit( + MatrixRTCSessionEvent.DidSendCallNotification, + mockRingEvent("$notif2", 100), + mockLegacyRingEvent, + ); + }, + d: () => { + connectionState$.next(ConnectionState.Disconnected); + }, + }); + + expectObservable(vm.callPickupState$).toBe("a 4ms b 5ms c", { + a: "unknown", + b: "ringing", + c: "unknown", + }); + }, + { + waitForCallPickup: true, + encryptionSystem: { kind: E2eeType.PER_PARTICIPANT }, + }, + ); + }); + }); + test("success when someone joins before we notify", () => { withTestScheduler(({ behavior, schedule, expectObservable }) => { // Join at 10ms, notify later at 20ms (state should stay success) diff --git a/src/state/CallViewModel.ts b/src/state/CallViewModel.ts index ca797aa8..8289369f 100644 --- a/src/state/CallViewModel.ts +++ b/src/state/CallViewModel.ts @@ -947,6 +947,7 @@ export class CallViewModel extends ViewModel { * The current call pickup state of the call. * - "unknown": The client has not yet sent the notification event. We don't know if it will because it first needs to send its own membership. * Then we can conclude if we were the first one to join or not. + * This may also be set if we are disconnected. * - "ringing": The call is ringing on other devices in this room (This client should give audiovisual feedback that this is happening). * - "timeout": No-one picked up in the defined time this call should be ringing on others devices. * The call failed. If desired this can be used as a trigger to exit the call. @@ -959,13 +960,20 @@ export class CallViewModel extends ViewModel { ? this.scope.behavior< "unknown" | "ringing" | "timeout" | "decline" | "success" >( - this.someoneElseJoined$.pipe( - switchMap((someoneElseJoined) => - someoneElseJoined - ? of("success" as const) - : // Show the ringing state of the most recent ringing attempt. - this.ring$.pipe(switchAll()), - ), + combineLatest([ + this.livekitConnectionState$, + this.someoneElseJoined$, + ]).pipe( + switchMap(([livekitConnectionState, someoneElseJoined]) => { + if (livekitConnectionState === ConnectionState.Disconnected) { + // Do not ring until we're connected. + return of("unknown" as const); + } else if (someoneElseJoined) { + return of("success" as const); + } + // Show the ringing state of the most recent ringing attempt. + return this.ring$.pipe(switchAll()); + }), // The state starts as 'unknown' because we don't know if the RTC // session will actually send a notify event yet. It will only be // known once we send our own membership and see that we were the @@ -1682,7 +1690,7 @@ export class CallViewModel extends ViewModel { private readonly livekitRoom: LivekitRoom, private readonly mediaDevices: MediaDevices, private readonly options: CallViewModelOptions, - private readonly livekitConnectionState$: Observable, + public readonly livekitConnectionState$: Observable, private readonly handsRaisedSubject$: Observable< Record >, From df7bd8ff2bc6465a566491d14fd979526d77056f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 18 Sep 2025 11:46:55 +0200 Subject: [PATCH 2/9] Update dependency livekit-client to v2.15.7 (#3496) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index b2189691..5f224576 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10065,8 +10065,8 @@ __metadata: linkType: hard "livekit-client@npm:^2.13.0": - version: 2.15.6 - resolution: "livekit-client@npm:2.15.6" + version: 2.15.7 + resolution: "livekit-client@npm:2.15.7" dependencies: "@livekit/mutex": "npm:1.1.1" "@livekit/protocol": "npm:1.39.3" @@ -10079,7 +10079,7 @@ __metadata: webrtc-adapter: "npm:^9.0.1" peerDependencies: "@types/dom-mediacapture-record": ^1 - checksum: 10c0/f1ab6cdf2b85647036e9de906734c1394dac497da0bd879a29d0c587c437ada262021478fcef24df99b3489a39d97fe67ab33de0785ed0a63335da2fef577192 + checksum: 10c0/17ea084565a4fef31038af4ce1aa6951125dc80b78d6d320789d67bfc544d95b6b661032848221758c9ece6dab6ab0e867da683759f11b0bc65a1d94ab27d185 languageName: node linkType: hard From 4be395500fdf896ffc74b1b389b26e4912e18da1 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 18 Sep 2025 12:58:47 +0200 Subject: [PATCH 3/9] Fix the interactivity of buttons while reconnecting or in earpiece mode (#3486) * Fix the interactivity of buttons while reconnecting or in earpiece mode When we're in one of these modes, we need to ensure that everything above the overlay (the header and footer buttons) is interactive, while everything obscured by the overlay (the media tiles) is non-interactive and removed from the accessibility tree. It's not a very easy task to trap focus *outside* an element, so the best solution I could come up with is to set tabindex="-1" manually on all interactive elements belonging to the media tiles. * Write a Playwright test for reconnecting * fix lints Signed-off-by: Timo K * fix test Signed-off-by: Timo K * enable http2 for matrx-rtc host to allow the jwt service to talk to the SFU * remove rate limit for delayed events * more time to connect to livekit SFU * Due to a Firefox issue we set the start anchor for the tab test to the Mute microphone button * adapt to most recent Element Web version * Use the "End call" button as proofe for a started call * Currrenty disabled due to recent Element Web - not indicating the number of participants - bypassing Lobby * linting * disable 'can only interact with header and footer while reconnecting' for firefox --------- Signed-off-by: Timo K Co-authored-by: Timo <16718859+toger5@users.noreply.github.com> Co-authored-by: Timo K Co-authored-by: fkwp --- backend/dev_nginx.conf | 2 + backend/playwright_homeserver.yaml | 4 ++ playwright/fixtures/widget-user.ts | 8 +-- playwright/reconnect.spec.ts | 60 +++++++++++++++++++ playwright/widget/simple-create.spec.ts | 43 +++++++------ src/Overlay.module.css | 2 + src/Toast.tsx | 12 ++-- src/reactions/RaisedHandIndicator.tsx | 3 + src/room/EarpieceOverlay.module.css | 1 - src/room/EarpieceOverlay.tsx | 2 +- src/room/InCallView.tsx | 58 +++++++++++++----- .../__snapshots__/InCallView.test.tsx.snap | 5 ++ src/tile/GridTile.test.tsx | 1 + src/tile/GridTile.tsx | 12 +++- src/tile/MediaView.test.tsx | 1 + src/tile/MediaView.tsx | 4 ++ src/tile/SpotlightTile.test.tsx | 1 + src/tile/SpotlightTile.tsx | 11 ++++ 18 files changed, 182 insertions(+), 48 deletions(-) create mode 100644 playwright/reconnect.spec.ts diff --git a/backend/dev_nginx.conf b/backend/dev_nginx.conf index a29b06d7..aadb612c 100644 --- a/backend/dev_nginx.conf +++ b/backend/dev_nginx.conf @@ -59,6 +59,8 @@ server { ssl_certificate /root/ssl/cert.pem; ssl_certificate_key /root/ssl/key.pem; + http2 on; + location ^~ /livekit/jwt/ { diff --git a/backend/playwright_homeserver.yaml b/backend/playwright_homeserver.yaml index ca45cf3f..38350a3c 100644 --- a/backend/playwright_homeserver.yaml +++ b/backend/playwright_homeserver.yaml @@ -44,6 +44,10 @@ rc_message: per_second: 10000 burst_count: 10000 +rc_delayed_event_mgmt: + per_second: 10000 + burst_count: 10000 + rc_login: address: per_second: 10000 diff --git a/playwright/fixtures/widget-user.ts b/playwright/fixtures/widget-user.ts index 3ccb2ab2..8089c9de 100644 --- a/playwright/fixtures/widget-user.ts +++ b/playwright/fixtures/widget-user.ts @@ -159,8 +159,8 @@ export const widgetTest = test.extend({ } = await registerUser(browser, userB); // Invite the second user - await ewPage1.getByRole("button", { name: "Add room" }).click(); - await ewPage1.getByText("New room").click(); + await ewPage1.getByRole("button", { name: "Add", exact: true }).click(); + await ewPage1.getByRole("menuitem", { name: "New Room" }).click(); await ewPage1.getByRole("textbox", { name: "Name" }).fill("Welcome Room"); await ewPage1.getByRole("button", { name: "Create room" }).click(); await expect(ewPage1.getByText("You created this room.")).toBeVisible(); @@ -184,9 +184,9 @@ export const widgetTest = test.extend({ // Accept the invite await expect( - ewPage2.getByRole("treeitem", { name: "Welcome Room" }), + ewPage2.getByRole("option", { name: "Welcome Room" }), ).toBeVisible(); - await ewPage2.getByRole("treeitem", { name: "Welcome Room" }).click(); + await ewPage2.getByRole("option", { name: "Welcome Room" }).click(); await ewPage2.getByRole("button", { name: "Accept" }).click(); await expect( ewPage2.getByRole("main").getByRole("heading", { name: "Welcome Room" }), diff --git a/playwright/reconnect.spec.ts b/playwright/reconnect.spec.ts new file mode 100644 index 00000000..3b419af4 --- /dev/null +++ b/playwright/reconnect.spec.ts @@ -0,0 +1,60 @@ +/* +Copyright 2025 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 { expect, test } from "@playwright/test"; + +// Skip test for Firefox, due to page.keyboard.press("Tab") not reliable on headless mode +test.skip( + ({ browserName }) => browserName === "firefox", + 'This test is not working on firefox, page.keyboard.press("Tab") not reliable in headless mode', +); + +test("can only interact with header and footer while reconnecting", async ({ + page, +}) => { + await page.goto("/"); + await page.getByTestId("home_callName").click(); + await page.getByTestId("home_callName").fill("Test call"); + await page.getByTestId("home_displayName").click(); + await page.getByTestId("home_displayName").fill("Test user"); + // If we do not call fastForward here, we end up with Date.now() returning an actual timestamp + // but once we call `await page.clock.fastForward(20000);` later this will reset Date.now() to 0 + // and we will never get into probablyDisconnected state? + await page.clock.fastForward(10); + await page.getByTestId("home_go").click(); + + await expect(page.locator("video")).toBeVisible(); + await expect(page.getByTestId("lobby_joinCall")).toBeVisible(); + + // Join the call + await page.getByTestId("lobby_joinCall").click(); + + // The media tile for the local user should become visible + await new Promise((resolve) => setTimeout(resolve, 1500)); + await expect(page.getByTestId("name_tag")).toContainText("Test user"); + + // Now disconnect from the internet + await page.route("https://synapse.m.localhost/**/*", async (route) => { + await new Promise((resolve) => setTimeout(resolve, 10000)); + await route.continue(); + }); + await page.clock.fastForward(20000); + + await expect( + page.getByRole("dialog", { name: "Reconnecting…" }), + ).toBeVisible(); + + // Tab order should jump directly from header to footer, skipping media tiles + await page.getByRole("button", { name: "Mute microphone" }).focus(); + await expect( + page.getByRole("button", { name: "Mute microphone" }), + ).toBeFocused(); + await page.keyboard.press("Tab"); + await expect(page.getByRole("button", { name: "Stop video" })).toBeFocused(); + // Most critically, we should be able to press the hangup button + await page.getByRole("button", { name: "End call" }).click(); +}); diff --git a/playwright/widget/simple-create.spec.ts b/playwright/widget/simple-create.spec.ts index 00d5c658..8c889892 100644 --- a/playwright/widget/simple-create.spec.ts +++ b/playwright/widget/simple-create.spec.ts @@ -49,7 +49,10 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => { // Check the join indicator on the room list await expect( - brooks.page.locator("div").filter({ hasText: /^Joined • 1$/ }), + brooks.page + .locator('iframe[title="Element Call"]') + .contentFrame() + .getByRole("button", { name: "End call" }), ).toBeVisible(); // Join from the other side @@ -59,26 +62,28 @@ widgetTest("Start a new call as widget", async ({ asWidget, browserName }) => { ).toBeVisible(); await whistler.page.getByRole("button", { name: "Join" }).click(); - await expect( - whistler.page - .locator('iframe[title="Element Call"]') - .contentFrame() - .getByTestId("lobby_joinCall"), - ).toBeVisible(); + // Currently disabled due to recent Element Web is bypassing Lobby + // await expect( + // whistler.page + // .locator('iframe[title="Element Call"]') + // .contentFrame() + // .getByTestId("lobby_joinCall"), + // ).toBeVisible(); + // + // await whistler.page + // .locator('iframe[title="Element Call"]') + // .contentFrame() + // .getByTestId("lobby_joinCall") + // .click(); - await whistler.page - .locator('iframe[title="Element Call"]') - .contentFrame() - .getByTestId("lobby_joinCall") - .click(); + // Currrenty disabled due to recent Element Web not indicating the number of participants + // await expect( + // whistler.page.locator("div").filter({ hasText: /^Joined • 2$/ }), + // ).toBeVisible(); - await expect( - whistler.page.locator("div").filter({ hasText: /^Joined • 2$/ }), - ).toBeVisible(); - - await expect( - brooks.page.locator("div").filter({ hasText: /^Joined • 2$/ }), - ).toBeVisible(); + // await expect( + // brooks.page.locator("div").filter({ hasText: /^Joined • 2$/ }), + // ).toBeVisible(); // Whistler leaves await whistler.page.waitForTimeout(1000); diff --git a/src/Overlay.module.css b/src/Overlay.module.css index fa972e6f..d711d05b 100644 --- a/src/Overlay.module.css +++ b/src/Overlay.module.css @@ -35,6 +35,8 @@ Please see LICENSE in the repository root for full details. .bg.animate[data-state="closed"] { animation: fade-out 130ms; + opacity: 0; + pointer-events: none; } .overlay { diff --git a/src/Toast.tsx b/src/Toast.tsx index 105572c8..83e220bc 100644 --- a/src/Toast.tsx +++ b/src/Toast.tsx @@ -46,11 +46,11 @@ interface Props { */ Icon?: ComponentType>; /** - * Whether the toast should be portaled into the root of the document (rather - * than rendered in-place within the component tree). + * Whether the toast should be modal, making it fill the screen (by portalling + * it into the root of the document) and trap focus until dismissed. * @default true */ - portal?: boolean; + modal?: boolean; } /** @@ -62,7 +62,7 @@ export const Toast: FC = ({ autoDismiss, children, Icon, - portal = true, + modal = true, }) => { const onOpenChange = useCallback( (open: boolean) => { @@ -103,8 +103,8 @@ export const Toast: FC = ({ ); return ( - - {portal ? {content} : content} + + {modal ? {content} : content} ); }; diff --git a/src/reactions/RaisedHandIndicator.tsx b/src/reactions/RaisedHandIndicator.tsx index 12974849..67d9cf16 100644 --- a/src/reactions/RaisedHandIndicator.tsx +++ b/src/reactions/RaisedHandIndicator.tsx @@ -22,11 +22,13 @@ export function RaisedHandIndicator({ miniature, showTimer, onClick, + tabIndex, }: { raisedHandTime?: Date; miniature?: boolean; showTimer?: boolean; onClick?: () => void; + tabIndex?: number; }): ReactNode { const { t } = useTranslation(); const [raisedHandDuration, setRaisedHandDuration] = useState(""); @@ -94,6 +96,7 @@ export function RaisedHandIndicator({ background: "none", }} onClick={clickCallback} + tabIndex={tabIndex} > {content} diff --git a/src/room/EarpieceOverlay.module.css b/src/room/EarpieceOverlay.module.css index fd46d0e2..d0757cdb 100644 --- a/src/room/EarpieceOverlay.module.css +++ b/src/room/EarpieceOverlay.module.css @@ -34,7 +34,6 @@ .overlay[data-show="false"] { animation: fade-out 130ms forwards; - content-visibility: hidden; pointer-events: none; } diff --git a/src/room/EarpieceOverlay.tsx b/src/room/EarpieceOverlay.tsx index 14ce33cc..6835bdd7 100644 --- a/src/room/EarpieceOverlay.tsx +++ b/src/room/EarpieceOverlay.tsx @@ -20,7 +20,7 @@ interface Props { export const EarpieceOverlay: FC = ({ show, onBackToVideoPressed }) => { const { t } = useTranslation(); return ( -
+
diff --git a/src/room/InCallView.tsx b/src/room/InCallView.tsx index 350af973..348a2c44 100644 --- a/src/room/InCallView.tsx +++ b/src/room/InCallView.tsx @@ -54,7 +54,6 @@ import { type HeaderStyle, useUrlParams } from "../UrlParams"; import { useCallViewKeyboardShortcuts } from "../useCallViewKeyboardShortcuts"; import { ElementWidgetActions, widget } from "../widget"; import styles from "./InCallView.module.css"; -import overlayStyles from "../Overlay.module.css"; import { GridTile } from "../tile/GridTile"; import { type OTelGroupCallMembership } from "../otel/OTelGroupCallMembership"; import { SettingsModal, defaultSettingsTab } from "../settings/SettingsModal"; @@ -119,6 +118,7 @@ import { EarpieceOverlay } from "./EarpieceOverlay.tsx"; import { useAppBarHidden, useAppBarSecondaryButton } from "../AppBar.tsx"; import { useBehavior } from "../useBehavior.ts"; import { Toast } from "../Toast.tsx"; +import overlayStyles from "../Overlay.module.css"; import { Avatar, Size as AvatarSize } from "../Avatar"; import waitingStyles from "./WaitingForJoin.module.css"; import { prefetchSounds } from "../soundUtils"; @@ -641,6 +641,38 @@ export const InCallView: FC = ({ } } + // The reconnecting toast cannot be dismissed + const onDismissReconnectingToast = useCallback(() => {}, []); + // We need to use a non-modal toast to avoid trapping focus within the toast. + // However, a non-modal toast will not render any background overlay on its + // own, so we must render one manually. + const reconnectingToast = ( + <> +
+ + {t("common.reconnecting")} + + + ); + + const earpieceOverlay = ( + + ); + + // If the reconnecting toast or earpiece overlay obscures the media tiles, we + // need to remove them from the accessibility tree and block focus. + const contentObscured = reconnecting || earpieceMode; + const Tile = useMemo( () => function Tile({ @@ -670,6 +702,7 @@ export const InCallView: FC = ({ className={classNames(className, styles.tile)} style={style} showSpeakingIndicators={showSpeakingIndicatorsValue} + focusable={!contentObscured} /> ) : ( = ({ targetWidth={targetWidth} targetHeight={targetHeight} showIndicators={showSpotlightIndicatorsValue} + focusable={!contentObscured} className={classNames(className, styles.tile)} style={style} /> ); }, - [vm, openProfile], + [vm, openProfile, contentObscured], ); const layouts = useMemo(() => { @@ -714,6 +748,8 @@ export const InCallView: FC = ({ targetWidth={gridBounds.height} targetHeight={gridBounds.width} showIndicators={false} + focusable={!contentObscured} + aria-hidden={contentObscured} /> ); } @@ -731,6 +767,7 @@ export const InCallView: FC = ({ model={layout} Layout={layers.fixed} Tile={Tile} + aria-hidden={contentObscured} /> ); const scrollingGrid = ( @@ -740,6 +777,7 @@ export const InCallView: FC = ({ model={layout} Layout={layers.scrolling} Tile={Tile} + aria-hidden={contentObscured} /> ); // The grid tiles go *under* the spotlight in the portrait layout, but @@ -869,9 +907,6 @@ export const InCallView: FC = ({
); - // The reconnecting toast cannot be dismissed - const onDismissReconnectingToast = useCallback(() => {}, []); - return (
= ({ {renderContent()} - - {t("common.reconnecting")} - - + {reconnectingToast} + {earpieceOverlay} {waitingOverlay} {footer} diff --git a/src/room/__snapshots__/InCallView.test.tsx.snap b/src/room/__snapshots__/InCallView.test.tsx.snap index bfbef499..8a2ef37e 100644 --- a/src/room/__snapshots__/InCallView.test.tsx.snap +++ b/src/room/__snapshots__/InCallView.test.tsx.snap @@ -111,6 +111,11 @@ exports[`InCallView > rendering > renders 1`] = `
+