fix(unified-mailbox): single-source unified counters, unified id space, background push

The unified-section sidebar badges (per-role unified folders + cross-view
All mail/Unread/Starred) failed to count down when messages were deleted/moved/
read from the unified views, and failed to count up for incoming mail - while
the underlying per-account folder counters updated correctly. Root cause: the
badges were a separate counter representation, recomputed only by a fresh server
fetch, completely decoupled from the optimistically-patched mailbox lists.

Three coordinated changes:

V1 - single source of truth: derive `unifiedCounts`/`crossUnreadCount` as a pure
live projection of `mailboxes` + `accountMailboxes` (the lists every mutation
already patches and push refreshes), over the last-known unified scope. A store
subscription re-projects whenever those lists change, so optimistic deletes and
push refreshes flow into the badges with no server round trip and no
eventual-consistency snap-back.

V3 - unified id space: searchEmails/advancedSearchEmails now namespace shared/
delegated mailboxIds (`${ownerId}:${id}`) like getEmails already did. The
cross-account views browse via advancedSearchEmails, so shared emails there
previously carried bare owner ids; now every fetch path is consistent and
emailInMailbox hits the `ids[mailbox.id]` fast path (originalId branches kept as
a defensive fallback). resolveSourceFolderName matches `m.id` first (also fixes
a latent missing source-folder name for shared emails).

Background push: bind push notifications for every connected login, not just the
active one - background accounts now drive the unified counters by rebuilding the
unified scope on their state changes. handleStateChange also refreshes the
mailbox list on a Mailbox change for ANY changed account key, so delegated
shared-folder activity arriving via the active client updates counters too.

Tests: unified-badge live projection on delete; client-level namespacing for
searchEmails/advancedSearchEmails (shared vs own account).
This commit is contained in:
Stefan Hildebrandt
2026-07-11 21:14:50 +02:00
parent fdad60cf03
commit 2e42693228
6 changed files with 300 additions and 36 deletions
+6 -1
View File
@@ -61,7 +61,12 @@ const ALL_UNIFIED_ROLES: UnifiedMailboxRole[] = [
*/
export function resolveSourceFolderName(email: Email, mailboxes: Mailbox[]): string | undefined {
for (const m of mailboxes) {
if (email.mailboxIds?.[m.originalId ?? m.id]) return m.name;
// All fetch paths now namespace shared emails' mailboxIds to the store id
// (`${ownerId}:${origId}`), so matching `m.id` works for own and shared
// alike. The `originalId` check stays as a defensive fallback for any email
// that still carries a bare owner id. (#281 V3)
if (email.mailboxIds?.[m.id]) return m.name;
if (m.originalId && email.mailboxIds?.[m.originalId]) return m.name;
}
return undefined;
}