ReaderViewModel.init (ui/reader/ReaderViewModel.kt, ~lines 67-88) awaits repository.openMessage(messageId) before flipping loading to false, so nothing renders
until it returns.
MailRepositoryImpl.openMessage() (data/repository/MailRepositoryImpl.kt, ~lines 106-122): when
the body is already cached (entity.bodyFetched == true) but the message is unread, it still
runs imapClient.setFlag(params, entity.folder, uidOf(id), Flags.Flag.SEEN, true) — a live IMAP
round trip (connection setup + STORE command) — before returning, purely to mark the
message read on the server:
That network call sits directly on the reader's critical path even though everything needed to
render the screen (body + attachments) is already fully local. This is very likely the dominant
cost behind "opening messages is still slow, even with all messages downloaded" — the body isn't
the bottleneck, the flag round trip is. It's related to, but distinct from, #125's investigation
of IMAP folder-open latency: this is specifically the message-open path's own network call.
Scope
Make server-side read-flag propagation asynchronous/best-effort: update messageDao.setRead(id, true) immediately (optimistic, local-only) and return from openMessage without waiting on imapClient.setFlag; push the SEEN flag to the server on a
background coroutine (fire-and-forget), not on the path the reader screen awaits.
Make sure a failed or slow background SEEN-flag push doesn't leave local and server state
permanently diverged — confirm whether the existing folder-sync flag-reconciliation logic
already self-heals this (the row is already marked read locally; the next syncFolder/ refresh should reconcile), or add an explicit retry path if it doesn't.
Re-measure reader-open latency before/after for an already-cached, unread message to confirm
the fix addresses the reported slowness (vs. some other cost also being present).
Acceptance criteria
Opening an already-downloaded, unread message renders immediately, bounded only by local
DB/disk reads — no network round trip on the critical path.
The SEEN flag still reaches the server on a best-effort basis.
A GreenMail-backed test asserts openMessage doesn't block its result on the setFlag call
(e.g. by asserting timing, or by asserting the flag call is dispatched independently).
Related to #125 (IMAP folder-open latency investigation) — that ticket covers SELECT/EXAMINE
and header-fetch latency when opening a folder; this one is about the message-open path's own
network call. Worth fixing independently of whatever #125 concludes.
## Context
`ReaderViewModel.init` (`ui/reader/ReaderViewModel.kt`, ~lines 67-88) awaits
`repository.openMessage(messageId)` before flipping `loading` to `false`, so nothing renders
until it returns.
`MailRepositoryImpl.openMessage()` (`data/repository/MailRepositoryImpl.kt`, ~lines 106-122): when
the body is already cached (`entity.bodyFetched == true`) but the message is unread, it still
runs `imapClient.setFlag(params, entity.folder, uidOf(id), Flags.Flag.SEEN, true)` — a live IMAP
round trip (connection setup + `STORE` command) — **before returning**, purely to mark the
message read on the server:
```kotlin
} else if (!entity.isRead) {
runCatching { imapClient.setFlag(params, entity.folder, uidOf(id), Flags.Flag.SEEN, true) }
messageDao.setRead(id, true)
}
```
That network call sits directly on the reader's critical path even though everything needed to
render the screen (body + attachments) is already fully local. This is very likely the dominant
cost behind "opening messages is still slow, even with all messages downloaded" — the body isn't
the bottleneck, the flag round trip is. It's related to, but distinct from, #125's investigation
of IMAP *folder-open* latency: this is specifically the *message-open* path's own network call.
## Scope
- [ ] Make server-side read-flag propagation asynchronous/best-effort: update
`messageDao.setRead(id, true)` immediately (optimistic, local-only) and return from
`openMessage` without waiting on `imapClient.setFlag`; push the SEEN flag to the server on a
background coroutine (fire-and-forget), not on the path the reader screen awaits.
- [ ] Make sure a failed or slow background SEEN-flag push doesn't leave local and server state
permanently diverged — confirm whether the existing folder-sync flag-reconciliation logic
already self-heals this (the row is already marked read locally; the next `syncFolder`/
`refresh` should reconcile), or add an explicit retry path if it doesn't.
- [ ] Re-measure reader-open latency before/after for an already-cached, unread message to confirm
the fix addresses the reported slowness (vs. some other cost also being present).
## Acceptance criteria
- Opening an already-downloaded, unread message renders immediately, bounded only by local
DB/disk reads — no network round trip on the critical path.
- The SEEN flag still reaches the server on a best-effort basis.
- A GreenMail-backed test asserts `openMessage` doesn't block its result on the `setFlag` call
(e.g. by asserting timing, or by asserting the flag call is dispatched independently).
## Relevant files
- `data/repository/MailRepositoryImpl.kt` (~lines 106-122), `ui/reader/ReaderViewModel.kt`
(~lines 60-96).
## Dependencies
Related to #125 (IMAP folder-open latency investigation) — that ticket covers `SELECT`/`EXAMINE`
and header-fetch latency when opening a *folder*; this one is about the *message*-open path's own
network call. Worth fixing independently of whatever #125 concludes.
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.
Context
ReaderViewModel.init(ui/reader/ReaderViewModel.kt, ~lines 67-88) awaitsrepository.openMessage(messageId)before flippingloadingtofalse, so nothing rendersuntil it returns.
MailRepositoryImpl.openMessage()(data/repository/MailRepositoryImpl.kt, ~lines 106-122): whenthe body is already cached (
entity.bodyFetched == true) but the message is unread, it stillruns
imapClient.setFlag(params, entity.folder, uidOf(id), Flags.Flag.SEEN, true)— a live IMAPround trip (connection setup +
STOREcommand) — before returning, purely to mark themessage read on the server:
That network call sits directly on the reader's critical path even though everything needed to
render the screen (body + attachments) is already fully local. This is very likely the dominant
cost behind "opening messages is still slow, even with all messages downloaded" — the body isn't
the bottleneck, the flag round trip is. It's related to, but distinct from, #125's investigation
of IMAP folder-open latency: this is specifically the message-open path's own network call.
Scope
messageDao.setRead(id, true)immediately (optimistic, local-only) and return fromopenMessagewithout waiting onimapClient.setFlag; push the SEEN flag to the server on abackground coroutine (fire-and-forget), not on the path the reader screen awaits.
permanently diverged — confirm whether the existing folder-sync flag-reconciliation logic
already self-heals this (the row is already marked read locally; the next
syncFolder/refreshshould reconcile), or add an explicit retry path if it doesn't.the fix addresses the reported slowness (vs. some other cost also being present).
Acceptance criteria
DB/disk reads — no network round trip on the critical path.
openMessagedoesn't block its result on thesetFlagcall(e.g. by asserting timing, or by asserting the flag call is dispatched independently).
Relevant files
data/repository/MailRepositoryImpl.kt(~lines 106-122),ui/reader/ReaderViewModel.kt(~lines 60-96).
Dependencies
Related to #125 (IMAP folder-open latency investigation) — that ticket covers
SELECT/EXAMINEand header-fetch latency when opening a folder; this one is about the message-open path's own
network call. Worth fixing independently of whatever #125 concludes.