@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.
- package/README.md +90 -2
- package/dist/index.cjs +11 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -14
- package/dist/index.d.ts +6 -14
- package/dist/index.js +11 -13
- package/dist/index.js.map +1 -1
- package/docs/integration-guide.html +306 -11
- package/package.json +1 -1
|
@@ -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 & 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 & 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 & 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">//
|
|
846
|
-
<span class="cm">//
|
|
847
|
-
<span class="
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
<span class="
|
|
851
|
-
<span class="
|
|
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
|
+
<>
|
|
1235
|
+
{isFirstUnread && (
|
|
1236
|
+
<<span class="fn">View</span> style={styles.unreadDivider}>
|
|
1237
|
+
<<span class="fn">Text</span>>↑ Unread messages</<span class="fn">Text</span>>
|
|
1238
|
+
</<span class="fn">View</span>>
|
|
1239
|
+
)}
|
|
1240
|
+
<<span class="fn">MessageBubble</span> message={message} />
|
|
1241
|
+
</>
|
|
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
|
|
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