@antzsoft/chat-core 1.0.3 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -842,6 +842,14 @@ chatClient.<span class="fn">disconnect</span>();
842
842
  <span class="at">name</span>: <span class="str">'Design Team'</span>, <span class="at">participantIds</span>: [<span class="str">'user-1'</span>, <span class="str">'user-2'</span>],
843
843
  });
844
844
 
845
+ <span class="cm">// Upload group icon after creation (admin only)</span>
846
+ <span class="cm">// Web — pass a File object from an &lt;input type="file"&gt;</span>
847
+ <span class="kw">await</span> conversationsApi.<span class="fn">uploadIcon</span>(group.id, file);
848
+
849
+ <span class="cm">// React Native — pass { uri, name, type } from image picker</span>
850
+ <span class="kw">await</span> conversationsApi.<span class="fn">uploadIcon</span>(group.id, { <span class="at">uri</span>: asset.uri, <span class="at">name</span>: <span class="str">'icon.jpg'</span>, <span class="at">type</span>: <span class="str">'image/jpeg'</span> });
851
+ <span class="cm">// Returns updated conversation with fresh iconUrl. Old icon deleted from storage automatically.</span>
852
+
845
853
  <span class="cm">// Participants</span>
846
854
  <span class="kw">const</span> members = <span class="kw">await</span> conversationsApi.<span class="fn">getMembers</span>(conversationId);
847
855
  <span class="kw">await</span> conversationsApi.<span class="fn">addParticipants</span>(conversationId, [<span class="str">'user-3'</span>]);
@@ -857,33 +865,89 @@ chatClient.<span class="fn">disconnect</span>();
857
865
 
858
866
  <!-- ─── STEP 7: ROOMS ──────────────────────────────────────────────────── -->
859
867
  <section id="step-rooms">
860
- <h2><span class="step">STEP 7</span> Join &amp; Leave a Room</h2>
861
- <p>Joining a room subscribes your socket to real-time events for that conversation. Both <code>joinRoom</code> and <code>leaveRoom</code> are fire-and-forget — they silently no-op if the socket isn't connected.</p>
868
+ <h2><span class="step">STEP 7</span> Rooms Auto-join, <code>joinRoom</code>, and <code>new_message</code></h2>
869
+
870
+ <h4>Auto-join on connect</h4>
871
+ <p>
872
+ <strong>On every socket connection, the server automatically joins the user into all their existing conversation rooms.</strong>
873
+ No client action is needed. The moment the socket connects, the user is already subscribed to real-time events
874
+ (<code>new_message</code>, <code>typing_indicator</code>, <code>read_receipt</code>, etc.) for every conversation they belong to.
875
+ </p>
876
+ <div class="callout info">
877
+ <strong>You do not need to call <code>joinRoom</code> when opening a chat screen.</strong>
878
+ The user is already in the room. Calling <code>joinRoom</code> on an already-joined room is safe (idempotent)
879
+ but triggers an unnecessary DB access check — avoid it in hot paths.
880
+ </div>
881
+
882
+ <h4>When to call <code>joinRoom</code></h4>
883
+ <p>
884
+ There is only one real use case — when the <strong>user is added to a conversation while their socket is already connected</strong>
885
+ (either someone created a new group and added them, or they were added to an existing group at runtime).
886
+ The server does <strong>not</strong> auto-join the user's socket to the new room — it only emits <code>conversation_created</code>
887
+ to the user's personal room. Without calling <code>joinRoom</code>, the socket will not receive any
888
+ <code>new_message</code>, <code>typing_indicator</code>, or other room events for that conversation.
889
+ </p>
890
+
891
+ <pre><code><span class="cm">// Fires when you're added to a new or existing conversation at runtime</span>
892
+ client.socket.<span class="fn">on</span>(<span class="str">'conversation_created'</span>, (conv) => {
893
+ client.socket.emit.<span class="fn">joinRoom</span>(conv.id);
894
+ });</code></pre>
895
+
896
+ <p><code>leaveRoom</code> is only needed if you want to intentionally stop receiving events for a room the user is still a member of (e.g. archiving client-side). It is not needed when navigating away from a screen.</p>
897
+
898
+ <h4>Where to listen to <code>new_message</code></h4>
899
+ <p>
900
+ Because the user is auto-joined to all rooms, <code>new_message</code> fires for <em>any</em> conversation — not just the one currently open.
901
+ Register <strong>one listener at app root level</strong>, right after <code>client.connect()</code>.
902
+ Never add it inside a screen or component. Use <code>message.conversationId</code> to route the message to the right place.
903
+ </p>
862
904
 
