fix: keep a quick reply as a draft when an automatic sign-out comes while the external recipient question is open

This commit is contained in:
Linus Rath
2026-10-28 05:23:25 +01:00
parent 823da8c810
commit 9f230ca8fc
2 changed files with 38 additions and 2 deletions
@@ -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(<EmailViewer email={message} isLoading={false} mailboxes={mailboxes} selectedMailbox="inbox" currentMailboxRole="inbox"
onQuickReply={onQuickReply} onSaveQuickReplyDraft={onSaveQuickReplyDraft} />);
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);
});
});
+10 -2
View File
@@ -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]);