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' : ''}
{filtered.map((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 ;
if (senders.length === 0) return (
Senders
No senders yet — run a sync first.
);
return (
{selected && }
);
}