863
905
  <div data-p="rn web">
864
- <pre><code><span class="kw">import</span> { socketEmit, onSocketStatus } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
906
+ <pre><code><span class="cm">// Register once at app root covers all conversations</span>
907
+ client.socket.<span class="fn">on</span>(<span class="str">'new_message'</span>, (event: <span class="tp">NewMessageEvent</span>) => {
908
+ <span class="kw">const</span> { message } = event;
865
909
 
866
- <span class="fn">useEffect</span>(() => {
867
- socketEmit.<span class="fn">joinRoom</span>(conversationId);
910
+ <span class="cm">// Update the open chat view if this conversation is active</span>
911
+ <span class="kw">if</span> (message.conversationId === activeConversationId) {
912
+ <span class="fn">appendMessageToView</span>(message);
913
+ }
868
914
 
869
- <span class="cm">// Re-join if socket reconnects while this screen is open (e.g. after foreground)</span>
870
- <span class="kw">const</span> unsub = <span class="fn">onSocketStatus</span>((s) => {
871
- <span class="kw">if</span> (s === <span class="str">'connected'</span>) socketEmit.<span class="fn">joinRoom</span>(conversationId);
915
+ <span class="cm">// Always update conversation list (last message + unread badge)</span>
916
+ <span class="fn">updateConversationList</span>(message.conversationId, {
917
+ lastMessage: message,
918
+ <span class="cm">// Don't increment unread if the user sent it or is currently viewing that chat</span>
919
+ incrementUnread: message.conversationId !== activeConversationId
920
+ &amp;&amp; message.senderId !== currentUserId,
872
921
  });
922
+ });
873
923
 
874
- <span class="kw">return</span> () => { socketEmit.<span class="fn">leaveRoom</span>(conversationId); unsub(); };
875
- }, [conversationId]);</code></pre>
924
+ <span class="cm">// Only case where joinRoom is needed</span>
925
+ client.socket.<span class="fn">on</span>(<span class="str">'conversation_created'</span>, (conv) => {
926
+ client.socket.emit.<span class="fn">joinRoom</span>(conv.id);
927
+ });</code></pre>
876
928
  </div>
877
929
  <div data-p="node">
878
- <pre><code><span class="kw">import</span> { socketEmit } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
930
+ <pre><code><span class="cm">// Bot / headless same pattern, one listener at startup</span>
931
+ client.socket.<span class="fn">on</span>(<span class="str">'new_message'</span>, (event: <span class="tp">NewMessageEvent</span>) => {
932
+ <span class="kw">const</span> { message } = event;
933
+ console.<span class="fn">log</span>(<span class="str">`[</span>${message.conversationId}<span class="str">]`</span>, message.content.text);
934
+ client.socket.emit.<span class="fn">markRead</span>(message.conversationId, message.id);
935
+ });
879
936
 
880
- socketEmit.<span class="fn">joinRoom</span>(conversationId);
881
- <span class="cm">// When done:</span>
882
- socketEmit.<span class="fn">leaveRoom</span>(conversationId);</code></pre>
937
+ client.socket.<span class="fn">on</span>(<span class="str">'conversation_created'</span>, (conv) => {
938
+ client.socket.emit.<span class="fn">joinRoom</span>(conv.id);
939
+ });</code></pre>
883
940
  </div>
941
+
884
942
  <div class="callout warn">
885
- <strong>Room subscriptions don't survive reconnects.</strong>
886
- The server forgets which rooms you were in if the socket disconnects. Always re-join after reconnect (the <code>onSocketStatus</code> pattern above handles this for React apps).
943
+ <strong>Summary of rules:</strong>
944
+ <ul>
945
+ <li>All existing rooms are auto-joined on socket connect — no <code>joinRoom</code> needed on screen open.</li>
946
+ <li>Call <code>joinRoom</code> only after a <code>conversation_created</code> event.</li>
947
+ <li>Add the <code>new_message</code> listener once at app root, never per screen.</li>
948
+ <li>Use <code>message.conversationId</code> to route to the right view or badge update.</li>
949
+ <li>Track <code>activeConversationId</code> to decide whether to increment the unread count.</li>
950
+ </ul>
887
951
  </div>
888
952
  </section>
889
953
 
@@ -970,18 +1034,113 @@ socket.<span class="fn">on</span>(<span class="str">'message_deleted'</span>, (e
970
1034
  </div>
971
1035
  <h4>All socket events</h4>
972
1036
  <table>
973
- <thead><tr><th>Event</th><th>When fired</th></tr></thead>
1037
+ <thead><tr><th>Event</th><th>When fired</th><th>Where to listen / unlisten</th></tr></thead>
974
1038
  <tbody>
975
- <tr><td><code>'new_message'</code></td><td>New message in a joined room</td></tr>
976
- <tr><td><code>'message_updated'</code></td><td>A message was edited</td></tr>
977
- <tr><td><code>'message_deleted'</code></td><td>Deleted for everyone</td></tr>
978
- <tr><td><code>'message_deleted_for_me'</code></td><td>Deleted for current user only</td></tr>
979
- <tr><td><code>'reaction_updated'</code></td><td>Emoji reaction added/removed</td></tr>
980
- <tr><td><code>'typing'</code></td><td>User started/stopped typing</td></tr>
981
- <tr><td><code>'user_online'</code> / <code>'user_offline'</code></td><td>User went online/offline — auto-updates <code>useChatStore.lastSeen</code></td></tr>
982
- <tr><td><code>'read_receipt'</code></td><td>Messages marked as read — auto-updates <code>useChatStore.lastRead</code></td></tr>
983
- <tr><td><code>'message_ack'</code></td><td>Your sent message acknowledged (tempId real ID)</td></tr>
984
- <tr><td><code>'message_delivered'</code></td><td>Message delivered to recipient</td></tr>
1039
+ <tr>
1040
+ <td><code>'new_message'</code></td>
1041
+ <td>New message in any of the user's conversations — all rooms are auto-joined on connect</td>
1042
+ <td><strong>App root</strong> after <code>client.connect()</code>. Never remove this listener — it must stay alive for the full app session to keep the conversation list and unread badges up to date.</td>
1043
+ </tr>
1044
+ <tr>
1045
+ <td><code>'message_updated'</code></td>
1046
+ <td>A message was edited</td>
1047
+ <td><strong>Chat detail screen</strong> add on mount, remove on unmount (<code>socket.off</code>).</td>
1048
+ </tr>
1049
+ <tr>
1050
+ <td><code>'message_deleted'</code></td>
1051
+ <td>Deleted for everyone</td>
1052
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1053
+ </tr>
1054
+ <tr>
1055
+ <td><code>'message_deleted_for_me'</code></td>
1056
+ <td>Deleted for current user only (fired only to that user's socket)</td>
1057
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1058
+ </tr>
1059
+ <tr>
1060
+ <td><code>'reaction_updated'</code></td>
1061
+ <td>Emoji reaction added/removed</td>
1062
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1063
+ </tr>
1064
+ <tr>
1065
+ <td><code>'message_pin_updated'</code></td>
1066
+ <td>A message was pinned or unpinned</td>
1067
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1068
+ </tr>
1069
+ <tr>
1070
+ <td><code>'typing_indicator'</code></td>
1071
+ <td>User started/stopped typing (payload: <code>{ conversationId, userId, displayName, isTyping }</code>)</td>
1072
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1073
+ </tr>
1074
+ <tr>
1075
+ <td><code>'user_online'</code></td>
1076
+ <td>A participant came online</td>
1077
+ <td><strong>App root</strong> — drives online indicators everywhere. Keep for full session.</td>
1078
+ </tr>
1079
+ <tr>
1080
+ <td><code>'user_offline'</code></td>
1081
+ <td>A participant went offline</td>
1082
+ <td><strong>App root</strong> — drives online indicators and last-seen everywhere. Keep for full session.</td>
1083
+ </tr>
1084
+ <tr>
1085
+ <td><code>'user_status'</code></td>
1086
+ <td>Global presence broadcast (online/offline/away) to all connected clients</td>
1087
+ <td><strong>App root</strong> — keep for full session.</td>
1088
+ </tr>
1089
+ <tr>
1090
+ <td><code>'online_users'</code></td>
1091
+ <td>Full list of online user IDs — sent on initial room join</td>
1092
+ <td><strong>App root</strong> — keep for full session.</td>
1093
+ </tr>
1094
+ <tr>
1095
+ <td><code>'read_receipt'</code></td>
1096
+ <td>Messages marked as read by a participant</td>
1097
+ <td><strong>Chat detail screen</strong> (to update tick marks) + <strong>app root</strong> (to clear your own unread count when read on another device).</td>
1098
+ </tr>
1099
+ <tr>
1100
+ <td><code>'unread_count_changed'</code></td>
1101
+ <td>Your unread count changed — sent to all your devices simultaneously</td>
1102
+ <td><strong>App root / conversation list screen</strong> — keep alive as long as the list is rendered.</td>
1103
+ </tr>
1104
+ <tr>
1105
+ <td><code>'message_ack'</code></td>
1106
+ <td>Server acknowledged your sent message (tempId → real ID)</td>
1107
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1108
+ </tr>
1109
+ <tr>
1110
+ <td><code>'message_delivered'</code></td>
1111
+ <td>A single message you sent was delivered to all active recipients</td>
1112
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1113
+ </tr>
1114
+ <tr>
1115
+ <td><code>'messages_delivered'</code></td>
1116
+ <td>Batch delivery catch-up when a recipient comes online</td>
1117
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1118
+ </tr>
1119
+ <tr>
1120
+ <td><code>'conversation_created'</code></td>
1121
+ <td>A new conversation was created (or you were added to one)</td>
1122
+ <td><strong>App root</strong> — call <code>joinRoom</code> here for the new conversation. Keep for full session.</td>
1123
+ </tr>
1124
+ <tr>
1125
+ <td><code>'conversation_updated'</code></td>
1126
+ <td>A conversation's last message or metadata changed</td>
1127
+ <td><strong>App root</strong> — keep for full session. The server emits this for every message across all conversations; a global listener keeps the in-memory conversation list and unread badge always in sync.</td>
1128
+ </tr>
1129
+ <tr>
1130
+ <td><code>'conversation_deleted'</code></td>
1131
+ <td>A conversation was deleted</td>
1132
+ <td><strong>App root / conversation list screen</strong> — remove the conversation from state and navigate away if it was open.</td>
1133
+ </tr>
1134
+ <tr>
1135
+ <td><code>'participant_joined'</code></td>
1136
+ <td>A new participant was added to a group conversation</td>
1137
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1138
+ </tr>
1139
+ <tr>
1140
+ <td><code>'participant_left'</code></td>
1141
+ <td>A participant left or was removed from a group conversation</td>
1142
+ <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
1143
+ </tr>
985
1144
  </tbody>
986
1145
  </table>
987
1146
  </section>
@@ -1910,19 +2069,114 @@ socket?.<span class="fn">on</span>(<span class="str">'unread_count_changed'</spa
1910
2069
  <span class="cm">// unreadCount is 0 — the conversation was just read.</span>
1911
2070
  });</code></pre>
1912
2071
 
1913
- <h3>Platform-specific badge updates</h3>
2072
+ <h3>Chat icon badge — the key insight</h3>
2073
+ <p>
2074
+ If you use <code>@antzsoft/chat-web-sdk</code> or <code>@antzsoft/chat-rn-sdk</code>,
2075
+ <strong>you do not need to listen to socket events manually</strong>.
2076
+ <code>useConversations()</code> is already subscribed internally — every
2077
+ <code>conversation_updated</code> and <code>unread_count_changed</code> event
2078
+ automatically updates the hook's data. Just sum the counts:
2079
+ </p>
1914
2080
 
1915
- <div data-p="rn">
1916
- <pre><code><span class="kw">import</span> * <span class="kw">as</span> Notifications <span class="kw">from</span> <span class="str">'expo-notifications'</span>;
1917
- <span class="kw">import</span> { AppState } <span class="kw">from</span> <span class="str">'react-native'</span>;
2081
+ <div data-p="web">
2082
+ <pre><code><span class="kw">import</span> { useConversations } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
2083
+
2084
+ <span class="kw">function</span> <span class="fn">ChatIconButton</span>({ onClick }: { onClick: () => <span class="tp">void</span> }) {
2085
+ <span class="kw">const</span> { conversations } = <span class="fn">useConversations</span>();
1918
2086
 
1919
- <span class="cm">// Keep badge in sync while app is active</span>
2087
+ <span class="cm">// Recalculates automatically on every socket update no extra listener needed</span>
2088
+ <span class="kw">const</span> totalUnread = conversations.<span class="fn">reduce</span>(
2089
+ (sum, c) => sum + (c.unreadCount ?? <span class="num">0</span>), <span class="num">0</span>
2090
+ );
2091
+
2092
+ <span class="kw">return</span> (
2093
+ &lt;button onClick={onClick} style={{ position: <span class="str">'relative'</span> }}&gt;
2094
+ 💬
2095
+ {totalUnread &gt; <span class="num">0</span> &amp;&amp; (
2096
+ &lt;span style={{
2097
+ position: <span class="str">'absolute'</span>, top: <span class="num">-6</span>, right: <span class="num">-6</span>,
2098
+ backgroundColor: <span class="str">'#e53935'</span>, color: <span class="str">'#fff'</span>,
2099
+ borderRadius: <span class="str">'50%'</span>, minWidth: <span class="num">18</span>, height: <span class="num">18</span>,
2100
+ fontSize: <span class="num">11</span>, fontWeight: <span class="num">700</span>,
2101
+ display: <span class="str">'flex'</span>, alignItems: <span class="str">'center'</span>, justifyContent: <span class="str">'center'</span>,
2102
+ padding: <span class="str">'0 4px'</span>,
2103
+ }}&gt;
2104
+ {totalUnread &gt; <span class="num">99</span> ? <span class="str">'99+'</span> : totalUnread}
2105
+ &lt;/span&gt;
2106
+ )}
2107
+ &lt;/button&gt;
2108
+ );
2109
+ }
2110
+
2111
+ <span class="cm">// Update browser tab title too</span>
1920
2112
  <span class="fn">useEffect</span>(() => {
1921
2113
  <span class="kw">const</span> total = conversations.<span class="fn">reduce</span>((s, c) => s + (c.unreadCount ?? <span class="num">0</span>), <span class="num">0</span>);
1922
- Notifications.<span class="fn">setBadgeCountAsync</span>(total);
1923
- }, [conversations]);
2114
+ document.title = total &gt; <span class="num">0</span> ? <span class="str">`(${total}) Antz Chat`</span> : <span class="str">'Antz Chat'</span>;
2115
+ }, [conversations]);</code></pre>
2116
+ </div>
1924
2117
 
1925
- <span class="cm">// Refresh when app comes to foreground (socket may have been down)</span>
2118
+ <div data-p="rn">
2119
+ <pre><code><span class="kw">import</span> { useConversations } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
2120
+ <span class="kw">import</span> * <span class="kw">as</span> Notifications <span class="kw">from</span> <span class="str">'expo-notifications'</span>;
2121
+
2122
+ <span class="kw">function</span> <span class="fn">ChatTabIcon</span>({ color }: { color: <span class="tp">string</span> }) {
2123
+ <span class="kw">const</span> { conversations } = <span class="fn">useConversations</span>();
2124
+
2125
+ <span class="cm">// Recalculates automatically on every socket update — no extra listener needed</span>
2126
+ <span class="kw">const</span> totalUnread = conversations.<span class="fn">reduce</span>(
2127
+ (sum, c) => sum + (c.unreadCount ?? <span class="num">0</span>), <span class="num">0</span>
2128
+ );
2129
+
2130
+ <span class="cm">// Sync OS app icon badge</span>
2131
+ <span class="fn">useEffect</span>(() => {
2132
+ Notifications.<span class="fn">setBadgeCountAsync</span>(totalUnread);
2133
+ }, [totalUnread]);
2134
+
2135
+ <span class="kw">return</span> (
2136
+ &lt;View&gt;
2137
+ &lt;Ionicons name=<span class="str">"chatbubbles-outline"</span> size={<span class="num">24</span>} color={color} /&gt;
2138
+ {totalUnread &gt; <span class="num">0</span> &amp;&amp; (
2139
+ &lt;View style={{
2140
+ position: <span class="str">'absolute'</span>, top: <span class="num">-4</span>, right: <span class="num">-6</span>,
2141
+ backgroundColor: <span class="str">'#e53935'</span>, borderRadius: <span class="num">9</span>,
2142
+ minWidth: <span class="num">18</span>, height: <span class="num">18</span>, alignItems: <span class="str">'center'</span>,
2143
+ justifyContent: <span class="str">'center'</span>, paddingHorizontal: <span class="num">4</span>,
2144
+ }}&gt;
2145
+ &lt;Text style={{ color: <span class="str">'#fff'</span>, fontSize: <span class="num">10</span>, fontWeight: <span class="str">'700'</span> }}&gt;
2146
+ {totalUnread &gt; <span class="num">99</span> ? <span class="str">'99+'</span> : String(totalUnread)}
2147
+ &lt;/Text&gt;
2148
+ &lt;/View&gt;
2149
+ )}
2150
+ &lt;/View&gt;
2151
+ );
2152
+ }
2153
+
2154
+ <span class="cm">// Wire into Tab.Navigator</span>
2155
+ &lt;Tab.Screen
2156
+ name=<span class="str">"Chat"</span>
2157
+ component={ChatScreen}
2158
+ options={{ tabBarIcon: ({ color }) =&gt; &lt;ChatTabIcon color={color} /&gt; }}
2159
+ /&gt;</code></pre>
2160
+ </div>
2161
+
2162
+ <p style="margin:16px 0 8px;color:#c8d0e0;font-size:14px">
2163
+ The full update chain — no polling, no manual socket code:
2164
+ </p>
2165
+ <pre style="background:var(--code-bg);padding:12px 16px;border-radius:8px;font-size:12px;color:#8892b0;border:1px solid var(--border)">New message sent
2166
+ → server emits conversation_updated (DB-accurate unreadCount per participant)
2167
+ → SocketProvider updates useConversations() React Query cache
2168
+ → totalUnread recalculates → badge re-renders
2169
+
2170
+ User reads conversation
2171
+ → server emits unread_count_changed to ALL devices of that user simultaneously
2172
+ → useConversations() cache updated → badge clears on every device at once</pre>
2173
+
2174
+ <h3>Platform-specific: foreground resume + tab title</h3>
2175
+
2176
+ <div data-p="rn">
2177
+ <pre><code><span class="kw">import</span> { AppState } <span class="kw">from</span> <span class="str">'react-native'</span>;
2178
+
2179
+ <span class="cm">// Refresh when app comes to foreground (socket may have been down while backgrounded)</span>
1926
2180
  <span class="fn">useEffect</span>(() => {
1927
2181
  <span class="kw">const</span> sub = AppState.<span class="fn">addEventListener</span>(<span class="str">'change'</span>, <span class="kw">async</span> (state) => {
1928
2182
  <span class="kw">if</span> (state === <span class="str">'active'</span>) {
@@ -1935,13 +2189,7 @@ socket?.<span class="fn">on</span>(<span class="str">'unread_count_changed'</spa
1935
2189
  </div>
1936
2190
 
1937
2191
  <div data-p="web">
1938
- <pre><code><span class="cm">// Browser tab title badge</span>
1939
- <span class="fn">useEffect</span>(() => {
1940
- <span class="kw">const</span> total = conversations.<span class="fn">reduce</span>((s, c) => s + (c.unreadCount ?? <span class="num">0</span>), <span class="num">0</span>);
1941
- document.title = total > <span class="num">0</span> ? <span class="str">`(${total}) Antz Chat`</span> : <span class="str">'Antz Chat'</span>;
1942
- }, [conversations]);
1943
-
1944
- <span class="cm">// Refresh when tab becomes visible (socket may have been down)</span>
2192
+ <pre><code><span class="cm">// Refresh when tab becomes visible (socket may have been down)</span>
1945
2193
  <span class="fn">useEffect</span>(() => {
1946
2194
  <span class="kw">function</span> <span class="fn">onVisible</span>() {
1947
2195
  <span class="kw">if</span> (document.visibilityState === <span class="str">'visible'</span>) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antzsoft/chat-core",
3
- "version": "1.0.3",
3
+ "version": "1.0.4",
4
4
  "description": "Platform-agnostic core for Antz Chat — API, socket, stores, types. Works in browser, React Native (Expo or bare), and Node.js.",
5
5
  "author": "Antz",
6
6
  "license": "MIT",