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:
@@ -1001,29 +1001,52 @@ export default function Home() {
|
||||
};
|
||||
}, [isAuthenticated, client, fetchMailboxes, fetchEmails, fetchQuota, fetchTagCounts, refreshScheduledMetadata]);
|
||||
|
||||
// Push notifications: set up once per client and tear down when the client
|
||||
// goes away (logout or account switch). Kept separate from the fetch effect
|
||||
// above so it still runs when data was prefetched at login time.
|
||||
// Push notifications: set up once per CONNECTED client and tear down when the
|
||||
// clients go away (logout or account switch). Kept separate from the fetch
|
||||
// effect above so it still runs when data was prefetched at login time.
|
||||
//
|
||||
// We bind every connected login, not just the active one: background accounts
|
||||
// must drive the unified-section counters too. The active client keeps the
|
||||
// full handler (current list / scheduled / calendar / filters); background
|
||||
// logins only re-project the unified counts by rebuilding the unified scope
|
||||
// (which refreshes every account's cached mailbox list), since their changes
|
||||
// never touch the active `mailboxes`. (#281 background push)
|
||||
useEffect(() => {
|
||||
if (!isAuthenticated || !client) return;
|
||||
|
||||
try {
|
||||
client.onStateChange((change) => handleStateChange(change, client));
|
||||
const pushEnabled = client.setupPushNotifications();
|
||||
if (pushEnabled) {
|
||||
setPushConnected(true);
|
||||
debug.log('push', '[Push] Push notifications successfully enabled');
|
||||
} else {
|
||||
debug.log('push', '[Push] Push notifications not available on this server');
|
||||
const clients = useAuthStore.getState().getAllConnectedClients();
|
||||
const cleanups: Array<() => void> = [];
|
||||
|
||||
for (const [accId, c] of clients) {
|
||||
try {
|
||||
if (accId === activeAccountId) {
|
||||
c.onStateChange((change) => handleStateChange(change, c));
|
||||
} else {
|
||||
c.onStateChange(() => {
|
||||
buildPopulatedUnifiedAccounts()
|
||||
.then((built) => {
|
||||
refreshCrossCounts(built);
|
||||
refreshUnifiedCounts(built);
|
||||
})
|
||||
.catch(() => { /* per-account fetch failures surface elsewhere */ });
|
||||
});
|
||||
}
|
||||
c.setupPushNotifications();
|
||||
cleanups.push(() => c.closePushNotifications());
|
||||
} catch (error) {
|
||||
debug.log('push', '[Push] Failed to setup push notifications for account:', accId, error);
|
||||
}
|
||||
} catch (error) {
|
||||
debug.log('push', '[Push] Failed to setup push notifications:', error);
|
||||
}
|
||||
|
||||
if (cleanups.length > 0) {
|
||||
setPushConnected(true);
|
||||
debug.log('push', `[Push] Push notifications enabled for ${cleanups.length} account(s)`);
|
||||
}
|
||||
|
||||
return () => {
|
||||
client.closePushNotifications();
|
||||
cleanups.forEach((fn) => fn());
|
||||
};
|
||||
}, [isAuthenticated, client, handleStateChange, setPushConnected]);
|
||||
}, [isAuthenticated, client, activeAccountId, connectedAccountsSignature, handleStateChange, setPushConnected, buildPopulatedUnifiedAccounts, refreshCrossCounts, refreshUnifiedCounts]);
|
||||
|
||||
// Keep unified mailbox counts in sync when the feature is enabled and more
|
||||
// than one account is connected. Runs whenever the set of connected accounts
|
||||
|
||||
Reference in New Issue
Block a user