Files
multica/packages/core/chat/unread.ts
Jiayuan Zhang e10bb3fff0 fix(chat): unify unread badge counting across sidebar, mobile, and thread list (MUL-4286) (#5239)
The aggregate chat unread badge used two different definitions: web/
desktop's sidebar summed unread messages while mobile's tab badge (and
the since-removed ChatFab badge it mirrored) counted sessions — the same
account state showed e.g. 5 on web and 2 on iOS. The sidebar also summed
ALL sessions while the thread list zeroes the row being viewed, so a
reply landing in the open conversation flashed a sidebar count with no
matching row.

- packages/core/chat/unread.ts: countUnreadChatMessages() as the single
  shared definition (IM-style message total, optional viewed-session
  exclusion); pure so mobile can import it.
- app-sidebar: use the helper and exclude the actively-viewed session,
  but only while a chat surface is actually showing it (chat route or
  floating window open) — a remembered selection with both surfaces
  closed still counts, since nothing will auto mark-read there.
- mobile: tab badge switches to the shared message count (99+ cap like
  the sidebar), ChatSessionSchema parses unread_count, and markRead's
  optimistic patch zeroes unread_count alongside has_unread.

Co-authored-by: Lambda <lambda@multica.ai>
Co-authored-by: multica-agent <github@multica.ai>
2026-07-11 02:46:12 +08:00

32 lines
1.3 KiB
TypeScript

import type { ChatSession } from "../types/chat";
/**
* The chat unread badge number: total unread assistant *messages* across
* sessions (IM-style), NOT the number of sessions with unread.
*
* Single source of truth for every surface that renders an aggregate chat
* unread count (web/desktop sidebar, mobile tab bar) — the platforms must
* show the same N for the same account state (see "Behavioral parity" in
* apps/mobile/CLAUDE.md). Pure function so mobile can import it directly.
*
* `excludeSessionId` drops one session from the sum: the session the user is
* currently reading. The chat thread list renders that session's own badge
* as 0 (its unread is about to be cleared by the viewer's auto mark-read),
* so an aggregate that still counted it would show a number the list can't
* account for. Callers pass it only while a chat surface is actually showing
* that session; a background pointer (e.g. a remembered selection while the
* chat page is closed) must NOT be excluded, or new replies would never
* badge.
*/
export function countUnreadChatMessages(
sessions: readonly ChatSession[] | undefined,
excludeSessionId?: string | null,
): number {
if (!sessions) return 0;
return sessions.reduce(
(sum, s) =>
s.id === excludeSessionId ? sum : sum + (s.unread_count ?? 0),
0,
);
}