@antzsoft/chat-core 1.0.6 → 1.0.8
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 +200 -3
- package/dist/index.cjs +98 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +63 -6
- package/dist/index.d.ts +63 -6
- package/dist/index.js +97 -7
- package/dist/index.js.map +1 -1
- package/docs/integration-guide.html +456 -9
- package/package.json +1 -1
|
@@ -118,6 +118,29 @@ li{margin-bottom:4px;color:#c8d0e0}
|
|
|
118
118
|
.badge.purple{color:var(--accent2);border-color:rgba(167,139,250,.4);background:rgba(167,139,250,.1)}
|
|
119
119
|
.badge.green{color:var(--green);border-color:rgba(52,211,153,.4);background:rgba(52,211,153,.1)}
|
|
120
120
|
.badge.orange{color:var(--yellow);border-color:rgba(251,191,36,.4);background:rgba(251,191,36,.1)}
|
|
121
|
+
|
|
122
|
+
/* ── Section accordion ── */
|
|
123
|
+
section.sec>h2{cursor:pointer;user-select:none;display:flex;align-items:center;justify-content:space-between;gap:10px}
|
|
124
|
+
section.sec>h2 .sec-chevron{font-size:11px;color:var(--muted);transition:transform .22s;margin-left:auto;flex-shrink:0}
|
|
125
|
+
section.sec.collapsed>h2 .sec-chevron{transform:rotate(180deg)}
|
|
126
|
+
section.sec .sec-body{display:block}
|
|
127
|
+
section.sec.collapsed .sec-body{display:none}
|
|
128
|
+
section.sec>h2:hover{color:#fff}
|
|
129
|
+
|
|
130
|
+
/* ── H3 sub-accordion inside sections ── */
|
|
131
|
+
.sub-acc{border:1px solid var(--border);border-radius:var(--radius);margin:18px 0}
|
|
132
|
+
.sub-acc-hdr{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;cursor:pointer;user-select:none;background:var(--surface)}
|
|
133
|
+
.sub-acc-hdr:hover{background:var(--surface2)}
|
|
134
|
+
.sub-acc-hdr h3{margin:0;font-size:14px;font-weight:700;color:var(--accent2)}
|
|
135
|
+
.sub-acc-hdr .sub-chev{font-size:10px;color:var(--muted);transition:transform .2s}
|
|
136
|
+
.sub-acc.closed .sub-chev{transform:rotate(180deg)}
|
|
137
|
+
.sub-acc-body{padding:4px 16px 14px}
|
|
138
|
+
.sub-acc.closed .sub-acc-body{display:none}
|
|
139
|
+
|
|
140
|
+
/* ── Go-to-top FAB ── */
|
|
141
|
+
#gototop{position:fixed;bottom:28px;right:28px;width:42px;height:42px;border-radius:50%;background:var(--accent);color:#fff;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;font-size:18px;box-shadow:0 4px 18px rgba(108,140,255,.45);opacity:0;transform:translateY(12px);transition:opacity .22s,transform .22s;pointer-events:none;z-index:300}
|
|
142
|
+
#gototop.show{opacity:1;transform:translateY(0);pointer-events:auto}
|
|
143
|
+
#gototop:hover{background:var(--accent2);box-shadow:0 6px 24px rgba(167,139,250,.5)}
|
|
121
144
|
</style>
|
|
122
145
|
</head>
|
|
123
146
|
<body>
|
|
@@ -132,7 +155,7 @@ li{margin-bottom:4px;color:#c8d0e0}
|
|
|
132
155
|
|
|
133
156
|
<div class="section-label">What's New</div>
|
|
134
157
|
<ul>
|
|
135
|
-
<li><a href="#whats-new">v1.0.
|
|
158
|
+
<li><a href="#whats-new">v1.0.8 Release Notes</a></li>
|
|
136
159
|
</ul>
|
|
137
160
|
|
|
138
161
|
<div class="section-label">Getting Started</div>
|
|
@@ -177,6 +200,7 @@ li{margin-bottom:4px;color:#c8d0e0}
|
|
|
177
200
|
<div class="section-label">Files & Advanced</div>
|
|
178
201
|
<ul>
|
|
179
202
|
<li><a href="#step-upload">16. Upload Files</a></li>
|
|
203
|
+
<li><a href="#step-compress">16.5 File Compression</a></li>
|
|
180
204
|
<li data-nav="rn-web"><a href="#step-push">17. Push Notifications</a></li>
|
|
181
205
|
<li><a href="#step-prefs">17.5 Notification Preferences</a></li>
|
|
182
206
|
<li><a href="#step-example">18. Full Example</a></li>
|
|
@@ -218,12 +242,197 @@ li{margin-bottom:4px;color:#c8d0e0}
|
|
|
218
242
|
<h2>What's New</h2>
|
|
219
243
|
<p style="color:var(--muted);font-size:13px;margin-bottom:20px">Version history and release notes. Click a version to expand.</p>
|
|
220
244
|
|
|
221
|
-
<!-- ── v1.0.
|
|
245
|
+
<!-- ── v1.0.8 (current) ── -->
|
|
246
|
+
<div class="wn-version open" id="wn-108">
|
|
247
|
+
<div class="wn-header" onclick="toggleVersion('wn-108')">
|
|
248
|
+
<div class="wn-title">
|
|
249
|
+
<span class="wn-ver">v1.0.8</span>
|
|
250
|
+
<span class="wn-badge current">Current</span>
|
|
251
|
+
<span class="wn-date">May 2026</span>
|
|
252
|
+
</div>
|
|
253
|
+
<span class="wn-chevron">▲</span>
|
|
254
|
+
</div>
|
|
255
|
+
<div class="wn-body">
|
|
256
|
+
|
|
257
|
+
<div class="wn-item" id="wn-108-1">
|
|
258
|
+
<div class="wn-item-header" onclick="toggleItem('wn-108-1')">
|
|
259
|
+
<span class="wn-tag new">New</span>
|
|
260
|
+
<span class="wn-item-title">File compression — client-side, before upload</span>
|
|
261
|
+
<span class="wn-chevron-sm">▾</span>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="wn-item-body">
|
|
264
|
+
<p>Files are now compressed on the client before the presigned URL is requested. Images are resized and re-encoded (WebP on web, JPEG on RN). Text-based documents (JSON, CSV, XML, YAML, plain text, SVG) are gzip-compressed on web. Audio, video, PDF, ZIP, and Office formats are skipped — they are already compressed.</p>
|
|
265
|
+
<p>If the compressed result is larger than the original, the original is used automatically. Compression metadata (<code>compressed</code>, <code>originalSize</code>, <code>compressionAlgorithm</code>) is stored on the file record.</p>
|
|
266
|
+
<table>
|
|
267
|
+
<tr><th>Platform</th><th>Images</th><th>Text docs</th><th>Default state</th></tr>
|
|
268
|
+
<tr><td>Web</td><td>WebP via <code>canvas</code></td><td>gzip via <code>CompressionStream</code></td><td>On (no deps)</td></tr>
|
|
269
|
+
<tr><td>React Native</td><td>JPEG via <code>expo-image-manipulator</code></td><td>Skip</td><td>On when peer dep installed</td></tr>
|
|
270
|
+
<tr><td>Node.js</td><td>JPEG via <code>sharp</code> (optional)</td><td>gzip via <code>zlib</code></td><td>Off — supply <code>platformCompressFn</code> to enable</td></tr>
|
|
271
|
+
</table>
|
|
272
|
+
<p><strong>Action required:</strong> None for web or RN — compression works automatically. Node.js users who want compression must supply a <code>platformCompressFn</code> — see <a href="#step-compress">Step 16.5</a>. To disable: pass <code>compression: { enabled: false }</code>.</p>
|
|
273
|
+
<p class="wn-ref">→ <a href="#step-compress">Step 16.5 — File Compression</a></p>
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
|
|
277
|
+
<div class="wn-item" id="wn-108-2">
|
|
278
|
+
<div class="wn-item-header" onclick="toggleItem('wn-108-2')">
|
|
279
|
+
<span class="wn-tag fix">Fix</span>
|
|
280
|
+
<span class="wn-item-title">Removed members keep read-only access to conversation</span>
|
|
281
|
+
<span class="wn-chevron-sm">▾</span>
|
|
282
|
+
</div>
|
|
283
|
+
<div class="wn-item-body">
|
|
284
|
+
<p>Previously, removing a member from a conversation caused it to disappear from their conversation list entirely. They could no longer read history or see who was in the conversation.</p>
|
|
285
|
+
<p>Now, removed members:</p>
|
|
286
|
+
<ul>
|
|
287
|
+
<li>Still see the conversation in their list</li>
|
|
288
|
+
<li>Can read message history up to the point of removal</li>
|
|
289
|
+
<li>Are kicked from the socket room immediately — they receive no new real-time events</li>
|
|
290
|
+
<li>Cannot send, react, type, edit, or delete — <code>validateWriteAccess</code> blocks all write operations</li>
|
|
291
|
+
</ul>
|
|
292
|
+
<p>This is implemented via a new <code>isHidden</code> field on the <code>Participant</code> subdocument (schema change, server only). The conversation list filter now shows conversations where the user is not hidden, regardless of <code>isActive</code>.</p>
|
|
293
|
+
<p><strong>Action required:</strong> None. Behavior change is entirely server-side. SDK types and API calls are unchanged.</p>
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
<div class="wn-item" id="wn-108-3">
|
|
298
|
+
<div class="wn-item-header" onclick="toggleItem('wn-108-3')">
|
|
299
|
+
<span class="wn-tag fix">Fix</span>
|
|
300
|
+
<span class="wn-item-title">Admin delete is now hide-only — other participants unaffected</span>
|
|
301
|
+
<span class="wn-chevron-sm">▾</span>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="wn-item-body">
|
|
304
|
+
<p>Previously, when an admin called <code>deleteConversation</code> it set <code>conversation.isActive = false</code>, making it disappear for <em>all</em> participants. This was wrong — only the admin who deleted it should lose access.</p>
|
|
305
|
+
<p>Now, <code>deleteConversation</code> sets <code>participant.isHidden = true</code> for the requesting user only. All other participants continue to see the conversation normally.</p>
|
|
306
|
+
<p><strong>Action required:</strong> None. Server-side change only.</p>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
<div class="wn-item" id="wn-108-4">
|
|
311
|
+
<div class="wn-item-header" onclick="toggleItem('wn-108-4')">
|
|
312
|
+
<span class="wn-tag fix">Fix</span>
|
|
313
|
+
<span class="wn-item-title"><code>lastMessage.status</code> stuck as <code>deleted</code> after new messages</span>
|
|
314
|
+
<span class="wn-chevron-sm">▾</span>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="wn-item-body">
|
|
317
|
+
<p>When a message was deleted, <code>lastMessage.status</code> on the conversation was set to <code>'deleted'</code>. Sending a new message afterwards never reset it — so the conversation list kept showing <code>deleted</code> as the last message state indefinitely.</p>
|
|
318
|
+
<p>Root cause: two separate code paths update <code>lastMessage</code> (REST and WebSocket) and neither was explicitly setting <code>status: 'active'</code> when writing a new message. Fixed in both paths.</p>
|
|
319
|
+
<p><strong>Action required:</strong> None. Server-side fix only.</p>
|
|
320
|
+
</div>
|
|
321
|
+
</div>
|
|
322
|
+
|
|
323
|
+
<div class="wn-item" id="wn-108-5">
|
|
324
|
+
<div class="wn-item-header" onclick="toggleItem('wn-108-5')">
|
|
325
|
+
<span class="wn-tag new">New</span>
|
|
326
|
+
<span class="wn-item-title"><code>duration</code> field in <code>SendMessageAttachment</code> for audio/video</span>
|
|
327
|
+
<span class="wn-chevron-sm">▾</span>
|
|
328
|
+
</div>
|
|
329
|
+
<div class="wn-item-body">
|
|
330
|
+
<p><code>SendMessageAttachment</code> now accepts an optional <code>duration</code> field (integer, seconds). Pass it when sending audio or video so receivers can render a player UI — e.g. show <code>0:30</code> before the file is loaded. The server stores and echoes this value in <code>new_message</code> and all message list responses.</p>
|
|
331
|
+
<pre><code><span class="kw">await</span> socketEmit.<span class="fn">sendMessage</span>({
|
|
332
|
+
conversationId, <span class="at">tempId</span>: <span class="fn">uuid</span>(),
|
|
333
|
+
<span class="at">attachments</span>: [{
|
|
334
|
+
<span class="at">fileId</span>: f.id, <span class="at">type</span>: <span class="str">'audio'</span>, <span class="at">url</span>: f.url,
|
|
335
|
+
<span class="at">filename</span>: f.filename, <span class="at">mimeType</span>: f.mimeType, <span class="at">size</span>: f.size,
|
|
336
|
+
<span class="at">duration</span>: recordingSeconds, <span class="cm">// ← pass this</span>
|
|
337
|
+
}],
|
|
338
|
+
});</code></pre>
|
|
339
|
+
<p><strong>Action required (React Native):</strong> Always pass <code>duration</code> when sending audio or video. Omitting it on RN causes receivers to get <code>undefined</code> duration, which crashes native audio player libraries that require an explicit value to initialize. Web is unaffected — browsers read duration natively from the audio stream.</p>
|
|
340
|
+
<p><strong>Action required (Web):</strong> None — recommended to pass for better UX (shows duration before file loads) but not required.</p>
|
|
341
|
+
<p class="wn-ref">→ <a href="#step-send">Step 9 — Sending Messages</a></p>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
344
|
+
|
|
345
|
+
</div>
|
|
346
|
+
</div>
|
|
347
|
+
|
|
348
|
+
<!-- ── v1.0.7 ── -->
|
|
349
|
+
<div class="wn-version open" id="wn-107">
|
|
350
|
+
<div class="wn-header" onclick="toggleVersion('wn-107')">
|
|
351
|
+
<div class="wn-title">
|
|
352
|
+
<span class="wn-ver">v1.0.7</span>
|
|
353
|
+
<span class="wn-date">May 2026</span>
|
|
354
|
+
</div>
|
|
355
|
+
<span class="wn-chevron">▲</span>
|
|
356
|
+
</div>
|
|
357
|
+
<div class="wn-body">
|
|
358
|
+
|
|
359
|
+
<div class="wn-item" id="wn-107-1">
|
|
360
|
+
<div class="wn-item-header" onclick="toggleItem('wn-107-1')">
|
|
361
|
+
<span class="wn-tag new">New</span>
|
|
362
|
+
<span class="wn-item-title"><code>deliveredTo</code> per-user delivery timestamps on all messages</span>
|
|
363
|
+
<span class="wn-chevron-sm">▾</span>
|
|
364
|
+
</div>
|
|
365
|
+
<div class="wn-item-body">
|
|
366
|
+
<p>All message responses (REST list, single fetch, send, edit, pin, star, search) now include a <code>deliveredTo</code> array alongside <code>readBy</code>, giving you per-user delivery timestamps for "message info" panels.</p>
|
|
367
|
+
<pre><code><span class="cm">// Message object — new fields</span>
|
|
368
|
+
{
|
|
369
|
+
<span class="at">readBy</span>: [{ <span class="at">userId</span>: <span class="str">'user_2'</span>, <span class="at">readAt</span>: <span class="str">'2026-05-18T10:00:00.000Z'</span> }],
|
|
370
|
+
<span class="at">deliveredTo</span>: [{ <span class="at">userId</span>: <span class="str">'user_2'</span>, <span class="at">deliveredAt</span>: <span class="str">'2026-05-18T09:59:00.000Z'</span> }],
|
|
371
|
+
}</code></pre>
|
|
372
|
+
<p>The <code>message_delivered</code> socket event payload has also changed — <code>deliveredTo</code> is now <code>Array<{ userId, deliveredAt }></code> (top-level <code>deliveredAt</code> removed).</p>
|
|
373
|
+
<p><strong>Breaking change:</strong> <code>Message.readBy</code> type changed from <code>string[]</code> to <code>Array<{ userId: string; readAt: string }></code>. Update any code reading <code>readBy</code> entries as plain strings.</p>
|
|
374
|
+
<p class="wn-ref">→ <a href="#step-realtime">Step 10 — Real-time Events</a> (<code>message_delivered</code> event)</p>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
|
|
378
|
+
<div class="wn-item" id="wn-107-2">
|
|
379
|
+
<div class="wn-item-header" onclick="toggleItem('wn-107-2')">
|
|
380
|
+
<span class="wn-tag new">New</span>
|
|
381
|
+
<span class="wn-item-title">Audio attachment support for <code>.m4a</code> / <code>audio/x-m4a</code></span>
|
|
382
|
+
<span class="wn-chevron-sm">▾</span>
|
|
383
|
+
</div>
|
|
384
|
+
<div class="wn-item-body">
|
|
385
|
+
<p>The server now accepts <code>audio/m4a</code> and <code>audio/x-m4a</code> MIME types for audio attachments. Previously only <code>audio/mpeg</code>, <code>audio/ogg</code>, and <code>audio/wav</code> were accepted — uploads of <code>.m4a</code> files returned a <code>400</code> unsupported type error.</p>
|
|
386
|
+
<p><strong>Action required:</strong> No SDK change needed. If you were filtering <code>.m4a</code> files out in the client before upload, remove that restriction.</p>
|
|
387
|
+
</div>
|
|
388
|
+
</div>
|
|
389
|
+
|
|
390
|
+
<div class="wn-item" id="wn-107-3">
|
|
391
|
+
<div class="wn-item-header" onclick="toggleItem('wn-107-3')">
|
|
392
|
+
<span class="wn-tag new">New</span>
|
|
393
|
+
<span class="wn-item-title">Participant filter on <code>getMembers</code></span>
|
|
394
|
+
<span class="wn-chevron-sm">▾</span>
|
|
395
|
+
</div>
|
|
396
|
+
<div class="wn-item-body">
|
|
397
|
+
<p><code>getMembers</code> now accepts a <code>filter</code> parameter to control which participants are returned.</p>
|
|
398
|
+
<pre><code><span class="cm">// active only (default — same as before)</span>
|
|
399
|
+
<span class="kw">await</span> conversationsApi.<span class="fn">getMembers</span>(conversationId);
|
|
400
|
+
|
|
401
|
+
<span class="cm">// all participants including removed ones</span>
|
|
402
|
+
<span class="kw">await</span> conversationsApi.<span class="fn">getMembers</span>(conversationId, { <span class="at">filter</span>: <span class="str">'all'</span> });
|
|
403
|
+
|
|
404
|
+
<span class="cm">// only removed participants</span>
|
|
405
|
+
<span class="kw">await</span> conversationsApi.<span class="fn">getMembers</span>(conversationId, { <span class="at">filter</span>: <span class="str">'deleted'</span> });</code></pre>
|
|
406
|
+
<p>Default is <code>'active'</code> — no behaviour change for existing callers.</p>
|
|
407
|
+
<p class="wn-ref">→ <a href="#step-convs">Step 6 — Conversations</a> (participants section)</p>
|
|
408
|
+
</div>
|
|
409
|
+
</div>
|
|
410
|
+
|
|
411
|
+
<div class="wn-item" id="wn-107-4">
|
|
412
|
+
<div class="wn-item-header" onclick="toggleItem('wn-107-4')">
|
|
413
|
+
<span class="wn-tag new">New</span>
|
|
414
|
+
<span class="wn-item-title">Message character limit — 10,000</span>
|
|
415
|
+
<span class="wn-chevron-sm">▾</span>
|
|
416
|
+
</div>
|
|
417
|
+
<div class="wn-item-body">
|
|
418
|
+
<p>Text message content is now capped at <strong>10,000 characters</strong>. Sending a message that exceeds this limit returns a <code>400</code> validation error.</p>
|
|
419
|
+
<p><strong>Action required:</strong> Add client-side length validation before send to give users immediate feedback rather than waiting for a server error.</p>
|
|
420
|
+
<pre><code><span class="kw">const</span> MAX_LENGTH = <span class="num">10000</span>;
|
|
421
|
+
<span class="kw">if</span> (text.length > MAX_LENGTH) {
|
|
422
|
+
<span class="fn">showError</span>(<span class="str">`Message too long (${text.length}/${MAX_LENGTH})`</span>);
|
|
423
|
+
<span class="kw">return</span>;
|
|
424
|
+
}</code></pre>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
</div><!-- /.wn-body -->
|
|
429
|
+
</div><!-- /.wn-version -->
|
|
430
|
+
|
|
431
|
+
<!-- ── v1.0.6 ── -->
|
|
222
432
|
<div class="wn-version" id="wn-106">
|
|
223
433
|
<div class="wn-header" onclick="toggleVersion('wn-106')">
|
|
224
434
|
<div class="wn-title">
|
|
225
435
|
<span class="wn-ver">v1.0.6</span>
|
|
226
|
-
<span class="wn-badge current">Current</span>
|
|
227
436
|
<span class="wn-date">May 2026</span>
|
|
228
437
|
</div>
|
|
229
438
|
<span class="wn-chevron">▲</span>
|
|
@@ -1320,7 +1529,13 @@ client.socket.<span class="fn">on</span>(<span class="str">'conversation_created
|
|
|
1320
1529
|
<span class="kw">const</span> attachments = successful.<span class="fn">map</span>(f => ({
|
|
1321
1530
|
<span class="at">fileId</span>: f.id, <span class="at">type</span>: f.type, <span class="at">url</span>: f.url, <span class="at">filename</span>: f.filename, <span class="at">mimeType</span>: f.mimeType, <span class="at">size</span>: f.size,
|
|
1322
1531
|
}));
|
|
1323
|
-
<span class="kw">await</span> socketEmit.<span class="fn">sendMessage</span>({ conversationId, <span class="at">tempId</span>: <span class="fn">uuid</span>(), attachments })
|
|
1532
|
+
<span class="kw">await</span> socketEmit.<span class="fn">sendMessage</span>({ conversationId, <span class="at">tempId</span>: <span class="fn">uuid</span>(), attachments });
|
|
1533
|
+
|
|
1534
|
+
<span class="cm">// Audio/video — always pass duration (seconds) so receivers can render a player UI</span>
|
|
1535
|
+
<span class="kw">await</span> socketEmit.<span class="fn">sendMessage</span>({
|
|
1536
|
+
conversationId, <span class="at">tempId</span>: <span class="fn">uuid</span>(),
|
|
1537
|
+
<span class="at">attachments</span>: [{ <span class="at">fileId</span>: f.id, <span class="at">type</span>: <span class="str">'audio'</span>, <span class="at">url</span>: f.url, <span class="at">filename</span>: f.filename, <span class="at">mimeType</span>: f.mimeType, <span class="at">size</span>: f.size, <span class="at">duration</span>: recordingSeconds }],
|
|
1538
|
+
});</code></pre>
|
|
1324
1539
|
</section>
|
|
1325
1540
|
|
|
1326
1541
|
<!-- ─── STEP 10: REAL-TIME ─────────────────────────────────────────────── -->
|
|
@@ -1442,12 +1657,12 @@ socket.<span class="fn">on</span>(<span class="str">'message_deleted'</span>, (e
|
|
|
1442
1657
|
</tr>
|
|
1443
1658
|
<tr>
|
|
1444
1659
|
<td><code>'message_delivered'</code></td>
|
|
1445
|
-
<td>A single message you sent was delivered to all active recipients</td>
|
|
1660
|
+
<td>A single message you sent was delivered to all active recipients. Payload: <code>{ messageId, conversationId, deliveredTo: Array<{ userId, deliveredAt }> }</code></td>
|
|
1446
1661
|
<td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
|
|
1447
1662
|
</tr>
|
|
1448
1663
|
<tr>
|
|
1449
1664
|
<td><code>'messages_delivered'</code></td>
|
|
1450
|
-
<td>Batch delivery catch-up when a recipient comes online</td>
|
|
1665
|
+
<td>Batch delivery catch-up when a recipient comes online. Payload: <code>{ conversationId, messageIds[], deliveredTo: string, deliveredAt }</code></td>
|
|
1451
1666
|
<td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
|
|
1452
1667
|
</tr>
|
|
1453
1668
|
<tr>
|
|
@@ -1781,6 +1996,8 @@ socket?.<span class="fn">on</span>(<span class="str">'read_receipt'</span>, ({ c
|
|
|
1781
1996
|
<pre><code><span class="kw">const</span> attachments = successful.<span class="fn">map</span>(f => ({
|
|
1782
1997
|
<span class="at">fileId</span>: f.id, <span class="at">type</span>: f.type, <span class="at">url</span>: f.url,
|
|
1783
1998
|
<span class="at">filename</span>: f.filename, <span class="at">mimeType</span>: f.mimeType, <span class="at">size</span>: f.size,
|
|
1999
|
+
<span class="cm">// For audio/video include duration (seconds) — required for receiver player UI</span>
|
|
2000
|
+
...(f.type === <span class="str">'audio'</span> || f.type === <span class="str">'video'</span> ? { <span class="at">duration</span>: knownDurationSeconds } : {}),
|
|
1784
2001
|
}));
|
|
1785
2002
|
<span class="kw">await</span> socketEmit.<span class="fn">sendMessage</span>({ conversationId, <span class="at">tempId</span>: <span class="fn">uuid</span>(), attachments });</code></pre>
|
|
1786
2003
|
|
|
@@ -1797,6 +2014,156 @@ socket?.<span class="fn">on</span>(<span class="str">'read_receipt'</span>, ({ c
|
|
|
1797
2014
|
});</code></pre>
|
|
1798
2015
|
</section>
|
|
1799
2016
|
|
|
2017
|
+
<!-- ─── STEP 16.5: COMPRESSION ───────────────────────────────────────── -->
|
|
2018
|
+
<section id="step-compress">
|
|
2019
|
+
<h2><span class="step">STEP 16.5</span> File Compression</h2>
|
|
2020
|
+
<p>Compression runs <strong>client-side before upload</strong> — the server receives the compressed file with the correct size and MIME type. It is optional and fully backward compatible. Omit <code>platformCompressFn</code> to keep the original behavior.</p>
|
|
2021
|
+
|
|
2022
|
+
<!-- ── REACT NATIVE ── -->
|
|
2023
|
+
<div data-p="rn">
|
|
2024
|
+
<div class="callout tip"><strong>Zero config for Expo</strong> Install <code>expo-image-manipulator</code> and compression works automatically. No other changes needed.</div>
|
|
2025
|
+
|
|
2026
|
+
<pre><code>npx expo install expo-image-manipulator</code></pre>
|
|
2027
|
+
|
|
2028
|
+
<p>Without <code>expo-image-manipulator</code> the SDK skips compression silently — files upload as-is.</p>
|
|
2029
|
+
|
|
2030
|
+
<h3>Default behavior</h3>
|
|
2031
|
+
<pre><code><span class="cm">// AntzChatNavigator / AntzChatProvider — compression on by default, nothing to add</span>
|
|
2032
|
+
<AntzChatNavigator config={{ <span class="at">apiUrl</span>: <span class="str">'...'</span> }} />
|
|
2033
|
+
|
|
2034
|
+
<span class="cm">// AntzChatClient (headless) — pass rnCompressFn explicitly</span>
|
|
2035
|
+
<span class="kw">import</span> { AntzChatClient, rnUploadFn, rnPersistStorage, rnCompressFn } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
|
|
2036
|
+
|
|
2037
|
+
<span class="kw">const</span> client = <span class="kw">new</span> <span class="fn">AntzChatClient</span>({
|
|
2038
|
+
<span class="at">apiUrl</span>: <span class="str">'https://api.example.com/api/v1'</span>,
|
|
2039
|
+
<span class="at">authToken</span>: token,
|
|
2040
|
+
<span class="at">platformUploadFn</span>: rnUploadFn,
|
|
2041
|
+
<span class="at">persistStorage</span>: rnPersistStorage,
|
|
2042
|
+
<span class="at">platformCompressFn</span>: rnCompressFn, <span class="cm">// add this for headless usage</span>
|
|
2043
|
+
});</code></pre>
|
|
2044
|
+
|
|
2045
|
+
<h3>Strategy by file type</h3>
|
|
2046
|
+
<table>
|
|
2047
|
+
<tr><th>File type</th><th>Strategy</th><th>Algorithm</th><th>Requires</th></tr>
|
|
2048
|
+
<tr><td>JPEG, PNG, GIF, BMP, TIFF, WebP</td><td>Resize + re-encode</td><td>JPEG</td><td><code>expo-image-manipulator</code></td></tr>
|
|
2049
|
+
<tr><td>Video, audio, PDF, ZIP, Office</td><td>Skip</td><td>—</td><td>—</td></tr>
|
|
2050
|
+
<tr><td>Text, JSON, CSV, XML, YAML</td><td>Skip on RN</td><td>—</td><td>No <code>CompressionStream</code> in RN JS engine</td></tr>
|
|
2051
|
+
</table>
|
|
2052
|
+
|
|
2053
|
+
<h3>Tune or disable</h3>
|
|
2054
|
+
<pre><code><span class="cm">// Tune</span>
|
|
2055
|
+
<AntzChatNavigator config={{ ..., <span class="at">compression</span>: { <span class="at">imageQuality</span>: <span class="num">0.75</span>, <span class="at">imageMaxDimension</span>: <span class="num">1280</span> } }} />
|
|
2056
|
+
|
|
2057
|
+
<span class="cm">// Disable entirely</span>
|
|
2058
|
+
<AntzChatNavigator config={{ ..., <span class="at">compression</span>: { <span class="at">enabled</span>: <span class="kw">false</span> } }} /></code></pre>
|
|
2059
|
+
</div>
|
|
2060
|
+
|
|
2061
|
+
<!-- ── WEB ── -->
|
|
2062
|
+
<div data-p="web">
|
|
2063
|
+
<div class="callout tip"><strong>Zero config on Web</strong> <code>webCompressFn</code> is pre-wired automatically in <code>AntzChatProvider</code> / <code>AntzChat</code>. No extra packages — uses browser-native <code>canvas</code> and <code>CompressionStream</code>.</div>
|
|
2064
|
+
|
|
2065
|
+
<h3>Default behavior</h3>
|
|
2066
|
+
<pre><code><span class="cm">// AntzChat / AntzChatProvider — compression on by default, nothing to add</span>
|
|
2067
|
+
<AntzChat config={{ <span class="at">apiUrl</span>: <span class="str">'...'</span>, <span class="at">authToken</span>: <span class="str">'...'</span> }} />
|
|
2068
|
+
|
|
2069
|
+
<span class="cm">// AntzChatClient (headless) — pass webCompressFn explicitly</span>
|
|
2070
|
+
<span class="kw">import</span> { AntzChatClient } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
|
|
2071
|
+
<span class="kw">import</span> { webUploadFn, webPersistStorage, webCompressFn } <span class="kw">from</span> <span class="str">'@antzsoft/chat-web-sdk'</span>;
|
|
2072
|
+
|
|
2073
|
+
<span class="kw">const</span> client = <span class="kw">new</span> <span class="fn">AntzChatClient</span>({
|
|
2074
|
+
<span class="at">apiUrl</span>: <span class="str">'https://api.example.com/api/v1'</span>,
|
|
2075
|
+
<span class="at">authToken</span>: token,
|
|
2076
|
+
<span class="at">platformUploadFn</span>: webUploadFn,
|
|
2077
|
+
<span class="at">persistStorage</span>: webPersistStorage,
|
|
2078
|
+
<span class="at">platformCompressFn</span>: webCompressFn, <span class="cm">// add this for headless usage</span>
|
|
2079
|
+
});</code></pre>
|
|
2080
|
+
|
|
2081
|
+
<h3>Strategy by file type</h3>
|
|
2082
|
+
<table>
|
|
2083
|
+
<tr><th>File type</th><th>Strategy</th><th>Algorithm</th><th>API used</th></tr>
|
|
2084
|
+
<tr><td>JPEG, PNG, GIF, BMP, TIFF, WebP</td><td>Resize + re-encode</td><td>WebP</td><td><code>canvas.toBlob('image/webp')</code></td></tr>
|
|
2085
|
+
<tr><td>SVG, JSON, CSV, plain text, XML, YAML, RTF, Markdown</td><td>Compress bytes</td><td>gzip</td><td><code>CompressionStream('gzip')</code></td></tr>
|
|
2086
|
+
<tr><td>Video, audio, PDF, ZIP, Office</td><td>Skip</td><td>—</td><td>Already compressed</td></tr>
|
|
2087
|
+
</table>
|
|
2088
|
+
|
|
2089
|
+
<h3>Tune or disable</h3>
|
|
2090
|
+
<pre><code><span class="cm">// Tune</span>
|
|
2091
|
+
<AntzChat config={{ ..., <span class="at">compression</span>: { <span class="at">imageQuality</span>: <span class="num">0.75</span>, <span class="at">imageMaxDimension</span>: <span class="num">1280</span>, <span class="at">compressDocuments</span>: <span class="kw">false</span> } }} />
|
|
2092
|
+
|
|
2093
|
+
<span class="cm">// Disable entirely</span>
|
|
2094
|
+
<AntzChat config={{ ..., <span class="at">compression</span>: { <span class="at">enabled</span>: <span class="kw">false</span> } }} /></code></pre>
|
|
2095
|
+
</div>
|
|
2096
|
+
|
|
2097
|
+
<!-- ── NODE.JS ── -->
|
|
2098
|
+
<div data-p="node">
|
|
2099
|
+
<div class="callout info"><strong>Node.js</strong> No compressor is provided by default. Supply your own using <code>sharp</code> (images) and Node's built-in <code>zlib</code> (text/docs). Omit <code>platformCompressFn</code> to skip compression entirely.</div>
|
|
2100
|
+
|
|
2101
|
+
<h3>Install optional dep</h3>
|
|
2102
|
+
<pre><code>npm install sharp <span class="cm"># optional — only needed for image compression</span></code></pre>
|
|
2103
|
+
|
|
2104
|
+
<h3>Implementation</h3>
|
|
2105
|
+
<pre><code><span class="kw">import</span> sharp <span class="kw">from</span> <span class="str">'sharp'</span>;
|
|
2106
|
+
<span class="kw">import</span> zlib <span class="kw">from</span> <span class="str">'zlib'</span>;
|
|
2107
|
+
<span class="kw">import</span> { promisify } <span class="kw">from</span> <span class="str">'util'</span>;
|
|
2108
|
+
<span class="kw">import</span> { writeFile, readFile } <span class="kw">from</span> <span class="str">'fs/promises'</span>;
|
|
2109
|
+
<span class="kw">import</span> { join } <span class="kw">from</span> <span class="str">'path'</span>;
|
|
2110
|
+
<span class="kw">import</span> { tmpdir } <span class="kw">from</span> <span class="str">'os'</span>;
|
|
2111
|
+
<span class="kw">import</span> { <span class="tp">PlatformCompressFn</span> } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
|
|
2112
|
+
<span class="kw">import</span> { getCompressionStrategy } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
|
|
2113
|
+
|
|
2114
|
+
<span class="kw">const</span> gzip = <span class="fn">promisify</span>(zlib.gzip);
|
|
2115
|
+
|
|
2116
|
+
<span class="kw">export const</span> nodeCompressFn: <span class="tp">PlatformCompressFn</span> = <span class="kw">async</span> (file, options) => {
|
|
2117
|
+
<span class="kw">const</span> noop = { ...file, <span class="at">originalSize</span>: file.size, <span class="at">compressed</span>: <span class="kw">false</span>, <span class="at">compressionAlgorithm</span>: <span class="str">'none'</span> <span class="kw">as const</span> };
|
|
2118
|
+
<span class="kw">const</span> strategy = <span class="fn">getCompressionStrategy</span>(file.type, options);
|
|
2119
|
+
|
|
2120
|
+
<span class="kw">if</span> (strategy === <span class="str">'image'</span>) {
|
|
2121
|
+
<span class="kw">const</span> buf = <span class="kw">await</span> sharp(file.uri)
|
|
2122
|
+
.<span class="fn">resize</span>({ <span class="at">width</span>: options.imageMaxDimension, <span class="at">height</span>: options.imageMaxDimension, <span class="at">fit</span>: <span class="str">'inside'</span>, <span class="at">withoutEnlargement</span>: <span class="kw">true</span> })
|
|
2123
|
+
.<span class="fn">jpeg</span>({ <span class="at">quality</span>: <span class="fn">Math.round</span>(options.imageQuality * <span class="num">100</span>) })
|
|
2124
|
+
.<span class="fn">toBuffer</span>();
|
|
2125
|
+
<span class="kw">if</span> (buf.length >= file.size) <span class="kw">return</span> noop;
|
|
2126
|
+
<span class="kw">const</span> tmp = <span class="fn">join</span>(<span class="fn">tmpdir</span>(), <span class="str">`</span><span class="num">${Date.now()}</span><span class="str">.jpg`</span>);
|
|
2127
|
+
<span class="kw">await</span> <span class="fn">writeFile</span>(tmp, buf);
|
|
2128
|
+
<span class="kw">return</span> { <span class="at">uri</span>: tmp, <span class="at">name</span>: file.name.<span class="fn">replace</span>(<span class="str">/\.[^.]+$/</span>, <span class="str">'.jpg'</span>), <span class="at">type</span>: <span class="str">'image/jpeg'</span>, <span class="at">size</span>: buf.length, <span class="at">originalSize</span>: file.size, <span class="at">compressed</span>: <span class="kw">true</span>, <span class="at">compressionAlgorithm</span>: <span class="str">'jpeg'</span> <span class="kw">as const</span> };
|
|
2129
|
+
}
|
|
2130
|
+
|
|
2131
|
+
<span class="kw">if</span> (strategy === <span class="str">'gzip'</span>) {
|
|
2132
|
+
<span class="kw">const</span> input = <span class="kw">await</span> <span class="fn">readFile</span>(file.uri);
|
|
2133
|
+
<span class="kw">const</span> compressed = <span class="kw">await</span> <span class="fn">gzip</span>(input);
|
|
2134
|
+
<span class="kw">if</span> (compressed.length >= file.size) <span class="kw">return</span> noop;
|
|
2135
|
+
<span class="kw">const</span> tmp = <span class="fn">join</span>(<span class="fn">tmpdir</span>(), <span class="str">`</span><span class="num">${Date.now()}</span><span class="str">.gz`</span>);
|
|
2136
|
+
<span class="kw">await</span> <span class="fn">writeFile</span>(tmp, compressed);
|
|
2137
|
+
<span class="kw">return</span> { <span class="at">uri</span>: tmp, <span class="at">name</span>: file.name + <span class="str">'.gz'</span>, <span class="at">type</span>: file.type, <span class="at">size</span>: compressed.length, <span class="at">originalSize</span>: file.size, <span class="at">compressed</span>: <span class="kw">true</span>, <span class="at">compressionAlgorithm</span>: <span class="str">'gzip'</span> <span class="kw">as const</span> };
|
|
2138
|
+
}
|
|
2139
|
+
|
|
2140
|
+
<span class="kw">return</span> noop;
|
|
2141
|
+
};</code></pre>
|
|
2142
|
+
|
|
2143
|
+
<h3>Wire it in</h3>
|
|
2144
|
+
<pre><code><span class="kw">const</span> client = <span class="kw">new</span> <span class="fn">AntzChatClient</span>({
|
|
2145
|
+
<span class="at">apiUrl</span>: <span class="str">'https://api.example.com/api/v1'</span>,
|
|
2146
|
+
<span class="at">authToken</span>: process.env.<span class="at">AUTH_TOKEN</span>,
|
|
2147
|
+
<span class="at">platformUploadFn</span>: myNodeUploadFn,
|
|
2148
|
+
<span class="at">persistStorage</span>: myStorage,
|
|
2149
|
+
<span class="at">platformCompressFn</span>: nodeCompressFn,
|
|
2150
|
+
<span class="at">compression</span>: { <span class="at">imageQuality</span>: <span class="num">0.85</span>, <span class="at">imageMaxDimension</span>: <span class="num">1920</span> },
|
|
2151
|
+
});</code></pre>
|
|
2152
|
+
</div>
|
|
2153
|
+
|
|
2154
|
+
<!-- ── SHARED: config reference ── -->
|
|
2155
|
+
<h3>CompressionConfig reference</h3>
|
|
2156
|
+
<table>
|
|
2157
|
+
<tr><th>Field</th><th>Type</th><th>Default</th><th>Description</th></tr>
|
|
2158
|
+
<tr><td><code>enabled</code></td><td><code>boolean</code></td><td><code>true</code> when <code>platformCompressFn</code> provided</td><td>Master switch</td></tr>
|
|
2159
|
+
<tr><td><code>imageQuality</code></td><td><code>number</code> (0–1)</td><td><code>0.85</code></td><td>Encode quality for images</td></tr>
|
|
2160
|
+
<tr><td><code>imageMaxDimension</code></td><td><code>number</code></td><td><code>1920</code></td><td>Longest side cap in px before encoding</td></tr>
|
|
2161
|
+
<tr><td><code>compressDocuments</code></td><td><code>boolean</code></td><td><code>true</code></td><td>gzip text/JSON/CSV/XML/YAML/SVG/RTF (web only)</td></tr>
|
|
2162
|
+
</table>
|
|
2163
|
+
|
|
2164
|
+
<div class="callout info"><strong>Automatic fallback</strong> If the compressed result is larger than the original, the SDK uploads the original automatically — no extra code needed.</div>
|
|
2165
|
+
</section>
|
|
2166
|
+
|
|
1800
2167
|
<!-- ─── STEP 17: PUSH (RN + WEB only) ────────────────────────────────── -->
|
|
1801
2168
|
<section id="step-push" data-p="rn web">
|
|
1802
2169
|
<h2><span class="step">STEP 17</span> Push Notifications</h2>
|
|
@@ -2546,6 +2913,9 @@ console.<span class="fn">log</span>(<span class="str">`${summary.totalUnread} un
|
|
|
2546
2913
|
|
|
2547
2914
|
</main>
|
|
2548
2915
|
|
|
2916
|
+
<!-- ── Go-to-top FAB ── -->
|
|
2917
|
+
<button id="gototop" onclick="window.scrollTo({top:0,behavior:'smooth'})" title="Back to top">↑</button>
|
|
2918
|
+
|
|
2549
2919
|
<script>
|
|
2550
2920
|
// ── State ────────────────────────────────────────────────────────────────
|
|
2551
2921
|
let _platform = 'rn';
|
|
@@ -2632,10 +3002,87 @@ function toggleItem(id) {
|
|
|
2632
3002
|
document.getElementById(id).classList.toggle('open');
|
|
2633
3003
|
}
|
|
2634
3004
|
|
|
2635
|
-
// ── Progress bar
|
|
3005
|
+
// ── Progress bar + go-to-top FAB ─────────────────────────────────────────
|
|
3006
|
+
const _fab = document.getElementById('gototop');
|
|
2636
3007
|
window.addEventListener('scroll', () => {
|
|
2637
|
-
const
|
|
2638
|
-
|
|
3008
|
+
const ratio = window.scrollY / (document.body.scrollHeight - window.innerHeight);
|
|
3009
|
+
document.getElementById('prog').style.width = (ratio * 100) + '%';
|
|
3010
|
+
_fab.classList.toggle('show', window.scrollY > 300);
|
|
3011
|
+
});
|
|
3012
|
+
|
|
3013
|
+
// ── Section accordion ─────────────────────────────────────────────────────
|
|
3014
|
+
// Make every <section> (except #whats-new which has its own accordion) collapsible.
|
|
3015
|
+
// Wrap content below h2 in a .sec-body div and add a chevron to h2.
|
|
3016
|
+
document.querySelectorAll('section[id]').forEach(sec => {
|
|
3017
|
+
if (sec.id === 'whats-new') return; // already has its own accordion
|
|
3018
|
+
sec.classList.add('sec');
|
|
3019
|
+
|
|
3020
|
+
const h2 = sec.querySelector(':scope > h2');
|
|
3021
|
+
if (!h2) return;
|
|
3022
|
+
|
|
3023
|
+
// Add chevron to h2
|
|
3024
|
+
const chev = document.createElement('span');
|
|
3025
|
+
chev.className = 'sec-chevron';
|
|
3026
|
+
chev.textContent = '▲';
|
|
3027
|
+
h2.appendChild(chev);
|
|
3028
|
+
|
|
3029
|
+
// Wrap all siblings after h2 into .sec-body
|
|
3030
|
+
const body = document.createElement('div');
|
|
3031
|
+
body.className = 'sec-body';
|
|
3032
|
+
const siblings = [];
|
|
3033
|
+
let node = h2.nextSibling;
|
|
3034
|
+
while (node) { siblings.push(node); node = node.nextSibling; }
|
|
3035
|
+
siblings.forEach(n => body.appendChild(n));
|
|
3036
|
+
sec.appendChild(body);
|
|
3037
|
+
|
|
3038
|
+
// Toggle on h2 click
|
|
3039
|
+
h2.addEventListener('click', () => sec.classList.toggle('collapsed'));
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
// ── H3 sub-accordion ─────────────────────────────────────────────────────
|
|
3043
|
+
// Wrap each h3 + its following siblings (until next h3/h2/section end) into
|
|
3044
|
+
// a collapsible .sub-acc block. Only applied inside .sec-body.
|
|
3045
|
+
document.querySelectorAll('.sec-body').forEach(body => {
|
|
3046
|
+
const h3s = Array.from(body.querySelectorAll(':scope > h3'));
|
|
3047
|
+
h3s.forEach(h3 => {
|
|
3048
|
+
// Collect nodes that belong to this h3 (until next h3 or end)
|
|
3049
|
+
const nodes = [];
|
|
3050
|
+
let n = h3.nextSibling;
|
|
3051
|
+
while (n && !(n.nodeType === 1 && (n.tagName === 'H3' || n.tagName === 'H2'))) {
|
|
3052
|
+
nodes.push(n);
|
|
3053
|
+
n = n.nextSibling;
|
|
3054
|
+
}
|
|
3055
|
+
if (!nodes.length) return; // h3 with no content — skip
|
|
3056
|
+
|
|
3057
|
+
// Build accordion shell
|
|
3058
|
+
const acc = document.createElement('div');
|
|
3059
|
+
acc.className = 'sub-acc';
|
|
3060
|
+
|
|
3061
|
+
const hdr = document.createElement('div');
|
|
3062
|
+
hdr.className = 'sub-acc-hdr';
|
|
3063
|
+
|
|
3064
|
+
const title = document.createElement('h3');
|
|
3065
|
+
title.innerHTML = h3.innerHTML;
|
|
3066
|
+
|
|
3067
|
+
const chev = document.createElement('span');
|
|
3068
|
+
chev.className = 'sub-chev';
|
|
3069
|
+
chev.textContent = '▲';
|
|
3070
|
+
|
|
3071
|
+
hdr.appendChild(title);
|
|
3072
|
+
hdr.appendChild(chev);
|
|
3073
|
+
|
|
3074
|
+
const accBody = document.createElement('div');
|
|
3075
|
+
accBody.className = 'sub-acc-body';
|
|
3076
|
+
nodes.forEach(nd => accBody.appendChild(nd));
|
|
3077
|
+
|
|
3078
|
+
acc.appendChild(hdr);
|
|
3079
|
+
acc.appendChild(accBody);
|
|
3080
|
+
|
|
3081
|
+
hdr.addEventListener('click', () => acc.classList.toggle('closed'));
|
|
3082
|
+
|
|
3083
|
+
// Replace original h3 with the accordion
|
|
3084
|
+
h3.replaceWith(acc);
|
|
3085
|
+
});
|
|
2639
3086
|
});
|
|
2640
3087
|
|
|
2641
3088
|
// ── Sidebar active link ───────────────────────────────────────────────────
|
package/package.json
CHANGED