Follow-up to #148 — opening an already-cached message was still slow. Fixes the three dominant costs the profiling in #186 identified on the cached-open critical path.
Fix 1 — WebView renders once (biggest win)
ui/reader/HtmlBody.kt + ui/reader/ReaderViewModel.kt. The reader resolved cid: inline images after the first render, so the AndroidViewupdate key (which included inlineImages.keys) changed and reloaded the whole document a second time for any inline-image email. Now:
ReaderViewModel resolves inline images and folds them into the same state update as the body, so HtmlBody first composes with the images already in place.
HtmlBody drops inline images from the reload key — a late inline-image change no longer reloads the page.
The WebView is destroyed in onRelease so neither it nor its Context leaks.
WebView pool/pre-warm is intentionally deferred (leak-prone) with a TODO(#186) — the single-render fix is the dominant win.
Fix 2 — openMessage does no wasted work for a cached/read message
data/repository/MailRepositoryImpl.kt + a new MessageRouting projection.
Body-less MessageRouting projection (mirrors MessageSummary, no migration). Routing/flag callers route on it; getById (SELECT *) is reserved for the single read that returns the body.
connectionFactory.imapParamsFor (Keystore decrypt + DataStore read) is resolved lazily, only in the fetch / SEEN-push branches. The cached + already-read path also skips the account lookup entirely.
De-duped the inlineImages attachment N+1 (was a getById + getForMessage per cid: image) via a shared ensureAttachmentFile helper taking the already-resolved account/folder.
Fix 3 — repository IO off the main thread
openMessage, inlineImages, downloadedAttachmentParts, and downloadAttachment now run in withContext(Dispatchers.IO), so their DB / file / crypto work no longer runs on the Main.immediateviewModelScope during the open animation.
Tests / validation
New MailRepositoryImplTest: a cached, already-read openMessage does no imapParamsFor, no network, no setRead, and exactly one full-body getById (routing goes through the projection).
New ReaderViewModelTest: inline images land in the same state update as the body (reader renders once).
New instrumented MessageDaoRoutingTest: the projection maps every routing/flag column.
Existing repository tests updated to the projection DAO methods.
Follow-up to #148 — opening an already-cached message was still slow. Fixes the three dominant costs the profiling in #186 identified on the cached-open critical path.
## Fix 1 — WebView renders once (biggest win)
`ui/reader/HtmlBody.kt` + `ui/reader/ReaderViewModel.kt`. The reader resolved `cid:` inline images *after* the first render, so the `AndroidView` `update` key (which included `inlineImages.keys`) changed and reloaded the whole document a **second time** for any inline-image email. Now:
- `ReaderViewModel` resolves inline images and folds them into the **same** state update as the body, so `HtmlBody` first composes with the images already in place.
- `HtmlBody` drops inline images from the reload key — a late inline-image change no longer reloads the page.
- The WebView is destroyed in `onRelease` so neither it nor its `Context` leaks.
WebView pool/pre-warm is intentionally **deferred** (leak-prone) with a `TODO(#186)` — the single-render fix is the dominant win.
## Fix 2 — `openMessage` does no wasted work for a cached/read message
`data/repository/MailRepositoryImpl.kt` + a new `MessageRouting` projection.
- Body-less `MessageRouting` projection (mirrors `MessageSummary`, **no migration**). Routing/flag callers route on it; `getById` (`SELECT *`) is reserved for the single read that returns the body.
- `connectionFactory.imapParamsFor` (Keystore decrypt + DataStore read) is resolved **lazily**, only in the fetch / SEEN-push branches. The cached + already-read path also skips the account lookup entirely.
- De-duped the `inlineImages` attachment N+1 (was a `getById` + `getForMessage` per `cid:` image) via a shared `ensureAttachmentFile` helper taking the already-resolved account/folder.
## Fix 3 — repository IO off the main thread
`openMessage`, `inlineImages`, `downloadedAttachmentParts`, and `downloadAttachment` now run in `withContext(Dispatchers.IO)`, so their DB / file / crypto work no longer runs on the `Main.immediate` `viewModelScope` during the open animation.
## Tests / validation
- New `MailRepositoryImplTest`: a cached, already-read `openMessage` does no `imapParamsFor`, no network, no `setRead`, and exactly one full-body `getById` (routing goes through the projection).
- New `ReaderViewModelTest`: inline images land in the same state update as the body (reader renders once).
- New instrumented `MessageDaoRoutingTest`: the projection maps every routing/flag column.
- Existing repository tests updated to the projection DAO methods.
- Local gate green: `assembleDebug` + `testDebugUnitTest` + `lintDebug` + `ktlintCheck` + `detekt` + `compileDebugAndroidTestKotlin` (JDK 21).
Reader behavior (content, read/SEEN semantics) is unchanged. Does **not** touch the async SEEN network push handled separately by #170.
Closes #186
🤖 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.
Follow-up to #148 — opening an already-cached message was still slow. Fixes the three dominant costs the profiling in #186 identified on the cached-open critical path.
Fix 1 — WebView renders once (biggest win)
ui/reader/HtmlBody.kt+ui/reader/ReaderViewModel.kt. The reader resolvedcid:inline images after the first render, so theAndroidViewupdatekey (which includedinlineImages.keys) changed and reloaded the whole document a second time for any inline-image email. Now:ReaderViewModelresolves inline images and folds them into the same state update as the body, soHtmlBodyfirst composes with the images already in place.HtmlBodydrops inline images from the reload key — a late inline-image change no longer reloads the page.onReleaseso neither it nor itsContextleaks.WebView pool/pre-warm is intentionally deferred (leak-prone) with a
TODO(#186)— the single-render fix is the dominant win.Fix 2 —
openMessagedoes no wasted work for a cached/read messagedata/repository/MailRepositoryImpl.kt+ a newMessageRoutingprojection.MessageRoutingprojection (mirrorsMessageSummary, no migration). Routing/flag callers route on it;getById(SELECT *) is reserved for the single read that returns the body.connectionFactory.imapParamsFor(Keystore decrypt + DataStore read) is resolved lazily, only in the fetch / SEEN-push branches. The cached + already-read path also skips the account lookup entirely.inlineImagesattachment N+1 (was agetById+getForMessagepercid:image) via a sharedensureAttachmentFilehelper taking the already-resolved account/folder.Fix 3 — repository IO off the main thread
openMessage,inlineImages,downloadedAttachmentParts, anddownloadAttachmentnow run inwithContext(Dispatchers.IO), so their DB / file / crypto work no longer runs on theMain.immediateviewModelScopeduring the open animation.Tests / validation
MailRepositoryImplTest: a cached, already-readopenMessagedoes noimapParamsFor, no network, nosetRead, and exactly one full-bodygetById(routing goes through the projection).ReaderViewModelTest: inline images land in the same state update as the body (reader renders once).MessageDaoRoutingTest: the projection maps every routing/flag column.assembleDebug+testDebugUnitTest+lintDebug+ktlintCheck+detekt+compileDebugAndroidTestKotlin(JDK 21).Reader behavior (content, read/SEEN semantics) is unchanged. Does not touch the async SEEN network push handled separately by #170.
Closes #186
🤖 Generated with Claude Code