feat(drawer): unread-count badges per folder and bold accounts with unread mail #116

Merged
JMR-dev merged 1 commits from feat-drawer-unread-indicators into main 2026-07-02 07:49:42 +00:00
JMR-dev commented 2026-07-02 07:39:12 +00:00 (Migrated from github.com)

What

Two related drawer-UX tickets, shipped together because they share one unread-count data source.

  • #83 — unread count per folder. Each folder row in the navigation drawer shows a trailing badge with its unread count, hidden when zero and capped at 99+. The badge is a NavigationDrawerItem badge slot.
  • #84 — bold accounts with unread mail. Accounts that have unread mail render their email in bold in the drawer account switcher (button label + dropdown rows) and in the mailbox account-filter chips, matching the FontWeight.Bold convention MessageRow already uses for unread messages.

How the unread count is queried and plumbed

One efficient Room aggregate is the single source for both features:

SELECT accountId, folder, COUNT(*) AS unreadCount FROM messages
WHERE inInbox = 1 AND isRead = 0 GROUP BY accountId, folder
  • MessageDao.observeUnreadCounts(): Flow<List<FolderUnreadCount>> — a pure COUNT(*) aggregate that pulls no message rows into memory. Only folder-synced rows (inInbox = 1) count, so transient server-search hits never inflate a badge.
  • No schema change / migration. The GROUP BY accountId, folder is served by the existing (accountId, folder, uid) index's leftmost prefix, so nothing new was added to the schema.
  • MailRepository.observeUnreadCounts() maps the projection to a domain UnreadCount(accountId, folder, count).
  • MailboxViewModel collects it once (unreadCounts) and derives two states from it:
    • folderUnreadCounts: StateFlow<Map<String, Int>> — folder full-name → count for the drawer account (#83).
    • accountsWithUnread: StateFlow<Set<String>> — account ids with unread mail in any folder (#84).

Scope decision (resolves #84's open question)

"Unread" means folder-synced unread mail in any folder, kept consistent across both features: an account reads as bold exactly when one of its folders would show a badge. #83 inherently shows a badge on every folder that has unread mail, so scoping #84 to inbox-only would have been inconsistent with what the drawer already displays. Both consume the one query, as the tickets suggested.

Accessibility

The badge's visible glyph is terse (12, or 99+), so its semantics are overridden with a plurals content description — e.g. TalkBack announces "Inbox, 12 unread messages" — via clearAndSetSemantics.

Tests

  • JVM (MailboxViewModelTest): folderUnreadCounts maps only the drawer account's folders; it follows a drawer-account switch; accountsWithUnread lists every account with unread mail (any folder).
  • Instrumented (LibreMailDatabaseTest): observeUnreadCounts aggregates unread synced rows per (account, folder), excludes read rows and inInbox = 0 search hits.
  • Instrumented (FolderDrawerTest): a folder with unread mail shows its count badge (by content description); a read folder shows none.

Fast gate green locally: assembleDebug, testDebugUnitTest, lintDebug, ktlintCheck, detekt, and compileDebugAndroidTestKotlin.

Notes for the maintainer

  • Deliberately stayed clear of the label-disambiguation resolver internals (#59–61) and did not implement drawer memoization/perf (#67) — the badge lookups ride the existing per-recomposition label resolution, which #67 will address.
  • Rebased on latest origin/main (through #113).

Closes #83
Closes #84

🤖 Generated with Claude Code

## What Two related drawer-UX tickets, shipped together because they share one unread-count data source. - **#83 — unread count per folder.** Each folder row in the navigation drawer shows a trailing badge with its unread count, hidden when zero and capped at `99+`. The badge is a `NavigationDrawerItem` `badge` slot. - **#84 — bold accounts with unread mail.** Accounts that have unread mail render their email in **bold** in the drawer account switcher (button label + dropdown rows) and in the mailbox account-filter chips, matching the `FontWeight.Bold` convention `MessageRow` already uses for unread messages. ## How the unread count is queried and plumbed One efficient Room aggregate is the single source for both features: ```sql SELECT accountId, folder, COUNT(*) AS unreadCount FROM messages WHERE inInbox = 1 AND isRead = 0 GROUP BY accountId, folder ``` - `MessageDao.observeUnreadCounts(): Flow<List<FolderUnreadCount>>` — a pure `COUNT(*)` aggregate that pulls **no** message rows into memory. Only folder-synced rows (`inInbox = 1`) count, so transient server-search hits never inflate a badge. - **No schema change / migration.** The `GROUP BY accountId, folder` is served by the existing `(accountId, folder, uid)` index's leftmost prefix, so nothing new was added to the schema. - `MailRepository.observeUnreadCounts()` maps the projection to a domain `UnreadCount(accountId, folder, count)`. - `MailboxViewModel` collects it once (`unreadCounts`) and derives two states from it: - `folderUnreadCounts: StateFlow<Map<String, Int>>` — folder full-name → count for the **drawer account** (#83). - `accountsWithUnread: StateFlow<Set<String>>` — account ids with unread mail in **any** folder (#84). ## Scope decision (resolves #84's open question) "Unread" means folder-synced unread mail in **any** folder, kept consistent across both features: an account reads as bold exactly when one of its folders would show a badge. #83 inherently shows a badge on every folder that has unread mail, so scoping #84 to inbox-only would have been inconsistent with what the drawer already displays. Both consume the one query, as the tickets suggested. ## Accessibility The badge's visible glyph is terse (`12`, or `99+`), so its semantics are overridden with a plurals content description — e.g. TalkBack announces "Inbox, 12 unread messages" — via `clearAndSetSemantics`. ## Tests - **JVM (`MailboxViewModelTest`):** `folderUnreadCounts` maps only the drawer account's folders; it follows a drawer-account switch; `accountsWithUnread` lists every account with unread mail (any folder). - **Instrumented (`LibreMailDatabaseTest`):** `observeUnreadCounts` aggregates unread synced rows per (account, folder), excludes read rows and `inInbox = 0` search hits. - **Instrumented (`FolderDrawerTest`):** a folder with unread mail shows its count badge (by content description); a read folder shows none. Fast gate green locally: `assembleDebug`, `testDebugUnitTest`, `lintDebug`, `ktlintCheck`, `detekt`, and `compileDebugAndroidTestKotlin`. ## Notes for the maintainer - Deliberately stayed clear of the label-disambiguation resolver internals (#59–61) and did **not** implement drawer memoization/perf (#67) — the badge lookups ride the existing per-recomposition label resolution, which #67 will address. - Rebased on latest `origin/main` (through #113). Closes #83 Closes #84 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.