From a34314cef5557cbc497fc62d6aac18c028c2c53e Mon Sep 17 00:00:00 2001 From: Aaron Guise Date: Mon, 27 Jul 2026 17:13:53 +1200 Subject: [PATCH] fix: pass email explicitly to handleForwardAsAttachment from the list The list context-menu wiring called selectEmail(email) then invoked handleForwardAsAttachment() synchronously in the same tick. Since handleForwardAsAttachment read selectedEmail from its own closure, and Zustand's store update doesn't propagate into this render's closure until the next render, this could forward the previously selected message (or no-op if nothing was selected yet) instead of the row the user actually right-clicked. Parameterize handleForwardAsAttachment to accept an explicit `email` (defaulting to selectedEmail), the same pattern handleDelete already uses in this file for the same class of problem, and pass it explicitly from the list context-menu wiring. The EmailViewer overflow menu's wiring is unaffected - it always operates on the single currently-open email via the default parameter. Caught by GitHub Copilot's automated PR review. --- app/(main)/[locale]/page.tsx | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index f5091d73..98d71d31 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -1559,8 +1559,16 @@ export default function Home() { // by its existing blobId (no re-fetch/re-upload needed - JMAP blobs are // account-scoped, not per-email). Skips prepareComposerQuoteHeader // entirely, so the body starts blank instead of quoting the original. - const handleForwardAsAttachment = async () => { - if (!selectedEmail) return; + // Takes an explicit `email` (defaulting to selectedEmail), same pattern + // handleDelete uses just below, rather than always reading selectedEmail + // from this closure - callers that just called selectEmail(email) and + // invoke this synchronously in the same tick would otherwise see the + // PRE-update value (the Zustand store updates immediately, but this + // render's selectedEmail closure doesn't until the next render), + // forwarding the previously selected message or no-op'ing on an + // unselected row. See the list context-menu wiring below. + const handleForwardAsAttachment = async (email: Email | null = selectedEmail) => { + if (!email) return; // Same filename options "Export as .eml" uses (see emailFilenameOptions // in email-viewer.tsx), so the two actions produce consistent filenames // for the same message rather than the synthetic attachment silently @@ -1572,7 +1580,7 @@ export default function Home() { filenameStripDiacritics, filenameCollapseSeparators, } = useSettingsStore.getState(); - const payload = buildForwardAsAttachmentPayload(selectedEmail, t('email_composer.prefix.forward'), { + const payload = buildForwardAsAttachmentPayload(email, t('email_composer.prefix.forward'), { template: emailDownloadTemplate, spaceReplacement: filenameSpaceReplacement, lowercase: filenameLowercase, @@ -1582,8 +1590,8 @@ export default function Home() { if (!payload) return; const ok = await emailHooks.onBeforeForward.intercept({ - originalEmailId: selectedEmail.id, - originalEmail: emailToReadView(selectedEmail), + originalEmailId: email.id, + originalEmail: emailToReadView(email), mode: 'forward' as const, }); if (!ok) return; @@ -1602,7 +1610,7 @@ export default function Home() { mode: "forward", draftId: null, replyTo: { - subject: selectedEmail.subject, + subject: email.subject, attachments: [payload.attachment], }, }); @@ -3280,7 +3288,7 @@ export default function Home() { }} onForwardAsAttachment={(email) => { selectEmail(email); - handleForwardAsAttachment(); + handleForwardAsAttachment(email); }} onMarkAsRead={async (email, read) => { if (client) {