@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.
- package/README.md +123 -21
- package/dist/index.cjs +20 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -3
- package/dist/index.d.ts +16 -3
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -1
- package/docs/integration-guide.html +290 -42
- package/package.json +1 -1
|
@@ -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 <input type="file"></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>
|
|
861
|
-
|
|
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="
|
|
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="
|
|
867
|
-
|
|
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">//
|
|
870
|
-
<span class="
|
|
871
|
-
|
|
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
|
+
&& message.senderId !== currentUserId,
|
|
872
921
|
});
|
|
922
|
+
});
|
|
873
923
|
|
|
874
|
-
|
|
875
|
-
|
|
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="
|
|
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
|
-
|
|
881
|
-
|
|
882
|
-
|
|
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>
|
|
886
|
-
|
|
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
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
<tr
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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>
|
|
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="
|
|
1916
|
-
<pre><code><span class="kw">import</span>
|
|
1917
|
-
|
|
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">//
|
|
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
|
+
<button onClick={onClick} style={{ position: <span class="str">'relative'</span> }}>
|
|
2094
|
+
💬
|
|
2095
|
+
{totalUnread > <span class="num">0</span> && (
|
|
2096
|
+
<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
|
+
}}>
|
|
2104
|
+
{totalUnread > <span class="num">99</span> ? <span class="str">'99+'</span> : totalUnread}
|
|
2105
|
+
</span>
|
|
2106
|
+
)}
|
|
2107
|
+
</button>
|
|
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
|
-
|
|
1923
|
-
}, [conversations])
|
|
2114
|
+
document.title = total > <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
|
-
<
|
|
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
|
+
<View>
|
|
2137
|
+
<Ionicons name=<span class="str">"chatbubbles-outline"</span> size={<span class="num">24</span>} color={color} />
|
|
2138
|
+
{totalUnread > <span class="num">0</span> && (
|
|
2139
|
+
<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
|
+
}}>
|
|
2145
|
+
<Text style={{ color: <span class="str">'#fff'</span>, fontSize: <span class="num">10</span>, fontWeight: <span class="str">'700'</span> }}>
|
|
2146
|
+
{totalUnread > <span class="num">99</span> ? <span class="str">'99+'</span> : String(totalUnread)}
|
|
2147
|
+
</Text>
|
|
2148
|
+
</View>
|
|
2149
|
+
)}
|
|
2150
|
+
</View>
|
|
2151
|
+
);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
<span class="cm">// Wire into Tab.Navigator</span>
|
|
2155
|
+
<Tab.Screen
|
|
2156
|
+
name=<span class="str">"Chat"</span>
|
|
2157
|
+
component={ChatScreen}
|
|
2158
|
+
options={{ tabBarIcon: ({ color }) => <ChatTabIcon color={color} /> }}
|
|
2159
|
+
/></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">//
|
|
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