@antzsoft/chat-core 1.0.4 → 1.0.5

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.
@@ -130,6 +130,11 @@ li{margin-bottom:4px;color:#c8d0e0}
130
130
  <p id="nav-platform-label">Integration Guide</p>
131
131
  </div>
132
132
 
133
+ <div class="section-label">What's New</div>
134
+ <ul>
135
+ <li><a href="#whats-new">v1.0.5 Release Notes</a></li>
136
+ </ul>
137
+
133
138
  <div class="section-label">Getting Started</div>
134
139
  <ul>
135
140
  <li><a href="#overview">Overview</a></li>
@@ -159,7 +164,7 @@ li{margin-bottom:4px;color:#c8d0e0}
159
164
 
160
165
  <div class="section-label">Messaging</div>
161
166
  <ul>
162
- <li><a href="#step-load">8. Load Messages</a></li>
167
+ <li><a href="#step-load">8. Load Messages &amp; Jump to Unread</a></li>
163
168
  <li><a href="#step-send">9. Send a Message</a></li>
164
169
  <li><a href="#step-realtime">10. Real-time Events</a></li>
165
170
  <li><a href="#step-typing">11. Typing Indicators</a></li>
@@ -208,6 +213,190 @@ li{margin-bottom:4px;color:#c8d0e0}
208
213
  </button>
209
214
  </div>
210
215
 
216
+ <!-- ─── WHAT'S NEW ────────────────────────────────────────────────────── -->
217
+ <section id="whats-new">
218
+ <h2>What's New</h2>
219
+ <p style="color:var(--muted);font-size:13px;margin-bottom:20px">Version history and release notes. Click a version to expand.</p>
220
+
221
+ <!-- ── v1.0.5 (current) ── -->
222
+ <div class="wn-version" id="wn-105">
223
+ <div class="wn-header" onclick="toggleVersion('wn-105')">
224
+ <div class="wn-title">
225
+ <span class="wn-ver">v1.0.5</span>
226
+ <span class="wn-badge current">Current</span>
227
+ <span class="wn-date">May 2026</span>
228
+ </div>
229
+ <span class="wn-chevron">▲</span>
230
+ </div>
231
+ <div class="wn-body">
232
+
233
+ <div class="wn-item" id="wn-105-1">
234
+ <div class="wn-item-header" onclick="toggleItem('wn-105-1')">
235
+ <span class="wn-tag new">New</span>
236
+ <span class="wn-item-title"><code>externalId</code> on all user responses</span>
237
+ <span class="wn-chevron-sm">▾</span>
238
+ </div>
239
+ <div class="wn-item-body">
240
+ <p>All API methods that return a user object now include an <code>externalId</code> field — this is the Antz User ID. Maps chat users directly back to your own system without a separate lookup.</p>
241
+ <p><strong>Usage:</strong> <code>user.externalId</code> is available on every <code>User</code> object — from <code>usersApi</code>, conversation participants, and message senders. No code change needed.</p>
242
+ <p class="wn-ref">→ <a href="#step-convs">Step 6 — Conversations</a> (participant objects) · Users API reference</p>
243
+ </div>
244
+ </div>
245
+
246
+ <div class="wn-item" id="wn-105-2">
247
+ <div class="wn-item-header" onclick="toggleItem('wn-105-2')">
248
+ <span class="wn-tag new">New</span>
249
+ <span class="wn-item-title">Single unified User List API</span>
250
+ <span class="wn-chevron-sm">▾</span>
251
+ </div>
252
+ <div class="wn-item-body">
253
+ <p>One <code>usersApi.list()</code> call now handles listing, searching, and pagination. Previous separate endpoints are consolidated.</p>
254
+ <pre><code><span class="fn">usersApi</span>.<span class="fn">list</span>() <span class="cm">// all users</span>
255
+ <span class="fn">usersApi</span>.<span class="fn">list</span>({ <span class="at">query</span>: <span class="str">'john'</span> }) <span class="cm">// search by name / username / email</span>
256
+ <span class="fn">usersApi</span>.<span class="fn">list</span>({ <span class="at">page</span>: <span class="num">1</span>, <span class="at">limit</span>: <span class="num">20</span> }) <span class="cm">// paginated</span></code></pre>
257
+ <p class="wn-ref">→ <a href="#step-convs">Step 6 — Conversations</a> (user list examples) · Users API reference</p>
258
+ </div>
259
+ </div>
260
+
261
+ <div class="wn-item" id="wn-105-3">
262
+ <div class="wn-item-header" onclick="toggleItem('wn-105-3')">
263
+ <span class="wn-tag new">New</span>
264
+ <span class="wn-item-title">Single unified Conversation List API</span>
265
+ <span class="wn-chevron-sm">▾</span>
266
+ </div>
267
+ <div class="wn-item-body">
268
+ <p><code>conversationsApi.list()</code> now handles listing, filtering, searching, and pagination in one call. All filters run server-side before pagination.</p>
269
+ <pre><code><span class="fn">conversationsApi</span>.<span class="fn">list</span>() <span class="cm">// all</span>
270
+ <span class="fn">conversationsApi</span>.<span class="fn">list</span>({ <span class="at">type</span>: <span class="str">'group'</span>, <span class="at">hasUnread</span>: <span class="kw">true</span> }) <span class="cm">// filter</span>
271
+ <span class="fn">conversationsApi</span>.<span class="fn">list</span>({ <span class="at">search</span>: <span class="str">'design'</span>, <span class="at">page</span>: <span class="num">1</span>, <span class="at">limit</span>: <span class="num">20</span> }) <span class="cm">// search + page</span></code></pre>
272
+ <p class="wn-ref">→ <a href="#step-convs">Step 6 — Conversations</a> for the full filter reference</p>
273
+ </div>
274
+ </div>
275
+
276
+ <div class="wn-item" id="wn-105-4">
277
+ <div class="wn-item-header" onclick="toggleItem('wn-105-4')">
278
+ <span class="wn-tag fix">Fix</span>
279
+ <span class="wn-item-title">Attachment last-message content preview</span>
280
+ <span class="wn-chevron-sm">▾</span>
281
+ </div>
282
+ <div class="wn-item-body">
283
+ <p>When the last message in a conversation was an attachment, the conversation list showed no content preview. Fixed — the server now returns a formatted preview string (e.g. <em>"📎 photo.jpg"</em>).</p>
284
+ <p><strong>Action required:</strong> Display <code>conversation.lastMessage.content.text</code> directly. Remove any workarounds that were falling back to a placeholder for attachment messages.</p>
285
+ <p class="wn-ref">→ <a href="#step-convs">Step 6</a> (conversation object) · <a href="#step-realtime">Step 10 — Real-time Events</a> (<code>conversation_updated</code>)</p>
286
+ </div>
287
+ </div>
288
+
289
+ <div class="wn-item" id="wn-105-5">
290
+ <div class="wn-item-header" onclick="toggleItem('wn-105-5')">
291
+ <span class="wn-tag fix">Fix</span>
292
+ <span class="wn-item-title">Avatar upload via <code>AntzChatClient</code></span>
293
+ <span class="wn-chevron-sm">▾</span>
294
+ </div>
295
+ <div class="wn-item-body">
296
+ <p><code>client.auth.uploadAvatar()</code> and <code>client.auth.syncAvatar()</code> were not correctly storing the avatar in all storage configurations. Fixed.</p>
297
+ <p><strong>Action required:</strong> Remove any workarounds for failed avatar uploads. Both methods now work correctly.</p>
298
+ <p class="wn-ref">→ <a href="#step1">Step 1 — Avatar section</a> for usage of <code>uploadAvatar()</code> and <code>syncAvatar()</code></p>
299
+ </div>
300
+ </div>
301
+
302
+ <div class="wn-item" id="wn-105-6">
303
+ <div class="wn-item-header" onclick="toggleItem('wn-105-6')">
304
+ <span class="wn-tag fix">Fix</span>
305
+ <span class="wn-item-title"><code>client.connect()</code> on React Native</span>
306
+ <span class="wn-chevron-sm">▾</span>
307
+ </div>
308
+ <div class="wn-item-body">
309
+ <p><code>chatClient.connect()</code> now correctly initialises and establishes the Socket.IO connection on React Native (Expo and bare). Previously the socket could fail to connect silently in certain RN configurations.</p>
310
+ <p><strong>Action required:</strong> Verify <code>await chatClient.connect()</code> transitions <code>onSocketStatus</code> to <code>'connected'</code>. No API change — same call, now reliable.</p>
311
+ <p class="wn-ref">→ <a href="#step-socket">Step 3 — Connect Socket</a> for the full RN root provider setup</p>
312
+ </div>
313
+ </div>
314
+
315
+ <div class="wn-item" id="wn-105-7">
316
+ <div class="wn-item-header" onclick="toggleItem('wn-105-7')">
317
+ <span class="wn-tag new">New</span>
318
+ <span class="wn-item-title">Group icon — create &amp; update</span>
319
+ <span class="wn-chevron-sm">▾</span>
320
+ </div>
321
+ <div class="wn-item-body">
322
+ <p>Group conversations now support a custom icon. Admins can set or replace it at any time. The icon is stored server-side and a fresh signed URL is returned on every conversation response.</p>
323
+ <pre><code><span class="cm">// Upload icon — admin only</span>
324
+ <span class="kw">const</span> updated = <span class="kw">await</span> client.<span class="fn">uploadIcon</span>(groupId, {
325
+ <span class="at">uri</span>: file.uri, <span class="at">name</span>: <span class="str">'icon.jpg'</span>, <span class="at">type</span>: <span class="str">'image/jpeg'</span>, <span class="at">size</span>: file.size,
326
+ });
327
+ <span class="cm">// updated.iconUrl → fresh signed URL</span></code></pre>
328
+ <p>Non-admins receive <code>403 Forbidden</code>. The URL is never stored in the DB — regenerated from <code>iconMeta.storageKey</code> on every response. Replacing an icon automatically deletes the previous one.</p>
329
+ <p class="wn-ref">→ <a href="#step-convs">Step 6 — Conversations</a> (group icon section) for the full upload pipeline</p>
330
+ </div>
331
+ </div>
332
+
333
+ <div class="wn-item" id="wn-105-8">
334
+ <div class="wn-item-header" onclick="toggleItem('wn-105-8')">
335
+ <span class="wn-tag new">New</span>
336
+ <span class="wn-item-title">Scroll to first unread message</span>
337
+ <span class="wn-chevron-sm">▾</span>
338
+ </div>
339
+ <div class="wn-item-body">
340
+ <p>Open a conversation directly at the first unread message with an "↑ Unread messages" divider. Uses <code>getLastRead()</code> combined with <code>list()</code> <code>direction: 'after'</code>.</p>
341
+ <pre><code><span class="cm">// 1. Get last-read pointer</span>
342
+ <span class="kw">const</span> { lastReadMessageId, lastReadAt } = <span class="kw">await</span> messagesApi.<span class="fn">getLastRead</span>(conversationId);
343
+
344
+ <span class="cm">// 2. Fetch unread messages (everything after the last-read)</span>
345
+ <span class="kw">const</span> { data: unread, meta } = <span class="kw">await</span> messagesApi.<span class="fn">list</span>(conversationId, {
346
+ <span class="at">cursor</span>: lastReadMessageId, <span class="at">direction</span>: <span class="str">'after'</span>, <span class="at">limit</span>: <span class="num">50</span>,
347
+ });
348
+
349
+ <span class="cm">// 3. Scroll to first unread</span>
350
+ <span class="kw">if</span> (unread.length) <span class="fn">scrollToMessage</span>(unread[<span class="num">0</span>].id);
351
+ <span class="cm">// meta.hasMore = true → more than 50 unread</span>
352
+
353
+ <span class="cm">// 4. Render divider above first unread message</span>
354
+ <span class="kw">const</span> isFirstUnread = lastRead && prevMessage?.id === lastRead.messageId;</code></pre>
355
+ <p class="wn-ref">→ <a href="#step-load">Step 8 — Jump to first unread</a> for full code · <a href="#step-read">Step 12 — Read Receipts &amp; Last Seen</a> for the complete last-read reference</p>
356
+ </div>
357
+ </div>
358
+
359
+ </div><!-- /.wn-body -->
360
+ </div><!-- /.wn-version -->
361
+
362
+ </section>
363
+
364
+ <style>
365
+ /* ── What's New ── */
366
+ .wn-version{border:1px solid var(--border);border-radius:var(--radius);margin-bottom:12px;overflow:hidden}
367
+ .wn-header{display:flex;align-items:center;justify-content:space-between;padding:14px 18px;cursor:pointer;background:var(--surface);user-select:none}
368
+ .wn-header:hover{background:var(--surface2)}
369
+ .wn-title{display:flex;align-items:center;gap:10px}
370
+ .wn-ver{font-weight:800;font-size:15px;color:#fff}
371
+ .wn-badge{font-size:10px;font-weight:700;padding:2px 8px;border-radius:20px;letter-spacing:.4px}
372
+ .wn-badge.current{background:rgba(52,211,153,.15);color:var(--green);border:1px solid rgba(52,211,153,.3)}
373
+ .wn-date{font-size:12px;color:var(--muted)}
374
+ .wn-chevron{font-size:11px;color:var(--muted);transition:transform .2s}
375
+ .wn-version:not(.open) .wn-chevron{transform:rotate(180deg)}
376
+ .wn-body{display:none;padding:4px 0 8px}
377
+ .wn-version.open .wn-body{display:block}
378
+
379
+ .wn-item{border-top:1px solid var(--border)}
380
+ .wn-item-header{display:flex;align-items:center;gap:10px;padding:11px 18px;cursor:pointer;user-select:none}
381
+ .wn-item-header:hover{background:var(--surface2)}
382
+ .wn-item-title{flex:1;font-size:13.5px;color:var(--text)}
383
+ .wn-item-title code{font-family:var(--font-mono);font-size:12px;color:var(--accent2);background:rgba(167,139,250,.12);padding:1px 5px;border-radius:3px}
384
+ .wn-chevron-sm{font-size:10px;color:var(--muted);transition:transform .2s}
385
+ .wn-item.open .wn-chevron-sm{transform:rotate(180deg)}
386
+ .wn-item-body{display:none;padding:4px 18px 14px 18px}
387
+ .wn-item.open .wn-item-body{display:block}
388
+ .wn-item-body p{font-size:13px;color:#c8d0e0;margin-bottom:8px}
389
+ .wn-item-body pre{margin:8px 0 10px}
390
+ .wn-ref{font-size:12px !important;color:var(--muted) !important}
391
+ .wn-ref a{color:var(--accent);text-decoration:none}
392
+ .wn-ref a:hover{text-decoration:underline}
393
+
394
+ .wn-tag{font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px;letter-spacing:.4px;white-space:nowrap}
395
+ .wn-tag.new{background:rgba(108,140,255,.15);color:var(--accent);border:1px solid rgba(108,140,255,.3)}
396
+ .wn-tag.fix{background:rgba(251,191,36,.12);color:var(--yellow);border:1px solid rgba(251,191,36,.25)}
397
+ .wn-tag.break{background:rgba(248,113,113,.12);color:var(--red);border:1px solid rgba(248,113,113,.25)}
398
+ </style>
399
+
211
400
  <!-- ─── OVERVIEW ──────────────────────────────────────────────────────── -->
212
401
  <section id="overview">
213
402
  <h2>Overview</h2>
@@ -842,13 +1031,38 @@ chatClient.<span class="fn">disconnect</span>();
842
1031
  <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
1032
  });
844
1033
 
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>
1034
+ <span class="cm">// ── Group icon (admin only) ───────────────────────────────────────────────────</span>
1035
+ <span class="cm">// Always upload AFTER createGroup the group must exist first.</span>
1036
+ <span class="cm">// Uses the SAME presigned URL pipeline as message attachments.</span>
1037
+ <span class="cm">// platformUploadFn is handled automatically — you never pass it explicitly.</span>
1038
+
1039
+ <span class="cm">// Headless (AntzChatClient) one call, mirrors client.uploadFiles()</span>
1040
+ <span class="kw">const</span> updated = <span class="kw">await</span> client.<span class="fn">uploadIcon</span>(group.id, {
1041
+ <span class="at">uri</span>: <span class="str">'blob:http://...'</span>, <span class="cm">// URL.createObjectURL(file) on web, file URI on RN</span>
1042
+ <span class="at">name</span>: <span class="str">'icon.jpg'</span>,
1043
+ <span class="at">type</span>: <span class="str">'image/jpeg'</span>,
1044
+ <span class="at">size</span>: file.size,
1045
+ });
1046
+ <span class="cm">// updated.iconUrl → fresh signed URL, regenerated on every API response</span>
1047
+
1048
+ <span class="cm">// What client.uploadIcon() does internally:</span>
1049
+ <span class="cm">// Step 1 — client.uploadFiles([file], conversationId)</span>
1050
+ <span class="cm">// → POST /storage/presigned-url creates temp chat_files record</span>
1051
+ <span class="cm">// → platformUploadFn() client uploads binary direct to S3/Azure/local</span>
1052
+ <span class="cm">// → POST /storage/confirm/:fileId marks chat_files active, returns fileId</span>
1053
+ <span class="cm">// Step 2 — conversationsApi.uploadIcon(conversationId, fileId)</span>
1054
+ <span class="cm">// → PUT /conversations/:id/icon { fileId }</span>
1055
+ <span class="cm">// Server: validateAdmin() → verify file → copy storageKey into iconMeta</span>
1056
+ <span class="cm">// → delete chat_files record → return conversation with fresh iconUrl</span>
1057
+
1058
+ <span class="cm">// iconUrl behaviour:</span>
1059
+ <span class="cm">// - Never stored in DB — regenerated from iconMeta.storageKey on every response</span>
1060
+ <span class="cm">// - Previous icon deleted from storage automatically when replaced</span>
1061
+ <span class="cm">// - iconMeta = { storageKey, provider, bucket, mimeType, size } embedded in conversation doc</span>
1062
+ <span class="cm">// - Non-admins get 403 Forbidden at server level</span>
1063
+
1064
+ <span class="cm">// Web/RN SDK components (GroupInfoPanel, NewChatModal, ChatHeader) handle this automatically.</span>
1065
+ <span class="cm">// Camera button shown only to admins. No code needed in your app.</span>
852
1066
 
853
1067
  <span class="cm">// Participants</span>
854
1068
  <span class="kw">const</span> members = <span class="kw">await</span> conversationsApi.<span class="fn">getMembers</span>(conversationId);
@@ -956,10 +1170,10 @@ client.socket.<span class="fn">on</span>(<span class="str">'conversation_created
956
1170
  <!-- ─── STEP 8: LOAD MESSAGES ─────────────────────────────────────────── -->
957
1171
  <section id="step-load">
958
1172
  <h2><span class="step">STEP 8</span> Load Messages</h2>
959
- <p>Cursor-paginated. Load latest on open, fetch older as user scrolls up.</p>
1173
+ <p>Cursor-paginated. Load latest on open, fetch older as user scrolls up. The <code>direction</code> param controls which side of the cursor to fetch — <code>'before'</code> for older messages (scroll up), <code>'after'</code> for newer messages (scroll down or jump-to-unread).</p>
960
1174
  <pre><code><span class="kw">import</span> { messagesApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
961
1175
 
962
- <span class="cm">// Initial load</span>
1176
+ <span class="cm">// Initial load — latest messages (no cursor = most recent first)</span>
963
1177
  <span class="kw">const</span> { data: messages, meta } = <span class="kw">await</span> messagesApi.<span class="fn">list</span>(conversationId, { <span class="at">limit</span>: <span class="num">30</span> });
964
1178
 
965
1179
  <span class="cm">// Load older (scroll up)</span>
@@ -967,6 +1181,79 @@ client.socket.<span class="fn">on</span>(<span class="str">'conversation_created
967
1181
  <span class="at">cursor</span>: meta.nextCursor, <span class="at">direction</span>: <span class="str">'before'</span>, <span class="at">limit</span>: <span class="num">30</span>,
968
1182
  });
969
1183
  <span class="cm">// meta.hasMore — false when you've reached the beginning</span></code></pre>
1184
+
1185
+ <hr class="divider" style="margin:28px 0"/>
1186
+
1187
+ <h3>Jump to first unread message</h3>
1188
+ <p>When a conversation has unread messages, you can open the chat directly at the first unread message instead of scrolling to the bottom. This gives users an immediate visual anchor — a <strong>"N unread messages"</strong> divider — right above the first message they haven't read.</p>
1189
+
1190
+ <div class="callout info">
1191
+ <strong>How it works</strong>
1192
+ <code>getLastRead()</code> returns <code>lastReadMessageId</code> — the last message the user read.
1193
+ Fetching with <code>direction: 'after'</code> and that ID as the cursor returns all messages <em>after</em> it — those are the unread ones.
1194
+ The first item in that response is the message to scroll to. <code>meta.hasMore</code> tells you if there are more than <code>limit</code> unread messages.
1195
+ </div>
1196
+
1197
+ <pre><code><span class="kw">import</span> { messagesApi, useChatStore } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
1198
+
1199
+ <span class="cm">// Step 1 — get the user's last-read pointer for this conversation</span>
1200
+ <span class="kw">const</span> { lastReadMessageId, lastReadAt } = <span class="kw">await</span> messagesApi.<span class="fn">getLastRead</span>(conversationId);
1201
+
1202
+ <span class="kw">if</span> (lastReadMessageId) {
1203
+ <span class="cm">// Seed the store so the unread divider renders correctly</span>
1204
+ useChatStore.<span class="fn">getState</span>().<span class="fn">setLastRead</span>(conversationId, lastReadMessageId, lastReadAt!);
1205
+
1206
+ <span class="cm">// Step 2 — fetch messages AFTER the last-read message (these are unread)</span>
1207
+ <span class="kw">const</span> { data: unreadMessages, meta } = <span class="kw">await</span> messagesApi.<span class="fn">list</span>(conversationId, {
1208
+ <span class="at">cursor</span>: lastReadMessageId,
1209
+ <span class="at">direction</span>: <span class="str">'after'</span>,
1210
+ <span class="at">limit</span>: <span class="num">50</span>,
1211
+ });
1212
+
1213
+ <span class="cm">// unreadMessages[0] is the first unread — scroll to it</span>
1214
+ <span class="cm">// meta.hasMore = true means there are more than 50 unread messages</span>
1215
+
1216
+ <span class="kw">if</span> (unreadMessages.length > <span class="num">0</span>) {
1217
+ <span class="fn">scrollToMessage</span>(unreadMessages[<span class="num">0</span>].id); <span class="cm">// scroll FlatList / ScrollView to this item</span>
1218
+ }
1219
+ } <span class="kw">else</span> {
1220
+ <span class="cm">// No prior read state — load latest messages as normal</span>
1221
+ <span class="kw">const</span> { data: messages } = <span class="kw">await</span> messagesApi.<span class="fn">list</span>(conversationId, { <span class="at">limit</span>: <span class="num">30</span> });
1222
+ }</code></pre>
1223
+
1224
+ <div data-p="rn web">
1225
+ <h4>Rendering the "unread" divider</h4>
1226
+ <p>Use <code>useChatStore.lastRead[conversationId]</code> to insert a divider row above the first unread message in your list:</p>
1227
+ <pre><code><span class="kw">const</span> lastRead = <span class="fn">useChatStore</span>(s => s.lastRead[conversationId]);
1228
+
1229
+ <span class="kw">function</span> <span class="fn">MessageRow</span>({ message, prevMessage }) {
1230
+ <span class="cm">// Insert divider between lastRead message and the next one</span>
1231
+ <span class="kw">const</span> isFirstUnread = lastRead && prevMessage?.id === lastRead.messageId;
1232
+
1233
+ <span class="kw">return</span> (
1234
+ &lt;&gt;
1235
+ {isFirstUnread && (
1236
+ &lt;<span class="fn">View</span> style={styles.unreadDivider}&gt;
1237
+ &lt;<span class="fn">Text</span>&gt;↑ Unread messages&lt;/<span class="fn">Text</span>&gt;
1238
+ &lt;/<span class="fn">View</span>&gt;
1239
+ )}
1240
+ &lt;<span class="fn">MessageBubble</span> message={message} /&gt;
1241
+ &lt;/&gt;
1242
+ );
1243
+ }</code></pre>
1244
+ </div>
1245
+
1246
+ <h4>Full pattern summary</h4>
1247
+ <table>
1248
+ <thead><tr><th>Step</th><th>Call</th><th>Purpose</th></tr></thead>
1249
+ <tbody>
1250
+ <tr><td>1</td><td><code>messagesApi.getLastRead(conversationId)</code></td><td>Get the last-read message ID and seed the store</td></tr>
1251
+ <tr><td>2</td><td><code>messagesApi.list(conversationId, { cursor: lastReadMessageId, direction: 'after' })</code></td><td>Fetch all unread messages after that pointer</td></tr>
1252
+ <tr><td>3</td><td>Scroll to <code>unreadMessages[0].id</code></td><td>Jump the list to the first unread message</td></tr>
1253
+ <tr><td>4</td><td>Render divider when <code>prevMessage.id === lastRead.messageId</code></td><td>Show "↑ Unread messages" label above the first unread</td></tr>
1254
+ <tr><td>5</td><td><code>socketEmit.markRead(conversationId)</code> on screen focus</td><td>Tell server the user has read the conversation</td></tr>
1255
+ </tbody>
1256
+ </table>
970
1257
  </section>
971
1258
 
972
1259
  <!-- ─── STEP 9: SEND ───────────────────────────────────────────────────── -->
@@ -1218,7 +1505,7 @@ socketEmit.<span class="fn">markRead</span>(conversationId); <span
1218
1505
  <hr class="divider" style="margin:28px 0"/>
1219
1506
 
1220
1507
  <h3>Last-read pointer — where the user left off</h3>
1221
- <p>The store keeps a <strong>last-read pointer</strong> per conversation: the ID and timestamp of the last message the current user read. This is useful for scroll-to-unread and "unread from here" badges.</p>
1508
+ <p>The store keeps a <strong>last-read pointer</strong> per conversation: the ID and timestamp of the last message the current user read. This is used for scroll-to-first-unread, "unread from here" dividers, and unread count badges. See <a href="#step-load">Step 8 — Jump to first unread</a> for the complete implementation pattern.</p>
1222
1509
 
1223
1510
  <div class="callout tip">
1224
1511
  <strong>Automatic after connect</strong>
@@ -2290,6 +2577,14 @@ document.getElementById('nav-platform-label').textContent =
2290
2577
  savedP === 'rn' ? 'React Native' : savedP === 'web' ? 'React / Next.js' : 'Node.js';
2291
2578
  applyGating();
2292
2579
 
2580
+ // ── What's New toggles ───────────────────────────────────────────────────
2581
+ function toggleVersion(id) {
2582
+ document.getElementById(id).classList.toggle('open');
2583
+ }
2584
+ function toggleItem(id) {
2585
+ document.getElementById(id).classList.toggle('open');
2586
+ }
2587
+
2293
2588
  // ── Progress bar ──────────────────────────────────────────────────────────
2294
2589
  window.addEventListener('scroll', () => {
2295
2590
  const el = document.getElementById('prog');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antzsoft/chat-core",
3
- "version": "1.0.4",
3
+ "version": "1.0.5",
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",