From dc72122ed80394b38406bd4689bdb75203b1dd09 Mon Sep 17 00:00:00 2001 From: Stefan Hildebrandt <695494+hildebrandttk@users.noreply.github.com> Date: Thu, 25 Jun 2026 23:35:19 +0200 Subject: [PATCH] feat(unified-mailbox): enable search in the unified views Enable text AND advanced search in all Unified Mailbox views (the per-role mailboxes and the folder-selected All mail / Unread / Starred cross views). The search input was hard-disabled for every unified view; the store fan-out already supported text search. - page.tsx: the search text input and the advanced-filter toggle are enabled for all unified views (only the scheduled view stays disabled). Clear-search also restores a cross view (not just per-role). - Advanced filters now apply in cross views too: new advancedSearchCrossViewEmails ANDs the advanced filter (text + field conditions from buildJMAPFilter, built without an inMailbox clause) onto the cross-view membership. Per-role unified views keep using advancedSearchUnifiedEmails. Both honor the filter on the first page, on load-more, and on the folder-switch re-run. Fixes: an active Starred filter not applying after switching into a cross view, and the Unread filter in the Unread view returning nothing. - Search persistence on folder switch: an active search is kept and re-run in the target view, preserving advanced filters. handleMailboxSelect picks advancedSearch when filters are set (normal, per-role unified, and cross views, after setting the unified state), text searchEmails when only a query is set, and browses otherwise. The scheduled view is the only view that resets the search on enter (unavailable there; setScheduledView clears searchQuery + searchFilters). Account scope is intentionally left unrestricted in search (it already fanned out across all accounts); the per-view folder selection still applies via crossIncludedMailboxIds. --- FEATURES.md | 1 + app/(main)/[locale]/page.tsx | 45 ++++++++++++++++----- lib/__tests__/unified-mailbox-cross.test.ts | 26 ++++++++++++ lib/unified-mailbox.ts | 23 +++++++++++ stores/email-store.ts | 22 +++++++--- 5 files changed, 102 insertions(+), 15 deletions(-) diff --git a/FEATURES.md b/FEATURES.md index 6a78de96..1043c7c1 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -6,6 +6,7 @@ - Gmail-style threading with inline expansion and an optional conversation toggle - Unified Mailbox – combined Inbox, Sent, Drafts, Junk, Archive, and Trash, scoped by default to the active account and its shared/group folders, with an optional admin-gated cross-account mode that spans every connected account - Aggregated All mail / Unread / Starred entries in the Unified Mailbox – scoped by the same account boundary (or all accounts in cross-account mode) and narrowed by a per-account folder selection; each list labels the source folder of every message +- Search inside the Unified Mailbox – text search across every unified view (the per-role mailboxes and the folder-selected All mail / Unread / Starred lists); advanced filters are additionally available in the per-role unified mailboxes - Three selectable mail layouts: split (three-pane), focused list, and reading pane at bottom - Draft auto-save with identity preservation, persisted HTML body, and proper `In-Reply-To` / `References` headers on replies - Attachment upload, download, drag-out to local file system, and inline preview – images, inline PDF on desktop and mobile, composer attachments (click to open), and `.eml` (`message/rfc822`) attachments rendered like an email; image thumbnails and forgotten-attachment warning diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index 64947e0d..26352683 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -1838,7 +1838,18 @@ export default function Home() { } const populated = await buildPopulatedUnifiedAccounts(); - await fetchUnifiedEmailsAction(populated, role); + // Keep an active search across the switch and re-run it in this view + // (mirrors normal mailboxes), preserving advanced filters; otherwise browse. + if (client && (!isFilterEmpty(searchFilters) || searchQuery)) { + useEmailStore.setState({ isUnifiedView: true, unifiedRole: role, crossView: null }); + if (!isFilterEmpty(searchFilters)) { + await advancedSearch(client); + } else { + await searchEmails(client, searchQuery); + } + } else { + await fetchUnifiedEmailsAction(populated, role); + } refreshUnifiedCounts(populated); return; } @@ -1860,7 +1871,18 @@ export default function Home() { } const populated = await buildPopulatedUnifiedAccounts(); - await fetchCrossViewAction(populated, view); + // Keep an active search across the switch and re-run it in this view + // (mirrors normal mailboxes), preserving advanced filters; otherwise browse. + if (client && (!isFilterEmpty(searchFilters) || searchQuery)) { + useEmailStore.setState({ isUnifiedView: true, crossView: view, unifiedRole: null }); + if (!isFilterEmpty(searchFilters)) { + await advancedSearch(client); + } else { + await searchEmails(client, searchQuery); + } + } else { + await fetchCrossViewAction(populated, view); + } refreshCrossCounts(populated); return; } @@ -2206,13 +2228,16 @@ export default function Home() { setSearchQuery(""); clearSearchFilters(); if (!client) return; - // In unified view the active "mailbox" is a virtual role, so refresh via - // the unified fan-out instead of fetchEmails. + // In unified view the active "mailbox" is a virtual role or cross view, so + // refresh via the unified fan-out instead of fetchEmails. if (isUnifiedView) { + const populated = await buildPopulatedUnifiedAccounts(); const role = useEmailStore.getState().unifiedRole; + const cross = useEmailStore.getState().crossView; if (role) { - const populated = await buildPopulatedUnifiedAccounts(); await fetchUnifiedEmailsAction(populated, role); + } else if (cross) { + await fetchCrossViewAction(populated, cross); } return; } @@ -2848,8 +2873,8 @@ export default function Home() { className={cn("ps-9 h-9", searchQuery && "pe-8")} data-search-input data-tour="search-input" - disabled={isUnifiedView || isScheduledView} - title={isUnifiedView ? t("unified_mailbox.search_unavailable") : isScheduledView ? t('email_viewer.scheduled_actions_only') : undefined} + disabled={isScheduledView} + title={isScheduledView ? t('email_viewer.scheduled_actions_only') : undefined} /> {searchQuery && (