From 1f4b0669ca7466acfff0afdf51532824905e9cf3 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 4 Nov 2026 05:38:43 +0100 Subject: [PATCH] feat: ask between a new message and Files for a file opened with the desktop app --- lib/desktop/__tests__/os-commands.test.ts | 17 +++++++++++++++++ lib/desktop/os-commands.ts | 6 +++++- lib/desktop/types.ts | 3 ++- 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/desktop/__tests__/os-commands.test.ts b/lib/desktop/__tests__/os-commands.test.ts index 35338c572..0410e2ce6 100644 --- a/lib/desktop/__tests__/os-commands.test.ts +++ b/lib/desktop/__tests__/os-commands.test.ts @@ -114,6 +114,23 @@ describe('launches', () => { expect(draft?.files.map((file) => file.name)).toEqual(['report.pdf']); }); + it('several files sent to the app go on one draft, without a question', async () => { + const { takeHandoff } = await import('@/lib/launch-handoff'); + await run({ type: 'files', reason: 'attach', failed: [], files: [{ name: 'a.pdf', data: text('a') }, { name: 'b.png', data: text('b') }] }); + expect(takeHandoff('compose')?.files.map((file) => file.name)).toEqual(['a.pdf', 'b.png']); + }); + + it('a file opened with the app asks: new message or Files, a new message first', async () => { + const { takeHandoff } = await import('@/lib/launch-handoff'); + const { listenForFileChoices } = await import('@/lib/file-drop/choice'); + const asked = vi.fn(); + const stop = listenForFileChoices(asked); + await run({ type: 'files', reason: 'attach', ask: true, failed: [], files: [{ name: 'report.pdf', data: text('pdf') }] }); + expect(asked).toHaveBeenCalledWith({ files: [expect.objectContaining({ name: 'report.pdf' })], preferred: 'new-message' }); + expect(takeHandoff('compose')).toBeNull(); + stop(); + }); + it('says so when a file could not be read', async () => { await run({ type: 'files', reason: 'open', failed: ['gone.eml'], files: [] }); expect(notifyFailed).toHaveBeenCalledWith(['gone.eml']); diff --git a/lib/desktop/os-commands.ts b/lib/desktop/os-commands.ts index c52c1baad..5209e07ea 100644 --- a/lib/desktop/os-commands.ts +++ b/lib/desktop/os-commands.ts @@ -9,6 +9,7 @@ import { isAppLocked } from '@/lib/desktop/app-lock-state'; import { getDesktopBridge } from '@/lib/desktop/bridge'; import type { DesktopOsCommand } from '@/lib/desktop/types'; import { openMailLink } from '@/lib/desktop/mail-links'; +import { requestFileChoice } from '@/lib/file-drop/choice'; import { handleReminderCommand } from '@/lib/desktop/reminder-notifications'; import type { MailNotificationContext } from '@/lib/desktop/mail-notifications'; import { handOffDraft, handOffFiles, handOffNewMessage, kindForFile } from '@/lib/launch-handoff'; @@ -131,7 +132,10 @@ async function handleNotification(action: string, rawContext: string, reply: str function handleFiles(command: Extract, env: OsCommandEnv): void { const made = command.files.map(({ name, data }) => new File([new Uint8Array(data)], name)); const failed = [...command.failed]; - if (command.reason === 'attach') { + if (command.reason === 'attach' && command.ask) { + // Opened with the app ("Open with"): the same choice as dropping it on the window. + requestFileChoice({ files: made, preferred: 'new-message' }); + } else if (command.reason === 'attach') { if (made.length > 0) handOffDraft({ files: made }); } else { for (const file of made) { diff --git a/lib/desktop/types.ts b/lib/desktop/types.ts index 69cbd18c0..9bbdfdbc8 100644 --- a/lib/desktop/types.ts +++ b/lib/desktop/types.ts @@ -550,7 +550,8 @@ export type DesktopOsCommand = | { type: 'open-mailbox'; mailboxId: string } | { type: 'mailto'; url: string } | { type: 'webcal'; url: string } - | { type: 'files'; reason: 'open' | 'attach'; files: DesktopOsFile[]; failed: string[] } + /** `ask`: opened with the app, so the user picks between a new message and Files. Without it, attach. */ + | { type: 'files'; reason: 'open' | 'attach'; files: DesktopOsFile[]; failed: string[]; ask?: boolean } | { type: 'notification'; action: string; context: string; reply?: string }; export interface DesktopOsNotification {