import { useEffect, useRef, useState, type FormEvent } from 'react'; import { ModalPortal } from '../components/modal-portal'; import { useCompose, useInbox, useMailThread } from './hooks'; import type { InboxItem, InboxState, MailAttachment, MailPerson } from './types'; const fmtBytes = (n: number): string => { if (!n) return ''; if (n < 1024) return `${n} B`; if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`; return `${(n / (1024 * 1024)).toFixed(1)} MB`; }; const FILTERS: { value: InboxState; label: string }[] = [ { value: 'OPEN', label: 'Open' }, { value: 'SNOOZED', label: 'Snoozed' }, { value: 'DONE', label: 'Done' }, { value: 'ARCHIVED', label: 'Archived' }, ]; const KIND_LABEL: Record = { MAIL: 'Mail', MENTION: 'Mention', NEEDS_REPLY: 'Needs reply', SYSTEM_ALERT: 'Alert', SUPPORT_UPDATE: 'Support', }; const timeOf = (iso?: string): string => { if (!iso) return ''; const d = new Date(iso); return Number.isNaN(+d) ? '' : d.toLocaleString([], { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }); }; /** The unified inbox: work items + mail in one list; click a threaded row to read + reply. */ export function Inbox() { const [filter, setFilter] = useState('OPEN'); const { items, loading, error, transition, refetch } = useInbox(filter); const compose = useCompose(); const [selectedId, setSelectedId] = useState(null); const [composing, setComposing] = useState(false); useEffect(() => { if (selectedId && items.some((i) => i.id === selectedId)) return; setSelectedId(items[0]?.id ?? null); }, [items, selectedId]); const selected = items.find((i) => i.id === selectedId) ?? null; return (
{FILTERS.map((f) => ( ))}
{compose.supported ? ( ) : null}
{selected ? void transition(selected.id, s)} /> :
Select an item to read.
}
{composing ? setComposing(false)} onSent={() => { setComposing(false); refetch(); }} /> : null}
); } function Detail({ item, onTransition }: { item: InboxItem; onTransition: (state: InboxState) => void }) { return (
{item.state === 'OPEN' && item.kind !== 'MAIL' ? (
) : null} {item.threadId ? ( ) : (
{item.title}
{item.summary ?
{item.summary}
: null}
)}
); } /** Read a mail thread (HTML in a sandboxed iframe) + reply. */ export function MailReader({ threadId, subject }: { threadId: string; subject: string }) { const { messages, loading, error, reply, canAttach, upload, canDownload, download } = useMailThread(threadId); const [draft, setDraft] = useState(''); const [sending, setSending] = useState(false); const [pending, setPending] = useState(null); const [attaching, setAttaching] = useState(false); const [uploadingName, setUploadingName] = useState(null); const [attachErr, setAttachErr] = useState(null); const fileRef = useRef(null); async function openAttachment(att: MailAttachment): Promise { try { const url = await download(att); window.open(url, '_blank', 'noopener,noreferrer'); } catch (err) { setAttachErr(err instanceof Error ? err.message : String(err)); } } async function pick(e: React.ChangeEvent): Promise { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; setAttaching(true); setUploadingName(file.name); setAttachErr(null); try { setPending(await upload(file)); } catch (err) { setAttachErr(err instanceof Error ? err.message : String(err)); } finally { setAttaching(false); setUploadingName(null); } } async function submit(e: FormEvent): Promise { e.preventDefault(); const text = draft.trim(); if ((!text && !pending) || sending) return; const att = pending; setDraft(''); setPending(null); setSending(true); try { await reply(text, att ?? undefined); } catch { setDraft(text); setPending(att); } finally { setSending(false); } } return (
{subject || '(no subject)'}
{loading && messages.length === 0 ?
Loading…
: null} {error ?
{error}
: null} {messages.map((m) => (
{m.kind === 'EMAIL' ? 'Email' : 'Reply'}{m.actorId ? ` · ${m.actorId}` : ''} {timeOf(m.at)}
{m.html ? (