diff --git a/routes/email_routes.py b/routes/email_routes.py index a9cc1ed..1dc621e 100644 --- a/routes/email_routes.py +++ b/routes/email_routes.py @@ -1285,17 +1285,24 @@ def setup_email_routes(): logger.debug(f"mark-seen after cached read failed uid={uid}: {e}") @router.get("/read/{uid}") - async def read_email_by_uid(uid: str, folder: str = Query("INBOX"), account_id: str | None = Query(None), owner: str = Depends(require_owner)): + async def read_email_by_uid( + uid: str, + folder: str = Query("INBOX"), + account_id: str | None = Query(None), + mark_seen: bool = Query(True), + owner: str = Depends(require_owner), + ): """Read email body. Cached for 30m, sync IMAP work runs in a thread.""" ck = _read_cache_key(account_id, folder, uid, owner=owner) cached = _read_cache_get(ck) if cached is not None: - try: - _asyncio.create_task(_asyncio.to_thread(_mark_email_seen_sync, uid, folder, account_id, owner)) - except RuntimeError: - pass + if mark_seen: + try: + _asyncio.create_task(_asyncio.to_thread(_mark_email_seen_sync, uid, folder, account_id, owner)) + except RuntimeError: + pass return cached - result = await _asyncio.to_thread(_read_email_sync, uid, folder, account_id, owner) + result = await _asyncio.to_thread(_read_email_sync, uid, folder, account_id, owner, mark_seen) if result and not result.get("error"): _read_cache_put(ck, result) return result diff --git a/static/js/emailLibrary.js b/static/js/emailLibrary.js index 4ea7b9b..7e658ef 100644 --- a/static/js/emailLibrary.js +++ b/static/js/emailLibrary.js @@ -1788,6 +1788,34 @@ function _syncCardNavArrows(card) { if (next) next.disabled = !_findSiblingEmailCard(card, 1); } +const _emailReadPrefetching = new Set(); + +function _prefetchAdjacentEmails(card, count = 3) { + if (!card || state._libFolder === '__scheduled__') return; + const grid = card.closest('.doclib-grid'); + if (!grid) return; + const cards = [...grid.querySelectorAll('.doclib-card[data-uid]')]; + const idx = cards.indexOf(card); + if (idx === -1) return; + const targets = []; + for (let i = 1; i <= count; i++) { + if (cards[idx + i]) targets.push(cards[idx + i]); + } + if (targets.length < count) { + for (let i = 1; targets.length < count && cards[idx - i]; i++) targets.push(cards[idx - i]); + } + for (const target of targets) { + const uid = target.dataset.uid; + if (!uid) continue; + const key = `${state._libAccountId || ''}|${state._libFolder}|${uid}`; + if (_emailReadPrefetching.has(key)) continue; + _emailReadPrefetching.add(key); + fetch(`${API_BASE}/api/email/read/${encodeURIComponent(uid)}?folder=${encodeURIComponent(state._libFolder)}${_acct()}&mark_seen=false`) + .catch(() => {}) + .finally(() => _emailReadPrefetching.delete(key)); + } +} + async function _toggleCardPreview(card, em) { const grid = card.closest('.doclib-grid'); @@ -1843,6 +1871,7 @@ async function _toggleCardPreview(card, em) { // Mark as read locally _syncEmailReadState(em.uid, true); + _prefetchAdjacentEmails(card); // Build the attachments wrap using the shared helper so the signature- // image filter (small inline PNGs/JPGs, Outlook image001 placeholders,