From 07e1506dd816b53208312952f170e9dd02bf4e9d Mon Sep 17 00:00:00 2001 From: Lotus CI Date: Sun, 18 Oct 2026 00:54:48 -0400 Subject: [PATCH] feat(lotus): tell the host when the call drops, and let it press Reconnect (cinny #318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After a homeserver stall the frame shows "Connection lost · Reconnect" while the host kept showing the call as joined, with controls that did nothing. While the error page is up, report io.lotus.call_error { code, recoverable } (and { code: null } when it clears), and accept io.lotus.reconnect to run the page's own Reconnect / Try again. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5 --- src/lotus/lotusActions.test.ts | 2 ++ src/lotus/lotusActions.ts | 9 +++++++++ src/room/GroupCallErrorBoundary.tsx | 26 ++++++++++++++++++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/lotus/lotusActions.test.ts b/src/lotus/lotusActions.test.ts index df290e60..25216b9a 100644 --- a/src/lotus/lotusActions.test.ts +++ b/src/lotus/lotusActions.test.ts @@ -34,6 +34,7 @@ describe("LotusWidgetActions", () => { LotusWidgetActions.SetHotkeys, LotusWidgetActions.SetFrameScreenshare, LotusWidgetActions.PromptScreenshare, + LotusWidgetActions.Reconnect, ]; expect(new Set(LOTUS_TO_WIDGET_ACTIONS)).toEqual(new Set(expectedToWidget)); @@ -56,5 +57,6 @@ describe("LotusWidgetActions", () => { ); expect(LOTUS_TO_WIDGET_ACTIONS).not.toContain(LotusWidgetActions.MicLevel); expect(LOTUS_TO_WIDGET_ACTIONS).not.toContain(LotusWidgetActions.Hotkey); + expect(LOTUS_TO_WIDGET_ACTIONS).not.toContain(LotusWidgetActions.CallError); }); }); diff --git a/src/lotus/lotusActions.ts b/src/lotus/lotusActions.ts index 030ba795..1bce5b4c 100644 --- a/src/lotus/lotusActions.ts +++ b/src/lotus/lotusActions.ts @@ -116,6 +116,14 @@ export enum LotusWidgetActions { * only when it changes (≤ 10 Hz); 0 while muted or with no mic. */ MicLevel = "io.lotus.mic_level", + /** + * fromWidget: the call's error page is up (`{ code, recoverable }`) or + * cleared (`{ code: null }`) (cinny #318), so the host stops showing the call + * as joined after "Connection lost". + */ + CallError = "io.lotus.call_error", + /** toWidget: press the error page's Reconnect / Try again (cinny #318). */ + Reconnect = "io.lotus.reconnect", } /** toWidget Lotus actions that must be allow-listed in `initializeWidget`. */ @@ -133,4 +141,5 @@ export const LOTUS_TO_WIDGET_ACTIONS: LotusWidgetActions[] = [ LotusWidgetActions.SetHotkeys, LotusWidgetActions.SetFrameScreenshare, LotusWidgetActions.PromptScreenshare, + LotusWidgetActions.Reconnect, ]; diff --git a/src/room/GroupCallErrorBoundary.tsx b/src/room/GroupCallErrorBoundary.tsx index 4db0ab64..f50b7906 100644 --- a/src/room/GroupCallErrorBoundary.tsx +++ b/src/room/GroupCallErrorBoundary.tsx @@ -13,6 +13,7 @@ import { type ReactNode, type SVGAttributes, useCallback, + useEffect, } from "react"; import { Trans, useTranslation } from "react-i18next"; import { @@ -24,6 +25,7 @@ import { import { Button } from "@vector-im/compound-web"; import { logger } from "matrix-js-sdk/lib/logger"; import { MatrixError } from "matrix-js-sdk"; +import { type IWidgetApiRequest } from "matrix-widget-api"; import { ConnectionLostError, @@ -38,6 +40,7 @@ import { FullScreenView } from "../FullScreenView.tsx"; import { ErrorView } from "../ErrorView.tsx"; import { type WidgetHelpers } from "../widget.ts"; import styles from "../ErrorView.module.css"; +import { LotusWidgetActions, lotusSendToHost } from "../lotus/lotusWidget"; export type CallErrorRecoveryAction = "reconnect"; // | "retry" ; @@ -74,6 +77,29 @@ const ErrorPage: FC = ({ icon = ErrorSolidIcon; } + // [cinny #318] Tell the host the call dropped (it kept showing the call as + // joined), and let its call bar press Reconnect for us. + const recoverable = + error instanceof ConnectionLostError || + error instanceof LivekitConnectionError || + error instanceof PeerConnectionTimeoutError; + useEffect(() => { + lotusSendToHost(LotusWidgetActions.CallError, { + code: error.code, + recoverable, + }); + const w = widget; + const onReconnect = (ev: CustomEvent): void => { + w?.api.transport.reply(ev.detail, {}); + if (recoverable) void recoveryActionHandler("reconnect"); + }; + w?.lazyActions?.on(LotusWidgetActions.Reconnect, onReconnect); + return (): void => { + w?.lazyActions?.off(LotusWidgetActions.Reconnect, onReconnect); + lotusSendToHost(LotusWidgetActions.CallError, { code: null }); + }; + }, [error.code, recoverable, recoveryActionHandler, widget]); + const actions: { label: string; onClick: () => void }[] = []; if (error instanceof ConnectionLostError) { actions.push({