Drop ErrorView's widget prop in favour of the host bridge

ErrorView needed a widget only to decide whether to offer a close button
or a link home. That prop was threaded down through ErrorPage, RichError
and GroupCallErrorBoundary from seven call sites, to answer the single
question of whether the host can dismiss Element Call — which the host
bridge now answers directly through the presence of close().

Read the bridge from context in ErrorView and remove the prop, along with
the plumbing that carried it.

No functional change: the rendered output is unchanged, as the existing
snapshot confirms.

Move GroupCallView onto the host bridge

GroupCallView asked the widget API to keep it on screen, to close it, and
to tell it when a preloaded call should join. Route all three through the
host bridge and drop the widget prop.
This commit is contained in:
Valere
2026-09-02 13:03:23 +02:00
parent d8d70812b8
commit 8e78ac76bd
18 changed files with 542 additions and 365 deletions
+20 -38
View File
@@ -36,7 +36,11 @@ import {
UnknownCallError,
} from "../utils/errors.ts";
import { mockConfig } from "../utils/test.ts";
import { ElementWidgetActions, type WidgetHelpers } from "../widget.ts";
import {
type HostBridge,
HostBridgeProvider,
nullHostBridge,
} from "../HostBridge.ts";
test.each([
{
@@ -79,7 +83,6 @@ test.each([
<GroupCallErrorBoundary
onError={onErrorMock}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
@@ -108,7 +111,6 @@ test("should render the error page with link back to home", async () => {
<GroupCallErrorBoundary
onError={onErrorMock}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
@@ -154,10 +156,7 @@ test("ConnectionLostError: Action handling should reset error state", async () =
return (
<BrowserRouter>
<GroupCallErrorBoundary
recoveryActionHandler={reconnectCallback}
widget={null}
>
<GroupCallErrorBoundary recoveryActionHandler={reconnectCallback}>
<TestComponent fail={failState} />
</GroupCallErrorBoundary>
</BrowserRouter>
@@ -199,7 +198,6 @@ describe("Rageshake button", () => {
<GroupCallErrorBoundary
onError={vi.fn()}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
@@ -224,29 +222,27 @@ describe("Rageshake button", () => {
});
});
test("should have a close button in widget mode", async () => {
test("should have a close button when the host can dismiss us", async () => {
const error = new MatrixRTCTransportMissingError("example.com");
const TestComponent = (): ReactNode => {
throw error;
};
const mockWidget = {
api: {
transport: { send: vi.fn().mockResolvedValue(undefined), stop: vi.fn() },
},
} as unknown as WidgetHelpers;
const close = vi.fn().mockResolvedValue(undefined);
const hostBridge: HostBridge = { ...nullHostBridge, close };
const user = userEvent.setup();
const onErrorMock = vi.fn();
const { asFragment } = render(
<BrowserRouter>
<GroupCallErrorBoundary
widget={mockWidget}
onError={onErrorMock}
recoveryActionHandler={vi.fn()}
>
<TestComponent />
</GroupCallErrorBoundary>
<HostBridgeProvider value={hostBridge}>
<GroupCallErrorBoundary
onError={onErrorMock}
recoveryActionHandler={vi.fn()}
>
<TestComponent />
</GroupCallErrorBoundary>
</HostBridgeProvider>
</BrowserRouter>,
);
@@ -258,11 +254,7 @@ test("should have a close button in widget mode", async () => {
await user.click(screen.getByRole("button", { name: "Close" }));
expect(mockWidget.api.transport.send).toHaveBeenCalledWith(
ElementWidgetActions.Close,
expect.anything(),
);
expect(mockWidget.api.transport.stop).toHaveBeenCalled();
expect(close).toHaveBeenCalled();
});
test("should show technical details when error has a matrixError cause", async () => {
@@ -282,11 +274,7 @@ test("should show technical details when error has a matrixError cause", async (
render(
<BrowserRouter>
<GroupCallErrorBoundary
onError={vi.fn()}
recoveryActionHandler={vi.fn()}
widget={null}
>
<GroupCallErrorBoundary onError={vi.fn()} recoveryActionHandler={vi.fn()}>
<TestComponent />
</GroupCallErrorBoundary>
</BrowserRouter>,
@@ -315,11 +303,7 @@ test("should not show technical details when error has no matrix error cause", a
render(
<BrowserRouter>
<GroupCallErrorBoundary
onError={vi.fn()}
recoveryActionHandler={vi.fn()}
widget={null}
>
<GroupCallErrorBoundary onError={vi.fn()} recoveryActionHandler={vi.fn()}>
<TestComponent />
</GroupCallErrorBoundary>
</BrowserRouter>,
@@ -376,7 +360,6 @@ describe("LiveKit ConnectionError variants", () => {
<GroupCallErrorBoundary
onError={vi.fn()}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
@@ -406,7 +389,6 @@ describe("LiveKit ConnectionError variants", () => {
<GroupCallErrorBoundary
onError={vi.fn()}
recoveryActionHandler={vi.fn()}
widget={null}
>
<TestComponent />
</GroupCallErrorBoundary>
+1 -8
View File
@@ -34,7 +34,6 @@ import {
} from "../utils/errors.ts";
import { FullScreenView } from "../FullScreenView.tsx";
import { ErrorView } from "../ErrorView.tsx";
import { type WidgetHelpers } from "../widget.ts";
import styles from "../ErrorView.module.css";
export type CallErrorRecoveryAction = "reconnect"; // | "retry" ;
@@ -47,13 +46,11 @@ interface ErrorPageProps {
error: ElementCallError;
recoveryActionHandler: RecoveryActionHandler;
resetError: () => void;
widget: WidgetHelpers | null;
}
const ErrorPage: FC<ErrorPageProps> = ({
error,
recoveryActionHandler,
widget,
}: ErrorPageProps): ReactElement => {
const { t } = useTranslation();
logger.error("Error boundary caught:", error);
@@ -89,7 +86,6 @@ const ErrorPage: FC<ErrorPageProps> = ({
Icon={icon}
title={error.localisedTitle}
rageshake={error.code == ErrorCode.UNKNOWN_ERROR}
widget={widget}
>
<p>
{error.localisedMessageKey ? (
@@ -148,14 +144,12 @@ interface BoundaryProps {
children: ReactNode | (() => ReactNode);
recoveryActionHandler: RecoveryActionHandler;
onError?: (error: unknown) => void;
widget: WidgetHelpers | null;
}
export const GroupCallErrorBoundary = ({
recoveryActionHandler,
onError,
children,
widget,
}: BoundaryProps): ReactElement => {
const fallbackRenderer: FallbackRender = useCallback(
({ error, resetError }): ReactElement => {
@@ -165,7 +159,6 @@ export const GroupCallErrorBoundary = ({
: new UnknownCallError(error instanceof Error ? error : new Error());
return (
<ErrorPage
widget={widget ?? null}
error={callError}
resetError={resetError}
recoveryActionHandler={async (action: CallErrorRecoveryAction) => {
@@ -175,7 +168,7 @@ export const GroupCallErrorBoundary = ({
/>
);
},
[recoveryActionHandler, widget],
[recoveryActionHandler],
);
return (
+49 -83
View File
@@ -36,7 +36,6 @@ import userEvent, {
import { type RelationsContainer } from "matrix-js-sdk/lib/models/relations-container";
import { useState } from "react";
import { TooltipProvider } from "@vector-im/compound-web";
import { type ITransport } from "matrix-widget-api";
import { prefetchSounds } from "../soundUtils";
import { useAudioContext } from "../useAudioContext";
@@ -52,8 +51,11 @@ import {
} from "../utils/test";
import { GroupCallView } from "./GroupCallView";
import { GroupCallErrorBoundary } from "./GroupCallErrorBoundary";
import { ElementWidgetActions, type WidgetHelpers } from "../widget";
import { LazyEventEmitter } from "../LazyEventEmitter";
import {
type HostBridge,
HostBridgeProvider,
nullHostBridge,
} from "../HostBridge";
import { MatrixRTCTransportMissingError } from "../utils/errors";
import { ProcessorProvider } from "../livekit/TrackProcessorContext";
import { MediaDevicesContext } from "../MediaDevicesContext";
@@ -134,7 +136,7 @@ beforeEach(() => {
});
function createGroupCallView(
widget: WidgetHelpers | null,
hostBridge: HostBridge,
joined = true,
options: {
withErrorBoundary?: boolean;
@@ -184,7 +186,6 @@ function createGroupCallView(
skipLobby={false}
rtcSession={rtcSession.asMockedSession()}
muteStates={muteState}
widget={widget}
// TODO-MULTI-SFU: Make joined and setJoined work
joined={true}
setJoined={function (value: boolean): void {}}
@@ -192,22 +193,21 @@ function createGroupCallView(
);
const { getByText } = render(
<BrowserRouter>
<TooltipProvider>
<MediaDevicesContext value={mockMediaDevices({})}>
<ProcessorProvider>
{options.withErrorBoundary ? (
<GroupCallErrorBoundary
recoveryActionHandler={vi.fn()}
widget={null}
>
{groupCallView}
</GroupCallErrorBoundary>
) : (
groupCallView
)}
</ProcessorProvider>
</MediaDevicesContext>
</TooltipProvider>
<HostBridgeProvider value={hostBridge}>
<TooltipProvider>
<MediaDevicesContext value={mockMediaDevices({})}>
<ProcessorProvider>
{options.withErrorBoundary ? (
<GroupCallErrorBoundary recoveryActionHandler={vi.fn()}>
{groupCallView}
</GroupCallErrorBoundary>
) : (
groupCallView
)}
</ProcessorProvider>
</MediaDevicesContext>
</TooltipProvider>
</HostBridgeProvider>
</BrowserRouter>,
);
return {
@@ -218,7 +218,7 @@ function createGroupCallView(
test.skip("GroupCallView plays a leave sound asynchronously in SPA mode", async () => {
const user = userEvent.setup();
const { getByText, rtcSession } = createGroupCallView(null);
const { getByText, rtcSession } = createGroupCallView(nullHostBridge);
const leaveButton = getByText("Leave");
await user.click(leaveButton);
expect(playSound).toHaveBeenCalledWith("left");
@@ -235,12 +235,7 @@ test.skip("GroupCallView plays a leave sound asynchronously in SPA mode", async
test.skip("GroupCallView plays a leave sound synchronously in widget mode", async () => {
const user = userEvent.setup();
const widget = {
api: {
setAlwaysOnScreen: async () => Promise.resolve(true),
} as Partial<WidgetHelpers["api"]>,
lazyActions: new LazyEventEmitter(),
};
const hostBridge: HostBridge = { ...nullHostBridge, close: vi.fn() };
let resolvePlaySound: () => void;
playSound = vi
.fn()
@@ -253,9 +248,7 @@ test.skip("GroupCallView plays a leave sound synchronously in widget mode", asyn
soundDuration: {},
});
const { getByText, rtcSession } = createGroupCallView(
widget as WidgetHelpers,
);
const { getByText, rtcSession } = createGroupCallView(hostBridge);
const leaveButton = getByText("Leave");
await user.click(leaveButton);
await flushPromises();
@@ -272,28 +265,13 @@ test.skip("GroupCallView plays a leave sound synchronously in widget mode", asyn
expect(leaveRTCSession).toHaveBeenCalledOnce();
});
test("Should close widget when all other left and play a sound", async () => {
test("Should ask the host to close when all other left and play a sound", async () => {
const user = userEvent.setup();
let widgetClosedCalled = false;
const { promise: widgetClosedPromise, resolve: widgetClosedResolver } =
Promise.withResolvers<void>();
const widgetSendMock = vi.fn().mockImplementation((action: string) => {
if (action === ElementWidgetActions.Close) {
widgetClosedCalled = true;
widgetClosedResolver();
}
});
const widgetStopMock = vi.fn().mockResolvedValue(undefined);
const widget = {
api: {
setAlwaysOnScreen: vi.fn().mockResolvedValue(true),
transport: {
send: widgetSendMock,
reply: vi.fn().mockResolvedValue(undefined),
stop: widgetStopMock,
} as unknown as ITransport,
} as Partial<WidgetHelpers["api"]>,
lazyActions: new LazyEventEmitter(),
const close = vi.fn().mockResolvedValue(undefined);
const hostBridge: HostBridge = {
...nullHostBridge,
setAlwaysOnScreen: vi.fn().mockResolvedValue(undefined),
close,
};
const resolvePlaySound = Promise.withResolvers<void>();
playSound = vi.fn().mockReturnValue(resolvePlaySound.promise);
@@ -303,50 +281,38 @@ test("Should close widget when all other left and play a sound", async () => {
soundDuration: {},
});
const { getByText } = createGroupCallView(widget as WidgetHelpers);
const { getByText } = createGroupCallView(hostBridge);
const leaveButton = getByText("SimulateOtherLeft");
await user.click(leaveButton);
await flushPromises();
expect(widgetClosedCalled).toBeFalsy();
expect(close).not.toHaveBeenCalled();
resolvePlaySound.resolve();
expect(playSound).toHaveBeenCalledWith("left", 0);
await widgetClosedPromise;
await flushPromises();
expect(widgetClosedCalled).toBeTruthy();
expect(widgetStopMock).toHaveBeenCalledOnce();
await waitFor(() => expect(close).toHaveBeenCalledOnce());
}, 80000);
test("Should not close widget when auto leave due to error", async () => {
test("Should not ask the host to close when auto leave due to error", async () => {
const user = userEvent.setup();
const widgetStopMock = vi.fn().mockResolvedValue(undefined);
const widgetSendMock = vi.fn().mockResolvedValue(undefined);
const widget = {
api: {
setAlwaysOnScreen: vi.fn().mockResolvedValue(true),
transport: {
send: widgetSendMock,
reply: vi.fn().mockResolvedValue(undefined),
stop: widgetStopMock,
} as unknown as ITransport,
} as Partial<WidgetHelpers["api"]>,
lazyActions: new LazyEventEmitter(),
const close = vi.fn().mockResolvedValue(undefined);
const setAlwaysOnScreen = vi.fn().mockResolvedValue(undefined);
const hostBridge: HostBridge = {
...nullHostBridge,
setAlwaysOnScreen,
close,
};
const alwaysOnScreenSpy = vi.spyOn(widget.api, "setAlwaysOnScreen");
const { getByText } = createGroupCallView(widget as WidgetHelpers);
const { getByText } = createGroupCallView(hostBridge);
const leaveButton = getByText("SimulateErrorLeft");
await user.click(leaveButton);
await flushPromises();
// When onLeft is called, we first set always on screen to false
await waitFor(() => expect(alwaysOnScreenSpy).toHaveBeenCalledWith(false));
await waitFor(() => expect(setAlwaysOnScreen).toHaveBeenCalledWith(false));
await flushPromises();
// But then we do not close the widget automatically
expect(widgetStopMock).not.toHaveBeenCalledOnce();
expect(widgetSendMock).not.toHaveBeenCalledOnce();
// But then we do not ask to be closed automatically
expect(close).not.toHaveBeenCalled();
});
test.skip("GroupCallView leaves the session when an error occurs", async () => {
@@ -360,7 +326,7 @@ test.skip("GroupCallView leaves the session when an error occurs", async () => {
);
});
const user = userEvent.setup();
const { rtcSession } = createGroupCallView(null);
const { rtcSession } = createGroupCallView(nullHostBridge);
await user.click(screen.getByRole("button", { name: "Panic!" }));
screen.getByText("Something went wrong");
expect(leaveRTCSession).toHaveBeenCalledWith(
@@ -377,7 +343,7 @@ test.skip("GroupCallView shows errors that occur during joining", async () => {
onTestFinished(() => {
enterRTCSession.mockReset();
});
createGroupCallView(null, false);
createGroupCallView(nullHostBridge, false);
await user.click(screen.getByRole("button", { name: "Join call" }));
screen.getByText("Call is not supported");
});
@@ -396,7 +362,7 @@ test("translates wrapped UnsupportedStickyEventsEndpointError to the StickyEvent
{ cause: stickyError },
);
const { rtcSession } = createGroupCallView(null, true, {
const { rtcSession } = createGroupCallView(nullHostBridge, true, {
withErrorBoundary: true,
});
@@ -408,7 +374,7 @@ test("translates wrapped UnsupportedStickyEventsEndpointError to the StickyEvent
});
test("falls back to ConnectionLostError for unrecognised membership manager errors", async () => {
const { rtcSession } = createGroupCallView(null, true, {
const { rtcSession } = createGroupCallView(nullHostBridge, true, {
withErrorBoundary: true,
});
@@ -424,7 +390,7 @@ test("falls back to ConnectionLostError for unrecognised membership manager erro
test("user can reconnect after a membership manager error", async () => {
const user = userEvent.setup();
const { rtcSession } = createGroupCallView(null, true);
const { rtcSession } = createGroupCallView(nullHostBridge, true);
await act(() =>
rtcSession.emit(MatrixRTCSessionEvent.MembershipManagerError, undefined),
);
+56 -64
View File
@@ -30,12 +30,7 @@ import {
} from "matrix-js-sdk/lib/matrixrtc";
import { useNavigate } from "react-router-dom";
import type { IWidgetApiRequest } from "matrix-widget-api";
import {
ElementWidgetActions,
type JoinCallData,
type WidgetHelpers,
} from "../widget";
import { type JoinCallData } from "../widget";
import { LobbyView } from "./LobbyView";
import { type MatrixInfo } from "./VideoPreview";
import { CallEndedView } from "./CallEndedView";
@@ -76,6 +71,7 @@ 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";
/**
* If there already are this many participants in the call, we automatically mute
@@ -99,7 +95,6 @@ interface Props {
joined: boolean;
setJoined: (value: boolean) => void;
muteStates: MuteStates;
widget: WidgetHelpers | null;
}
export const GroupCallView: FC<Props> = ({
@@ -112,7 +107,6 @@ export const GroupCallView: FC<Props> = ({
joined,
setJoined,
muteStates,
widget,
}) => {
// Used to thread through any errors that occur outside the error boundary
const [externalError, setExternalError] = useState<ElementCallError | null>(
@@ -120,6 +114,14 @@ export const GroupCallView: FC<Props> = ({
);
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.
// TODO: this reads a capability as a proxy for who owns our lifetime. Worth
// finding a more direct way to express it — see the guidance in UrlParams.ts
// on naming behaviours rather than situations.
const hostControlsLifetime = hostBridge.close !== undefined;
const muteAllAudio = useBehavior(muteAllAudio$);
const leaveSoundContext = useLatest(
@@ -294,28 +296,26 @@ export const GroupCallView: FC<Props> = ({
};
if (skipLobby) {
if (widget && preload) {
// `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 onJoin = (ev: CustomEvent<IWidgetApiRequest>): void => {
const subscription = hostBridge.join$.subscribe(({ data, reply }) => {
(async (): Promise<void> => {
await defaultDeviceSetup(ev.detail.data as unknown as JoinCallData);
await defaultDeviceSetup(data);
setJoined(true);
widget.api.transport.reply(ev.detail, {});
reply();
})().catch((e) => {
logger.error("Error joining RTC session on preload", e);
});
};
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
return (): void => {
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
};
});
return (): void => subscription.unsubscribe();
} else {
// No lobby and no preload: we enter the rtc session right away
setJoined(true);
}
}
}, [
widget,
hostBridge,
rtcSession,
preload,
skipLobby,
@@ -341,7 +341,7 @@ export const GroupCallView: FC<Props> = ({
// 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.
// (used to destory the widget)
// (which is what delays asking the host to close us)
audioPromise = leaveSoundContext.current?.playSound("left", 0);
break;
case "timeout":
@@ -356,12 +356,12 @@ export const GroupCallView: FC<Props> = ({
setLeft(true);
// We need to wait until the callEnded event is tracked on PostHog,
// otherwise the iframe may get killed first.
// otherwise we may be torn down first.
const posthogRequest = new Promise((resolve) => {
// To increase the likelihood of the PostHog event being sent out in
// widget mode before the iframe is killed, we ask it to skip the
// usual queuing/batching of requests.
const sendInstantly = widget !== null;
// 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,
@@ -369,8 +369,8 @@ export const GroupCallView: FC<Props> = ({
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 the
// closing of the widget that long anyway, so giving it 10 ms will do.
// 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);
});
@@ -389,25 +389,19 @@ export const GroupCallView: FC<Props> = ({
)
void navigate("/");
if (widget) {
// After this point the iframe could die at any moment!
// 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 widget.api.setAlwaysOnScreen(false);
await hostBridge.close?.();
} catch (e) {
logger.error(
"Failed to set call widget `alwaysOnScreen` to false",
e,
);
}
// On a normal user hangup we can shut down and close the widget. But if an
// error occurs we should keep the widget open until the user reads it.
if (reason != "error" && !returnToLobby) {
try {
await widget.api.transport.send(ElementWidgetActions.Close, {});
} catch (e) {
logger.error("Failed to send close action", e);
}
widget.api.transport.stop();
logger.error("Failed to ask the host to close Element Call", e);
}
}
});
@@ -415,7 +409,8 @@ export const GroupCallView: FC<Props> = ({
[
setJoined,
leaveSoundContext,
widget,
hostBridge,
hostControlsLifetime,
room.roomId,
rtcSession,
isPasswordlessUser,
@@ -426,12 +421,12 @@ export const GroupCallView: FC<Props> = ({
);
useEffect(() => {
if (widget && joined)
// set widget to sticky once joined.
widget.api.setAlwaysOnScreen(true).catch((e) => {
if (joined)
// ask to be kept on screen once joined.
hostBridge.setAlwaysOnScreen(true).catch((e) => {
logger.error("Error calling setAlwaysOnScreen(true)", e);
});
}, [widget, joined, rtcSession]);
}, [hostBridge, joined, rtcSession]);
const joinRule = useJoinRule(room);
@@ -501,19 +496,16 @@ export const GroupCallView: FC<Props> = ({
/>
</>
);
} else if (left && widget === null) {
// Left in SPA mode:
} 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 to widget users however (at
// least for now), because we don't yet have designs that would allow widget
// users to dismiss the feedback prompt and close the call window without
// submitting anything.
if (
isPasswordlessUser ||
(PosthogAnalytics.instance.isEnabled() && widget === null)
) {
// 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}
@@ -529,8 +521,8 @@ export const GroupCallView: FC<Props> = ({
// LobbyView again which would open capture devices again.
body = null;
}
} else if (left && widget !== null) {
// Left in widget mode:
} 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.
@@ -541,7 +533,6 @@ export const GroupCallView: FC<Props> = ({
return (
<GroupCallErrorBoundary
widget={widget}
recoveryActionHandler={async (action) => {
setExternalError(null);
if (action == "reconnect") {
@@ -553,9 +544,10 @@ export const GroupCallView: FC<Props> = ({
}}
onError={(_error) => {
if (rtcSession.isJoined()) onLeft("error");
// If there is an error we need to be able to close the widget. This is done in `onLeft` as well
// We need it here explicitly in case rtcSession.isJoined is false.
void widget?.api.setAlwaysOnScreen(false);
// 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}
+2 -5
View File
@@ -123,7 +123,6 @@ export const RoomPage: FC = (): ReactNode => {
return (
muteStates && (
<GroupCallView
widget={widget}
client={client!}
rtcSession={groupCallState.rtcSession}
joined={joined}
@@ -198,7 +197,6 @@ export const RoomPage: FC = (): ReactNode => {
<ErrorView
Icon={UnknownSolidIcon}
title={t("error.call_not_found")}
widget={widget}
>
<Trans i18nKey="error.call_not_found_description">
<p>
@@ -216,7 +214,6 @@ export const RoomPage: FC = (): ReactNode => {
<ErrorView
Icon={groupCallState.error.icon}
title={groupCallState.error.message}
widget={widget}
>
<p>{groupCallState.error.messageBody}</p>
{groupCallState.error.reason && (
@@ -230,7 +227,7 @@ export const RoomPage: FC = (): ReactNode => {
</FullScreenView>
);
} else {
return <ErrorPage widget={widget} error={groupCallState.error} />;
return <ErrorPage error={groupCallState.error} />;
}
default:
return <> </>;
@@ -238,7 +235,7 @@ export const RoomPage: FC = (): ReactNode => {
};
if (loading || isRegistering) return <LoadingPage />;
if (error) return <ErrorPage widget={widget} error={error} />;
if (error) return <ErrorPage error={error} />;
if (!client) return <RoomAuthView />;
// TODO: This doesn't belong here, the app routes need to be reworked
if (!roomIdOrAlias) return <HomePage />;
@@ -1100,7 +1100,7 @@ exports[`LiveKit ConnectionError variants > should link to troubleshoot guide wh
</DocumentFragment>
`;
exports[`should have a close button in widget mode 1`] = `
exports[`should have a close button when the host can dismiss us 1`] = `
<DocumentFragment>
<div
class="_page_4be5c0"