Unread badge
One number across every conversation - for the Chat item in your app's nav or a bell icon.
Overview
The workspace badge is one number across every conversation. Per-row badges in the conversation list are separate (unreadCount on each conversation).
The package keeps it correct: it goes up when a message arrives and down when a conversation is read, cleared, deleted or left. The server pushes the whole summary - you only assign it, never increment or decrement.
Keep the socket alive
useTotalUnreadCount() only reads the store. Login and the socket start when <ChatMain> / <Chat> mounts - if chat lives only on /chat, leaving that route disconnects the socket and the badge stops updating. Mount one chat instance at a time: the visible ChatMain on /chat, and a hidden keep-alive with the same config everywhere else.
// ChatSocketKeepAlive.tsx - render inside the router, next to Layoutimport { useLocation } from "react-router-dom";import { ChatMain } from "@realtimexsco/live-chat"; const CHAT_PATH = "/chat"; export function ChatSocketKeepAlive() { const { pathname } = useLocation(); const onChat = pathname === CHAT_PATH || pathname.startsWith(`${CHAT_PATH}/`); if (onChat) return null; // the visible chat page already mounted ChatMain return ( <div className="pointer-events-none invisible fixed left-0 top-0 h-0 w-0 overflow-hidden" aria-hidden> <ChatMain {...chatConfig} /> </div> );} // App.tsx<BrowserRouter> <ChatSocketKeepAlive /> <Layout /></BrowserRouter>Option 1 - Hook (recommended)
Works outside <ChatMain> (sidebar, header, another route) as long as a chat instance is mounted somewhere.
import { useTotalUnreadCount } from "@realtimexsco/live-chat"; const SidebarChatLink = () => { const { totalUnreadCount } = useTotalUnreadCount(); return ( <NavItem icon={<BellIcon />} label="Chat"> {totalUnreadCount > 0 && <Badge>{totalUnreadCount > 99 ? "99+" : totalUnreadCount}</Badge>} </NavItem> );};It fetches once over REST when the session is ready, so the badge is right before the socket connects. Pass { fetchOnMount: false } to skip that.
Option 2 - Prop
import type { UnreadSummary } from "@realtimexsco/live-chat"; const [unread, setUnread] = useState<UnreadSummary>({ /* zeros */ }); <ChatMain {...chatConfig} onUnreadCountChange={(summary) => setUnread(summary)} /> // or with the composable provider<Chat {...chatProps} onUnreadCountChange={(summary) => setUnread(summary)}> <ChatLayout /></Chat>Assign, don't accumulate - each call carries the complete current number. Keep the handler stable (useCallback) if it does anything expensive.
Payload
interface UnreadSummary { userId: string; totalUnreadCount: number; // the badge - direct + group directUnreadCount: number; groupUnreadCount: number; unreadConversationCount: number; // conversations with ≥1 unread updatedAt: string; // server ISO timestamp}REST response from GET /api/v2/unread/count:
{ "success": true, "message": "Unread count fetched successfully", "data": { "userId": "6a3ccddb62017d384b7e14fe", "totalUnreadCount": 0, "directUnreadCount": 0, "groupUnreadCount": 0, "unreadConversationCount": 0, "updatedAt": "2026-08-05T15:07:34.680Z" }, "error": null}Store / imperative
import { useChatStore, fetchUnreadCount } from "@realtimexsco/live-chat"; const summary = await fetchUnreadCount(); // REST - the session must be configured const total = useChatStore.getState().unreadSummary.totalUnreadCount;useChatStore.getState().requestUnreadTotal(); // socket resenduseChatStore.getState().fetchUnreadTotal(); // REST → storeChecklist & troubleshooting
- Keep-alive (or an always-mounted ChatMain) inside the router
- Keep-alive returns null on the chat route - never two sockets
- Same clientId, token, apiUrl, socketUrl and apiVersions.v2 as the chat page
- useTotalUnreadCount() on the Chat nav item
- Stay on Home, receive a DM → the badge ticks up with no reload