From 1228a6c4a14ab67ff9893c81c972350f3881a3f7 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 8 Nov 2026 12:36:51 +0100 Subject: [PATCH] fix(app): show late archive failures after the sidebar unmounts An archive keeps the toast API it started with. Unmounting the sidebar (focus mode, a layout change, a plugin view) left the ref saying it was visible, so a failure arriving afterwards went to the removed host and the user saw nothing. --- .../sidebar/sidebar-toast-region.test.tsx | 71 +++++++++++++++++++ .../sidebar/sidebar-toast-region.tsx | 8 ++- 2 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 packages/app/src/components/sidebar/sidebar-toast-region.test.tsx diff --git a/packages/app/src/components/sidebar/sidebar-toast-region.test.tsx b/packages/app/src/components/sidebar/sidebar-toast-region.test.tsx new file mode 100644 index 0000000000..319e72226b --- /dev/null +++ b/packages/app/src/components/sidebar/sidebar-toast-region.test.tsx @@ -0,0 +1,71 @@ +// @vitest-environment jsdom + +import { render } from "@testing-library/react"; +import { SafeAreaProvider } from "react-native-safe-area-context"; +import { describe, expect, it, vi } from "vitest"; +import type { ToastApi } from "@/components/toast-host"; +import { ToastApiProvider, useToast } from "@/contexts/toast-context"; +import { SidebarToastRegion } from "./sidebar-toast-region"; + +const safeAreaMetrics = { + frame: { x: 0, y: 0, width: 800, height: 600 }, + insets: { top: 0, left: 0, right: 0, bottom: 0 }, +}; + +function createFakeToast(): ToastApi { + return { show: vi.fn(() => () => {}), copied: vi.fn(), error: vi.fn() }; +} + +function renderRegion(active: boolean) { + const appToast = createFakeToast(); + let regionToast: ToastApi | null = null; + function CaptureToast() { + regionToast = useToast(); + return null; + } + const tree = (isActive: boolean) => ( + + + + + + + + ); + const view = render(tree(active)); + return { + appToast, + toast: () => regionToast as unknown as ToastApi, + setActive: (next: boolean) => view.rerender(tree(next)), + unmount: view.unmount, + }; +} + +describe("SidebarToastRegion", () => { + it("keeps toasts in the sidebar while it is visible", () => { + const { appToast, toast } = renderRegion(true); + + toast().error("Archive failed"); + + expect(appToast.error).not.toHaveBeenCalled(); + }); + + it("sends toasts to the app while the sidebar is hidden", () => { + const { appToast, toast, setActive } = renderRegion(true); + + setActive(false); + toast().error("Archive failed"); + + expect(appToast.error).toHaveBeenCalledWith("Archive failed"); + }); + + it("sends a late failure to the app after the sidebar unmounts", () => { + const { appToast, toast, unmount } = renderRegion(true); + const savedToast = toast(); + + unmount(); + savedToast.error("Archive failed"); + + expect(appToast.error).toHaveBeenCalledWith("Archive failed"); + }); +}); diff --git a/packages/app/src/components/sidebar/sidebar-toast-region.tsx b/packages/app/src/components/sidebar/sidebar-toast-region.tsx index ef8c8ffdc9..852ce7302d 100644 --- a/packages/app/src/components/sidebar/sidebar-toast-region.tsx +++ b/packages/app/src/components/sidebar/sidebar-toast-region.tsx @@ -18,8 +18,12 @@ export function SidebarToastRegion({ active, children }: { active: boolean; chil useEffect(() => { activeRef.current = active; - // A toast must not outlive its visibility: its dismissal is what ends an archive's undo. - return active ? dismiss : undefined; + return () => { + // An archive holds on to this API, so a failure that lands later needs a host on screen. + activeRef.current = false; + // A toast must not outlive its visibility: its dismissal is what ends an archive's undo. + dismiss(); + }; }, [active, dismiss]); const api = useMemo(() => {