From c52745bfbfd1499b6fb07ae6ac3c709ebad56117 Mon Sep 17 00:00:00 2001 From: Niklas Kietaibl Date: Thu, 18 Jun 2026 13:07:18 +0700 Subject: [PATCH 1/2] fix(desktop): restore correct filename in save dialog for attachment downloads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Without a will-download handler, Electron derives the suggested filename from the URL path (/api/attachments//download → download.txt) and ignores the Content-Disposition header sent by the server. Adding the handler and forwarding item.getFilename() — which reads the Content-Disposition filename — restores the correct name and extension in the native save dialog. Fixes #4153 --- apps/desktop/src/main/index.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index 03f25cf39b..ac4bcf4690 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -219,6 +219,16 @@ function createWindow(): void { window.webContents.send("locale:system-changed", current); }); + // Without this handler, Electron derives the save-dialog filename from the + // URL path (e.g. "/api/attachments//download" → "download.txt") and + // ignores the Content-Disposition header sent by the server. Forwarding + // getFilename() restores the correct name from Content-Disposition. + window.webContents.session.on("will-download", (_event, item) => { + item.setSaveDialogOptions({ + defaultPath: join(app.getPath("downloads"), item.getFilename()), + }); + }); + window.webContents.setWindowOpenHandler((details) => { openExternalSafely(details.url); return { action: "deny" }; From 2a3a849f9ef9891a8644d6a3b99d1b41c90114e0 Mon Sep 17 00:00:00 2001 From: Niklas Kietaibl Date: Thu, 18 Jun 2026 14:57:29 +0700 Subject: [PATCH 2/2] refactor: guard will-download handler against duplicate session registration On macOS, app activate can call createWindow() again after all windows are closed. window.webContents.session is shared, so registering the will-download handler inside createWindow() would add duplicate listeners. Extract into installDownloadSaveDialogHandler() with a WeakSet guard so the handler is registered at most once per session. --- apps/desktop/src/main/index.ts | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src/main/index.ts b/apps/desktop/src/main/index.ts index ac4bcf4690..5b6192fa08 100644 --- a/apps/desktop/src/main/index.ts +++ b/apps/desktop/src/main/index.ts @@ -29,6 +29,22 @@ import { clearFreezeBreadcrumb, } from "./freeze-breadcrumb"; +// Guards against registering the will-download handler more than once on the +// same session. window.webContents.session is shared, and createWindow() can +// be called again on macOS (app "activate" after all windows are closed). +const downloadDialogSessions = new WeakSet(); + +function installDownloadSaveDialogHandler(window: BrowserWindow): void { + const { session } = window.webContents; + if (downloadDialogSessions.has(session)) return; + downloadDialogSessions.add(session); + session.on("will-download", (_event, item) => { + item.setSaveDialogOptions({ + defaultPath: join(app.getPath("downloads"), item.getFilename()), + }); + }); +} + // Bundled icon used for dock/taskbar branding. macOS/Windows production // builds let the OS pick up the icon from the .app bundle / .exe resources, // but Linux production needs an explicit BrowserWindow `icon` — AppImage @@ -219,15 +235,7 @@ function createWindow(): void { window.webContents.send("locale:system-changed", current); }); - // Without this handler, Electron derives the save-dialog filename from the - // URL path (e.g. "/api/attachments//download" → "download.txt") and - // ignores the Content-Disposition header sent by the server. Forwarding - // getFilename() restores the correct name from Content-Disposition. - window.webContents.session.on("will-download", (_event, item) => { - item.setSaveDialogOptions({ - defaultPath: join(app.getPath("downloads"), item.getFilename()), - }); - }); + installDownloadSaveDialogHandler(window); window.webContents.setWindowOpenHandler((details) => { openExternalSafely(details.url);