From 31d17098d61da028f3480f7ebb2498299f5b575a Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 1 May 2026 02:13:25 +0200 Subject: [PATCH] feat: open mail from push notification clicks --- app/[locale]/page.tsx | 37 +++++++++++++++++++++++++++++++++++++ public/sw.js | 5 ++++- 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index c8f25724..4e226392 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -652,6 +652,43 @@ export default function Home() { }); }, [enableUnifiedMailbox, isAuthenticated, client, mailboxes, connectedAccountsSignature, buildUnifiedAccounts, populateUnifiedAccountMailboxes, refreshUnifiedCounts]); + // System-notification click handler. The push SW navigates the user back + // here with `?email=` (specific email it built the toast from) or + // `?openLatestUnread=1` (generic "New mail" toast — happens when the + // preview API failed). We resolve those params once after the inbox has + // finished loading and open the right message, then strip the params so a + // refresh doesn't re-open it. + const notificationParamHandledRef = useRef(false); + useEffect(() => { + if (notificationParamHandledRef.current) return; + if (!isAuthenticated || !client) return; + if (mailboxes.length === 0) return; + + const params = new URLSearchParams(window.location.search); + const emailIdParam = params.get('email'); + const openLatestUnread = params.get('openLatestUnread') === '1'; + if (!emailIdParam && !openLatestUnread) return; + + // For the latest-unread case we need the inbox emails loaded; bail and + // let the effect re-run once `emails` is populated. + if (openLatestUnread && emails.length === 0) return; + + notificationParamHandledRef.current = true; + window.history.replaceState({}, '', window.location.pathname); + + if (emailIdParam) { + setLoadingEmail(true); + fetchEmailContent(client, emailIdParam).finally(() => setLoadingEmail(false)); + return; + } + + // emails are sorted receivedAt-desc, so the first unread is the newest. + const newestUnread = emails.find(e => !e.keywords?.$seen); + if (newestUnread) { + selectEmail(newestUnread); + } + }, [isAuthenticated, client, mailboxes.length, emails, fetchEmailContent, selectEmail, setLoadingEmail]); + // Auto-fetch full email content when an email is auto-selected (e.g. after delete/archive) useEffect(() => { if (!selectedEmail || !client) return; diff --git a/public/sw.js b/public/sw.js index 9aea35e6..1d188648 100644 --- a/public/sw.js +++ b/public/sw.js @@ -141,5 +141,8 @@ function buildClickUrl(data) { if (data.kind === "email" && data.emailId) { return `/?email=${encodeURIComponent(data.emailId)}`; } - return "/"; + // Generic "New mail" toast (preview API failed or returned no email): land + // the user on the latest unread message in their Inbox rather than just the + // app shell, so the click still feels purposeful. + return "/?openLatestUnread=1"; }