function Sidebar({ view, tagView, setView, tags, unread, total, status, onCreateTag }) { const [newTag, setNewTag] = React.useState(""); const [tagBusy, setTagBusy] = React.useState(false); const navBtn = (active, hasUnread) => "w-full text-left px-3 py-2 rounded-md text-sm mb-0.5 " + (active ? "bg-accent-soft text-accent" : hasUnread ? "text-ink font-semibold hover:bg-paper-line/50" : "text-ink-muted hover:bg-paper-line/50 hover:text-ink"); const userTags = (tags || []).filter((t) => t !== "archive"); const label = (name, n) => (n > 0 ? name + " (" + n + ")" : name); const submitTag = (e) => { e.preventDefault(); const name = (newTag || "").trim(); if (!name || tagBusy) return; setTagBusy(true); onCreateTag(name) .then(() => setNewTag("")) .catch((err) => console.error(err)) .finally(() => setTagBusy(false)); }; return ( ); } function Toolbar({ q, setQ, onSearch, selCount, onBulk, onMarkAll, view, tags, busy }) { const n = selCount; const btn = "text-xs px-2.5 py-1 rounded border border-paper-line text-ink-muted hover:border-accent hover:text-accent disabled:opacity-30"; const userTags = (tags || []).filter((t) => t !== "archive"); return (
{ e.preventDefault(); onSearch(); }} > setQ(e.target.value)} placeholder="Filter subject / from / body…" className="flex-1 bg-paper border border-paper-line rounded px-2 py-1.5 text-sm text-ink placeholder:text-ink-dim focus:outline-none focus:border-accent" />
{view === "archive" ? ( ) : ( )} {n > 0 && userTags.length > 0 && ( )}
); } function MessageList({ items, selected, onSelect, loading, checked, setChecked, total, selectAllPages, setSelectAllPages, }) { const allIds = items.map((m) => m.id); const pageOn = allIds.length > 0 && allIds.every((id) => checked.includes(id)); const multiPage = total > allIds.length && allIds.length > 0; if (loading) return
Loading…
; if (!items.length) { return
No messages in this view.
; } return (
{ setSelectAllPages(false); setChecked(e.target.checked ? allIds.slice() : []); }} /> Select page
{pageOn && multiPage && ( )}
{items.map((m) => { const active = selected === m.id; const on = selectAllPages || checked.includes(m.id); return (
{ e.stopPropagation(); setSelectAllPages(false); setChecked( e.target.checked ? checked.concat([m.id]) : checked.filter((x) => x !== m.id) ); }} />
); })}
); } function MessagePane({ msg, msgTags, attachments, token, onToggleUnread, onArchive, onUnarchive, onDelete, onReply, archived, busy, }) { const [reply, setReply] = React.useState(""); React.useEffect(() => { setReply(""); }, [msg?.id]); if (!msg) { return (
Select a message
); } const html = decodeBodyHtml(msg.body_html) || "
" + (msg.body_text || "") + "
"; const btn = "text-xs px-2.5 py-1 rounded border border-paper-line text-ink-muted hover:border-accent hover:text-accent"; const chips = (msgTags || []).filter((t) => t !== "archive"); return (

{msg.subject}

From {msg.from_addr} To {msg.to_addr} {formatWhen(msg.created_at)}
{chips.length > 0 && (
{chips.map((t) => ( #{t} ))}
)} {(attachments || []).length > 0 && (
{attachments.map((a) => ( {a.filename || "attachment"} ))}
)}
{archived ? ( ) : ( )}
{msg.direction !== "out" && (
{ e.preventDefault(); if (!reply.trim() || busy) return; onReply(reply.trim()).then(() => setReply("")); }} >