From c807b8ef7d20f37aa86eb1aa1c110d73eef12ad0 Mon Sep 17 00:00:00 2001 From: Valere Date: Wed, 2 Sep 2026 19:26:59 +0200 Subject: [PATCH] Always stop the widget transport when closing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close() sent io.element.close and then stopped the transport, so a rejected send skipped the stop. Both call sites this replaced stopped it unconditionally — ErrorView in a finally, GroupCallView outside its try/catch — because a host that never acknowledges the request would otherwise leave the messaging live and the close button doing nothing. Restore that with a finally. --- src/HostBridge.test.ts | 32 +++++++++++++++++++++++++++++++- src/HostBridge.ts | 10 ++++++++-- 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/HostBridge.test.ts b/src/HostBridge.test.ts index 2ddee9890..e93fcfb1e 100644 --- a/src/HostBridge.test.ts +++ b/src/HostBridge.test.ts @@ -10,7 +10,7 @@ import { type WidgetApi } from "matrix-widget-api"; import EventEmitter from "events"; import { createWidgetHostBridge, nullHostBridge } from "./HostBridge"; -import { type WidgetHelpers } from "./widget"; +import { ElementWidgetActions, type WidgetHelpers } from "./widget"; function mockWidget(api: Partial): WidgetHelpers { return { @@ -60,6 +60,36 @@ describe("createWidgetHostBridge", () => { }); }); + describe("close", () => { + test("asks the host to close, then stops the transport", async () => { + const transport = { + send: vi.fn().mockResolvedValue(undefined), + stop: vi.fn(), + }; + const bridge = createWidgetHostBridge(mockWidget({ transport } as never)); + + await bridge.close!(); + + expect(transport.send).toHaveBeenCalledWith( + ElementWidgetActions.Close, + {}, + ); + expect(transport.stop).toHaveBeenCalledOnce(); + }); + + test("stops the transport even when the host refuses to close", async () => { + const transport = { + send: vi.fn().mockRejectedValue(new Error("no")), + stop: vi.fn(), + }; + const bridge = createWidgetHostBridge(mockWidget({ transport } as never)); + + // Leaving the messaging live would leave the close affordance dead + await expect(bridge.close!()).rejects.toThrow("no"); + expect(transport.stop).toHaveBeenCalledOnce(); + }); + }); + describe("supportsReactions", () => { const capabilities = [ "org.matrix.msc2762.send.event:m.reaction", diff --git a/src/HostBridge.ts b/src/HostBridge.ts index f31b5a4af..bb078672d 100644 --- a/src/HostBridge.ts +++ b/src/HostBridge.ts @@ -163,8 +163,14 @@ export function createWidgetHostBridge(widget: WidgetHelpers): HostBridge { notifyDeviceMute: async (state) => send(ElementWidgetActions.DeviceMute, state), close: async () => { - await send(ElementWidgetActions.Close); - widget.api.transport.stop(); + try { + await send(ElementWidgetActions.Close); + } finally { + // Stop regardless of whether the host acknowledged the request. A host + // that rejects or never answers would otherwise leave the messaging + // live, and the close affordance doing nothing at all. + widget.api.transport.stop(); + } }, themeChange$: requests(WidgetApiToWidgetAction.ThemeChange), join$: requests(ElementWidgetActions.JoinCall),