Issue #124 paged only the unified All inboxes browse list. The per-account folder view and every search path (unified and per-account) were still non-paged: they loaded the whole folder / entire unified inbox into memory and re-materialized it on each cache write. This extends Paging 3 to all of them, mirroring the existing unified pager, so every mailbox list's query, mapping, and recomposition cost scales with the visible window rather than the whole cache.
Changes
MessageDao — three new read-only PagingSource<Int, MessageSummary>@Query methods:
pagingFolderSummaries(accountId, folder) — per-account browse (inInbox = 1), the account-scoped counterpart of pagingUnifiedFolderSummaries.
pagingUnifiedFolderSearchSummaries(folder, pattern) / pagingFolderSearchSummaries(accountId, folder, pattern) — paged search. They LIKE-match the same columns the old in-memory matchesSearch scanned (sender, sender address, subject, snippet) and, like the old filter, leave inInboxunfiltered so transient inInbox = 0 server-search hits still surface.
The now-dead observeFolderSummaries / observeUnifiedFolderSummaries are removed. No schema change / migration (read-only queries only).
MailRepository — pagedFolderMessages + the two paged-search flows, built via a shared mailboxPager whose PagingConfig uses pageSize = MAILBOX_PAGE_SIZE, initialLoadSize = MAILBOX_PAGE_SIZE * 3, enablePlaceholders = false, and maxSize = MAILBOX_PAGE_SIZE * 5 (so a long scroll drops far-offscreen pages). A likePattern helper escapes the LIKE metacharacters (\ % _) so a query containing them still matches literally. The dead observe*FolderMessages methods are removed.
MailboxViewModel — collapses the old messages list flow and the unified-only paged flow into a single pagedMessages that dispatches each (account, folder, query) to the matching pager; .cachedIn(viewModelScope) is kept so "select all" reads the loaded snapshot. matchesSearch (now done in SQL) is removed.
MailboxScreen — renders every mode from the one paged list. The empty state is gated on itemCount == 0 && loadState.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached, so it no longer flashes on an empty→loaded transition — while still preserving search "no results", the initial-sync spinner (#149), and browse "no messages".
Behaviour preserved
Search filtering columns/scope, multi-select and "select all", unread counts, and the whole browse-vs-search state machine (server search + 400 ms debounce + open/close/clear) are unchanged — only the local list materialization moved from Kotlin into paged SQL. LIKE is ASCII-case-insensitive by SQLite default, matching matchesSearch's common case.
#216 (perf-mailbox-list) owns pagedUnifiedFolderMessages (adding the same maxSize) and the MessageRow timestamp memoization — both are left untouched here. The maxSize above is applied only to the new configs.
Tests / gate
New MailRepositoryImplTest cases cover the three new paged flows and assert the escaped LIKE pattern (%50\%\_off%).
MailboxViewModelTest now verifies pagedMessages dispatches to the right pager for each of the four modes; obsolete observe* repo tests are removed.
E2E Fakes.kt gained the three paged overrides; MailboxScreenTest compiles unchanged.
Green locally on the fast gate (JDK 21): assembleDebug + testDebugUnitTest + compileDebugAndroidTestKotlin + lintDebug + ktlintCheck + detekt. Emulator E2E left to CI.
Nothing deferred — both per-account browse and search are paged.
## What
Issue #124 paged only the unified **All inboxes** browse list. The **per-account folder view** and **every search path** (unified and per-account) were still non-paged: they loaded the whole folder / entire unified inbox into memory and re-materialized it on each cache write. This extends Paging 3 to all of them, mirroring the existing unified pager, so every mailbox list's query, mapping, and recomposition cost scales with the visible window rather than the whole cache.
## Changes
- **`MessageDao`** — three new read-only `PagingSource<Int, MessageSummary>` `@Query` methods:
- `pagingFolderSummaries(accountId, folder)` — per-account browse (`inInbox = 1`), the account-scoped counterpart of `pagingUnifiedFolderSummaries`.
- `pagingUnifiedFolderSearchSummaries(folder, pattern)` / `pagingFolderSearchSummaries(accountId, folder, pattern)` — paged search. They `LIKE`-match the **same columns** the old in-memory `matchesSearch` scanned (sender, sender address, subject, snippet) and, like the old filter, leave `inInbox` **unfiltered** so transient `inInbox = 0` server-search hits still surface.
- The now-dead `observeFolderSummaries` / `observeUnifiedFolderSummaries` are removed. No schema change / migration (read-only queries only).
- **`MailRepository`** — `pagedFolderMessages` + the two paged-search flows, built via a shared `mailboxPager` whose `PagingConfig` uses `pageSize = MAILBOX_PAGE_SIZE`, `initialLoadSize = MAILBOX_PAGE_SIZE * 3`, `enablePlaceholders = false`, **and `maxSize = MAILBOX_PAGE_SIZE * 5`** (so a long scroll drops far-offscreen pages). A `likePattern` helper escapes the `LIKE` metacharacters (`\ % _`) so a query containing them still matches literally. The dead `observe*FolderMessages` methods are removed.
- **`MailboxViewModel`** — collapses the old `messages` list flow and the unified-only paged flow into a **single `pagedMessages`** that dispatches each `(account, folder, query)` to the matching pager; `.cachedIn(viewModelScope)` is kept so "select all" reads the loaded snapshot. `matchesSearch` (now done in SQL) is removed.
- **`MailboxScreen`** — renders every mode from the one paged list. The empty state is gated on `itemCount == 0 && loadState.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached`, so it no longer flashes on an empty→loaded transition — while still preserving search "no results", the initial-sync spinner (#149), and browse "no messages".
## Behaviour preserved
Search filtering columns/scope, multi-select and "select all", unread counts, and the whole browse-vs-search state machine (server search + 400 ms debounce + open/close/clear) are unchanged — only the local list *materialization* moved from Kotlin into paged SQL. `LIKE` is ASCII-case-insensitive by SQLite default, matching `matchesSearch`'s common case.
## Coordination with #216
#216 (`perf-mailbox-list`) owns `pagedUnifiedFolderMessages` (adding the same `maxSize`) and the `MessageRow` timestamp memoization — both are left **untouched** here. The `maxSize` above is applied only to the *new* configs.
## Tests / gate
- New `MailRepositoryImplTest` cases cover the three new paged flows and assert the escaped `LIKE` pattern (`%50\%\_off%`).
- `MailboxViewModelTest` now verifies `pagedMessages` dispatches to the right pager for each of the four modes; obsolete `observe*` repo tests are removed.
- E2E `Fakes.kt` gained the three paged overrides; `MailboxScreenTest` compiles unchanged.
- Green locally on the fast gate (JDK 21): `assembleDebug` + `testDebugUnitTest` + `compileDebugAndroidTestKotlin` + `lintDebug` + `ktlintCheck` + `detekt`. Emulator E2E left to CI.
Nothing deferred — both per-account browse **and** search are paged.
Closes #214
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
What
Issue #124 paged only the unified All inboxes browse list. The per-account folder view and every search path (unified and per-account) were still non-paged: they loaded the whole folder / entire unified inbox into memory and re-materialized it on each cache write. This extends Paging 3 to all of them, mirroring the existing unified pager, so every mailbox list's query, mapping, and recomposition cost scales with the visible window rather than the whole cache.
Changes
MessageDao— three new read-onlyPagingSource<Int, MessageSummary>@Querymethods:pagingFolderSummaries(accountId, folder)— per-account browse (inInbox = 1), the account-scoped counterpart ofpagingUnifiedFolderSummaries.pagingUnifiedFolderSearchSummaries(folder, pattern)/pagingFolderSearchSummaries(accountId, folder, pattern)— paged search. TheyLIKE-match the same columns the old in-memorymatchesSearchscanned (sender, sender address, subject, snippet) and, like the old filter, leaveinInboxunfiltered so transientinInbox = 0server-search hits still surface.observeFolderSummaries/observeUnifiedFolderSummariesare removed. No schema change / migration (read-only queries only).MailRepository—pagedFolderMessages+ the two paged-search flows, built via a sharedmailboxPagerwhosePagingConfigusespageSize = MAILBOX_PAGE_SIZE,initialLoadSize = MAILBOX_PAGE_SIZE * 3,enablePlaceholders = false, andmaxSize = MAILBOX_PAGE_SIZE * 5(so a long scroll drops far-offscreen pages). AlikePatternhelper escapes theLIKEmetacharacters (\ % _) so a query containing them still matches literally. The deadobserve*FolderMessagesmethods are removed.MailboxViewModel— collapses the oldmessageslist flow and the unified-only paged flow into a singlepagedMessagesthat dispatches each(account, folder, query)to the matching pager;.cachedIn(viewModelScope)is kept so "select all" reads the loaded snapshot.matchesSearch(now done in SQL) is removed.MailboxScreen— renders every mode from the one paged list. The empty state is gated onitemCount == 0 && loadState.refresh is LoadState.NotLoading && loadState.append.endOfPaginationReached, so it no longer flashes on an empty→loaded transition — while still preserving search "no results", the initial-sync spinner (#149), and browse "no messages".Behaviour preserved
Search filtering columns/scope, multi-select and "select all", unread counts, and the whole browse-vs-search state machine (server search + 400 ms debounce + open/close/clear) are unchanged — only the local list materialization moved from Kotlin into paged SQL.
LIKEis ASCII-case-insensitive by SQLite default, matchingmatchesSearch's common case.Coordination with #216
#216 (
perf-mailbox-list) ownspagedUnifiedFolderMessages(adding the samemaxSize) and theMessageRowtimestamp memoization — both are left untouched here. ThemaxSizeabove is applied only to the new configs.Tests / gate
MailRepositoryImplTestcases cover the three new paged flows and assert the escapedLIKEpattern (%50\%\_off%).MailboxViewModelTestnow verifiespagedMessagesdispatches to the right pager for each of the four modes; obsoleteobserve*repo tests are removed.Fakes.ktgained the three paged overrides;MailboxScreenTestcompiles unchanged.assembleDebug+testDebugUnitTest+compileDebugAndroidTestKotlin+lintDebug+ktlintCheck+detekt. Emulator E2E left to CI.Nothing deferred — both per-account browse and search are paged.
Closes #214
🤖 Generated with Claude Code