@antzsoft/chat-core 1.2.2 → 1.2.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 +152 -38
- package/dist/chat.store-DLNRJ5ZT.js +7 -0
- package/dist/{chunk-P7VAN6NA.js → chunk-6NMA64BX.js} +71 -11
- package/dist/chunk-6NMA64BX.js.map +1 -0
- package/dist/{chunk-GUO5QQGK.js → chunk-EOL5B7GS.js} +4 -2
- package/dist/chunk-EOL5B7GS.js.map +1 -0
- package/dist/index.cjs +207 -101
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +22 -3
- package/dist/index.d.ts +22 -3
- package/dist/index.js +137 -94
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +68 -8
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/{storage-2unXhGDN.d.cts → storage-Bp6fk9aM.d.cts} +79 -4
- package/dist/{storage-2unXhGDN.d.ts → storage-Bp6fk9aM.d.ts} +79 -4
- package/docs/integration-guide.html +504 -31
- package/package.json +1 -1
- package/dist/chat.store-JC6QYDDL.js +0 -7
- package/dist/chunk-GUO5QQGK.js.map +0 -1
- package/dist/chunk-P7VAN6NA.js.map +0 -1
- /package/dist/{chat.store-JC6QYDDL.js.map → chat.store-DLNRJ5ZT.js.map} +0 -0
|
@@ -155,7 +155,7 @@ section.sec>h2:hover{color:#fff}
|
|
|
155
155
|
|
|
156
156
|
<div class="section-label">What's New</div>
|
|
157
157
|
<ul>
|
|
158
|
-
<li><a href="#whats-new">v1.2.
|
|
158
|
+
<li><a href="#whats-new">v1.2.4 Release Notes</a></li>
|
|
159
159
|
</ul>
|
|
160
160
|
|
|
161
161
|
<div class="section-label">Getting Started</div>
|
|
@@ -192,6 +192,7 @@ section.sec>h2:hover{color:#fff}
|
|
|
192
192
|
<li><a href="#step-realtime">10. Real-time Events</a></li>
|
|
193
193
|
<li><a href="#step-typing">11. Typing Indicators</a></li>
|
|
194
194
|
<li><a href="#step-read">12. Read Receipts & Last Seen</a></li>
|
|
195
|
+
<li><a href="#step-message-info">12b. Message Info Screen</a></li>
|
|
195
196
|
<li><a href="#step-edit">13. Edit & Delete</a></li>
|
|
196
197
|
<li><a href="#step-search">14. Search Messages</a></li>
|
|
197
198
|
<li><a href="#step-reactions">15. Reactions, Pin, Star</a></li>
|
|
@@ -242,16 +243,225 @@ section.sec>h2:hover{color:#fff}
|
|
|
242
243
|
<h2>What's New</h2>
|
|
243
244
|
<p style="color:var(--muted);font-size:13px;margin-bottom:20px">Version history and release notes. Click a version to expand.</p>
|
|
244
245
|
|
|
245
|
-
<!-- ── v1.2.
|
|
246
|
-
<div class="wn-version open" id="wn-
|
|
247
|
-
<div class="wn-header" onclick="toggleVersion('wn-
|
|
246
|
+
<!-- ── v1.2.4 (current) ── -->
|
|
247
|
+
<div class="wn-version open" id="wn-124">
|
|
248
|
+
<div class="wn-header" onclick="toggleVersion('wn-124')">
|
|
248
249
|
<div class="wn-title">
|
|
249
|
-
<span class="wn-ver">v1.2.
|
|
250
|
+
<span class="wn-ver">v1.2.4</span>
|
|
250
251
|
<span class="wn-badge current">Current</span>
|
|
251
252
|
<span class="wn-date">May 2026</span>
|
|
252
253
|
</div>
|
|
253
254
|
<span class="wn-chevron">▲</span>
|
|
254
255
|
</div>
|
|
256
|
+
<div class="wn-body">
|
|
257
|
+
|
|
258
|
+
<div class="wn-item" id="wn-124-mp1">
|
|
259
|
+
<div class="wn-item-header" onclick="toggleItem('wn-124-mp1')">
|
|
260
|
+
<span class="wn-tag new">New</span>
|
|
261
|
+
<span class="wn-item-title">Chunked multipart upload for files ≥ 10 MB (S3 and local)</span>
|
|
262
|
+
<span class="wn-chevron-sm">▾</span>
|
|
263
|
+
</div>
|
|
264
|
+
<div class="wn-item-body">
|
|
265
|
+
<p>Files at or above <strong>10 MB</strong> on S3 and local storage now use chunked multipart upload instead of a single presigned POST. The SDK splits the file into 10 MB chunks, uploads up to 3 parts in parallel, then completes the upload server-side.</p>
|
|
266
|
+
<p><strong>What changes per provider:</strong></p>
|
|
267
|
+
<table>
|
|
268
|
+
<thead><tr><th>Provider</th><th>File < 10 MB</th><th>File ≥ 10 MB</th></tr></thead>
|
|
269
|
+
<tbody>
|
|
270
|
+
<tr><td>AWS S3</td><td>Presigned POST (unchanged)</td><td><strong>New:</strong> chunked multipart — N presigned PUT URLs (one per part) → <code>CompleteMultipartUpload</code></td></tr>
|
|
271
|
+
<tr><td>Local (same-server / mount)</td><td>Presigned POST (unchanged)</td><td><strong>New:</strong> chunked multipart — N signed PUT URLs (one per part) → <code>POST /storage/multipart/complete/:fileId</code> assembles parts on disk</td></tr>
|
|
272
|
+
<tr><td>Azure Blob Storage</td><td>Presigned PUT (unchanged)</td><td>Presigned PUT (unchanged)</td></tr>
|
|
273
|
+
</tbody>
|
|
274
|
+
</table>
|
|
275
|
+
<p><strong>Benefits at 50 MB+:</strong> resumes from last completed part on network failure, parallel throughput, no single-request timeout, supports files up to 5 TB.</p>
|
|
276
|
+
<p><strong>No integration changes required</strong> for Web SDK (<code>@antzsoft/chat-web-sdk</code>) and RN SDK (<code>@antzsoft/chat-rn-sdk</code>) users — <code>webUploadPartFn</code> and <code>rnUploadPartFn</code> are wired in automatically. Node.js / custom integrators must pass <code>platformUploadPartFn</code> to <code>uploadBatch</code> to enable chunked uploads.</p>
|
|
277
|
+
<p class="wn-ref">→ <a href="#step-upload">Step 16 — Upload Files</a></p>
|
|
278
|
+
</div>
|
|
279
|
+
</div>
|
|
280
|
+
|
|
281
|
+
<div class="wn-item" id="wn-124-mp2">
|
|
282
|
+
<div class="wn-item-header" onclick="toggleItem('wn-124-mp2')">
|
|
283
|
+
<span class="wn-tag new">New</span>
|
|
284
|
+
<span class="wn-item-title"><code>storageApi.completeMultipartUpload()</code> · <code>PlatformUploadPartFn</code> type · <code>CompletedPart</code> / <code>MultipartUploadInfo</code> types</span>
|
|
285
|
+
<span class="wn-chevron-sm">▾</span>
|
|
286
|
+
</div>
|
|
287
|
+
<div class="wn-item-body">
|
|
288
|
+
<p><code>storageApi.completeMultipartUpload(fileId, uploadId, parts)</code> is called automatically by <code>uploadBatch</code> — only needed in custom manual upload flows. It assembles S3 parts and transitions the file record to active in one round-trip (replaces the separate <code>confirmUpload</code> call for the multipart path).</p>
|
|
289
|
+
<p><code>PlatformUploadPartFn</code> is the new platform adapter type: <code>(uploadUrl: string, blob: Blob | ArrayBuffer, onProgress?) => Promise<string></code> — returns the ETag S3 sends back for the part. Exported from <code>@antzsoft/chat-core</code>.</p>
|
|
290
|
+
<p>New exported types: <code>MultipartUploadInfo</code>, <code>MultipartPartUrl</code>, <code>CompletedPart</code>.</p>
|
|
291
|
+
</div>
|
|
292
|
+
</div>
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
<div class="wn-item" id="wn-124-1">
|
|
296
|
+
<div class="wn-item-header" onclick="toggleItem('wn-124-1')">
|
|
297
|
+
<span class="wn-tag improvement">Improvement</span>
|
|
298
|
+
<span class="wn-item-title">S3 and local storage uploads now use presigned POST multipart — file bytes never touch the chat server</span>
|
|
299
|
+
<span class="wn-chevron-sm">▾</span>
|
|
300
|
+
</div>
|
|
301
|
+
<div class="wn-item-body">
|
|
302
|
+
<p>File uploads to S3 and the local storage provider now use <strong>presigned POST</strong> (AWS POST policy / HMAC-signed form fields) instead of presigned PUT. The client sends a <code>multipart/form-data</code> request directly to the storage endpoint — S3 or the chat server's own <code>/storage/files/upload</code> endpoint — with the signed fields embedded in the form body. No file bytes pass through the chat server.</p>
|
|
303
|
+
<p><strong>What changed per provider:</strong></p>
|
|
304
|
+
<table>
|
|
305
|
+
<thead><tr><th>Provider</th><th>Upload method</th><th>Change</th></tr></thead>
|
|
306
|
+
<tbody>
|
|
307
|
+
<tr><td>AWS S3</td><td>Presigned POST (multipart/form-data)</td><td>Changed from presigned PUT → POST. AWS signs a policy; client POSTs a FormData with all fields + file.</td></tr>
|
|
308
|
+
<tr><td>Local (same-server / mount)</td><td>Presigned POST (multipart/form-data)</td><td>HMAC-signed fields now go in the form body (not query string) — WAF-friendly.</td></tr>
|
|
309
|
+
<tr><td>Azure Blob Storage</td><td>Presigned PUT (SAS token)</td><td>Unchanged — Azure has no form-based upload API.</td></tr>
|
|
310
|
+
</tbody>
|
|
311
|
+
</table>
|
|
312
|
+
<p><strong>How the SDK handles both paths:</strong> The <code>PresignedUrlResponse</code> carries <code>method: 'POST'</code> and a <code>fields</code> object for S3/local. Your <code>platformUploadFn</code> should branch on this:</p>
|
|
313
|
+
<pre><code><span class="cm">// S3 / local → multipart FormData (method: 'POST', fields present)</span>
|
|
314
|
+
<span class="cm">// Azure → raw PUT body (method: 'PUT', no fields)</span>
|
|
315
|
+
<span class="kw">if</span> (presigned.method === <span class="str">'POST'</span> && presigned.fields) {
|
|
316
|
+
<span class="kw">const</span> fd = <span class="kw">new</span> <span class="fn">FormData</span>();
|
|
317
|
+
Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k, v]) => fd.<span class="fn">append</span>(k, v));
|
|
318
|
+
fd.<span class="fn">append</span>(<span class="str">'file'</span>, blob, file.name); <span class="cm">// file must be last</span>
|
|
319
|
+
xhr.<span class="fn">send</span>(fd);
|
|
320
|
+
} <span class="kw">else</span> {
|
|
321
|
+
xhr.<span class="fn">send</span>(blob); <span class="cm">// Azure PUT</span>
|
|
322
|
+
}</code></pre>
|
|
323
|
+
<p><strong>No integration changes required.</strong> Both the web SDK and RN SDK already implement this branching. If you are using the recommended <code>platformUploadFn</code> from Step 1 of this guide, it already handles both paths correctly. Only custom <code>platformUploadFn</code> implementations that unconditionally used <code>presigned.method</code> as PUT need updating — see the updated examples in <a href="#step1">Step 1</a>.</p>
|
|
324
|
+
<p class="wn-ref">→ <a href="#step1">Step 1 — platformUploadFn examples</a> · <a href="#step-upload">Step 16 — Upload Files</a></p>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
|
|
328
|
+
<div class="wn-item" id="wn-124-2">
|
|
329
|
+
<div class="wn-item-header" onclick="toggleItem('wn-124-2')">
|
|
330
|
+
<span class="wn-tag fix">Fix</span>
|
|
331
|
+
<span class="wn-item-title"><code>POST /storage/files/upload</code> returns <code>403</code> when transit encryption is enabled</span>
|
|
332
|
+
<span class="wn-chevron-sm">▾</span>
|
|
333
|
+
</div>
|
|
334
|
+
<div class="wn-item-body">
|
|
335
|
+
<p>The local-storage direct upload endpoint (<code>POST /storage/files/upload</code>) was missing the <code>@PreTransit()</code> decorator. With <code>transitEncryption: true</code>, the <code>TransitEnforceGuard</code> rejected every upload request with <code>403 Transit encryption required</code>.</p>
|
|
336
|
+
<p><strong>Why this endpoint is pre-transit:</strong> The client uploads directly via XHR/fetch — not through the Axios client — so no <code>x-transit-session</code> header is ever present on this request. Auth is the HMAC signature embedded in the <code>multipart/form-data</code> fields (<code>token</code>, <code>ts</code>) which the server validates independently.</p>
|
|
337
|
+
<p><strong>No integration changes required.</strong> This fix unblocks uploads on deployments with <code>transitEncryption: true</code>. No client-side changes needed.</p>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<!-- ── v1.2.3 ── -->
|
|
345
|
+
<div class="wn-version" id="wn-123">
|
|
346
|
+
<div class="wn-header" onclick="toggleVersion('wn-123')">
|
|
347
|
+
<div class="wn-title">
|
|
348
|
+
<span class="wn-ver">v1.2.3</span>
|
|
349
|
+
<span class="wn-date">May 2026</span>
|
|
350
|
+
</div>
|
|
351
|
+
<span class="wn-chevron">▼</span>
|
|
352
|
+
</div>
|
|
353
|
+
<div class="wn-body">
|
|
354
|
+
|
|
355
|
+
<div class="wn-item" id="wn-123-1">
|
|
356
|
+
<div class="wn-item-header" onclick="toggleItem('wn-123-1')">
|
|
357
|
+
<span class="wn-tag new">New</span>
|
|
358
|
+
<span class="wn-item-title"><code>messagesApi.getReceipts(messageId)</code> — per-user read & delivery receipt detail</span>
|
|
359
|
+
<span class="wn-chevron-sm">▾</span>
|
|
360
|
+
</div>
|
|
361
|
+
<div class="wn-item-body">
|
|
362
|
+
<p>New endpoint <code>GET /messages/:id/receipts</code> returns the full read and delivery receipt list for a single message, with user profiles (name, avatar) resolved server-side. Designed as the initial load for a "Read by / Delivered to" message info screen.</p>
|
|
363
|
+
<p>Response shape:</p>
|
|
364
|
+
<pre><code>{
|
|
365
|
+
messageId: string,
|
|
366
|
+
readBy: [{ userId, displayName, avatarUrl, readAt }],
|
|
367
|
+
deliveredTo: [{ userId, displayName, avatarUrl, deliveredAt }]
|
|
368
|
+
}</code></pre>
|
|
369
|
+
<p>No secondary user lookup needed — profiles are included. The "Sent to" bucket (not yet delivered) is derived client-side by subtracting <code>readBy</code> and <code>deliveredTo</code> user IDs from the conversation participant list.</p>
|
|
370
|
+
<p class="wn-ref">→ <a href="#step-message-info">Step 12b — Message Info Screen</a> for the complete pattern including live updates.</p>
|
|
371
|
+
</div>
|
|
372
|
+
</div>
|
|
373
|
+
|
|
374
|
+
<div class="wn-item" id="wn-123-2">
|
|
375
|
+
<div class="wn-item-header" onclick="toggleItem('wn-123-2')">
|
|
376
|
+
<span class="wn-tag fix">Fix</span>
|
|
377
|
+
<span class="wn-item-title"><code>message_delivered</code> now fires per recipient, not all-or-nothing</span>
|
|
378
|
+
<span class="wn-chevron-sm">▾</span>
|
|
379
|
+
</div>
|
|
380
|
+
<div class="wn-item-body">
|
|
381
|
+
<p>Previously <code>message_delivered</code> only fired to the sender when <em>all</em> recipients were online at the moment of sending — if even one recipient was offline, the sender received no delivery event at all until every recipient came online simultaneously (which often never happens in large groups).</p>
|
|
382
|
+
<p>The server now fires <code>message_delivered</code> once per online recipient at send time. The payload's <code>deliveredTo</code> field is now a single object <code>{ userId, deliveredAt }</code> (previously an array) — one event per person, matching how <code>read_receipt</code> works.</p>
|
|
383
|
+
<p>This means the delivery section of a message info screen builds up live one entry at a time, exactly as WhatsApp does.</p>
|
|
384
|
+
<p><strong>Backward compatible</strong> — existing <code>message_delivered</code> handlers in both the web and RN SDKs only read <code>event.messageId</code> to update the tick mark and are unaffected by the <code>deliveredTo</code> shape change. The event fires more frequently (good — more accurate delivery feedback) but the existing handler logic is identical.</p>
|
|
385
|
+
<p class="wn-ref">→ <a href="#step-message-info">Step 12b — Message Info Screen</a> · <a href="#step-realtime">Step 10 — Real-time Events</a></p>
|
|
386
|
+
</div>
|
|
387
|
+
</div>
|
|
388
|
+
|
|
389
|
+
<div class="wn-item" id="wn-123-3">
|
|
390
|
+
<div class="wn-item-header" onclick="toggleItem('wn-123-3')">
|
|
391
|
+
<span class="wn-tag new">New</span>
|
|
392
|
+
<span class="wn-item-title">Viewer-aware system message text — SDK resolves "You", "you", or full names automatically</span>
|
|
393
|
+
<span class="wn-chevron-sm">▾</span>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="wn-item-body">
|
|
396
|
+
<p>System messages (member added/removed, admin promoted/demoted, group created/updated) now render differently depending on who is reading them — matching WhatsApp behaviour.</p>
|
|
397
|
+
<table>
|
|
398
|
+
<thead><tr><th>Viewer</th><th>Rendered text</th></tr></thead>
|
|
399
|
+
<tbody>
|
|
400
|
+
<tr><td>Actor (Anil removed Ajay)</td><td>"You removed Ajay Antony"</td></tr>
|
|
401
|
+
<tr><td>Target (Ajay was removed)</td><td>"Anil Rathod removed you"</td></tr>
|
|
402
|
+
<tr><td>Bystander (Saket)</td><td>"Anil Rathod removed Ajay Antony"</td></tr>
|
|
403
|
+
</tbody>
|
|
404
|
+
</table>
|
|
405
|
+
<p><strong>How it works:</strong> The server stores the neutral third-person form in <code>content.text</code> (used for push notifications and exports) and sends structured <code>actorUserId</code>, <code>actorUserName</code>, <code>targetUserId</code>, <code>targetUserName</code> fields in the message <code>metadata</code>. The SDK's <code>messagesApi.list()</code> applies <code>resolveSystemMessageText()</code> in-memory before returning messages — <code>content.text</code> is already correct by the time it reaches your UI. No frontend logic changes needed.</p>
|
|
406
|
+
<p><strong>Socket path:</strong> For raw <code>new_message</code> socket events handled outside the SDK pipeline (e.g. <code>AntzChatClient</code> headless usage), call <code>resolveSystemMessageText(message, currentUserId)</code> manually:</p>
|
|
407
|
+
<pre><code>import { resolveSystemMessageText, getAuthStore } from '@antzsoft/chat-core';
|
|
408
|
+
|
|
409
|
+
client.socket.on('new_message', (event) => {
|
|
410
|
+
const message = event.message;
|
|
411
|
+
if (message.content.type === 'system') {
|
|
412
|
+
const uid = getAuthStore().useAuthStore.getState().user?.id ?? '';
|
|
413
|
+
message.content.text = resolveSystemMessageText(message, uid);
|
|
414
|
+
}
|
|
415
|
+
appendMessageToView(message);
|
|
416
|
+
});</code></pre>
|
|
417
|
+
<p><strong>Backward compatible:</strong></p>
|
|
418
|
+
<ul>
|
|
419
|
+
<li>Old SDK + new server: <code>actorUserId</code> fields ignored, raw <code>content.text</code> displayed — degrades gracefully.</li>
|
|
420
|
+
<li>New SDK + old server: <code>actorUserId</code> absent, guard fires, raw <code>content.text</code> returned unchanged.</li>
|
|
421
|
+
</ul>
|
|
422
|
+
<p>Applies to: <code>group_created</code>, <code>user_added</code>, <code>user_removed</code>, <code>user_left</code>, <code>admin_promoted</code>, <code>admin_demoted</code>, <code>group_updated</code>.</p>
|
|
423
|
+
<p><strong>Also fixed in this release:</strong></p>
|
|
424
|
+
<ul>
|
|
425
|
+
<li><code>group_updated</code> system message text now includes the actor name ("Anil changed the group name to 'X'" instead of the previous "Group name changed to 'X'").</li>
|
|
426
|
+
<li>Push notification body for <code>admin_demoted</code> corrected from "made you member" to "removed you as admin".</li>
|
|
427
|
+
<li>Removed users who are offline at the time of removal now receive a push notification ("Anil Rathod removed you from the group") since the socket event cannot reach them.</li>
|
|
428
|
+
</ul>
|
|
429
|
+
</div>
|
|
430
|
+
</div>
|
|
431
|
+
|
|
432
|
+
</div>
|
|
433
|
+
</div><!-- /.wn-version -->
|
|
434
|
+
|
|
435
|
+
<!-- ── Version history button ── -->
|
|
436
|
+
<button class="wn-history-btn" onclick="openVersionHistory()">
|
|
437
|
+
<span class="wn-history-icon">⏱</span>
|
|
438
|
+
View version history
|
|
439
|
+
<span class="wn-history-count">15 older versions</span>
|
|
440
|
+
</button>
|
|
441
|
+
|
|
442
|
+
</section>
|
|
443
|
+
|
|
444
|
+
<!-- ── Version history modal ── -->
|
|
445
|
+
<div id="version-history-modal" class="vh-overlay" onclick="if(event.target===this)closeVersionHistory()">
|
|
446
|
+
<div class="vh-drawer">
|
|
447
|
+
<div class="vh-header">
|
|
448
|
+
<div class="vh-header-left">
|
|
449
|
+
<span class="vh-title">Version History</span>
|
|
450
|
+
<span class="vh-subtitle">All previous releases</span>
|
|
451
|
+
</div>
|
|
452
|
+
<button class="vh-close" onclick="closeVersionHistory()">✕</button>
|
|
453
|
+
</div>
|
|
454
|
+
<div class="vh-body">
|
|
455
|
+
|
|
456
|
+
<!-- ── v1.2.0 ── -->
|
|
457
|
+
<div class="wn-version" id="wn-120">
|
|
458
|
+
<div class="wn-header" onclick="toggleVersion('wn-120')">
|
|
459
|
+
<div class="wn-title">
|
|
460
|
+
<span class="wn-ver">v1.2.0</span>
|
|
461
|
+
<span class="wn-date">May 2026</span>
|
|
462
|
+
</div>
|
|
463
|
+
<span class="wn-chevron">▾</span>
|
|
464
|
+
</div>
|
|
255
465
|
<div class="wn-body">
|
|
256
466
|
|
|
257
467
|
<div class="wn-item" id="wn-120-1">
|
|
@@ -280,7 +490,7 @@ section.sec>h2:hover{color:#fff}
|
|
|
280
490
|
</div>
|
|
281
491
|
<span class="wn-chevron">▾</span>
|
|
282
492
|
</div>
|
|
283
|
-
<div class="wn-body"
|
|
493
|
+
<div class="wn-body">
|
|
284
494
|
|
|
285
495
|
<div class="wn-item" id="wn-119-1">
|
|
286
496
|
<div class="wn-item-header" onclick="toggleItem('wn-119-1')">
|
|
@@ -320,7 +530,7 @@ section.sec>h2:hover{color:#fff}
|
|
|
320
530
|
</div>
|
|
321
531
|
<span class="wn-chevron">▾</span>
|
|
322
532
|
</div>
|
|
323
|
-
<div class="wn-body"
|
|
533
|
+
<div class="wn-body">
|
|
324
534
|
|
|
325
535
|
<div class="wn-item" id="wn-118-1">
|
|
326
536
|
<div class="wn-item-header" onclick="toggleItem('wn-118-1')">
|
|
@@ -373,7 +583,7 @@ section.sec>h2:hover{color:#fff}
|
|
|
373
583
|
</div>
|
|
374
584
|
<span class="wn-chevron">▾</span>
|
|
375
585
|
</div>
|
|
376
|
-
<div class="wn-body"
|
|
586
|
+
<div class="wn-body">
|
|
377
587
|
|
|
378
588
|
<div class="wn-item" id="wn-117-1">
|
|
379
589
|
<div class="wn-item-header" onclick="toggleItem('wn-117-1')">
|
|
@@ -413,7 +623,7 @@ section.sec>h2:hover{color:#fff}
|
|
|
413
623
|
</div>
|
|
414
624
|
<span class="wn-chevron">▾</span>
|
|
415
625
|
</div>
|
|
416
|
-
<div class="wn-body"
|
|
626
|
+
<div class="wn-body">
|
|
417
627
|
|
|
418
628
|
<div class="wn-item" id="wn-116-1">
|
|
419
629
|
<div class="wn-item-header" onclick="toggleItem('wn-116-1')">
|
|
@@ -1540,7 +1750,9 @@ messaging().<span class="fn">onTokenRefresh</span>(<span class="kw">async</span>
|
|
|
1540
1750
|
</div><!-- /.wn-body -->
|
|
1541
1751
|
</div><!-- /.wn-version -->
|
|
1542
1752
|
|
|
1543
|
-
</
|
|
1753
|
+
</div><!-- /.vh-body -->
|
|
1754
|
+
</div><!-- /.vh-drawer -->
|
|
1755
|
+
</div><!-- /#version-history-modal -->
|
|
1544
1756
|
|
|
1545
1757
|
<style>
|
|
1546
1758
|
/* ── What's New ── */
|
|
@@ -1575,7 +1787,29 @@ messaging().<span class="fn">onTokenRefresh</span>(<span class="kw">async</span>
|
|
|
1575
1787
|
.wn-tag{font-size:10px;font-weight:700;padding:2px 7px;border-radius:20px;letter-spacing:.4px;white-space:nowrap}
|
|
1576
1788
|
.wn-tag.new{background:rgba(108,140,255,.15);color:var(--accent);border:1px solid rgba(108,140,255,.3)}
|
|
1577
1789
|
.wn-tag.fix{background:rgba(251,191,36,.12);color:var(--yellow);border:1px solid rgba(251,191,36,.25)}
|
|
1790
|
+
.wn-tag.improvement{background:rgba(52,211,153,.1);color:var(--green);border:1px solid rgba(52,211,153,.25)}
|
|
1578
1791
|
.wn-tag.break{background:rgba(248,113,113,.12);color:var(--red);border:1px solid rgba(248,113,113,.25)}
|
|
1792
|
+
|
|
1793
|
+
/* ── Version history button ── */
|
|
1794
|
+
.wn-history-btn{display:flex;align-items:center;gap:10px;width:100%;padding:13px 18px;background:transparent;border:1px dashed var(--border);border-radius:var(--radius);cursor:pointer;color:var(--muted);font-size:13px;font-family:var(--font-sans);transition:all .15s;margin-top:4px}
|
|
1795
|
+
.wn-history-btn:hover{border-color:var(--accent);color:var(--accent);background:rgba(108,140,255,.04)}
|
|
1796
|
+
.wn-history-icon{font-size:14px}
|
|
1797
|
+
.wn-history-count{margin-left:auto;font-size:11px;background:var(--surface2);padding:2px 8px;border-radius:20px;border:1px solid var(--border)}
|
|
1798
|
+
.wn-history-btn:hover .wn-history-count{border-color:rgba(108,140,255,.4);color:var(--accent)}
|
|
1799
|
+
|
|
1800
|
+
/* ── Version history modal ── */
|
|
1801
|
+
.vh-overlay{display:none;position:fixed;inset:0;background:rgba(0,0,0,.6);backdrop-filter:blur(4px);z-index:500;align-items:flex-start;justify-content:flex-end}
|
|
1802
|
+
.vh-overlay.open{display:flex}
|
|
1803
|
+
.vh-drawer{width:min(680px,92vw);height:100vh;background:var(--surface);border-left:1px solid var(--border);display:flex;flex-direction:column;animation:slideIn .22s ease}
|
|
1804
|
+
@keyframes slideIn{from{transform:translateX(40px);opacity:0}to{transform:translateX(0);opacity:1}}
|
|
1805
|
+
.vh-header{display:flex;align-items:center;justify-content:space-between;padding:20px 24px;border-bottom:1px solid var(--border);flex-shrink:0}
|
|
1806
|
+
.vh-header-left{display:flex;flex-direction:column;gap:2px}
|
|
1807
|
+
.vh-title{font-size:16px;font-weight:800;color:#fff}
|
|
1808
|
+
.vh-subtitle{font-size:12px;color:var(--muted)}
|
|
1809
|
+
.vh-close{background:var(--surface2);border:1px solid var(--border);color:var(--muted);width:30px;height:30px;border-radius:6px;cursor:pointer;font-size:13px;display:flex;align-items:center;justify-content:center;transition:all .15s;font-family:var(--font-sans)}
|
|
1810
|
+
.vh-close:hover{color:#fff;border-color:var(--accent);background:rgba(108,140,255,.1)}
|
|
1811
|
+
.vh-body{flex:1;overflow-y:auto;padding:20px 24px 40px}
|
|
1812
|
+
.vh-body .wn-version{margin-bottom:10px}
|
|
1579
1813
|
</style>
|
|
1580
1814
|
|
|
1581
1815
|
<!-- ─── OVERVIEW ──────────────────────────────────────────────────────── -->
|
|
@@ -1724,14 +1958,16 @@ npx pod-install</code></pre>
|
|
|
1724
1958
|
|
|
1725
1959
|
<span class="kw">export const</span> <span class="at">rnUploadFn</span>: <span class="tp">PlatformUploadFn</span> = <span class="kw">async</span> (presigned, file, onProgress) => {
|
|
1726
1960
|
<span class="kw">let</span> body: <span class="tp">Blob</span> | <span class="tp">FormData</span>;
|
|
1727
|
-
<span class="kw">if</span> (presigned.method === <span class="str">'
|
|
1728
|
-
<span class="
|
|
1729
|
-
body = <span class="kw">await</span> res.<span class="fn">blob</span>();
|
|
1730
|
-
} <span class="kw">else</span> {
|
|
1961
|
+
<span class="kw">if</span> (presigned.method === <span class="str">'POST'</span> && presigned.fields) {
|
|
1962
|
+
<span class="cm">// S3 / local — multipart FormData with signed policy fields</span>
|
|
1731
1963
|
<span class="kw">const</span> form = <span class="kw">new</span> <span class="fn">FormData</span>();
|
|
1732
|
-
|
|
1964
|
+
Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k,v]) => form.<span class="fn">append</span>(k, v <span class="kw">as</span> <span class="tp">string</span>));
|
|
1733
1965
|
form.<span class="fn">append</span>(<span class="str">'file'</span>, { <span class="at">uri</span>: file.uri, <span class="at">type</span>: file.type, <span class="at">name</span>: file.name } <span class="kw">as any</span>);
|
|
1734
1966
|
body = form;
|
|
1967
|
+
} <span class="kw">else</span> {
|
|
1968
|
+
<span class="cm">// Azure — raw blob PUT</span>
|
|
1969
|
+
<span class="kw">const</span> res = <span class="kw">await</span> <span class="fn">fetch</span>(file.uri);
|
|
1970
|
+
body = <span class="kw">await</span> res.<span class="fn">blob</span>();
|
|
1735
1971
|
}
|
|
1736
1972
|
<span class="kw">await</span> <span class="fn">fetch</span>(presigned.uploadUrl, { <span class="at">method</span>: presigned.method, <span class="at">headers</span>: presigned.headers, body });
|
|
1737
1973
|
onProgress?.(<span class="num">100</span>);
|
|
@@ -1748,26 +1984,37 @@ npx pod-install</code></pre>
|
|
|
1748
1984
|
Object.<span class="fn">entries</span>(presigned.headers ?? {}).<span class="fn">forEach</span>(([k,v]) => xhr.<span class="fn">setRequestHeader</span>(k, v));
|
|
1749
1985
|
xhr.onload = () => xhr.status < <span class="num">300</span> ? <span class="fn">resolve</span>() : <span class="fn">reject</span>(<span class="kw">new</span> <span class="fn">Error</span>(`Upload failed: ${xhr.status}`));
|
|
1750
1986
|
xhr.onerror = () => <span class="fn">reject</span>(<span class="kw">new</span> <span class="fn">Error</span>(<span class="str">'Upload network error'</span>));
|
|
1751
|
-
<span class="kw">if</span> (presigned.method === <span class="str">'
|
|
1752
|
-
|
|
1753
|
-
} <span class="kw">else</span> {
|
|
1987
|
+
<span class="kw">if</span> (presigned.method === <span class="str">'POST'</span> && presigned.fields) {
|
|
1988
|
+
<span class="cm">// S3 / local — multipart FormData with signed policy fields</span>
|
|
1754
1989
|
<span class="kw">const</span> form = <span class="kw">new</span> <span class="fn">FormData</span>();
|
|
1755
|
-
|
|
1756
|
-
form.<span class="fn">append</span>(<span class="str">'file'</span>, file
|
|
1757
|
-
|
|
1990
|
+
Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k,v]) => form.<span class="fn">append</span>(k, v));
|
|
1991
|
+
<span class="fn">fetch</span>(file.uri).<span class="fn">then</span>(r => r.<span class="fn">blob</span>()).<span class="fn">then</span>(blob => { form.<span class="fn">append</span>(<span class="str">'file'</span>, blob, file.name); xhr.<span class="fn">send</span>(form); }).<span class="fn">catch</span>(reject);
|
|
1992
|
+
} <span class="kw">else</span> {
|
|
1993
|
+
<span class="cm">// Azure — raw blob PUT</span>
|
|
1994
|
+
<span class="fn">fetch</span>(file.uri).<span class="fn">then</span>(r => r.<span class="fn">blob</span>()).<span class="fn">then</span>(blob => xhr.<span class="fn">send</span>(blob)).<span class="fn">catch</span>(reject);
|
|
1758
1995
|
}
|
|
1759
1996
|
});</code></pre>
|
|
1760
1997
|
</div>
|
|
1761
1998
|
<div data-p="node">
|
|
1762
|
-
<pre><code><span class="kw">import</span> {
|
|
1999
|
+
<pre><code><span class="kw">import</span> { readFileSync } <span class="kw">from</span> <span class="str">'fs'</span>;
|
|
1763
2000
|
<span class="kw">import</span> { <span class="tp">PlatformUploadFn</span> } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
|
|
1764
2001
|
|
|
1765
2002
|
<span class="kw">export const</span> <span class="at">nodeUploadFn</span>: <span class="tp">PlatformUploadFn</span> = <span class="kw">async</span> (presigned, file, onProgress) => {
|
|
1766
|
-
<span class="kw">
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
<span class="
|
|
1770
|
-
|
|
2003
|
+
<span class="kw">let</span> body: <span class="tp">any</span>;
|
|
2004
|
+
<span class="kw">let</span> headers: <span class="tp">Record</span><<span class="tp">string</span>, <span class="tp">string</span>> = { ...presigned.headers };
|
|
2005
|
+
<span class="kw">if</span> (presigned.method === <span class="str">'POST'</span> && presigned.fields) {
|
|
2006
|
+
<span class="cm">// S3 / local — multipart FormData with signed policy fields</span>
|
|
2007
|
+
<span class="kw">const</span> { <span class="fn">FormData</span>, <span class="fn">Blob</span> } = <span class="kw">await</span> <span class="kw">import</span>(<span class="str">'node:buffer'</span>) <span class="kw">as any</span>;
|
|
2008
|
+
<span class="kw">const</span> fd = <span class="kw">new</span> <span class="fn">FormData</span>();
|
|
2009
|
+
Object.<span class="fn">entries</span>(presigned.fields).<span class="fn">forEach</span>(([k, v]) => fd.<span class="fn">append</span>(k, v <span class="kw">as</span> <span class="tp">string</span>));
|
|
2010
|
+
fd.<span class="fn">append</span>(<span class="str">'file'</span>, <span class="kw">new</span> <span class="fn">Blob</span>([<span class="fn">readFileSync</span>(file.uri.<span class="fn">replace</span>(<span class="str">'file://'</span>, <span class="str">''</span>))], { <span class="at">type</span>: file.type }), file.name);
|
|
2011
|
+
body = fd;
|
|
2012
|
+
} <span class="kw">else</span> {
|
|
2013
|
+
<span class="cm">// Azure — raw buffer PUT</span>
|
|
2014
|
+
body = <span class="fn">readFileSync</span>(file.uri.<span class="fn">replace</span>(<span class="str">'file://'</span>, <span class="str">''</span>));
|
|
2015
|
+
headers[<span class="str">'Content-Type'</span>] = file.type;
|
|
2016
|
+
}
|
|
2017
|
+
<span class="kw">const</span> res = <span class="kw">await</span> <span class="fn">fetch</span>(presigned.uploadUrl, { <span class="at">method</span>: presigned.method, headers, body });
|
|
1771
2018
|
<span class="kw">if</span> (!res.ok) <span class="kw">throw new</span> <span class="fn">Error</span>(`Upload failed: ${res.status}`);
|
|
1772
2019
|
onProgress?.(<span class="num">100</span>);
|
|
1773
2020
|
};</code></pre>
|
|
@@ -2641,12 +2888,12 @@ socket.<span class="fn">on</span>(<span class="str">'message_deleted'</span>, (e
|
|
|
2641
2888
|
</tr>
|
|
2642
2889
|
<tr>
|
|
2643
2890
|
<td><code>'message_delivered'</code></td>
|
|
2644
|
-
<td>
|
|
2645
|
-
<td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
|
|
2891
|
+
<td>Fired to the sender each time a recipient receives a message — <strong>one event per recipient</strong>. Fires at send time for online recipients, and again per recipient as offline recipients reconnect. Payload: <code>{ messageId, conversationId, deliveredTo: { userId, deliveredAt } }</code></td>
|
|
2892
|
+
<td><strong>Chat detail screen / message info screen</strong> — add on mount, remove on unmount.</td>
|
|
2646
2893
|
</tr>
|
|
2647
2894
|
<tr>
|
|
2648
2895
|
<td><code>'messages_delivered'</code></td>
|
|
2649
|
-
<td>Batch delivery catch-up when
|
|
2896
|
+
<td>Batch delivery catch-up fired to the sender when an offline recipient reconnects and all pending messages are marked delivered in bulk. Payload: <code>{ conversationId, messageIds[], deliveredTo: string, deliveredAt }</code></td>
|
|
2650
2897
|
<td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
|
|
2651
2898
|
</tr>
|
|
2652
2899
|
<tr>
|
|
@@ -2848,6 +3095,79 @@ socket?.<span class="fn">on</span>(<span class="str">'read_receipt'</span>, ({ c
|
|
|
2848
3095
|
</table>
|
|
2849
3096
|
</section>
|
|
2850
3097
|
|
|
3098
|
+
<!-- ─── STEP 12b: MESSAGE INFO SCREEN ────────────────────────────────── -->
|
|
3099
|
+
<section id="step-message-info">
|
|
3100
|
+
<h2><span class="step">STEP 12b</span> Message Info Screen (Read by / Delivered to)</h2>
|
|
3101
|
+
|
|
3102
|
+
<p>The message info screen shows all group participants split into three buckets: <strong>Read by</strong>, <strong>Delivered to</strong>, and <strong>Sent to</strong> (not yet received). The list updates live as participants read or receive the message.</p>
|
|
3103
|
+
|
|
3104
|
+
<h3>Initial load</h3>
|
|
3105
|
+
<p>Call <code>messagesApi.getReceipts(messageId)</code> on open. This returns <code>readBy[]</code> and <code>deliveredTo[]</code> with resolved user profiles — no secondary lookup needed.</p>
|
|
3106
|
+
|
|
3107
|
+
<pre><code><span class="kw">import</span> { messagesApi, conversationsApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
|
|
3108
|
+
<span class="kw">import</span> <span class="tp">{ MessageReceiptsResponse }</span> <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
|
|
3109
|
+
|
|
3110
|
+
<span class="cm">// Fetch receipts + participants in parallel</span>
|
|
3111
|
+
<span class="kw">const</span> [receipts, conversation] = <span class="kw">await</span> Promise.<span class="fn">all</span>([
|
|
3112
|
+
messagesApi.<span class="fn">getReceipts</span>(messageId),
|
|
3113
|
+
conversationsApi.<span class="fn">get</span>(conversationId),
|
|
3114
|
+
]);
|
|
3115
|
+
|
|
3116
|
+
<span class="kw">const</span> readByIds = <span class="kw">new</span> <span class="fn">Set</span>(receipts.readBy.<span class="fn">map</span>(r => r.userId));
|
|
3117
|
+
<span class="kw">const</span> deliveredIds = <span class="kw">new</span> <span class="fn">Set</span>(receipts.deliveredTo.<span class="fn">map</span>(d => d.userId));
|
|
3118
|
+
|
|
3119
|
+
<span class="kw">const</span> sentTo = conversation.participants
|
|
3120
|
+
.<span class="fn">filter</span>(p => !readByIds.<span class="fn">has</span>(p.userId) && !deliveredIds.<span class="fn">has</span>(p.userId) && p.userId !== senderId)
|
|
3121
|
+
.<span class="fn">map</span>(p => p.user);</code></pre>
|
|
3122
|
+
|
|
3123
|
+
<h3>Live updates</h3>
|
|
3124
|
+
<p>Subscribe to <code>read_receipt</code> and <code>message_delivered</code> while the screen is open. Both fire per-person so the buckets update one entry at a time.</p>
|
|
3125
|
+
|
|
3126
|
+
<pre><code><span class="kw">import</span> { tryGetSocket } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
|
|
3127
|
+
<span class="kw">import</span> <span class="tp">{ ReadReceiptEvent, MessageDeliveredEvent }</span> <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
|
|
3128
|
+
|
|
3129
|
+
<span class="kw">const</span> socket = <span class="fn">tryGetSocket</span>();
|
|
3130
|
+
|
|
3131
|
+
<span class="kw">function</span> <span class="fn">onReadReceipt</span>(evt: <span class="tp">ReadReceiptEvent</span>) {
|
|
3132
|
+
<span class="kw">if</span> (!evt.updatedMessageIds?.<span class="fn">includes</span>(messageId)) <span class="kw">return</span>;
|
|
3133
|
+
<span class="cm">// Move evt.userId from deliveredTo / sentTo → readBy</span>
|
|
3134
|
+
<span class="fn">addToReadBy</span>({ userId: evt.userId, readAt: evt.readAt });
|
|
3135
|
+
<span class="fn">removeFromDeliveredTo</span>(evt.userId);
|
|
3136
|
+
<span class="fn">removeFromSentTo</span>(evt.userId);
|
|
3137
|
+
}
|
|
3138
|
+
|
|
3139
|
+
<span class="kw">function</span> <span class="fn">onMessageDelivered</span>(evt: <span class="tp">MessageDeliveredEvent</span>) {
|
|
3140
|
+
<span class="kw">if</span> (evt.messageId !== messageId) <span class="kw">return</span>;
|
|
3141
|
+
<span class="cm">// Move evt.deliveredTo.userId from sentTo → deliveredTo</span>
|
|
3142
|
+
<span class="fn">addToDeliveredTo</span>(evt.deliveredTo);
|
|
3143
|
+
<span class="fn">removeFromSentTo</span>(evt.deliveredTo.userId);
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
socket?.<span class="fn">on</span>(<span class="str">'read_receipt'</span>, onReadReceipt);
|
|
3147
|
+
socket?.<span class="fn">on</span>(<span class="str">'message_delivered'</span>, onMessageDelivered);
|
|
3148
|
+
|
|
3149
|
+
<span class="cm">// Clean up on screen close</span>
|
|
3150
|
+
<span class="kw">return</span> () => {
|
|
3151
|
+
socket?.<span class="fn">off</span>(<span class="str">'read_receipt'</span>, onReadReceipt);
|
|
3152
|
+
socket?.<span class="fn">off</span>(<span class="str">'message_delivered'</span>, onMessageDelivered);
|
|
3153
|
+
};</code></pre>
|
|
3154
|
+
|
|
3155
|
+
<div class="callout tip">
|
|
3156
|
+
<strong>Three-bucket logic</strong> — All group participants minus the sender form the full list. <code>readBy</code> takes priority: once someone is in <code>readBy</code>, remove them from <code>deliveredTo</code> and <code>sentTo</code>. <code>deliveredTo</code> takes priority over <code>sentTo</code>. The <code>sentTo</code> bucket is always derived as the remainder.
|
|
3157
|
+
</div>
|
|
3158
|
+
|
|
3159
|
+
<h4>Summary — what is automatic vs. what you do</h4>
|
|
3160
|
+
<table>
|
|
3161
|
+
<thead><tr><th>Action</th><th>Automatic?</th><th>What you do</th></tr></thead>
|
|
3162
|
+
<tbody>
|
|
3163
|
+
<tr><td>Initial receipt list with user profiles</td><td>❌ Manual</td><td>Call <code>messagesApi.getReceipts(messageId)</code> on screen open</td></tr>
|
|
3164
|
+
<tr><td>Live read updates</td><td>❌ Manual</td><td>Listen to <code>read_receipt</code>, check <code>updatedMessageIds</code></td></tr>
|
|
3165
|
+
<tr><td>Live delivery updates</td><td>❌ Manual</td><td>Listen to <code>message_delivered</code>, check <code>messageId</code></td></tr>
|
|
3166
|
+
<tr><td>Participant list for "Sent to" bucket</td><td>❌ Manual</td><td>Fetch via <code>conversationsApi.get(conversationId)</code></td></tr>
|
|
3167
|
+
</tbody>
|
|
3168
|
+
</table>
|
|
3169
|
+
</section>
|
|
3170
|
+
|
|
2851
3171
|
<!-- ─── STEP 13: EDIT & DELETE ────────────────────────────────────────── -->
|
|
2852
3172
|
<section id="step-edit">
|
|
2853
3173
|
<h2><span class="step">STEP 13</span> Edit & Delete Messages</h2>
|
|
@@ -2935,6 +3255,8 @@ socket?.<span class="fn">on</span>(<span class="str">'read_receipt'</span>, ({ c
|
|
|
2935
3255
|
<h2><span class="step">STEP 16</span> Upload Files</h2>
|
|
2936
3256
|
<p>The SDK handles the full flow: presigned URL request → binary upload via your <code>platformUploadFn</code> → server confirmation. You get back <code>fileId</code>s to attach to messages.</p>
|
|
2937
3257
|
|
|
3258
|
+
<div class="callout tip"><strong>Direct-to-storage upload · Chunked multipart for large files (v1.2.4+)</strong> File bytes go directly from the client to the storage provider — no file data passes through the chat server. S3 and local use presigned POST (<code>multipart/form-data</code>). Azure uses presigned PUT. <strong>For files ≥ 10 MB on S3 or local</strong>, the SDK automatically uses chunked multipart upload (10 MB parts, up to 3 parallel) — Web and RN SDK users get this for free. Your <code>platformUploadFn</code> still handles small files via <code>presigned.method === 'POST' && presigned.fields</code>; the new <code>platformUploadPartFn</code> handles individual parts — see <a href="#step1">Step 1</a>.</div>
|
|
3259
|
+
|
|
2938
3260
|
<div data-p="rn">
|
|
2939
3261
|
<pre><code><span class="kw">import</span> * <span class="kw">as</span> ImagePicker <span class="kw">from</span> <span class="str">'expo-image-picker'</span>;
|
|
2940
3262
|
|
|
@@ -3432,6 +3754,133 @@ self.<span class="fn">addEventListener</span>(<span class="str">'notificationcli
|
|
|
3432
3754
|
simultaneously when a notification is triggered. Removing a token only deactivates that specific
|
|
3433
3755
|
device; other devices continue to receive notifications.
|
|
3434
3756
|
</p>
|
|
3757
|
+
|
|
3758
|
+
<!-- ── Notification payload + channel/category setup (RN only) ──────── -->
|
|
3759
|
+
<div data-p="rn">
|
|
3760
|
+
<h3>Handling incoming notifications</h3>
|
|
3761
|
+
|
|
3762
|
+
<h4>Notification payload</h4>
|
|
3763
|
+
<p>
|
|
3764
|
+
Every push notification the server sends includes <code>channelId</code> and <code>categoryId</code>
|
|
3765
|
+
at the top level (read by the OS / Expo SDK) and mirrored inside the <code>data</code> object
|
|
3766
|
+
(readable by your JS notification handler):
|
|
3767
|
+
</p>
|
|
3768
|
+
<pre><code>{
|
|
3769
|
+
<span class="str">"title"</span>: <span class="str">"Alice replied to you"</span>,
|
|
3770
|
+
<span class="str">"body"</span>: <span class="str">"Sounds good!"</span>,
|
|
3771
|
+
<span class="str">"priority"</span>: <span class="str">"high"</span>,
|
|
3772
|
+
<span class="str">"channelId"</span>: <span class="str">"reply"</span>,
|
|
3773
|
+
<span class="str">"categoryId"</span>: <span class="str">"reply"</span>,
|
|
3774
|
+
<span class="str">"data"</span>: {
|
|
3775
|
+
<span class="str">"event"</span>: <span class="str">"reply"</span>,
|
|
3776
|
+
<span class="str">"channelId"</span>: <span class="str">"reply"</span>,
|
|
3777
|
+
<span class="str">"categoryId"</span>: <span class="str">"reply"</span>,
|
|
3778
|
+
<span class="str">"conversation_id"</span>: <span class="str">"..."</span>,
|
|
3779
|
+
<span class="str">"message_id"</span>: <span class="str">"..."</span>,
|
|
3780
|
+
<span class="str">"original_message_id"</span>: <span class="str">"..."</span>,
|
|
3781
|
+
<span class="str">"sender_id"</span>: <span class="str">"..."</span>,
|
|
3782
|
+
<span class="str">"sender_name"</span>: <span class="str">"Alice"</span>
|
|
3783
|
+
}
|
|
3784
|
+
}</code></pre>
|
|
3785
|
+
|
|
3786
|
+
<h4>Per-event channelId and categoryId</h4>
|
|
3787
|
+
<table style="width:100%;border-collapse:collapse;font-size:13px;margin:12px 0">
|
|
3788
|
+
<thead><tr style="border-bottom:1px solid var(--border)">
|
|
3789
|
+
<th style="text-align:left;padding:6px 10px;color:var(--muted)">Event</th>
|
|
3790
|
+
<th style="text-align:left;padding:6px 10px;color:var(--muted)">channelId</th>
|
|
3791
|
+
<th style="text-align:left;padding:6px 10px;color:var(--muted)">categoryId</th>
|
|
3792
|
+
<th style="text-align:left;padding:6px 10px;color:var(--muted)">Notes</th>
|
|
3793
|
+
</tr></thead>
|
|
3794
|
+
<tbody>
|
|
3795
|
+
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>dm_message</code></td><td style="padding:6px 10px"><code>messages</code></td><td style="padding:6px 10px"><code>message</code></td><td style="padding:6px 10px">Direct message</td></tr>
|
|
3796
|
+
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>group_message</code></td><td style="padding:6px 10px"><code>messages</code></td><td style="padding:6px 10px"><code>message</code></td><td style="padding:6px 10px">Group message</td></tr>
|
|
3797
|
+
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>mention</code></td><td style="padding:6px 10px"><code>messages</code></td><td style="padding:6px 10px"><code>message</code></td><td style="padding:6px 10px">Mentioned in a group</td></tr>
|
|
3798
|
+
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>reply</code></td><td style="padding:6px 10px"><code>reply</code></td><td style="padding:6px 10px"><code>reply</code></td><td style="padding:6px 10px">Reply to your message</td></tr>
|
|
3799
|
+
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>dm_reaction</code> / <code>group_reaction</code></td><td style="padding:6px 10px"><code>reactions</code></td><td style="padding:6px 10px"><code>reaction</code></td><td style="padding:6px 10px">Emoji reaction</td></tr>
|
|
3800
|
+
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>group_invite</code></td><td style="padding:6px 10px"><code>group_invites</code></td><td style="padding:6px 10px"><code>invite</code></td><td style="padding:6px 10px">Added to a group</td></tr>
|
|
3801
|
+
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>group_removed</code></td><td style="padding:6px 10px"><code>group_updates</code></td><td style="padding:6px 10px">—</td><td style="padding:6px 10px">Removed from a group</td></tr>
|
|
3802
|
+
<tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>group_role_changed</code></td><td style="padding:6px 10px"><code>group_updates</code></td><td style="padding:6px 10px">—</td><td style="padding:6px 10px">Role changed in a group</td></tr>
|
|
3803
|
+
<tr><td style="padding:6px 10px"><code>message_edited</code> / <code>message_deleted</code></td><td style="padding:6px 10px"><code>silent</code></td><td style="padding:6px 10px">—</td><td style="padding:6px 10px">Silent background sync, no sound</td></tr>
|
|
3804
|
+
</tbody>
|
|
3805
|
+
</table>
|
|
3806
|
+
<div class="callout tip">
|
|
3807
|
+
<strong>channelId</strong> is Android-only (Android 8+) — controls sound, importance, and DND behaviour per channel.<br>
|
|
3808
|
+
<strong>categoryId</strong> is iOS-only — links to interactive action buttons registered with <code>setNotificationCategoryAsync</code>.<br>
|
|
3809
|
+
If the channel/category is not registered yet on the client, the OS falls back to default behaviour — notifications still arrive normally.
|
|
3810
|
+
</div>
|
|
3811
|
+
|
|
3812
|
+
<h4>Android — register channels at app startup</h4>
|
|
3813
|
+
<p>Create all channels unconditionally on every app launch, before the first notification arrives:</p>
|
|
3814
|
+
<pre><code><span class="kw">import</span> * <span class="kw">as</span> Notifications <span class="kw">from</span> <span class="str">'expo-notifications'</span>;
|
|
3815
|
+
|
|
3816
|
+
<span class="kw">async function</span> <span class="fn">setupNotificationChannels</span>() {
|
|
3817
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'messages'</span>, {
|
|
3818
|
+
name: <span class="str">'Messages'</span>,
|
|
3819
|
+
importance: Notifications.AndroidImportance.HIGH,
|
|
3820
|
+
sound: <span class="str">'default'</span>,
|
|
3821
|
+
});
|
|
3822
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'reply'</span>, {
|
|
3823
|
+
name: <span class="str">'Replies'</span>,
|
|
3824
|
+
importance: Notifications.AndroidImportance.HIGH,
|
|
3825
|
+
sound: <span class="str">'default'</span>,
|
|
3826
|
+
});
|
|
3827
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'reactions'</span>, {
|
|
3828
|
+
name: <span class="str">'Reactions'</span>,
|
|
3829
|
+
importance: Notifications.AndroidImportance.DEFAULT,
|
|
3830
|
+
});
|
|
3831
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'group_invites'</span>, {
|
|
3832
|
+
name: <span class="str">'Group Invites'</span>,
|
|
3833
|
+
importance: Notifications.AndroidImportance.HIGH,
|
|
3834
|
+
sound: <span class="str">'default'</span>,
|
|
3835
|
+
});
|
|
3836
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'group_updates'</span>, {
|
|
3837
|
+
name: <span class="str">'Group Updates'</span>,
|
|
3838
|
+
importance: Notifications.AndroidImportance.LOW,
|
|
3839
|
+
});
|
|
3840
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'silent'</span>, {
|
|
3841
|
+
name: <span class="str">'Background Sync'</span>,
|
|
3842
|
+
importance: Notifications.AndroidImportance.MIN,
|
|
3843
|
+
sound: <span class="kw">undefined</span>,
|
|
3844
|
+
enableVibrate: <span class="kw">false</span>,
|
|
3845
|
+
});
|
|
3846
|
+
}</code></pre>
|
|
3847
|
+
|
|
3848
|
+
<h4>iOS — register categories with interactive actions</h4>
|
|
3849
|
+
<p>Register categories before requesting push permission. The <code>reply</code> category adds a Reply button when the app is killed:</p>
|
|
3850
|
+
<pre><code><span class="kw">import</span> * <span class="kw">as</span> Notifications <span class="kw">from</span> <span class="str">'expo-notifications'</span>;
|
|
3851
|
+
|
|
3852
|
+
<span class="kw">async function</span> <span class="fn">setupNotificationCategories</span>() {
|
|
3853
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationCategoryAsync</span>(<span class="str">'reply'</span>, [
|
|
3854
|
+
{
|
|
3855
|
+
identifier: <span class="str">'REPLY_ACTION'</span>,
|
|
3856
|
+
buttonTitle: <span class="str">'Reply'</span>,
|
|
3857
|
+
textInput: { submitButtonTitle: <span class="str">'Send'</span>, placeholder: <span class="str">'Type a reply...'</span> },
|
|
3858
|
+
},
|
|
3859
|
+
]);
|
|
3860
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationCategoryAsync</span>(<span class="str">'message'</span>, [
|
|
3861
|
+
{
|
|
3862
|
+
identifier: <span class="str">'REPLY_ACTION'</span>,
|
|
3863
|
+
buttonTitle: <span class="str">'Reply'</span>,
|
|
3864
|
+
textInput: { submitButtonTitle: <span class="str">'Send'</span>, placeholder: <span class="str">'Type a reply...'</span> },
|
|
3865
|
+
},
|
|
3866
|
+
]);
|
|
3867
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationCategoryAsync</span>(<span class="str">'reaction'</span>, []);
|
|
3868
|
+
<span class="kw">await</span> Notifications.<span class="fn">setNotificationCategoryAsync</span>(<span class="str">'invite'</span>, []);
|
|
3869
|
+
}</code></pre>
|
|
3870
|
+
|
|
3871
|
+
<h4>Reading data in your notification handler</h4>
|
|
3872
|
+
<p>Use <code>data.conversation_id</code> for deep-linking and <code>data.event</code> to distinguish notification types:</p>
|
|
3873
|
+
<pre><code><span class="kw">import</span> * <span class="kw">as</span> Notifications <span class="kw">from</span> <span class="str">'expo-notifications'</span>;
|
|
3874
|
+
|
|
3875
|
+
Notifications.<span class="fn">addNotificationResponseReceivedListener</span>((response) => {
|
|
3876
|
+
<span class="kw">const</span> { event, conversation_id, channelId } =
|
|
3877
|
+
response.notification.request.content.data;
|
|
3878
|
+
|
|
3879
|
+
<span class="kw">if</span> (conversation_id) {
|
|
3880
|
+
navigation.<span class="fn">navigate</span>(<span class="str">'Chat'</span>, { conversationId: conversation_id });
|
|
3881
|
+
}
|
|
3882
|
+
});</code></pre>
|
|
3883
|
+
</div>
|
|
3435
3884
|
</section>
|
|
3436
3885
|
|
|
3437
3886
|
<!-- ─── STEP 17.5: NOTIFICATION PREFERENCES ──────────────────────────── -->
|
|
@@ -3978,12 +4427,36 @@ applyGating();
|
|
|
3978
4427
|
|
|
3979
4428
|
// ── What's New toggles ───────────────────────────────────────────────────
|
|
3980
4429
|
function toggleVersion(id) {
|
|
3981
|
-
document.getElementById(id)
|
|
4430
|
+
const el = document.getElementById(id);
|
|
4431
|
+
const isInsideModal = !!el.closest('#version-history-modal');
|
|
4432
|
+
if (isInsideModal) {
|
|
4433
|
+
// Accordion: close all siblings first, then open the clicked one
|
|
4434
|
+
const alreadyOpen = el.classList.contains('open');
|
|
4435
|
+
document.querySelectorAll('#version-history-modal .wn-version.open').forEach(v => v.classList.remove('open'));
|
|
4436
|
+
if (!alreadyOpen) el.classList.add('open');
|
|
4437
|
+
} else {
|
|
4438
|
+
el.classList.toggle('open');
|
|
4439
|
+
}
|
|
3982
4440
|
}
|
|
3983
4441
|
function toggleItem(id) {
|
|
3984
4442
|
document.getElementById(id).classList.toggle('open');
|
|
3985
4443
|
}
|
|
3986
4444
|
|
|
4445
|
+
// ── Version history modal ─────────────────────────────────────────────────
|
|
4446
|
+
function openVersionHistory() {
|
|
4447
|
+
const modal = document.getElementById('version-history-modal');
|
|
4448
|
+
modal.classList.add('open');
|
|
4449
|
+
document.body.style.overflow = 'hidden';
|
|
4450
|
+
}
|
|
4451
|
+
function closeVersionHistory() {
|
|
4452
|
+
const modal = document.getElementById('version-history-modal');
|
|
4453
|
+
modal.classList.remove('open');
|
|
4454
|
+
document.body.style.overflow = '';
|
|
4455
|
+
}
|
|
4456
|
+
document.addEventListener('keydown', (e) => {
|
|
4457
|
+
if (e.key === 'Escape') closeVersionHistory();
|
|
4458
|
+
});
|
|
4459
|
+
|
|
3987
4460
|
// ── Progress bar + go-to-top FAB ─────────────────────────────────────────
|
|
3988
4461
|
const _fab = document.getElementById('gototop');
|
|
3989
4462
|
window.addEventListener('scroll', () => {
|