RealTimeX
Sign inStart for free
RealTimeXWeb Package
IntroductionPrerequisitesInstallationProject setupQuick start
Package structureConfigurationComponentsHooksAPI integrationSocket events
OverviewReplace entire UIReplace componentsStyle with classNamesThemes & localePhone & video callingIn-call control barFeatures
Unread badgeRich-text messagesPins, stars & datesMessage threadsMentionsCanvas & draftsNotificationsAdmin permissionsServer refusalsSession & token refresh
Host app integrationTroubleshootingFAQVersion historyBest practicesSupport & contact
Web packageBuilt-in features

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.

tsx
1// ChatSocketKeepAlive.tsx - render inside the router, next to Layout2import { useLocation } from "react-router-dom";3import { ChatMain } from "@realtimexsco/live-chat";4 5const CHAT_PATH = "/chat";6 7export function ChatSocketKeepAlive() {8  const { pathname } = useLocation();9  const onChat = pathname === CHAT_PATH || pathname.startsWith(`${CHAT_PATH}/`);10 11  if (onChat) return null; // the visible chat page already mounted ChatMain12 13  return (14    <div className="pointer-events-none invisible fixed left-0 top-0 h-0 w-0 overflow-hidden" aria-hidden>15      <ChatMain {...chatConfig} />16    </div>17  );18}19 20// App.tsx21<BrowserRouter>22  <ChatSocketKeepAlive />23  <Layout />24</BrowserRouter>

Important

Put the hook on the Chat nav item, not the app root. Hide the badge at 0 and cap it at 99+. apiVersions must include v2 - the unread REST call is GET /api/v2/unread/count.

Option 1 - Hook (recommended)

Works outside <ChatMain> (sidebar, header, another route) as long as a chat instance is mounted somewhere.

tsx
1import { useTotalUnreadCount } from "@realtimexsco/live-chat";2 3const SidebarChatLink = () => {4  const { totalUnreadCount } = useTotalUnreadCount();5  return (6    <NavItem icon={<BellIcon />} label="Chat">7      {totalUnreadCount > 0 && <Badge>{totalUnreadCount > 99 ? "99+" : totalUnreadCount}</Badge>}8    </NavItem>9  );10};
FieldMeaningTypical UI
totalUnreadCountAll unread messages (DM + group)Main nav badge
directUnreadCountUnread messages in DMs only“Direct” tab badge
groupUnreadCountUnread messages in groups only“Groups” tab badge
unreadConversationCountChats with ≥1 unread (not a message count)“7 chats unread”
summaryFull UnreadSummary (userId, updatedAt, …)Custom logic
refresh()Ask the server to resend over the socketManual resync
refetch()Re-fetch over RESTNo socket needed

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

tsx
1import type { UnreadSummary } from "@realtimexsco/live-chat";2 3const [unread, setUnread] = useState<UnreadSummary>({ /* zeros */ });4 5<ChatMain {...chatConfig} onUnreadCountChange={(summary) => setUnread(summary)} />6 7// or with the composable provider8<Chat {...chatProps} onUnreadCountChange={(summary) => setUnread(summary)}>9  <ChatLayout />10</Chat>

Assign, don't accumulate - each call carries the complete current number. Keep the handler stable (useCallback) if it does anything expensive.

Payload

typescript
1interface UnreadSummary {2  userId: string;3  totalUnreadCount: number;        // the badge - direct + group4  directUnreadCount: number;5  groupUnreadCount: number;6  unreadConversationCount: number; // conversations with ≥1 unread7  updatedAt: string;               // server ISO timestamp8}

REST response from GET /api/v2/unread/count:

json
1{2  "success": true,3  "message": "Unread count fetched successfully",4  "data": {5    "userId": "6a3ccddb62017d384b7e14fe",6    "totalUnreadCount": 0,7    "directUnreadCount": 0,8    "groupUnreadCount": 0,9    "unreadConversationCount": 0,10    "updatedAt": "2026-08-05T15:07:34.680Z"11  },12  "error": null13}

Store / imperative

typescript
1import { useChatStore, fetchUnreadCount } from "@realtimexsco/live-chat";2 3const summary = await fetchUnreadCount(); // REST - the session must be configured4 5const total = useChatStore.getState().unreadSummary.totalUnreadCount;6useChatStore.getState().requestUnreadTotal(); // socket resend7useChatStore.getState().fetchUnreadTotal();   // REST → store

Checklist & 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
SymptomCause
Always 0Chat never mounted / session not ready / keep-alive missing
Updates only on /chatThe socket dies when chat unmounts - add the keep-alive
Two connections / flaky badgeKeep-alive and the chat page are both mounted
REST 404 on unreadMissing apiVersions={{ v2: "v2" }}
Hook in App.tsx logs 0Expected until the chat session starts - move the hook to the badge
PreviousFeaturesNextRich-text messages

On this page

OverviewKeep the socket aliveOption 1 - Hook (recommended)Option 2 - PropPayloadStore / imperativeChecklist & troubleshooting
Back to top