diff --git a/src/App.tsx b/src/App.tsx index 25c025d..4ba0359 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -76,12 +76,32 @@ export default function App() { const liveAge = liveFetchedAt ? `${Math.max(0, Math.floor((Date.now() - liveFetchedAt) / 1000))}s ago` : null; - const [chatThreadCount, setChatThreadCount] = useState(0); + const [chatUnreadCount, setChatUnreadCount] = useState(0); const [queueCount, setQueueCount] = useState(0); const tenantId = useApp((s) => s.tenantId); + + // Topbar badge poll. listThreads + workItems only (one request each, no + // per-thread reads — that's what caused the idle-poll regression earlier). + // Unread count is computed locally from heads + read maps persisted by the + // Chat scene to localStorage, so no extra network is needed. useEffect(() => { if (!isAuthed || !tenantId || !userEmail) return; let cancelled = false; + const computeUnread = (threadKeys: string[]): number => { + let heads: Record = {}; + let readMap: Record = {}; + try { heads = JSON.parse(localStorage.getItem(`fm.canvas.chat.heads.${userEmail}`) || "{}"); } catch { /* ignore */ } + try { readMap = JSON.parse(localStorage.getItem(`fm.canvas.chat.read.${userEmail}`) || "{}"); } catch { /* ignore */ } + let n = 0; + for (const k of threadKeys) { + const head = heads[k]; + if (!head) continue; + if (head.by === userEmail) continue; + const readAt = readMap[k]; + if (!readAt || head.at > readAt) n++; + } + return n; + }; const refresh = async () => { try { const { chatApi } = await import("./lib/chatApi"); @@ -91,7 +111,7 @@ export default function App() { apiMod.workItems().catch(() => []), ]); if (cancelled) return; - setChatThreadCount(threads.length); + setChatUnreadCount(computeUnread(threads.map((t) => t._key))); setQueueCount(items.filter((it) => (it as any).tenant_id === tenantId).length); } catch { /* ignore */ } }; @@ -127,7 +147,7 @@ export default function App() {