/* global React, Icon, Avatar, ChannelIcon, ChannelDot, StageChip */ const { useState, useEffect, useMemo } = React; const CHANNEL_TYPE_TO_KIND = { LINKEDIN: 'linkedin', WHATSAPP: 'whatsapp', INSTAGRAM: 'instagram', GOOGLE_OAUTH: 'email', GOOGLE: 'email', OUTLOOK: 'email', IMAP: 'email', }; function relativeTime(iso) { if (!iso) return ''; const d = new Date(iso); const diff = (Date.now() - d.getTime()) / 1000; if (diff < 60) return 'now'; if (diff < 3600) return `${Math.round(diff / 60)}m`; if (diff < 86400) return `${Math.round(diff / 3600)}h`; if (diff < 86400 * 7) return `${Math.round(diff / 86400)}d`; return d.toLocaleDateString(); } function colorFromString(s) { const palette = ['#22c55e', '#3b82f6', '#a855f7', '#f97316', '#eab308', '#ec4899', '#06b6d4', '#84cc16']; let h = 0; for (let i = 0; i < (s || '').length; i++) h = ((h << 5) - h) + s.charCodeAt(i); return palette[Math.abs(h) % palette.length]; } function api(path, opts = {}) { return fetch(path, { credentials: 'include', ...opts }).then(async (r) => { if (r.status === 401) { window.location.href = '/login.html?next=' + encodeURIComponent(window.location.pathname); return null; } if (!r.ok) throw new Error(`${r.status} ${r.statusText}`); return r.json(); }); } function Inbox({ goto }) { const [threads, setThreads] = useState(null); const [activeId, setActiveId] = useState(null); const [filter, setFilter] = useState('all'); const [activeMessages, setActiveMessages] = useState([]); const [composer, setComposer] = useState(''); const [sending, setSending] = useState(false); const [sendError, setSendError] = useState(null); const [error, setError] = useState(null); const reloadThreads = () => api('/api/messages/threads?limit=100').then((d) => { if (!d) return; setThreads(d.items || []); if ((d.items || []).length > 0 && !activeId) setActiveId(d.items[0].id); }); useEffect(() => { reloadThreads().catch((e) => setError(e.message)); }, []); const reloadActiveMessages = (threadKey) => { if (!threadKey) { setActiveMessages([]); return Promise.resolve(); } return api(`/api/messages?thread_id=${encodeURIComponent(threadKey)}&limit=200`) .then((d) => d && setActiveMessages(d.items || [])) .catch(() => setActiveMessages([])); }; useEffect(() => { if (!activeId || !threads) return; const active = threads.find((t) => t.id === activeId); if (!active || !active.thread_id) { setActiveMessages([]); return; } reloadActiveMessages(active.thread_id); }, [activeId, threads]); const handleSend = async () => { const active = threads?.find((t) => t.id === activeId); if (!active || !active.thread_id || !composer.trim()) return; setSending(true); setSendError(null); try { const res = await fetch('/api/messages/send', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ thread_id: active.thread_id, text: composer.trim() }), }); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || `${res.status} ${res.statusText}`); } setComposer(''); await reloadActiveMessages(active.thread_id); await reloadThreads(); } catch (e) { setSendError(e.message || 'Send failed'); } finally { setSending(false); } }; const filtered = useMemo(() => { if (!threads) return []; if (filter === 'all') return threads; return threads.filter((t) => CHANNEL_TYPE_TO_KIND[t.channel_type] === filter); }, [threads, filter]); const active = threads ? threads.find((t) => t.id === activeId) : null; const activeKind = active ? CHANNEL_TYPE_TO_KIND[active.channel_type] || 'email' : null; return (
{/* Thread list */}

Inbox

{threads ? `${threads.length} threads` : '…'}
{[ { id: 'all', label: 'All' }, { id: 'linkedin', label: 'LinkedIn', kind: 'linkedin' }, { id: 'whatsapp', label: 'WhatsApp', kind: 'whatsapp' }, { id: 'instagram', label: 'IG', kind: 'instagram' }, { id: 'email', label: 'Email', kind: 'email' }, ].map((f) => ( ))}
{threads === null && (
Loading…
)} {threads !== null && filtered.length === 0 && (
{threads.length === 0 ? 'No conversations yet. Send yourself a message on LinkedIn, WhatsApp or Gmail to test.' : 'No messages match this filter'} {error &&
{error}
}
)} {filtered.map((t) => { const kind = CHANNEL_TYPE_TO_KIND[t.channel_type] || 'email'; const isActive = t.id === activeId; const name = t.contact_name || t.sender_handle || '(unknown)'; const initials = t.contact_avatar_initials || '?'; const color = colorFromString(name); return (
setActiveId(t.id)} style={{ padding: '12px 16px', borderBottom: '1px solid var(--divider)', cursor: 'pointer', display: 'flex', alignItems: 'flex-start', gap: 10, position: 'relative', background: isActive ? 'var(--raised)' : 'transparent', }}> {isActive &&
}
{name} {relativeTime(t.received_at)}
{kind} · {t.message_count} message{t.message_count === 1 ? '' : 's'}
{t.preview}
); })}
{/* Thread view */}
{!active && (
Select a conversation
)} {active && ( <>
{active.contact_name || active.sender_handle} · {activeKind}
via {active.channel_name}
{active.contact_id && ( )}
{activeMessages.length === 0 && (
Loading messages…
)} {activeMessages.map((m) => (
{m.subject && (
Subject: {m.subject}
)}
{m.text}
{relativeTime(m.received_at)}
))}
{/* Composer */}