// §15.2 — the inbox panel. // // One mental space across every RFC the contributor has any relationship // to. Filter chips (Unread only, RFC: …, Category: personal-direct / // structural / churn) are AND-combined. The bundle toggle collapses // rows by (RFC, event_kind) per §15.2's per-bundle markable surface. // // The header badge in App.jsx subscribes to the same SSE stream and so // stays in lockstep with the inbox per §15.3. import { useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { listNotifications, markNotificationRead, markNotificationsReadByFilter, } from '../api.js' const CATEGORIES = [ { value: '', label: 'All categories' }, { value: 'personal-direct', label: 'Personal' }, { value: 'structural', label: 'Structural' }, { value: 'churn', label: 'Churn' }, ] export default function Inbox({ onClose, lastChangeTick }) { const [items, setItems] = useState([]) const [unreadCount, setUnreadCount] = useState(0) const [filters, setFilters] = useState({ unread: false, rfcSlug: '', category: '', bundled: false, }) const [loading, setLoading] = useState(true) useEffect(() => { setLoading(true) listNotifications({ unread: filters.unread, rfcSlug: filters.rfcSlug || undefined, category: filters.category || undefined, bundled: filters.bundled, }) .then(r => { setItems(r.items || []) setUnreadCount(r.unread_count || 0) }) .finally(() => setLoading(false)) }, [filters, lastChangeTick]) const rfcOptions = useMemo(() => { const seen = new Map() for (const it of items) { if (it.rfc_slug && !seen.has(it.rfc_slug)) { seen.set(it.rfc_slug, it.rfc_title || it.rfc_slug) } } return Array.from(seen.entries()) }, [items]) async function handleRowClick(item) { if (!item.read_at) { await markNotificationRead(item.id) setItems(prev => prev.map(p => p.id === item.id ? { ...p, read_at: new Date().toISOString() } : p)) } } async function markAllUnderFilter() { await markNotificationsReadByFilter({ rfc_slug: filters.rfcSlug || undefined, category: filters.category || undefined, }) setItems(prev => prev.map(p => ({ ...p, read_at: p.read_at || new Date().toISOString() }))) setUnreadCount(0) } return (
Loading…
} {!loading && items.length === 0 && (No notifications match. Try a different filter, or come back later.
)}