import { useEffect, useCallback, useRef, useState } from 'react'; import { AnalyticsApi, SearchApi, EmailApi } from '../api/client.js'; // ── helpers ────────────────────────────────────────────────────────────────── const fmtDate = (iso) => { const d = new Date(iso); const now = new Date(); const diffDays = (now - d) / 86400000; if (diffDays < 1) return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); if (diffDays < 7) return d.toLocaleDateString([], { weekday: 'short' }); return d.toLocaleDateString([], { month: 'short', day: 'numeric' }); }; const fmtSize = (b) => { if (!b) return ''; if (b < 1024) return `${b} B`; if (b < 1048576) return `${(b / 1024).toFixed(0)} KB`; return `${(b / 1048576).toFixed(1)} MB`; }; const PAGE_SIZE = 50; // ── SenderList (left panel) ─────────────────────────────────────────────────── function SenderList({ senders, selectedId, onSelect, search, onSearch }) { const filtered = senders.filter((s) => { const q = search.toLowerCase(); return !q || (s.address + ' ' + (s.displayName ?? '')).toLowerCase().includes(q); }); return (
onSearch(e.target.value)} aria-label="Filter senders" />
{filtered.length.toLocaleString()} sender{filtered.length !== 1 ? 's' : ''}
); } // ── EmailDetail (full single-email view) ────────────────────────────────────── function EmailDetail({ email, onBack }) { return (
{email.subject || '(no subject)'}
{email.senderDisplayName || email.senderAddress} · {new Date(email.sentAtUtc).toLocaleString()} {email.sizeEstimateBytes > 0 && ( <> · {fmtSize(email.sizeEstimateBytes)} )}
{email.snippet && (
{email.snippet}
)}
); } // ── EmailListRow (row inside sender email list) ─────────────────────────────── function EmailListRow({ email: initial, onRemove, onOpen }) { const [email, setEmail] = useState(initial); const [acting, setActing] = useState(false); const act = (fn, patch) => async (e) => { e.stopPropagation(); if (acting) return; setActing(true); try { await fn(email.id); setEmail((p) => ({ ...p, ...patch })); } finally { setActing(false); } }; const handleTrash = async (e) => { e.stopPropagation(); if (acting) return; setActing(true); try { await EmailApi.trash(email.id); onRemove?.(email.id); } finally { setActing(false); } }; return ( onOpen(email)} title="View email" style={{ cursor: 'pointer' }} > {email.isUnread && } {email.subject || '(no subject)'} {email.snippet && — {email.snippet}} {email.hasAttachments && 📎} {email.sizeEstimateBytes > 1048576 && {fmtSize(email.sizeEstimateBytes)}} {fmtDate(email.sentAtUtc)} e.stopPropagation()}> ); } // ── SenderEmails (right panel) ──────────────────────────────────────────────── function SenderEmails({ sender }) { const [emails, setEmails] = useState([]); const [totalCount, setTotalCount] = useState(null); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false); const [openEmail, setOpenEmail] = useState(null); const sentinelRef = useRef(null); // Reset when sender changes useEffect(() => { setEmails([]); setTotalCount(null); setPage(1); setHasMore(true); setOpenEmail(null); }, [sender.senderId]); const fetchPage = useCallback((p) => { setLoading(true); SearchApi.query(`from:${sender.address}`, p, PAGE_SIZE) .then((r) => { setEmails((prev) => p === 1 ? r.items : [...prev, ...r.items]); setTotalCount(r.totalCount); setHasMore(p < r.totalPages); setPage(p); }) .finally(() => setLoading(false)); }, [sender]); useEffect(() => { fetchPage(1); }, [fetchPage]); // Infinite scroll sentinel useEffect(() => { const el = sentinelRef.current; if (!el) return; const obs = new IntersectionObserver( (entries) => { if (entries[0].isIntersecting && hasMore && !loading) fetchPage(page + 1); }, { rootMargin: '200px' } ); obs.observe(el); return () => obs.disconnect(); }, [hasMore, loading, page, fetchPage]); if (openEmail) { return setOpenEmail(null)} />; } return (
{sender.displayName || sender.address}
{sender.displayName ? sender.address : ''}
{totalCount !== null && (
{totalCount.toLocaleString()} email{totalCount !== 1 ? 's' : ''}
)}
{emails.length > 0 && ( {emails.map((e) => ( setEmails((prev) => prev.filter((x) => x.id !== id))} /> ))}
)}
{loading &&
Loading…
} {!loading && !hasMore && emails.length > 0 && (
— {emails.length.toLocaleString()} emails —
)} {!loading && emails.length === 0 && (
No emails found from this sender.
)}
); } // ── Page ────────────────────────────────────────────────────────────────────── export default function Senders() { const [senders, setSenders] = useState(null); const [selected, setSelected] = useState(null); const [search, setSearch] = useState(''); useEffect(() => { AnalyticsApi.allSenders().then((list) => { setSenders(list); if (list.length > 0) setSelected(list[0]); }).catch(() => setSenders([])); }, []); if (!senders) return

Senders

Loading…

; if (senders.length === 0) return (

Senders

No senders yet — run a sync first.

); return (
{selected && }
); }