Pre Render next email #367
Closed
opened 2026-06-03 17:49:50 +00:00 by guettli
·
2 comments
No Branch/Tag Specified
main
issue-563-agentloop-validation
dummy-pr-test
issue-560-fix-firebase-run-url
issue-539-stable-imap-uid
issue-533-shared-email-list
plan-issue-555
drop-nix
plan-issue-484
plan-issue-539
plan-issue-535
plan-issue-474
plan-issue-533
fix-dagger-engineless-precommit
issue-521-fix-deploy-yml-wait-time-api
issue-502-fix-email-id-collision-mailbox
issue-492-eliminate-duplicate-build-runner
issue-494-website-change-detection
issue-491-parallelize-check
issue-478-fix-stalwart-dual-stack-bind
issue-475-allowed-addresses-glob
issue-473-search-result-reorder
issue-453-update-agentloop-defaults
issue-466-structured-search
issue-505-exclude-chaos-monkey-from-regular-ci
issue-509-fix-search-result-sorting
fix-ink-sparkle-remaining-tests
issue-506-fix-search-emails-tests
issue-504-runner-wait-time
issue-488-search-notes
issue-472-changelog-issue-links
issue-501-folder-search-local-sqlite
issue-486-fix-stale-test-shader-mismatch
fix/prevent-settled-search-rerun-473
issue-467-fix-search-stale-results
issue-446-installed-versions-in-changelog
issue-462-fix-pr
issue-448-chaos-monkey-test
issue-436-notes-on-emails
issue-429-unify-mail-display
issue-422-move-to-folder-create-new
issue-414-ensure-not-run-as-root
issue-424-unify-email-list-views
issue-419-trusted-senders-page
issue-425-fix-prs
test-foo
issue-421-bug-report
issue-383-fix-ci
issue-394-fix-deploy-flutter-version
issue-391-fix-ci-double-trigger
issue-376-combined-inbox-v2
issue-376-combined-inbox
issue-384-fix-open-prs
sops-migrate
issue-339-safe-first-on-imap-fetch
issue-340-try-catch-measure-height
issue-342-pin-intl-version
issue-341-guard-threademails-last
issue-335-agentloop-code-test
issue-329-fix
issue-315-fix
issue-320-fix
issue-325-fix
issue-312-fix
issue-311-fix
issue-305-fix
issue-304-fix
issue-299-fix
issue-300-fix
issue-298-fix
issue-296-fix
issue-294-fix
issue-289-fix
issue-288-fix
issue-287-fix
issue-286-fix
issue-277-fix
issue-282-fix
issue-280-fix
issue-272-fix
issue-268-fix
issue-267-fix
issue-266-fix
issue-258-fix
issue-260-fix
issue-257-fix
issue-253-fix
issue-216-fix
issue-251-fix
issue-249-fix
issue-question-fixes
issue-235-fix
issue-236-fix-v2
issue-237-fix
issue-236-fix
issue-228-fix
issue-217-fix
issue-214-fix
issue-213-fix
issue-208-fix
issue-205-fix
issue-204-fix
issue-203-fix
issue-202-fix
issue-129-fix
issue-161-fix
issue-160-fix
issue-201-fix
issue-210-fix
issue-198-fix
issue-200-fix
issue-144-fix
issue-199-fix
fix/playstore-upload-use-requests
issue-193-fix
issue-186-fix
issue-185-fix
issue-192-fix
issue-183-fix
issue-175-fix
issue-172-fix
issue-171-fix
issue-167-fix
issue-136-fix
issue-162-fix
issue-179-fix
issue-155-fix
issue-154-fix
issue-152-fix
issue-151-fix
issue-141-fix
issue-150-fix
issue-164-fix
migrate-to-dagger
task/d1-ci-matrix
task/a4-typeconverter-json
task/u7-onboarding-walkthrough
task/d3-sync-doc
task/a5-layer-boundary-lint
task/t5-golden-tests
task/p5-date-cache
task/s4-link-handling
task/p3-html-parse-isolate
task/u8-mark-all-read
task/u3-recent-searches
task/a3-jmap-injectable-http-client
task/r5-tls-error-handling
fix/playstore-redirect-retry
task/t3-repository-contract-tests
task/p2-email-list-pagination
task/p1-fts5-search
fix/playstore-upload-timeout
task/a1-email-detail-notifier
fix/upgrade-workmanager-0.9
fix/android-core-library-desugaring
task/p4-db-indexes
task/r3-html-error-boundary
task/d2-check-coverage
task/a2-email-tile
task/t4-migration-tests
task/t2-widget-tests
task/t1-email-repo-coverage
task/u6-connection-status
task/u4-push-notifications
task/u2-draft-sync
task/u1-list-unsubscribe
task/s2-hostname-validation
task/r6-reliability-fuzz-tests
task/r4-sync-error-banner
task/r2-force-resync
task/r1-undo-history-persistence
No results found.
Labels
Clear labels
NeedSupervisor
State/InProgress
State/Later
State/Planned
automerge
ci-failure
do-not-merge
loop/code
loop/code-ci-pending
loop/code-done
loop/code-in-process
loop/merge
loop/merge-done
loop/merge-in-process
loop/plan
loop/plan-done
loop/plan-in-process
Issue escalated to a human supervisor; agentloop will skip it until cleared.
Eligible for automatic merge by CI
Issue opened by agentloop to track a failing CI workflow; used for deduplication.
Plan PR — review only, do not merge.
Add to run the built-in "code" prompt; override at prompts/code.md.
Prompt "code" finished; waiting for the PR's CI to pass before advancing.
Prompt "code" finished successfully.
Agent for the "code" prompt is currently running on this issue.
Managed by agentloop
Managed by agentloop
Managed by agentloop
Add to run the built-in "plan" prompt; override at prompts/plan.md.
Prompt "plan" finished successfully.
Agent for the "plan" prompt is currently running on this issue.
No labels
loop/code-done
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: guettli/sharedinbox#367
Reference in New Issue
Block a user
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.
When looking at one email, then delete it, then I see a hourglass turning. It takes some time until I see the next message.
How to avoid that waiting?
Maybe pre render the next message somehow?
Let me read the project docs and explore the relevant email viewing code.Now let me look at the
emailDetailProviderand the email repository to understand where the slowness comes from.Now let me look at the email repository implementation to understand whatgetEmailBodyactually does.Now I have enough context to write the plan. Let me check one more thing — whetheremailDetailProvideruseskeepAliveanywhere, and how threads relate to next-email lookup.I have a complete picture. Here's the implementation plan:Implementation Plan: Pre-fetch next email body to eliminate hourglass delay
Root cause
EmailDetailScreenshows a spinner becauseemailDetailProvider.build()callsrepo.getEmailBody(emailId), which — on a cache miss — opens an IMAP connection and issuesUID FETCH (BODY.PEEK[]), a full network round-trip. This happens after navigation, so the user waits with nothing to see.Strategy
Pre-warm
emailDetailProvider(nextEmailId)while the user is still reading the current email. Because the provider isautoDispose.family, the trick is toref.watchit from withinEmailDetailScreenfor email N; that holds a Riverpod subscription that keeps the prefetch alive until navigation happens. By the time the user taps Delete/Archive, the next email's data is already in the provider cache.Changes (two files, no schema changes, no
build_runnerneeded)1.
lib/di.dart— addnextEmailIdProviderAdd a
FutureProvider.autoDispose.family<String?, String>keyed on the currentemailId. It:getEmail) to getaccountIdandmailboxPath.observeThreads(accountId, mailboxPath)(same stream the thread-list screen uses, already backed by Drift/SQLite — no network call).threads[currentIndex + 1].latestEmailId, ornullif it's the last message.This is intentionally a one-shot
FutureProvider(not reactive) — it runs once when first watched and its result is stable until the provider is disposed.2.
lib/ui/screens/email_detail_screen.dart— watch next email's provider inbuild()In
_EmailDetailScreenState.build(), after the existingref.watch(emailDetailProvider(widget.emailId))call:Because
ref.watchregisters a live subscription, Riverpod keepsemailDetailProvider(nextId)alive (and fetching) for as long as the currentEmailDetailScreenis mounted. The pre-fetched value is held in the provider's in-memory state, so whencontext.go(...)navigates to the next email,emailDetailProvider(nextId)already hasAsyncData— the new screen renders immediately instead of showing a spinner.The
build()output does not change: the pre-fetchedAsyncValueis watched but its value is not used in the widget tree of the current screen.What this does NOT change
_getNextEmailIdIfNeeded— unchanged; it still drives the routing decision (incl. checkingAfterMailViewActionuser preference).emailDetailProvider— unchanged; it already handles the cache-hit path (_bodyCacheTtl = 7 days).build_runner— not touched.nextEmailIdis null →context.pop()) is unaffected.Edge cases
AsyncLoadingand shows a spinner, but the fetch began earlier, so the wait is shorter. No regression.AfterMailViewActionis notnextMessage: the prefetch runs anyway (wasted bandwidth) but the cache TTL is 7 days, so any future visit to that email is also faster. Acceptable trade-off; adding the pref check would require passing it through tonextEmailIdProviderand adds complexity without user-visible benefit.observeThreadsis live but we take only.first; stale result at worst pre-fetches a slightly wrong neighbour. Harmless.PR #381 is open at https://codeberg.org/guettli/sharedinbox/pulls/381.
What I did:
The delay after deleting an email was caused by a cache miss in
getEmailBody— the method fetches from the network when the email body isn't in the localEmailBodiestable yet.Fix (25 lines in
lib/di.dart):EmailDetailNotifier.build()now calls a new_prefetchNextEmailBodyhelper viaunawaited(fire-and-forget) immediately after the current email finishes loading. The helper:afterMailViewActionuser preference — skips the prefetch if the user prefers to go back to the mailbox list instead of the next message.repo.getEmailBody(nextId).Since
getEmailBodycaches results in theEmailBodiesSQLite table with a 7-day TTL, by the time the user hits delete/archive/move/snooze, the next email's body is already warm and renders instantly.