Files
multica/apps/mobile/lib
Naiyuan Qing 2a3accb5d7 feat(mobile): unread badges on Inbox and Chat tabs
Surface the same unread signals web puts on the sidebar (inbox) and
the ChatFab (chat). On a phone the user lives on the tab bar, so
mounting badges directly on the Inbox and Chat tabs is the closest
equivalent.

Display semantics mirror web exactly (apps/mobile/CLAUDE.md "counts
must agree"):

- Inbox badge = `deduplicateInboxItems(items).filter(i => !i.read).length`,
  same as web's `useInboxUnreadCount` (packages/core/inbox/queries.ts:22).
  99+ truncation matches the sidebar.
- Chat badge = `sessions.filter(s => s.has_unread).length`, same as web's
  ChatFab (packages/views/chat/components/chat-fab.tsx:29). 9+ truncation
  matches the fab.

Implementation:
- New `apps/mobile/lib/unread-counts.ts` with two `useQuery + select`
  hooks; mirror-don't-import the web design.
- Wired into `(tabs)/_layout.tsx` as React Navigation's native
  `tabBarBadge` + `tabBarBadgeStyle`. Style is JUST `backgroundColor`
  (brand blue `#4571e0`); @react-navigation/elements `Badge` internally
  uses `borderRadius = size / 2` and `minWidth = size`, so the
  single-character badge renders as a true circle. Overriding minWidth /
  fontSize / fontWeight breaks that geometry — keep the override minimal.
- Brand blue chosen over the iOS default red: matches web's
  ChatFab `bg-brand` pip and avoids the "error / critical" connotation
  red carries for an everyday new-comment notification.

Both queries (`inboxListOptions`, `chatSessionsOptions`) are already
kept fresh by listing-level realtime hooks mounted in
`app/(app)/[workspace]/_layout.tsx` (`useInboxRealtime` /
`useChatSessionsRealtime`), so badges update via WS events without a
poll or focus refetch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:54:21 +08:00
..