function Sidebar({ view, tagView, setView, tags, unread, total, status, onCreateTag, onRenameTag, onDeleteTag, onLogout, token, account, onComposeClick, brand, onCloseNav }) { const { t } = useI18n(); 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 { t } = useI18n(); 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={t("search_placeholder")} 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, onStar, loading, checked, setChecked, total, selectAllPages, setSelectAllPages, }) { const { t, lang } = useI18n(); 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
{t("loading")}
; if (!items.length) { return
{t("no_messages")}
; } return (
{ setSelectAllPages(false); setChecked(e.target.checked ? allIds.slice() : []); }} /> {t("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 StorageBar({ token }) { const { t } = useI18n(); const [usage, setUsage] = React.useState(null); const [loading, setLoading] = React.useState(true); React.useEffect(() => { if (!token) return; let active = true; const fetchUsage = () => { fetch("/api/mailbox/usage", { headers: { Authorization: "Bearer " + token } }) .then((r) => r.json()) .then((j) => { if (active && j.ok) { setUsage(j.data); setLoading(false); } }) .catch(() => {}); }; fetchUsage(); const interval = setInterval(fetchUsage, 30000); return () => { active = false; clearInterval(interval); }; }, [token]); // Admin tokens have no mailbox, so the endpoint answers with a per-mailbox // breakdown instead of a single figure — nothing to show in a per-user bar. if (loading || !usage || usage.mailboxes || typeof usage.used_bytes !== "number") return null; const fmt = (b) => { if (b >= 1073741824) return (b / 1073741824).toFixed(1) + " GB"; if (b >= 1048576) return (b / 1048576).toFixed(1) + " MB"; if (b >= 1024) return (b / 1024).toFixed(0) + " KB"; return b + " B"; }; const pct = Math.min(100, Math.round(usage.percent * 10) / 10); const barWidth = Math.max(2, usage.percent); const barColor = pct > 90 ? "bg-red-400" : pct > 70 ? "bg-yellow-400" : "bg-accent"; return (
{t("storage")} {fmt(usage.used_bytes)} / {fmt(usage.max_bytes)}
{pct}% · {t("msgs", usage.message_count)}
); } // Mail disappearing on a schedule is surprising unless the schedule is stated. function RetentionNote({ usage }) { const { t } = useI18n(); const months = Number(usage.retention_months); if (!months || months <= 0) return null; const period = t("months", months); const cap = usage.max_messages ? t("capped_at", usage.max_messages.toLocaleString()) : ""; return (
{t("kept_months", period)} · {t("kept_starred")}
); } function ThemeToggle() { const { t } = useI18n(); const [theme, toggle] = useTheme(); const light = theme === "light"; return ( ); } function TagRow({ name, count, active, navBtn, label, onOpen, onRename, onDelete }) { const { t } = useI18n(); const [busy, setBusy] = React.useState(false); const act = "px-1 text-[0.7rem] text-ink-dim hover:text-accent disabled:opacity-30"; const rename = (e) => { e.stopPropagation(); const next = window.prompt(t("rename_prompt", name), name); if (!next || next.trim() === name) return; setBusy(true); Promise.resolve(onRename(name, next.trim())) .catch((err) => alert(String(err.message || err))) .finally(() => setBusy(false)); }; const remove = (e) => { e.stopPropagation(); if (!window.confirm(t("delete_confirm", name))) return; setBusy(true); Promise.resolve(onDelete(name)) .catch((err) => alert(String(err.message || err))) .finally(() => setBusy(false)); }; return (
); } function CopyAddress({ address }) { const { t } = useI18n(); const [state, setState] = React.useState(""); const copy = () => { copyToClipboard(address) .then(() => setState(t("copied"))) .catch(() => setState(t("copy_blocked"))) .finally(() => setTimeout(() => setState(""), 1800)); }; return ( ); }