diff --git a/components/mail/__tests__/compose-plugin-hooks.test.tsx b/components/mail/__tests__/compose-plugin-hooks.test.tsx index 8e0c18506..8645e3d3e 100644 --- a/components/mail/__tests__/compose-plugin-hooks.test.tsx +++ b/components/mail/__tests__/compose-plugin-hooks.test.tsx @@ -3,6 +3,7 @@ import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import type { Mailbox } from '@/lib/jmap/types'; import { useAuthStore } from '@/stores/auth-store'; +import { useAccountStore } from '@/stores/account-store'; import { useEmailStore } from '@/stores/email-store'; import { useSettingsStore } from '@/stores/settings-store'; import { usePluginStore } from '@/stores/plugin-store'; @@ -222,6 +223,29 @@ describe('a mail link from another program in the desktop app', () => { expect(screen.queryByText('stub composer')).not.toBeInTheDocument(); }); + it('with two accounts it opens from the default one, in its own window, with no question', async () => { + const client = useAuthStore.getState().client; + useAuthStore.setState({ getAllConnectedClients: () => new Map([['login', client], ['second', client]]) as never }); + useAccountStore.setState({ + accounts: [ + { id: 'login', email: 'me@example.com', isConnected: true, isDefault: false }, + { id: 'second', email: 'work@example.com', isConnected: true, isDefault: true }, + ], + defaultAccountId: 'second', + } as never); + try { + render(); + await fromOs(); + await waitFor(() => expect(desktopWindows.handOver).toHaveBeenCalledTimes(1)); + expect(desktopWindows.open).toHaveBeenCalledWith('second'); + expect(screen.queryByText('select_account_title')).not.toBeInTheDocument(); + // The main window keeps the account it shows. + expect(useAuthStore.getState().activeAccountId).toBe('login'); + } finally { + useAccountStore.setState({ accounts: [], defaultAccountId: null } as never); + } + }); + it('a link inside the app still opens here', async () => { render(); act(() => { requestInternalMailto('mailto:bob@example.com?subject=Hi'); }); diff --git a/components/mail/mail-app.tsx b/components/mail/mail-app.tsx index 8fd320a02..ca0f59b53 100644 --- a/components/mail/mail-app.tsx +++ b/components/mail/mail-app.tsx @@ -156,7 +156,7 @@ import { mailTitleContext } from "@/lib/tab-title"; import { usePluginStore } from "@/stores/plugin-store"; import { AppTopBannerSlot } from "@/components/plugins/app-top-banner-slot"; import { canDownload } from "@/lib/device-policy"; -import { consumePendingMailto, subscribeToPendingMailto } from "@/lib/protocol-handlers/session"; +import { consumePendingMailto, mailLinkAccountId, subscribeToPendingMailto } from "@/lib/protocol-handlers/session"; import { setNotificationReplyOpener } from "@/lib/pwa-notify/page-actions"; import { setMailLinkOpener } from "@/lib/desktop/mail-links"; import { requestTabReply } from "@/lib/pwa-notify/tab-reply"; @@ -1716,7 +1716,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { // A mail link from another program, in the desktop app: a compose window of its own, // as Outlook and Thunderbird do, and the main window stays where it was. Plugins get // their say first. False when it has to open here instead. - const openMailtoInWindow = useCallback(async (pending: DraftRequest): Promise => { + const openMailtoInWindow = useCallback(async (pending: DraftRequest, accountId?: string): Promise => { if (!pending.fromOs || !hasDesktopWindows() || !popoutsAvailable()) return false; let prefill: ComposePrefill | null = null; if (emailHooks.onBeforeCompose.size > 0) { @@ -1729,7 +1729,8 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { // A plugin said no: nothing opens, here or there. if (!prefill) return true; } - const opened = openComposerWindow(useAuthStore.getState().activeAccountId); + // The account the link goes out from, in its own window: the main window keeps the one it shows. + const opened = openComposerWindow(accountId ?? useAuthStore.getState().activeAccountId); if (!opened) return false; void opened.handOver({ mode: 'compose', @@ -1768,6 +1769,10 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { }, [openMailtoInWindow, openMailtoHere]); const openMailtoForAccount = useCallback(async (pending: DraftRequest, accountId: string) => { + if (await openMailtoInWindow(pending, accountId)) { + setPendingMailtoAccountChoice(null); + return; + } setIsProtocolAccountSwitching(true); try { if (useAuthStore.getState().activeAccountId !== accountId) { @@ -1778,7 +1783,7 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { } finally { setIsProtocolAccountSwitching(false); } - }, [openMailtoDraft, switchAccount]); + }, [openMailtoDraft, openMailtoInWindow, switchAccount]); // onMailtoIntercept: a plugin may stop a mailto: from opening the composer. const mailtoAllowed = useCallback(async (pending: ParsedMailto & { href?: string }): Promise => { @@ -1791,17 +1796,10 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) { }); }, []); - // A draft that comes from outside: which account it goes out from. + // A draft that comes from outside goes out from the default account, as in Outlook and + // Thunderbird, with no question: the composer's From changes it (desktop-p2-8). const routeOutsideDraft = useCallback(async (pending: DraftRequest): Promise => { - const protocolAccounts = getMailtoProtocolAccounts(); - if (protocolAccounts.length > 1) { - // The question is asked in this window: bring it up. - if (pending.fromOs) getDesktopBridge()?.windows?.focus?.(); - setPendingMailtoAccountChoice(pending); - return; - } - - const accountId = protocolAccounts[0]?.id ?? activeAccountId; + const accountId = mailLinkAccountId(getMailtoProtocolAccounts(), useAccountStore.getState().defaultAccountId, activeAccountId); if (accountId) { await openMailtoForAccount(pending, accountId); return; diff --git a/components/protocol/protocol-account-picker.tsx b/components/protocol/protocol-account-picker.tsx index 9bc672a4a..bb594ce9e 100644 --- a/components/protocol/protocol-account-picker.tsx +++ b/components/protocol/protocol-account-picker.tsx @@ -167,7 +167,7 @@ export function ProtocolAccountPicker({ {t("switching_account")} ) : ( - {t("select_account_note")} + )}