@antzsoft/chat-core 1.0.9 → 1.1.1

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.
@@ -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.0.9 Release Notes</a></li>
158
+ <li><a href="#whats-new">v1.1.0 Release Notes</a></li>
159
159
  </ul>
160
160
 
161
161
  <div class="section-label">Getting Started</div>
@@ -242,12 +242,125 @@ section.sec>h2:hover{color:#fff}
242
242
  <h2>What's New</h2>
243
243
  <p style="color:var(--muted);font-size:13px;margin-bottom:20px">Version history and release notes. Click a version to expand.</p>
244
244
 
245
- <!-- ── v1.0.9 (current) ── -->
246
- <div class="wn-version open" id="wn-109">
245
+ <!-- ── v1.1.0 (current) ── -->
246
+ <div class="wn-version open" id="wn-110">
247
+ <div class="wn-header" onclick="toggleVersion('wn-110')">
248
+ <div class="wn-title">
249
+ <span class="wn-ver">v1.1.0</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-110-1">
258
+ <div class="wn-item-header" onclick="toggleItem('wn-110-1')">
259
+ <span class="wn-tag fix">Fix</span>
260
+ <span class="wn-item-title">500 error on device token registration after reinstall or account switch</span>
261
+ <span class="wn-chevron-sm">▾</span>
262
+ </div>
263
+ <div class="wn-item-body">
264
+ <p>Registering a push token (Expo, FCM, APNs) that was previously registered under a different user or device ID returned a 500. This happened because the global <code>token_unique</code> sparse index rejected the duplicate before the upsert could complete.</p>
265
+ <p>Common triggers: app reinstall (new <code>deviceId</code> UUID generated), user switches account on same device, or token reuse after logout/login.</p>
266
+ <p>The server now removes any stale record holding the same token under a different owner before performing the upsert. Registration always succeeds for the new owner.</p>
267
+ </div>
268
+ </div>
269
+
270
+ <div class="wn-item" id="wn-110-2">
271
+ <div class="wn-item-header" onclick="toggleItem('wn-110-2')">
272
+ <span class="wn-tag fix">Fix</span>
273
+ <span class="wn-item-title">500 error when removing a participant from a conversation</span>
274
+ <span class="wn-chevron-sm">▾</span>
275
+ </div>
276
+ <div class="wn-item-body">
277
+ <p>Removing a participant caused a server error in certain cases. Message delivery stops and socket room eviction now correctly target only the remaining active members.</p>
278
+ </div>
279
+ </div>
280
+
281
+ <div class="wn-item" id="wn-110-3">
282
+ <div class="wn-item-header" onclick="toggleItem('wn-110-3')">
283
+ <span class="wn-tag fix">Fix</span>
284
+ <span class="wn-item-title">Leave group now works for removed members</span>
285
+ <span class="wn-chevron-sm">▾</span>
286
+ </div>
287
+ <div class="wn-item-body">
288
+ <p>After an admin removed a user from a group, that user still saw the conversation in their list (read-only, by design since v1.0.8). However, calling <code>leave()</code> to dismiss it returned a <strong>403 Forbidden</strong> because the active-participant guard rejected inactive users.</p>
289
+ <p><code>leave()</code> now uses read-access validation instead. If the caller is already inactive (removed), the conversation is hidden from their list immediately — no removal step is re-executed. If the caller is still active, the existing leave flow runs unchanged.</p>
290
+ </div>
291
+ </div>
292
+
293
+ <div class="wn-item" id="wn-110-4">
294
+ <div class="wn-item-header" onclick="toggleItem('wn-110-4')">
295
+ <span class="wn-tag fix">Fix</span>
296
+ <span class="wn-item-title">Group auto-disbands when last active member leaves or is removed</span>
297
+ <span class="wn-chevron-sm">▾</span>
298
+ </div>
299
+ <div class="wn-item-body">
300
+ <p>When the last active participant left or was removed, the group conversation persisted in the database as an orphan with zero active members. It could never be accessed or cleaned up.</p>
301
+ <p>The server now marks the conversation <code>isActive: false</code> automatically when <code>remainingParticipantIds</code> drops to zero after a leave or remove operation.</p>
302
+ </div>
303
+ </div>
304
+
305
+ <div class="wn-item" id="wn-110-5">
306
+ <div class="wn-item-header" onclick="toggleItem('wn-110-5')">
307
+ <span class="wn-tag fix">Fix</span>
308
+ <span class="wn-item-title"><code>lastMessage</code> content not updating after message edit</span>
309
+ <span class="wn-chevron-sm">▾</span>
310
+ </div>
311
+ <div class="wn-item-body">
312
+ <p>Editing a message that was also the conversation's last message did not update <code>conversation.lastMessage.contentPreview</code>. The conversation list kept showing the original text until a new message was sent.</p>
313
+ <p>The server now refreshes <code>lastMessage.contentPreview</code> on edit when the edited message ID matches <code>conversation.lastMessage.messageId</code>.</p>
314
+ </div>
315
+ </div>
316
+
317
+ <div class="wn-item" id="wn-110-6">
318
+ <div class="wn-item-header" onclick="toggleItem('wn-110-6')">
319
+ <span class="wn-tag fix">Fix</span>
320
+ <span class="wn-item-title">Reply-to attachment preview shows filename or file type instead of <code>[Attachment]</code></span>
321
+ <span class="wn-chevron-sm">▾</span>
322
+ </div>
323
+ <div class="wn-item-body">
324
+ <p>When replying to a message that contained an attachment, the quoted preview always showed the generic string <code>[Attachment]</code>. The server now returns the filename if available, otherwise the file type (e.g. <code>image</code>, <code>video</code>, <code>document</code>).</p>
325
+ <p>For previewable types (images, video), clients should use the parent message's attachment data (already included in the reply payload) to render a thumbnail or inline preview.</p>
326
+ </div>
327
+ </div>
328
+
329
+ <div class="wn-item" id="wn-110-7">
330
+ <div class="wn-item-header" onclick="toggleItem('wn-110-7')">
331
+ <span class="wn-tag fix">Fix</span>
332
+ <span class="wn-item-title">Remove reaction no longer errors when reaction does not exist</span>
333
+ <span class="wn-chevron-sm">▾</span>
334
+ </div>
335
+ <div class="wn-item-body">
336
+ <p>Removing a reaction that had already been removed (or never existed) returned a 500. The operation is now idempotent — removing a non-existent reaction returns success.</p>
337
+ </div>
338
+ </div>
339
+
340
+ <div class="wn-item" id="wn-110-8">
341
+ <div class="wn-item-header" onclick="toggleItem('wn-110-8')">
342
+ <span class="wn-tag new">New</span>
343
+ <span class="wn-item-title">Pin limit — max 5 pinned conversations</span>
344
+ <span class="wn-chevron-sm">▾</span>
345
+ </div>
346
+ <div class="wn-item-body">
347
+ <p>The server now enforces a maximum of <strong>5 pinned conversations</strong> per user. Attempting to pin a sixth returns a <code>400</code> error.</p>
348
+ <p>A new <code>GET /app/config</code> endpoint exposes server-side limits to clients. Use <code>appConfigApi.get()</code> to fetch them:</p>
349
+ <pre><code><span class="kw">const</span> config = <span class="kw">await</span> appConfigApi.<span class="fn">get</span>();
350
+ <span class="cm">// { maxPinnedConversations: 5 }</span></code></pre>
351
+ <p><code>useConversations()</code> (Web + RN) now fetches config automatically on mount, blocks the <code>pin</code> mutation client-side before it hits the API when the limit is reached, and exposes <code>maxPinnedConversations</code> for UI gating (e.g. hiding the pin option).</p>
352
+ <p>The RN <code>ConversationList</code> component now supports long-press to pin/unpin — this action was previously missing on React Native.</p>
353
+ </div>
354
+ </div>
355
+
356
+ </div>
357
+ </div><!-- /.wn-version -->
358
+
359
+ <!-- ── v1.0.9 ── -->
360
+ <div class="wn-version" id="wn-109">
247
361
  <div class="wn-header" onclick="toggleVersion('wn-109')">
248
362
  <div class="wn-title">
249
363
  <span class="wn-ver">v1.0.9</span>
250
- <span class="wn-badge current">Current</span>
251
364
  <span class="wn-date">May 2026</span>
252
365
  </div>
253
366
  <span class="wn-chevron">▲</span>
@@ -263,7 +376,6 @@ section.sec>h2:hover{color:#fff}
263
376
  <div class="wn-item-body">
264
377
  <p>On Android, the OS suspends idle WebSocket connections during long operations such as audio recording. When <code>sendMessage</code> was called immediately after a file upload completed, the socket was still reconnecting and the call failed immediately with "Socket not connected".</p>
265
378
  <p><code>sendMessage</code> now waits up to <strong>15 seconds</strong> for Socket.IO to auto-reconnect before attempting to send. Once the connection is restored the message goes through normally — no error, no retry logic needed in the app.</p>
266
- <p><strong>Action required:</strong> None. Behavior change is inside the SDK. No API or type changes.</p>
267
379
  </div>
268
380
  </div>
269
381
 
@@ -306,9 +418,8 @@ section.sec>h2:hover{color:#fff}
306
418
  <span class="wn-chevron-sm">▾</span>
307
419
  </div>
308
420
  <div class="wn-item-body">
309
- <p>When sending a message with an attachment and <code>text: ""</code>, the empty string was being stored in the database and returned as-is instead of being treated as absent. This caused the conversation-list preview for attachment-only messages to show blank instead of falling back to the filename or "Attachment".</p>
310
- <p>Both the HTTP API and socket paths now trim the text input and treat empty strings as absent before saving to the database. The preview now correctly shows the filename or "Attachment" for attachment-only messages.</p>
311
- <p><strong>Action required:</strong> None. Server-side fix only. No SDK or type changes.</p>
421
+ <p>When sending a message with an attachment and <code>text: ""</code>, the empty string was stored and returned instead of being treated as absent. The conversation-list preview for attachment-only messages showed blank instead of the filename or "Attachment". Both the HTTP and socket paths now trim and treat empty strings as absent before saving.</p>
422
+ <p><strong>Action required:</strong> None. Server-side fix only.</p>
312
423
  </div>
313
424
  </div>
314
425
 
@@ -319,9 +430,72 @@ section.sec>h2:hover{color:#fff}
319
430
  <span class="wn-chevron-sm">▾</span>
320
431
  </div>
321
432
  <div class="wn-item-body">
322
- <p>The <code>participant_left</code> socket event was only broadcast to the <code>conversation:&lt;id&gt;</code> room. Users only join that room when they actively open that specific conversation — so anyone on the conversation-list screen (or any other screen) never received the event and saw a stale participant count.</p>
323
- <p>The server now also emits directly to each remaining participant's personal <code>user:&lt;tenantId&gt;:&lt;userId&gt;</code> room, which every connected user joins on connect. To prevent users who have the conversation open from receiving the event twice (and double-decrementing the count), the server checks whether any of the user's sockets are already in the conversation room before sending the personal-room emit.</p>
324
- <p><strong>Action required:</strong> None. Server-side fix only. No SDK or type changes.</p>
433
+ <p>The <code>participant_left</code> socket event was only broadcast to the <code>conversation:&lt;id&gt;</code> room. Users only join that room when they actively open that specific conversation — so users on the conversation-list screen never received the event and saw a stale participant count. The server now also emits directly to each remaining participant's personal <code>user:&lt;tenantId&gt;:&lt;userId&gt;</code> room, skipping users already in the conversation room to avoid double-delivery.</p>
434
+ <p><strong>Action required:</strong> None. Server-side fix only.</p>
435
+ </div>
436
+ </div>
437
+
438
+ <div class="wn-item" id="wn-109-6">
439
+ <div class="wn-item-header" onclick="toggleItem('wn-109-6')">
440
+ <span class="wn-tag new">New</span>
441
+ <span class="wn-item-title">Pin limit — max 5 pinned conversations + <code>appConfigApi</code></span>
442
+ <span class="wn-chevron-sm">▾</span>
443
+ </div>
444
+ <div class="wn-item-body">
445
+ <p>The server now enforces a maximum of <strong>5 pinned conversations</strong> per user. Attempting to pin a 6th returns <code>400 Bad Request: "You can only pin up to 5 conversations"</code>.</p>
446
+ <p>A new <code>GET /app/config</code> endpoint returns server-side limits so clients can gate the UI without hardcoding values:</p>
447
+ <pre><code><span class="kw">import</span> { appConfigApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
448
+ <span class="kw">const</span> config = <span class="kw">await</span> appConfigApi.<span class="fn">get</span>();
449
+ <span class="cm">// { maxPinnedConversations: 5 }</span></code></pre>
450
+ <ul>
451
+ <li><code>useConversations()</code> (Web + RN) now fetches this automatically and exposes <code>maxPinnedConversations</code></li>
452
+ <li>The <code>pin</code> mutation in <code>useConversations()</code> throws before hitting the network if the limit is already reached</li>
453
+ <li>RN SDK <code>ConversationList</code> now shows pin/unpin on long-press (was missing previously)</li>
454
+ </ul>
455
+ <p><strong>Action required:</strong> None for SDK users — built-in check handles it. Raw API users should call <code>appConfigApi.get()</code> on init and gate the pin button using <code>config.maxPinnedConversations</code>.</p>
456
+ <p class="wn-ref">→ <a href="#step-conversations">Step 6 — Conversations</a></p>
457
+ </div>
458
+ </div>
459
+
460
+ <div class="wn-item" id="wn-109-7">
461
+ <div class="wn-item-header" onclick="toggleItem('wn-109-7')">
462
+ <span class="wn-tag fix">Fix</span>
463
+ <span class="wn-item-title">Removed user could re-enter conversation room via <code>join_room</code></span>
464
+ <span class="wn-chevron-sm">▾</span>
465
+ </div>
466
+ <div class="wn-item-body">
467
+ <p>The socket access check for <code>join_room</code> was missing the <code>isActive</code> participant check. A removed user whose client re-emitted <code>joinRoom</code> (e.g. on reconnect or by navigating back to the chat screen) would be silently re-admitted to the conversation room and continue receiving <code>new_message</code> events. The check now requires <code>isActive: true</code>, consistent with the write-access check.</p>
468
+ <p><strong>Action required:</strong> None. Server-side fix only.</p>
469
+ </div>
470
+ </div>
471
+
472
+ <div class="wn-item" id="wn-109-8">
473
+ <div class="wn-item-header" onclick="toggleItem('wn-109-8')">
474
+ <span class="wn-tag fix">Fix</span>
475
+ <span class="wn-item-title">Re-added user's conversation missing from list when they had previously deleted it</span>
476
+ <span class="wn-chevron-sm">▾</span>
477
+ </div>
478
+ <div class="wn-item-body">
479
+ <p>When a user deleted a conversation (<code>isHidden: true</code>), was then removed from the group, and later re-added by an admin, the conversation never reappeared in their list. The reactivation path now resets <code>isHidden</code> to <code>false</code> alongside <code>isActive</code>.</p>
480
+ <p>Additionally, the server now server-side joins the re-added user's active sockets into the conversation room and emits <code>conversation_created</code> to their personal room so the conversation appears immediately — no reconnect or <code>joinRoom</code> call needed.</p>
481
+ <p><strong>Action required:</strong> None. Server-side fix only. Existing <code>conversation_created → joinRoom</code> listeners remain safe (idempotent).</p>
482
+ </div>
483
+ </div>
484
+
485
+ <div class="wn-item" id="wn-109-9">
486
+ <div class="wn-item-header" onclick="toggleItem('wn-109-9')">
487
+ <span class="wn-tag new">New</span>
488
+ <span class="wn-item-title">Optional <code>role</code> parameter on <code>addParticipants</code></span>
489
+ <span class="wn-chevron-sm">▾</span>
490
+ </div>
491
+ <div class="wn-item-body">
492
+ <p><code>addParticipants</code> now accepts an optional <code>role</code> parameter (<code>'admin' | 'member'</code>, default <code>'member'</code>). This applies to both newly added users and previously removed users being re-added. A former admin who is re-added without specifying <code>role: 'admin'</code> will come back as a member — their previous role is never silently restored.</p>
493
+ <pre><code><span class="cm">// Default — added as member (backward compatible)</span>
494
+ <span class="kw">await</span> conversationsApi.<span class="fn">addParticipants</span>(conversationId, [<span class="str">'user-3'</span>]);
495
+
496
+ <span class="cm">// Explicitly add as admin</span>
497
+ <span class="kw">await</span> conversationsApi.<span class="fn">addParticipants</span>(conversationId, [<span class="str">'user-3'</span>], <span class="str">'admin'</span>);</code></pre>
498
+ <p><strong>Action required:</strong> None for existing code — the parameter is optional and defaults to <code>'member'</code>. No call sites need updating unless you want to add members as admins directly.</p>
325
499
  </div>
326
500
  </div>
327
501
 
@@ -329,7 +503,7 @@ section.sec>h2:hover{color:#fff}
329
503
  </div>
330
504
 
331
505
  <!-- ── v1.0.8 ── -->
332
- <div class="wn-version open" id="wn-108">
506
+ <div class="wn-version" id="wn-108">
333
507
  <div class="wn-header" onclick="toggleVersion('wn-108')">
334
508
  <div class="wn-title">
335
509
  <span class="wn-ver">v1.0.8</span>
@@ -375,7 +549,6 @@ section.sec>h2:hover{color:#fff}
375
549
  <li>Cannot send, react, type, edit, or delete — <code>validateWriteAccess</code> blocks all write operations</li>
376
550
  </ul>
377
551
  <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>
378
- <p><strong>Action required:</strong> None. Behavior change is entirely server-side. SDK types and API calls are unchanged.</p>
379
552
  </div>
380
553
  </div>
381
554
 
@@ -388,7 +561,6 @@ section.sec>h2:hover{color:#fff}
388
561
  <div class="wn-item-body">
389
562
  <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>
390
563
  <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>
391
- <p><strong>Action required:</strong> None. Server-side change only.</p>
392
564
  </div>
393
565
  </div>
394
566
 
@@ -401,7 +573,6 @@ section.sec>h2:hover{color:#fff}
401
573
  <div class="wn-item-body">
402
574
  <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>
403
575
  <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>
404
- <p><strong>Action required:</strong> None. Server-side fix only.</p>
405
576
  </div>
406
577
  </div>
407
578
 
@@ -431,7 +602,7 @@ section.sec>h2:hover{color:#fff}
431
602
  </div>
432
603
 
433
604
  <!-- ── v1.0.7 ── -->
434
- <div class="wn-version open" id="wn-107">
605
+ <div class="wn-version" id="wn-107">
435
606
  <div class="wn-header" onclick="toggleVersion('wn-107')">
436
607
  <div class="wn-title">
437
608
  <span class="wn-ver">v1.0.7</span>
@@ -468,7 +639,7 @@ section.sec>h2:hover{color:#fff}
468
639
  </div>
469
640
  <div class="wn-item-body">
470
641
  <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>
471
- <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>
642
+ <p>If you were filtering <code>.m4a</code> files out in the client before upload, remove that restriction.</p>
472
643
  </div>
473
644
  </div>
474
645
 
@@ -1407,15 +1578,33 @@ chatClient.<span class="fn">disconnect</span>();
1407
1578
 
1408
1579
  <span class="cm">// Participants</span>
1409
1580
  <span class="kw">const</span> members = <span class="kw">await</span> conversationsApi.<span class="fn">getMembers</span>(conversationId);
1410
- <span class="kw">await</span> conversationsApi.<span class="fn">addParticipants</span>(conversationId, [<span class="str">'user-3'</span>]);
1581
+ <span class="kw">await</span> conversationsApi.<span class="fn">addParticipants</span>(conversationId, [<span class="str">'user-3'</span>]); <span class="cm">// added as member (default)</span>
1582
+ <span class="kw">await</span> conversationsApi.<span class="fn">addParticipants</span>(conversationId, [<span class="str">'user-3'</span>], <span class="str">'admin'</span>); <span class="cm">// added as admin</span>
1411
1583
  <span class="kw">await</span> conversationsApi.<span class="fn">removeParticipant</span>(conversationId, <span class="str">'user-2'</span>);
1412
1584
  <span class="kw">await</span> conversationsApi.<span class="fn">updateParticipantRole</span>(conversationId, <span class="str">'user-1'</span>, <span class="str">'admin'</span>);
1413
1585
 
1414
1586
  <span class="cm">// Pin · Mute · Leave · Delete</span>
1415
- <span class="kw">await</span> conversationsApi.<span class="fn">pin</span>(conversationId);
1587
+ <span class="kw">await</span> conversationsApi.<span class="fn">pin</span>(conversationId); <span class="cm">// 400 if already at 5 pinned</span>
1416
1588
  <span class="kw">await</span> conversationsApi.<span class="fn">mute</span>(conversationId, <span class="str">'2025-12-31T23:59:59Z'</span>); <span class="cm">// omit date = indefinite</span>
1417
1589
  <span class="kw">await</span> conversationsApi.<span class="fn">leave</span>(conversationId);
1418
1590
  <span class="kw">await</span> conversationsApi.<span class="fn">delete</span>(conversationId); <span class="cm">// admin only</span></code></pre>
1591
+
1592
+ <div class="callout info">
1593
+ <strong>Pin limit — max 5</strong> — The server enforces a maximum of 5 pinned conversations per user. Attempting to pin a 6th returns <code>400 Bad Request</code>. Gate your UI before calling the API:
1594
+ <pre style="margin-top:8px"><code><span class="kw">import</span> { appConfigApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
1595
+
1596
+ <span class="cm">// Fetch once on init — SDK caches under ['app-config'] with staleTime: Infinity</span>
1597
+ <span class="kw">const</span> config = <span class="kw">await</span> appConfigApi.<span class="fn">get</span>(); <span class="cm">// { maxPinnedConversations: 5 }</span>
1598
+
1599
+ <span class="cm">// Before calling pin:</span>
1600
+ <span class="kw">const</span> pinnedCount = conversations.<span class="fn">filter</span>(c => c.isPinned).length;
1601
+ <span class="kw">if</span> (pinnedCount >= config.maxPinnedConversations) {
1602
+ <span class="cm">// show error toast — do NOT call the API</span>
1603
+ <span class="kw">return</span>;
1604
+ }
1605
+ <span class="kw">await</span> conversationsApi.<span class="fn">pin</span>(conversationId);</code></pre>
1606
+ <p>If you use <code>useConversations()</code> (Web/RN SDK), this check is built in — the <code>pin</code> mutation throws before hitting the network if the limit is reached. The hook also exposes <code>maxPinnedConversations</code> to disable the pin button in your UI.</p>
1607
+ </div>
1419
1608
  </section>
1420
1609
 
1421
1610
  <!-- ─── STEP 7: ROOMS ──────────────────────────────────────────────────── -->
@@ -1436,16 +1625,15 @@ chatClient.<span class="fn">disconnect</span>();
1436
1625
 
1437
1626
  <h4>When to call <code>joinRoom</code></h4>
1438
1627
  <p>
1439
- There is only one real use case — when the <strong>user is added to a conversation while their socket is already connected</strong>
1440
- (either someone created a new group and added them, or they were added to an existing group at runtime).
1441
- The server does <strong>not</strong> auto-join the user's socket to the new room — it only emits <code>conversation_created</code>
1442
- to the user's personal room. Without calling <code>joinRoom</code>, the socket will not receive any
1443
- <code>new_message</code>, <code>typing_indicator</code>, or other room events for that conversation.
1628
+ When the <strong>user is added to a conversation while their socket is already connected</strong>, the server now automatically joins their active sockets into the new room and emits <code>conversation_created</code> to their personal room. No <code>joinRoom</code> call is required — <code>new_message</code>, <code>typing_indicator</code>, and all other room events will arrive immediately.
1629
+ </p>
1630
+ <p>
1631
+ Calling <code>joinRoom</code> on <code>conversation_created</code> is still safe (idempotent) and can be kept for defensive compatibility, but is no longer necessary:
1444
1632
  </p>
1445
1633
 
1446
- <pre><code><span class="cm">// Fires when you're added to a new or existing conversation at runtime</span>
1634
+ <pre><code><span class="cm">// Optional server already handles this automatically</span>
1447
1635
  client.socket.<span class="fn">on</span>(<span class="str">'conversation_created'</span>, (conv) => {
1448
- client.socket.emit.<span class="fn">joinRoom</span>(conv.id);
1636
+ client.socket.emit.<span class="fn">joinRoom</span>(conv.id); <span class="cm">// safe no-op if already in room</span>
1449
1637
  });</code></pre>
1450
1638
 
1451
1639
  <p><code>leaveRoom</code> is only needed if you want to intentionally stop receiving events for a room the user is still a member of (e.g. archiving client-side). It is not needed when navigating away from a screen.</p>
@@ -1476,9 +1664,9 @@ client.socket.<span class="fn">on</span>(<span class="str">'new_message'</span>,
1476
1664
  });
1477
1665
  });
1478
1666
 
1479
- <span class="cm">// Only case where joinRoom is needed</span>
1667
+ <span class="cm">// Optional server auto-joins the room, but this is safe to keep</span>
1480
1668
  client.socket.<span class="fn">on</span>(<span class="str">'conversation_created'</span>, (conv) => {
1481
- client.socket.emit.<span class="fn">joinRoom</span>(conv.id);
1669
+ client.socket.emit.<span class="fn">joinRoom</span>(conv.id); <span class="cm">// idempotent no-op if already joined</span>
1482
1670
  });</code></pre>
1483
1671
  </div>
1484
1672
  <div data-p="node">
@@ -1489,6 +1677,7 @@ client.socket.<span class="fn">on</span>(<span class="str">'new_message'</span>,
1489
1677
  client.socket.emit.<span class="fn">markRead</span>(message.conversationId, message.id);
1490
1678
  });
1491
1679
 
1680
+ <span class="cm">// Optional — server auto-joins the room, but safe to keep</span>
1492
1681
  client.socket.<span class="fn">on</span>(<span class="str">'conversation_created'</span>, (conv) => {
1493
1682
  client.socket.emit.<span class="fn">joinRoom</span>(conv.id);
1494
1683
  });</code></pre>
@@ -1498,7 +1687,7 @@ client.socket.<span class="fn">on</span>(<span class="str">'conversation_created
1498
1687
  <strong>Summary of rules:</strong>
1499
1688
  <ul>
1500
1689
  <li>All existing rooms are auto-joined on socket connect — no <code>joinRoom</code> needed on screen open.</li>
1501
- <li>Call <code>joinRoom</code> only after a <code>conversation_created</code> event.</li>
1690
+ <li>When added to a conversation at runtime, the server now auto-joins the socket room and emits <code>conversation_created</code>. No <code>joinRoom</code> call needed, but safe to keep for compatibility.</li>
1502
1691
  <li>Add the <code>new_message</code> listener once at app root, never per screen.</li>
1503
1692
  <li>Use <code>message.conversationId</code> to route to the right view or badge update.</li>
1504
1693
  <li>Track <code>activeConversationId</code> to decide whether to increment the unread count.</li>
@@ -1957,11 +2146,11 @@ socket?.<span class="fn">on</span>(<span class="str">'read_receipt'</span>, ({ c
1957
2146
  <pre><code><span class="kw">await</span> socketEmit.<span class="fn">updateMessage</span>(messageId, <span class="str">'Updated text'</span>);
1958
2147
  <span class="cm">// REST fallback: await messagesApi.update(messageId, 'Updated text')</span></code></pre>
1959
2148
  <div class="callout info">
1960
- <strong>Edit window</strong> — check <code>conversation.settings.messageConfig.editWindowSeconds</code>:
2149
+ <strong>Edit window</strong> — server default is <strong>900 s (15 minutes)</strong>. <code>conversation.settings.messageConfig.editWindowSeconds</code> is always populated — use it directly:
1961
2150
  <pre style="margin-top:8px"><code><span class="kw">function</span> <span class="fn">canEdit</span>(msg: <span class="tp">Message</span>, conv: <span class="tp">Conversation</span>, userId: <span class="tp">string</span>) {
1962
2151
  <span class="kw">if</span> (msg.senderId !== userId) <span class="kw">return false</span>;
1963
- <span class="kw">const</span> w = conv.settings?.messageConfig?.editWindowSeconds;
1964
- <span class="kw">return</span> !w || (Date.<span class="fn">now</span>() - <span class="kw">new</span> <span class="fn">Date</span>(msg.sentAt).<span class="fn">getTime</span>()) &lt; w * <span class="num">1000</span>;
2152
+ <span class="kw">const</span> w = conv.settings?.messageConfig?.editWindowSeconds ?? <span class="num">900</span>;
2153
+ <span class="kw">return</span> (Date.<span class="fn">now</span>() - <span class="kw">new</span> <span class="fn">Date</span>(msg.sentAt).<span class="fn">getTime</span>()) &lt; w * <span class="num">1000</span>;
1965
2154
  }</code></pre>
1966
2155
  </div>
1967
2156
 
@@ -1975,20 +2164,18 @@ socket?.<span class="fn">on</span>(<span class="str">'read_receipt'</span>, ({ c
1975
2164
  </table>
1976
2165
 
1977
2166
  <div class="callout info">
1978
- <strong>Default delete window</strong> — if <code>conversation.settings.messageConfig.deleteWindowSeconds</code> is not set, the server defaults to <strong>1800 seconds (30 minutes)</strong>. Your UI helper must use the same fallback, otherwise you may show "Delete for everyone" after the window has already expired on the server and get a <code>403 Forbidden</code> response.
2167
+ <strong>Default delete window</strong> — server default is <strong>216,000 seconds (60 hours)</strong>. <code>conversation.settings.messageConfig.deleteWindowSeconds</code> is always populated use it directly. Your UI helper must use the same fallback, otherwise you may show "Delete for everyone" after the window has already expired on the server and get a <code>403 Forbidden</code> response.
1979
2168
  </div>
1980
2169
 
1981
2170
  <div class="callout info">
1982
2171
  <strong>DMs have no admins</strong> — in a direct message conversation <code>conv.type === 'direct'</code>, participants have no <code>admin</code> role. <code>isAdmin</code> will always be <code>false</code>, so "Delete for everyone" is only available to the message sender within the window. Do not show an admin-based delete option in DMs.
1983
2172
  </div>
1984
2173
 
1985
- <pre><code><span class="kw">const</span> DEFAULT_DELETE_WINDOW_SEC = <span class="num">1800</span>; <span class="cm">// matches server default</span>
1986
-
1987
- <span class="kw">function</span> <span class="fn">getDeleteOptions</span>(msg: <span class="tp">Message</span>, conv: <span class="tp">Conversation</span>, userId: <span class="tp">string</span>) {
2174
+ <pre><code><span class="kw">function</span> <span class="fn">getDeleteOptions</span>(msg: <span class="tp">Message</span>, conv: <span class="tp">Conversation</span>, userId: <span class="tp">string</span>) {
1988
2175
  <span class="kw">const</span> isMine = msg.senderId === userId;
1989
2176
  <span class="kw">const</span> isAdmin = conv.type !== <span class="str">'direct'</span> &&
1990
2177
  conv.participants.<span class="fn">find</span>(p => p.userId === userId)?.role === <span class="str">'admin'</span>;
1991
- <span class="kw">const</span> w = conv.settings?.messageConfig?.deleteWindowSeconds ?? DEFAULT_DELETE_WINDOW_SEC;
2178
+ <span class="kw">const</span> w = conv.settings?.messageConfig?.deleteWindowSeconds ?? <span class="num">216000</span>;
1992
2179
  <span class="kw">const</span> inWindow = (Date.<span class="fn">now</span>() - <span class="kw">new</span> <span class="fn">Date</span>(msg.sentAt).<span class="fn">getTime</span>()) &lt; w * <span class="num">1000</span>;
1993
2180
  <span class="kw">return</span> { <span class="at">canDeleteForEveryone</span>: (isMine && inWindow) || isAdmin, <span class="at">canDeleteForMe</span>: <span class="kw">true</span> };
1994
2181
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antzsoft/chat-core",
3
- "version": "1.0.9",
3
+ "version": "1.1.1",
4
4
  "description": "Platform-agnostic core for Antz Chat — API, socket, stores, types. Works in browser, React Native (Expo or bare), and Node.js.",
5
5
  "author": "Antz",
6
6
  "license": "MIT",