fix: sidebar tag unread counts go stale on read/unread changes#607
Merged
Merged
Conversation
Marking mail as read left the sidebar's tag unread counts untouched — the folder counts cleared, but a tag went on showing "47 unread" in bold until the page was reloaded. tagCounts is fetched from the server (Email/query per $label keyword) rather than derived from state, and no read/unread mutation refreshed or adjusted it. The per-mailbox unreadEmails counters were kept current by a local delta; tags simply had no equivalent. Add applyTagCountReadDelta alongside the existing mailbox-counter helpers and apply it wherever the affected emails are known locally: markAsRead, batchMarkAsRead, and setEmailKeywordsLocal. Only a genuine $seen flip moves a count, so re-marking a read email as read cannot drift it, and unread is clamped at zero. A tag's total is never touched by a read-state change. markMailboxAsRead is the exception and refetches instead: it is a server-side bulk operation over an entire mailbox, so it also marks emails that were never loaded into state.emails, and a local delta would leave the counts high.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Mark a mailbox as read and the folder unread counts clear, but the sidebar's tag unread counts do not: a tag goes on showing for example
47 / 65in bold with nothing actually unread. The same happens for individual read/unread toggles. Only a page reload puts it right.Cause
tagCountsis fetched from the server, not derived from state:fetchTagCounts→client.getTagCounts(tagIds), which runs anEmail/queryper$label:<id>keyword. Nothing in the read/unread path refreshes or adjusts it.The per-mailbox
unreadEmailscounters don't have this problem, because the store already keeps them current with a local delta on every mutation (applyMailboxCounterUpdateand friends). Tags simply never got the equivalent, so the only thing that ever corrected them was a full refetch — on mount, on aMailbox/Emailstate change pushed from the server, or on a reload.Fix
Add
applyTagCountReadDeltaalongside the existing mailbox-counter helpers and apply it wherever the affected emails are known locally —markAsRead,batchMarkAsRead,setEmailKeywordsLocal. It walks the configured keywords, and for each$label:<id>an email carries, moves that tag'sunreadby the delta. No server round-trip, so a single toggle stays instant.Three details worth calling out:
$seenflip moves a count, reusing thewasRead/isUnreadthe callers already compute. Re-marking a read email as read cannot drift the count.unreadis clamped at zero, as the mailbox counters are.total.markMailboxAsReadis the deliberate exception and refetches instead. It is a server-side bulk operation over a whole mailbox, so it also marks emails that were never loaded intostate.emails— a local delta would only see the loaded ones and leave the counts high. It firesfetchTagCountsafter its optimistic update, in the same style as the existing refetch inhandleStateChange. The asymmetry is commented in the code, since it's the non-obvious part.Not addressed here
Two pre-existing gaps I found while in this code and deliberately did not widen the diff to cover:
Colour-tagging from a Pro Interface tab appears not to persist at all.
handleSetColorTagincomponents/pro/pro-email-tab-body.tsx:193-207callssetEmailKeywordsLocaland patches its own component state, but nothing incomponents/pro/callsupdateEmailKeywords— andsetEmailKeywordsLocaltakes no JMAP client, so by construction it can only patch memory. The mail page does the same operation as a trio (app/(main)/[locale]/page.tsx:1737-1744):updateEmailKeywords→setEmailKeywordsLocal→fetchTagCounts. The Pro tab has only the middle step, so the tag should be lost on reload, and the tag counts (totalas well asunread) never move. That is a persistence bug rather than a counter bug, so it is out of scope here — but it is why I have not tried to keeptotalin step from the client: the delta in this PR deliberately only touchesunread, which is the part a read/unread change can actually affect. Flagging it rather than fixing it silently; happy to open a separate PR. (Read from the code — I have not reproduced it in a running Pro Interface, which is experimental and off by default.)tagCountshas no per-account dimension. It is a flatRecord<tagId, {total, unread}>, andfetchTagCountsonly ever queries the active client (getTagCountsruns itsEmail/queryagainst that client's singleaccountId), so the counts describe one account.state.emailscan hold mail from several accounts in a unified view, so the delta in this PR will move a count for an email belonging to an account those counts never included. The counts were already approximate in that configuration — a refetch corrects them — but this PR gives the approximation a new way to drift, so it seems worth stating. GivingtagCountsan account dimension (asaccountMailboxeshas) looked like a bigger change than this fix should carry; happy to take it on if you would rather it were handled here.Tests
11 new tests in
stores/__tests__/email-store-tag-counts.test.ts, six of which fail againstmain: the delta on read and unread, an email carrying two tags, an untagged email moving nothing, no double-decrement when re-marking an already-read email,unreadnever going negative,totalnever altered, andmarkMailboxAsReadtriggering a refetch rather than relying on a delta.npm run typecheckandnpm run lintare clean. The full suite passes apart from one pre-existing failure incontact-store(should expand group members when group name matches), which reproduces unchanged on a cleanmaincheckout —getAutocompletenow returns a group as a single Outlook-style entry while the test still asserts the old expand-to-members behaviour.Verified by hand against a live Stalwart instance: marking the inbox all-read now clears the tag counts and the bold immediately, with no reload.