From 37aac7d67dfe9040fa50404d29850f9151fd2212 Mon Sep 17 00:00:00 2001 From: Arjun <6592213+arkml@users.noreply.github.com> Date: Wed, 13 May 2026 18:10:24 +0530 Subject: [PATCH] prefetch on hover --- .../renderer/src/components/email-view.tsx | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/apps/x/apps/renderer/src/components/email-view.tsx b/apps/x/apps/renderer/src/components/email-view.tsx index 2a9be83f..3c6eb214 100644 --- a/apps/x/apps/renderer/src/components/email-view.tsx +++ b/apps/x/apps/renderer/src/components/email-view.tsx @@ -74,6 +74,38 @@ function latestMessage(thread: GmailThread): GmailThreadMessage | undefined { return thread.messages[thread.messages.length - 1] } +const PREFETCH_HOVER_MS = 180 +const PREFETCH_MAX_IMAGES_PER_THREAD = 12 + +function extractImageUrls(html: string): string[] { + const urls: string[] = [] + const re = /]*\bsrc=["']([^"']+)["']/gi + let match: RegExpExecArray | null + while ((match = re.exec(html)) !== null) { + const url = match[1] + if (url && (url.startsWith('http://') || url.startsWith('https://'))) { + urls.push(url) + } + } + return urls +} + +function prefetchThreadImages(thread: GmailThread): void { + const seen = new Set() + for (const msg of thread.messages) { + if (!msg.bodyHtml) continue + for (const url of extractImageUrls(msg.bodyHtml)) { + if (seen.has(url)) continue + seen.add(url) + if (seen.size > PREFETCH_MAX_IMAGES_PER_THREAD) return + const img = new Image() + img.decoding = 'async' + img.referrerPolicy = 'no-referrer' + img.src = url + } + } +} + function escapeHtml(text: string): string { return text .replace(/&/g, '&') @@ -336,6 +368,30 @@ export function EmailView() { }) }, []) + const prefetchedRef = useRef>(new Set()) + const hoverTimerRef = useRef | null>(null) + + const cancelHoverPrefetch = useCallback(() => { + if (hoverTimerRef.current) { + clearTimeout(hoverTimerRef.current) + hoverTimerRef.current = null + } + }, []) + + const scheduleHoverPrefetch = useCallback((thread: GmailThread) => { + cancelHoverPrefetch() + if (prefetchedRef.current.has(thread.threadId)) return + hoverTimerRef.current = setTimeout(() => { + hoverTimerRef.current = null + prefetchedRef.current.add(thread.threadId) + prefetchThreadImages(thread) + }, PREFETCH_HOVER_MS) + }, [cancelHoverPrefetch]) + + useEffect(() => () => { + if (hoverTimerRef.current) clearTimeout(hoverTimerRef.current) + }, []) + const loadThreads = useCallback(async () => { setError(null) let hasCachedContent = false @@ -447,6 +503,8 @@ export function EmailView() { type="button" className={cn('gmail-row', isSelected && 'gmail-row-selected', isUnread && 'gmail-row-unread')} onClick={() => toggleThread(thread.threadId)} + onMouseEnter={() => scheduleHoverPrefetch(thread)} + onMouseLeave={cancelHoverPrefetch} > {extractName(latest?.from || thread.from)}