From 9f230ca8fcbbde11eec25031b5b695230dc389eb Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 05:23:25 +0100 Subject: [PATCH] fix: keep a quick reply as a draft when an automatic sign-out comes while the external recipient question is open --- .../viewer-quick-reply-back.test.tsx | 28 +++++++++++++++++++ components/email/email-viewer.tsx | 12 ++++++-- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/components/email/__tests__/viewer-quick-reply-back.test.tsx b/components/email/__tests__/viewer-quick-reply-back.test.tsx index c9f478d82..39fae9a39 100644 --- a/components/email/__tests__/viewer-quick-reply-back.test.tsx +++ b/components/email/__tests__/viewer-quick-reply-back.test.tsx @@ -62,3 +62,31 @@ describe('quick reply', () => { expect(box.value).toBe(''); }); }); + +describe('automatic sign-out while the external recipient question is open', () => { + it('answers Back and keeps the typed reply as a draft, without asking again', async () => { + const { useExternalRecipientConfirmStore } = await import('@/stores/external-recipient-confirm-store'); + const { flushComposers, resetComposeFlushRegistry } = await import('@/lib/compose-flush-registry'); + resetComposeFlushRegistry(); + useExternalRecipientConfirmStore.setState({ pending: null, queue: [] }); + const onQuickReply = vi.fn((body: string) => { + void body; + return useExternalRecipientConfirmStore.getState().ask([{ address: 'org-dave@org-two.test', domain: 'org-two.test', field: 'to' }]); + }); + const onSaveQuickReplyDraft = vi.fn(async () => {}); + render(); + await act(async () => { await new Promise((r) => setTimeout(r, 10)); }); + const box = screen.getByPlaceholderText('quick_reply_placeholder') as HTMLTextAreaElement; + fireEvent.focus(box); + fireEvent.change(box, { target: { value: 'My answer' } }); + fireEvent.click(screen.getByRole('button', { name: 'send' })); + await waitFor(() => expect(useExternalRecipientConfirmStore.getState().pending).not.toBeNull()); + + await act(async () => { await flushComposers(2000); }); + expect(onSaveQuickReplyDraft).toHaveBeenCalledWith('My answer'); + expect(useExternalRecipientConfirmStore.getState().pending).toBeNull(); + expect(box.value).toBe('My answer'); + expect(onQuickReply).toHaveBeenCalledTimes(1); + }); +}); diff --git a/components/email/email-viewer.tsx b/components/email/email-viewer.tsx index bac5a0b13..d0f6099ab 100644 --- a/components/email/email-viewer.tsx +++ b/components/email/email-viewer.tsx @@ -103,6 +103,7 @@ import { useSettingsStore } from "@/stores/settings-store"; import { useShallow } from "zustand/react/shallow"; import { useEffectiveMailLayout } from "@/hooks/use-effective-mail-layout"; import { registerComposeFlush } from "@/lib/compose-flush-registry"; +import { useExternalRecipientConfirmStore } from "@/stores/external-recipient-confirm-store"; import { useUIStore } from "@/stores/ui-store"; import { useContactStore, getContactDisplayName, getContactPrimaryEmail } from "@/stores/contact-store"; import { toast } from "@/stores/toast-store"; @@ -1377,7 +1378,9 @@ export function EmailViewer({ const [isQuickReplyFocused, setIsQuickReplyFocused] = useState(false); const [isSendingQuickReply, setIsSendingQuickReply] = useState(false); // An automatic sign-out keeps typed quick-reply text as a draft; text on - // its way out as a reply needs none. + // its way out as a reply needs none. A reply still waiting on the + // "Send outside your organisation?" question hasn't gone out: the question + // is answered with Back and the text kept as a draft. const quickReplyDraftRef = useRef({ text: quickReplyText, sending: isSendingQuickReply, save: onSaveQuickReplyDraft }); quickReplyDraftRef.current = { text: quickReplyText, sending: isSendingQuickReply, save: onSaveQuickReplyDraft }; const hasQuickReplyDraft = quickReplyText.trim().length > 0 && !!onSaveQuickReplyDraft; @@ -1385,7 +1388,12 @@ export function EmailViewer({ if (!hasQuickReplyDraft) return; return registerComposeFlush(async () => { const { text, sending, save } = quickReplyDraftRef.current; - if (sending || !text.trim() || !save) return; + if (!text.trim() || !save) return; + if (sending) { + const confirm = useExternalRecipientConfirmStore.getState(); + if (!confirm.pending) return; + confirm.cancelAll(); + } await save(text); }); }, [hasQuickReplyDraft]);