From 9d4d3261da1bf428affcc5ab56031b90b6770794 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sun, 1 Nov 2026 06:50:13 +0100 Subject: [PATCH] fix: clear exported mail from the browser's storage and say plainly where an export goes - The private file an export writes in Firefox and Safari is deleted when the app starts and when someone signs in, not only at the next export, and at once on sign-out. Close keeps it until its download had time to finish. Each tab holds its own files under a Web Lock, so one tab's sweep no longer deletes another tab's running export. - A browser whose private file system can't write files (Safari before 26) is told the export stays in memory instead of being promised a download. - The export's live region announces only the phase and pauses; the count next to it no longer floods screen readers, and the format group is named once. --- .../__tests__/mail-export-dialog.test.tsx | 22 ++++ .../mail-transfer/mail-export-dialog.tsx | 21 ++-- .../mail-transfer/mail-import-dialog.tsx | 6 +- .../mail-transfer/mail-transfer-host.tsx | 11 +- lib/mail-export/__tests__/sinks.test.ts | 104 ++++++++++++++++++ lib/mail-export/sinks.ts | 84 ++++++++++++-- lib/sign-out-cleanup.ts | 2 +- stores/mail-transfer-store.ts | 15 ++- 8 files changed, 234 insertions(+), 31 deletions(-) create mode 100644 lib/mail-export/__tests__/sinks.test.ts diff --git a/components/mail-transfer/__tests__/mail-export-dialog.test.tsx b/components/mail-transfer/__tests__/mail-export-dialog.test.tsx index 53656da6f..bce120993 100644 --- a/components/mail-transfer/__tests__/mail-export-dialog.test.tsx +++ b/components/mail-transfer/__tests__/mail-export-dialog.test.tsx @@ -129,4 +129,26 @@ describe('MailExportDialog', () => { expect(screen.getByTestId('mail-transfer-chip').textContent).toContain('chip'); expect(controller.signal.aborted).toBe(false); }); + + it('keeps the running count out of the live region', () => { + render(); + act(() => { + openMailExport({ mailboxId: 'inbox' }); + useMailTransferStore.getState().startJob({ kind: 'export', controller: new AbortController(), progress: { phase: 'running', exported: 1, failed: 0, failures: [], total: 4, fraction: 0.25 } }); + }); + expect(screen.getByTestId('mail-export-status').textContent).toBe('running_title'); + expect(screen.getByTestId('mail-export-count').textContent).toBe('status_exported'); + }); + + it('says the export stays in memory where the private file system can\'t write files', () => { + vi.stubGlobal('navigator', { ...navigator, storage: { getDirectory: async () => ({}) } }); + vi.stubGlobal('FileSystemFileHandle', class {}); + try { + render(); + act(() => openMailExport()); + expect(screen.getByText('save_memory')).toBeTruthy(); + } finally { + vi.unstubAllGlobals(); + } + }); }); diff --git a/components/mail-transfer/mail-export-dialog.tsx b/components/mail-transfer/mail-export-dialog.tsx index af90c91a9..0f7ecf6c3 100644 --- a/components/mail-transfer/mail-export-dialog.tsx +++ b/components/mail-transfer/mail-export-dialog.tsx @@ -9,7 +9,7 @@ import { useAuthStore } from "@/stores/auth-store"; import { isJobActive, useMailTransferStore, type ExportRequest } from "@/stores/mail-transfer-store"; import type { ExportFormat, ExportProgress } from "@/lib/mail-export/engine"; import { safeName } from "@/lib/mail-export/engine"; -import { canPickSaveFile, memorySink, pickSaveFile, privateFileSink, sweepPrivateExports, type ExportSink } from "@/lib/mail-export/sinks"; +import { canPickSaveFile, canWritePrivateFile, memorySink, pickSaveFile, privateFileSink, sweepPrivateExports, type ExportSink } from "@/lib/mail-export/sinks"; import { formatFileSize } from "@/lib/utils"; import { formatClock, TransferBar } from "./transfer-progress"; import { startExport } from "./start-export"; @@ -21,8 +21,7 @@ function today(): string { /** Where the file will go in this browser, said before the export starts. */ function saveMode(): "picker" | "download" | "memory" { if (canPickSaveFile()) return "picker"; - const storage = typeof navigator !== "undefined" ? (navigator.storage as { getDirectory?: unknown } | undefined) : undefined; - return typeof storage?.getDirectory === "function" ? "download" : "memory"; + return canWritePrivateFile() ? "download" : "memory"; } export function MailExportDialog({ request }: { request: ExportRequest }) { @@ -103,7 +102,7 @@ export function MailExportDialog({ request }: { request: ExportRequest }) { {progress.phase === "done" && progress.file?.download && ( )} - + )} > @@ -120,14 +119,14 @@ export function MailExportDialog({ request }: { request: ExportRequest }) { return ( close()} size="full-on-phone" title={folder ? t("title_folder", { name: mailbox?.name ?? "" }) : t("title_account")} description={folder ? t("description_folder") : t("description_account")} testId="mail-export-dialog" footer={( <> - + )} @@ -135,7 +134,7 @@ export function MailExportDialog({ request }: { request: ExportRequest }) {
{t("format_label")} -
+
{formats.map((f) => (