@antzsoft/chat-core 1.2.6 → 1.2.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.
@@ -1,4 +1,4 @@
1
- export { ad as uploadBatchWithSlots } from './storage-CctLCOnZ.js';
1
+ export { al as uploadBatchWithSlots } from './storage-CW70b4vi.js';
2
2
 
3
3
  declare function generateUUID(): string;
4
4
 
@@ -359,6 +359,11 @@ interface ReactionUpdatedEvent {
359
359
  reactions: MessageReaction[];
360
360
  lastReaction: LastReaction | null;
361
361
  }
362
+ interface MessageStarUpdatedEvent {
363
+ messageId: string;
364
+ conversationId: string;
365
+ isStarred: boolean;
366
+ }
362
367
  interface TypingIndicatorEvent {
363
368
  conversationId: string;
364
369
  userId: string;
@@ -413,6 +418,53 @@ interface MessagesDeliveredEvent {
413
418
  deliveredTo: string;
414
419
  deliveredAt: string;
415
420
  }
421
+ interface SyncReadReceipt {
422
+ messageId: string;
423
+ conversationId: string;
424
+ userId: string;
425
+ readAt: string | null;
426
+ }
427
+ interface SyncDeletedForMe {
428
+ messageId: string;
429
+ conversationId: string;
430
+ deletedAt: string | null;
431
+ }
432
+ interface SyncParticipantChange {
433
+ conversationId: string;
434
+ userId: string;
435
+ role: string;
436
+ isActive: boolean;
437
+ isMuted: boolean;
438
+ mutedUntil: string | null;
439
+ updatedAt: string | null;
440
+ }
441
+ interface SyncStarEntry {
442
+ messageId: string;
443
+ conversationId: string;
444
+ /** true = starred, false = unstarred (soft-deleted) */
445
+ isActive: boolean;
446
+ updatedAt: string | null;
447
+ }
448
+ /** Full current reaction state keyed by messageId. Only message IDs with activity since `since` are included. */
449
+ type SyncReactions = Record<string, MessageReaction[]>;
450
+ interface CrossConversationSyncResponse {
451
+ syncedAt: string;
452
+ /** When true the gap exceeds 60 days — app should mark all conversations needs_refresh and lazy-sync on open */
453
+ stale: boolean;
454
+ messages: Message[];
455
+ deletedForMe: SyncDeletedForMe[];
456
+ participantChanges: SyncParticipantChange[];
457
+ readReceipts: SyncReadReceipt[];
458
+ }
459
+ interface ConversationSyncResponse {
460
+ syncedAt: string;
461
+ messages: Message[];
462
+ deletedForMe: SyncDeletedForMe[];
463
+ reactions: SyncReactions;
464
+ stars: SyncStarEntry[];
465
+ participantChanges: SyncParticipantChange[];
466
+ readReceipts: SyncReadReceipt[];
467
+ }
416
468
  interface SendMessageAttachment {
417
469
  fileId: string;
418
470
  type: FileType;
@@ -707,4 +759,4 @@ declare function uploadBatchWithSlots(files: UploadableFile[], platformUploadFn:
707
759
  slotToFile: Map<string, FileResponse>;
708
760
  }>;
709
761
 
710
- export { type QuietHours as $, type AuthResponse as A, type BatchUploadResult as B, type CursorPaginatedResponse as C, type MessageContent as D, type MessageDeletedEvent as E, type FileResponse as F, type MessageDeletedForMeEvent as G, type MessageDeliveredEvent as H, type MessageMetadata as I, type MessageReaction as J, type MessageReceiptEntry as K, type LoginCredentials as L, type Message as M, type MessageReplyReference as N, type MessageUpdatedEvent as O, type PaginatedResponse as P, type MessagesDeliveredEvent as Q, type ResolvedCompressionConfig as R, type SendMessagePayload as S, type MultipartPartUrl as T, type User as U, type MultipartUploadInfo as V, type NewMessageEvent as W, type OptimisticAttachment as X, type PlatformCompressFn as Y, type PlatformUploadFn as Z, type PlatformUploadPartFn as _, type RegisterData as a, type ReactionUpdatedEvent as a0, type ReadReceiptEvent as a1, type ReplyAttachmentSnapshot as a2, type ResolvedFileSizeLimits as a3, type SendMessageAttachment as a4, type SystemMessageMetadata as a5, type TypingIndicatorEvent as a6, type UploadConfig as a7, type UploadProgress as a8, type UserStatusEvent as a9, resolveConfig as aa, storageApi as ab, uploadBatch as ac, uploadBatchWithSlots as ad, type AuthTokens as b, type AppConfig as c, type MessageReceiptsResponse as d, type ConversationListParams as e, type Conversation as f, type Participant as g, type ConversationUnreadCount as h, type UnreadSummary as i, type UserPreferences as j, type ResolvedConfig as k, type PersistStorage as l, type PresignedUrlRequest as m, type PresignedUrlResponse as n, type CompletedPart as o, type FileType as p, type AntzChatConfig as q, type UploadableFile as r, type Attachment as s, type CompressedFile as t, type CompressionAlgorithm as u, type CompressionConfig as v, type ConversationType as w, type FileSizeLimits as x, type LastReaction as y, type MessageAckEvent as z };
762
+ export { type PlatformCompressFn as $, type AuthResponse as A, type BatchUploadResult as B, type CrossConversationSyncResponse as C, type LastReaction as D, type MessageAckEvent as E, type FileResponse as F, type MessageContent as G, type MessageDeletedEvent as H, type MessageDeletedForMeEvent as I, type MessageDeliveredEvent as J, type MessageMetadata as K, type LoginCredentials as L, type Message as M, type MessageReaction as N, type MessageReceiptEntry as O, type PaginatedResponse as P, type MessageReplyReference as Q, type ResolvedCompressionConfig as R, type SendMessagePayload as S, type MessageStarUpdatedEvent as T, type User as U, type MessageUpdatedEvent as V, type MessagesDeliveredEvent as W, type MultipartPartUrl as X, type MultipartUploadInfo as Y, type NewMessageEvent as Z, type OptimisticAttachment as _, type RegisterData as a, type PlatformUploadFn as a0, type PlatformUploadPartFn as a1, type QuietHours as a2, type ReactionUpdatedEvent as a3, type ReadReceiptEvent as a4, type ReplyAttachmentSnapshot as a5, type ResolvedFileSizeLimits as a6, type SendMessageAttachment as a7, type SyncDeletedForMe as a8, type SyncParticipantChange as a9, type SyncReactions as aa, type SyncReadReceipt as ab, type SyncStarEntry as ac, type SystemMessageMetadata as ad, type TypingIndicatorEvent as ae, type UploadConfig as af, type UploadProgress as ag, type UserStatusEvent as ah, resolveConfig as ai, storageApi as aj, uploadBatch as ak, uploadBatchWithSlots as al, type AuthTokens as b, type ConversationSyncResponse as c, type AppConfig as d, type CursorPaginatedResponse as e, type MessageReceiptsResponse as f, type ConversationListParams as g, type Conversation as h, type Participant as i, type ConversationUnreadCount as j, type UnreadSummary as k, type UserPreferences as l, type ResolvedConfig as m, type PersistStorage as n, type PresignedUrlRequest as o, type PresignedUrlResponse as p, type CompletedPart as q, type FileType as r, type AntzChatConfig as s, type UploadableFile as t, type Attachment as u, type CompressedFile as v, type CompressionAlgorithm as w, type CompressionConfig as x, type ConversationType as y, type FileSizeLimits as z };
@@ -359,6 +359,11 @@ interface ReactionUpdatedEvent {
359
359
  reactions: MessageReaction[];
360
360
  lastReaction: LastReaction | null;
361
361
  }
362
+ interface MessageStarUpdatedEvent {
363
+ messageId: string;
364
+ conversationId: string;
365
+ isStarred: boolean;
366
+ }
362
367
  interface TypingIndicatorEvent {
363
368
  conversationId: string;
364
369
  userId: string;
@@ -413,6 +418,53 @@ interface MessagesDeliveredEvent {
413
418
  deliveredTo: string;
414
419
  deliveredAt: string;
415
420
  }
421
+ interface SyncReadReceipt {
422
+ messageId: string;
423
+ conversationId: string;
424
+ userId: string;
425
+ readAt: string | null;
426
+ }
427
+ interface SyncDeletedForMe {
428
+ messageId: string;
429
+ conversationId: string;
430
+ deletedAt: string | null;
431
+ }
432
+ interface SyncParticipantChange {
433
+ conversationId: string;
434
+ userId: string;
435
+ role: string;
436
+ isActive: boolean;
437
+ isMuted: boolean;
438
+ mutedUntil: string | null;
439
+ updatedAt: string | null;
440
+ }
441
+ interface SyncStarEntry {
442
+ messageId: string;
443
+ conversationId: string;
444
+ /** true = starred, false = unstarred (soft-deleted) */
445
+ isActive: boolean;
446
+ updatedAt: string | null;
447
+ }
448
+ /** Full current reaction state keyed by messageId. Only message IDs with activity since `since` are included. */
449
+ type SyncReactions = Record<string, MessageReaction[]>;
450
+ interface CrossConversationSyncResponse {
451
+ syncedAt: string;
452
+ /** When true the gap exceeds 60 days — app should mark all conversations needs_refresh and lazy-sync on open */
453
+ stale: boolean;
454
+ messages: Message[];
455
+ deletedForMe: SyncDeletedForMe[];
456
+ participantChanges: SyncParticipantChange[];
457
+ readReceipts: SyncReadReceipt[];
458
+ }
459
+ interface ConversationSyncResponse {
460
+ syncedAt: string;
461
+ messages: Message[];
462
+ deletedForMe: SyncDeletedForMe[];
463
+ reactions: SyncReactions;
464
+ stars: SyncStarEntry[];
465
+ participantChanges: SyncParticipantChange[];
466
+ readReceipts: SyncReadReceipt[];
467
+ }
416
468
  interface SendMessageAttachment {
417
469
  fileId: string;
418
470
  type: FileType;
@@ -707,4 +759,4 @@ declare function uploadBatchWithSlots(files: UploadableFile[], platformUploadFn:
707
759
  slotToFile: Map<string, FileResponse>;
708
760
  }>;
709
761
 
710
- export { type QuietHours as $, type AuthResponse as A, type BatchUploadResult as B, type CursorPaginatedResponse as C, type MessageContent as D, type MessageDeletedEvent as E, type FileResponse as F, type MessageDeletedForMeEvent as G, type MessageDeliveredEvent as H, type MessageMetadata as I, type MessageReaction as J, type MessageReceiptEntry as K, type LoginCredentials as L, type Message as M, type MessageReplyReference as N, type MessageUpdatedEvent as O, type PaginatedResponse as P, type MessagesDeliveredEvent as Q, type ResolvedCompressionConfig as R, type SendMessagePayload as S, type MultipartPartUrl as T, type User as U, type MultipartUploadInfo as V, type NewMessageEvent as W, type OptimisticAttachment as X, type PlatformCompressFn as Y, type PlatformUploadFn as Z, type PlatformUploadPartFn as _, type RegisterData as a, type ReactionUpdatedEvent as a0, type ReadReceiptEvent as a1, type ReplyAttachmentSnapshot as a2, type ResolvedFileSizeLimits as a3, type SendMessageAttachment as a4, type SystemMessageMetadata as a5, type TypingIndicatorEvent as a6, type UploadConfig as a7, type UploadProgress as a8, type UserStatusEvent as a9, resolveConfig as aa, storageApi as ab, uploadBatch as ac, uploadBatchWithSlots as ad, type AuthTokens as b, type AppConfig as c, type MessageReceiptsResponse as d, type ConversationListParams as e, type Conversation as f, type Participant as g, type ConversationUnreadCount as h, type UnreadSummary as i, type UserPreferences as j, type ResolvedConfig as k, type PersistStorage as l, type PresignedUrlRequest as m, type PresignedUrlResponse as n, type CompletedPart as o, type FileType as p, type AntzChatConfig as q, type UploadableFile as r, type Attachment as s, type CompressedFile as t, type CompressionAlgorithm as u, type CompressionConfig as v, type ConversationType as w, type FileSizeLimits as x, type LastReaction as y, type MessageAckEvent as z };
762
+ export { type PlatformCompressFn as $, type AuthResponse as A, type BatchUploadResult as B, type CrossConversationSyncResponse as C, type LastReaction as D, type MessageAckEvent as E, type FileResponse as F, type MessageContent as G, type MessageDeletedEvent as H, type MessageDeletedForMeEvent as I, type MessageDeliveredEvent as J, type MessageMetadata as K, type LoginCredentials as L, type Message as M, type MessageReaction as N, type MessageReceiptEntry as O, type PaginatedResponse as P, type MessageReplyReference as Q, type ResolvedCompressionConfig as R, type SendMessagePayload as S, type MessageStarUpdatedEvent as T, type User as U, type MessageUpdatedEvent as V, type MessagesDeliveredEvent as W, type MultipartPartUrl as X, type MultipartUploadInfo as Y, type NewMessageEvent as Z, type OptimisticAttachment as _, type RegisterData as a, type PlatformUploadFn as a0, type PlatformUploadPartFn as a1, type QuietHours as a2, type ReactionUpdatedEvent as a3, type ReadReceiptEvent as a4, type ReplyAttachmentSnapshot as a5, type ResolvedFileSizeLimits as a6, type SendMessageAttachment as a7, type SyncDeletedForMe as a8, type SyncParticipantChange as a9, type SyncReactions as aa, type SyncReadReceipt as ab, type SyncStarEntry as ac, type SystemMessageMetadata as ad, type TypingIndicatorEvent as ae, type UploadConfig as af, type UploadProgress as ag, type UserStatusEvent as ah, resolveConfig as ai, storageApi as aj, uploadBatch as ak, uploadBatchWithSlots as al, type AuthTokens as b, type ConversationSyncResponse as c, type AppConfig as d, type CursorPaginatedResponse as e, type MessageReceiptsResponse as f, type ConversationListParams as g, type Conversation as h, type Participant as i, type ConversationUnreadCount as j, type UnreadSummary as k, type UserPreferences as l, type ResolvedConfig as m, type PersistStorage as n, type PresignedUrlRequest as o, type PresignedUrlResponse as p, type CompletedPart as q, type FileType as r, type AntzChatConfig as s, type UploadableFile as t, type Attachment as u, type CompressedFile as v, type CompressionAlgorithm as w, type CompressionConfig as x, type ConversationType as y, type FileSizeLimits as z };
@@ -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.6 Release Notes</a></li>
158
+ <li><a href="#whats-new">v1.2.8 Release Notes</a></li>
159
159
  </ul>
160
160
 
161
161
  <div class="section-label">Getting Started</div>
@@ -250,16 +250,248 @@ section.sec>h2:hover{color:#fff}
250
250
  <h2>What's New</h2>
251
251
  <p style="color:var(--muted);font-size:13px;margin-bottom:20px">Version history and release notes. Click a version to expand.</p>
252
252
 
253
- <!-- ── v1.2.6 (current) ── -->
254
- <div class="wn-version open" id="wn-126">
255
- <div class="wn-header" onclick="toggleVersion('wn-126')">
253
+ <!-- ── v1.2.8 (current) ── -->
254
+ <div class="wn-version open" id="wn-128">
255
+ <div class="wn-header" onclick="toggleVersion('wn-128')">
256
256
  <div class="wn-title">
257
- <span class="wn-ver">v1.2.6</span>
257
+ <span class="wn-ver">v1.2.8</span>
258
258
  <span class="wn-badge current">Current</span>
259
259
  <span class="wn-date">June 2026</span>
260
260
  </div>
261
261
  <span class="wn-chevron">▲</span>
262
262
  </div>
263
+ <div class="wn-body">
264
+
265
+ <div class="wn-item" id="wn-128-delivery-status">
266
+ <div class="wn-item-header" onclick="toggleItem('wn-128-delivery-status')">
267
+ <span class="wn-tag new">New</span>
268
+ <span class="wn-item-title"><code>deliveryStatus</code> on <code>Conversation.lastMessage</code> — tick state in conversation list</span>
269
+ <span class="wn-chevron-sm">▾</span>
270
+ </div>
271
+ <div class="wn-item-body">
272
+ <p>The conversation list API (<code>GET /conversations</code>) and the <code>conversation_updated</code> socket event now include <code>deliveryStatus</code> on the <code>lastMessage</code> object. This is the tick state of the last message as seen by the sender — stored directly on the conversation document and kept current by the server on every read and delivery event.</p>
273
+
274
+ <table>
275
+ <thead><tr><th>Value</th><th>Meaning</th></tr></thead>
276
+ <tbody>
277
+ <tr><td><code>'sent'</code></td><td>Message written to the server — not yet delivered to all recipients</td></tr>
278
+ <tr><td><code>'delivered'</code></td><td>All active recipients have received the message on at least one device</td></tr>
279
+ <tr><td><code>'read'</code></td><td>All active recipients have opened and read the message</td></tr>
280
+ </tbody>
281
+ </table>
282
+
283
+ <p><strong>Usage — rendering tick marks in a conversation list (custom UI):</strong></p>
284
+ <pre><code><span class="kw">const</span> { data: conversations } = <span class="kw">await</span> conversationsApi.<span class="fn">list</span>();
285
+
286
+ <span class="kw">for</span> (<span class="kw">const</span> conv <span class="kw">of</span> conversations) {
287
+ <span class="kw">const</span> lm = conv.lastMessage;
288
+ <span class="kw">if</span> (!lm) <span class="kw">continue</span>;
289
+
290
+ <span class="cm">// Only show ticks on messages sent by the current user</span>
291
+ <span class="kw">const</span> isMyMessage = lm.senderId === currentUserId;
292
+ <span class="kw">if</span> (!isMyMessage) <span class="kw">continue</span>;
293
+
294
+ <span class="cm">// deliveryStatus: 'sent' | 'delivered' | 'read'</span>
295
+ <span class="fn">renderTick</span>(conv.id, lm.deliveryStatus ?? <span class="str">'sent'</span>);
296
+ }</code></pre>
297
+
298
+ <p><strong>Live updates</strong> — the existing <code>read_receipt</code> socket event continues to carry real-time tick updates while a conversation room is open. <code>deliveryStatus</code> on <code>lastMessage</code> is the cold-start / list-load source of truth; socket events keep it live after that.</p>
299
+
300
+ <p><strong>Server-side changes:</strong></p>
301
+ <ul>
302
+ <li><code>lastMessage.deliveryStatus</code> is set to <code>'sent'</code> when a new message is written</li>
303
+ <li>Promoted to <code>'delivered'</code> when all active recipients have a delivery record for that message</li>
304
+ <li>Promoted to <code>'read'</code> when <code>fullyReadMessageIds</code> includes the last message (i.e. all active recipients have read it)</li>
305
+ <li>Status never demotes — a conditional update ensures it only advances: <code>sent → delivered → read</code></li>
306
+ </ul>
307
+
308
+ <p><strong>Migration</strong> — existing conversations are backfilled automatically by <code>005_backfill_last_message_delivery_status</code> on first deploy. It queries <code>chat_message_reads</code> and <code>chat_message_deliveries</code> to compute the correct status for each conversation's current last message. No manual step required.</p>
309
+
310
+ <p><strong>Backward compatibility</strong> — <code>deliveryStatus</code> defaults to <code>'sent'</code> when absent (<code>lm.deliveryStatus ?? 'sent'</code>). The field was already defined as optional on the <code>Message</code> type — it is now also populated on the <code>lastMessage</code> snapshot. No breaking changes.</p>
311
+ </div>
312
+ </div>
313
+
314
+ </div>
315
+ </div><!-- /.wn-version -->
316
+
317
+ <!-- ── v1.2.7 ── -->
318
+ <div class="wn-version open" id="wn-127">
319
+ <div class="wn-header" onclick="toggleVersion('wn-127')">
320
+ <div class="wn-title">
321
+ <span class="wn-ver">v1.2.7</span>
322
+ <span class="wn-date">June 2026</span>
323
+ </div>
324
+ <span class="wn-chevron">▲</span>
325
+ </div>
326
+ <div class="wn-body">
327
+
328
+ <div class="wn-item" id="wn-127-starsynce">
329
+ <div class="wn-item-header" onclick="toggleItem('wn-127-starsynce')">
330
+ <span class="wn-tag new">New</span>
331
+ <span class="wn-item-title">Real-time star sync across devices — <code>message_star_updated</code> socket event</span>
332
+ <span class="wn-chevron-sm">▾</span>
333
+ </div>
334
+ <div class="wn-item-body">
335
+ <p>When the current user stars or unstars a message on one device, the server now emits <code>message_star_updated</code> to their personal socket room. All other connected devices of the same user receive the event and update their message cache automatically — no refetch, no polling.</p>
336
+ <table>
337
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
338
+ <tbody>
339
+ <tr><td><code>messageId</code></td><td><code>string</code></td><td>ID of the starred/unstarred message</td></tr>
340
+ <tr><td><code>conversationId</code></td><td><code>string</code></td><td>Conversation the message belongs to</td></tr>
341
+ <tr><td><code>isStarred</code></td><td><code>boolean</code></td><td><code>true</code> = starred, <code>false</code> = unstarred</td></tr>
342
+ </tbody>
343
+ </table>
344
+ <p>The event is emitted <strong>only to the acting user's own sockets</strong> — other participants never see it. Stars are personal bookmarks, not shared state.</p>
345
+ <pre><code><span class="kw">import</span> { tryGetSocket } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
346
+ <span class="kw">import type</span> { MessageStarUpdatedEvent } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
347
+
348
+ <span class="cm">// Register at app root — fires when you star/unstar on any other device</span>
349
+ <span class="kw">const</span> socket = tryGetSocket();
350
+ socket?.<span class="fn">on</span>(<span class="str">'message_star_updated'</span>, (evt: MessageStarUpdatedEvent) =&gt; {
351
+ <span class="cm">// Update the open chat view for the affected conversation</span>
352
+ updateMessageStarState(evt.messageId, evt.isStarred);
353
+
354
+ <span class="cm">// Optionally invalidate the starred messages list if it's open</span>
355
+ <span class="kw">if</span> (!evt.isStarred) removeFromStarredList(evt.messageId);
356
+ });</code></pre>
357
+ <p><strong>Web and RN SDK users:</strong> both <code>SocketProvider</code> implementations handle this event internally and patch the React Query cache automatically. No code changes needed in the host app.</p>
358
+ <p><strong>No integration changes required</strong> for SDK users. Core SDK direct users should add the listener above at app root level.</p>
359
+ </div>
360
+ </div>
361
+
362
+ <div class="wn-item" id="wn-127-softustar">
363
+ <div class="wn-item-header" onclick="toggleItem('wn-127-softustar')">
364
+ <span class="wn-tag new">New</span>
365
+ <span class="wn-item-title">Soft-delete unstar + sync indexes — foundation for offline delta sync</span>
366
+ <span class="wn-chevron-sm">▾</span>
367
+ </div>
368
+ <div class="wn-item-body">
369
+ <p>Three server-side changes lay the groundwork for efficient offline delta sync in the mobile app:</p>
370
+ <ul>
371
+ <li><strong>Unstar is now a soft delete</strong> — <code>chat_message_stars</code> records are no longer hard-deleted when a user unstars. The record is updated to <code>isActive: false</code> with an <code>unstarredAt</code> timestamp. A sync query using <code>updatedAt &gt; since</code> can now detect both star and unstar events from a single collection scan. <code>getStarredMessages</code> filters <code>isActive: true</code> so visible behaviour is unchanged.</li>
372
+ <li><strong><code>updatedAt</code> on participant documents</strong> — <code>chat_conversation_participants</code> now carries <code>updatedAt</code> (Mongoose timestamps). Mute, pin, role change, and removal all bump this field, making the full participant state syncable via timestamp-based delta queries.</li>
373
+ <li><strong>New sync indexes</strong> — Three compound indexes added at startup (idempotent, no manual step needed):
374
+ <ul>
375
+ <li><code>{ conversationId, updatedAt }</code> on <code>chat_messages</code> — fetch all messages in a conversation mutated after a timestamp</li>
376
+ <li><code>{ userId, tenantId, updatedAt }</code> on <code>chat_conversation_participants</code> — fetch all participant records for a user mutated after a timestamp</li>
377
+ <li><code>{ userId, tenantId, updatedAt }</code> on <code>chat_message_stars</code> — fetch all star/unstar records for a user mutated after a timestamp</li>
378
+ </ul>
379
+ </li>
380
+ </ul>
381
+ <p><strong>Migrations 003 &amp; 004</strong> run automatically on first deploy:</p>
382
+ <ul>
383
+ <li><code>003_backfill_participant_updatedAt</code> — sets <code>updatedAt = joinedAt</code> on all existing participant documents that predate the schema change</li>
384
+ <li><code>004_backfill_star_isActive</code> — sets <code>isActive: true</code> on all existing star records (all surviving records represent active stars — hard-deleted unstar records no longer exist in the DB)</li>
385
+ </ul>
386
+ <p><strong>No integration changes required.</strong> All changes are server-side. The <code>getStarredMessages</code> API contract is unchanged.</p>
387
+ </div>
388
+ </div>
389
+
390
+ <div class="wn-item" id="wn-127-crossconv-sync">
391
+ <div class="wn-item-header" onclick="toggleItem('wn-127-crossconv-sync')">
392
+ <span class="wn-tag new">New</span>
393
+ <span class="wn-item-title">Offline delta sync — <code>GET /sync</code> cross-conversation reconnect sync</span>
394
+ <span class="wn-chevron-sm">▾</span>
395
+ </div>
396
+ <div class="wn-item-body">
397
+ <p>Mobile apps can now catch up on all missed changes in a single HTTP request after a socket reconnect. <code>GET /sync?since=&lt;ISO timestamp&gt;</code> returns every cross-conversation delta since the cursor — edits, deletes, pins, participant changes, read receipts, and messages deleted-for-me — across all conversations the user belongs to.</p>
398
+ <table>
399
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
400
+ <tbody>
401
+ <tr><td><code>syncedAt</code></td><td><code>string</code></td><td>Server-stamped ISO timestamp — store as the new <code>since</code> cursor. Always use server clock to avoid client clock skew.</td></tr>
402
+ <tr><td><code>stale</code></td><td><code>boolean</code></td><td><code>true</code> when gap exceeds the stale threshold (default 30 days, configurable via <code>CHAT_SYNC_STALE_DAYS</code> env var). No data returned — mark all conversations <code>needs_refresh</code> and lazy-sync on open.</td></tr>
403
+ <tr><td><code>messages</code></td><td><code>Message[]</code></td><td>Messages with <code>updatedAt &gt; since</code> — covers edits, deletes, pins, unpins. Excludes messages deleted-for-me (those are in <code>deletedForMe</code>).</td></tr>
404
+ <tr><td><code>deletedForMe</code></td><td><code>SyncDeletedForMe[]</code></td><td>Messages this user hid for themselves — remove from local DB.</td></tr>
405
+ <tr><td><code>participantChanges</code></td><td><code>SyncParticipantChange[]</code></td><td>Role changes, mute/unmute, removals (<code>isActive: false</code>) across all conversations.</td></tr>
406
+ <tr><td><code>readReceipts</code></td><td><code>SyncReadReceipt[]</code></td><td>Read receipts received since <code>since</code>.</td></tr>
407
+ </tbody>
408
+ </table>
409
+ <p><strong>Stale handling</strong> — when <code>stale: true</code> the server still returns the new <code>syncedAt</code> cursor. Save it and mark every conversation <code>needs_refresh = true</code> in local SQLite; open <code>GET /conversations/:id/sync</code> lazily when the user navigates to each conversation.</p>
410
+ <pre><code><span class="kw">import</span> { syncApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
411
+
412
+ <span class="cm">// Using @antzsoft/chat-rn-sdk built-in hook (recommended for RN)</span>
413
+ <span class="kw">import</span> { useSync } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
414
+
415
+ useSync({
416
+ onSync: <span class="kw">async</span> (data) =&gt; {
417
+ <span class="cm">// data: CrossConversationSyncResponse</span>
418
+ <span class="kw">await</span> mergeSyncDeltaIntoSQLite(data);
419
+ },
420
+ onStale: <span class="kw">async</span> () =&gt; {
421
+ <span class="cm">// Gap > stale threshold (default 30 days) — lazy-sync per conversation on open</span>
422
+ <span class="kw">await</span> db.execute(<span class="str">'UPDATE conversations SET needs_refresh = 1'</span>);
423
+ },
424
+ });
425
+
426
+ <span class="cm">// Or call syncApi directly (custom integration)</span>
427
+ <span class="kw">const</span> result = <span class="kw">await</span> syncApi.<span class="fn">pull</span>(lastSyncedAt);
428
+ <span class="kw">await</span> AsyncStorage.<span class="fn">setItem</span>(<span class="str">'@antz_last_synced_at'</span>, result.syncedAt);
429
+ <span class="kw">if</span> (result.stale) markAllNeedsRefresh();
430
+ <span class="kw">else</span> mergeSyncDeltaIntoSQLite(result);</code></pre>
431
+ <p><strong>Does not include reactions or stars</strong> — these are per-conversation and returned by <code>GET /conversations/:id/sync</code>.</p>
432
+ <p><strong>Web SDK users:</strong> the web SDK uses React Query cache invalidation on reconnect — no sync call needed. <code>syncApi</code> is intended for mobile (React Native) only.</p>
433
+ </div>
434
+ </div>
435
+
436
+ <div class="wn-item" id="wn-127-conv-sync">
437
+ <div class="wn-item-header" onclick="toggleItem('wn-127-conv-sync')">
438
+ <span class="wn-tag new">New</span>
439
+ <span class="wn-item-title">Offline delta sync — <code>GET /conversations/:id/sync</code> per-conversation full delta</span>
440
+ <span class="wn-chevron-sm">▾</span>
441
+ </div>
442
+ <div class="wn-item-body">
443
+ <p>Returns the complete delta for a single conversation, including reactions and stars. Call this when a conversation is marked <code>needs_refresh</code> (gap exceeded the stale threshold) or when a fresh-install user opens a conversation for the first time (omit <code>since</code> to receive the full history).</p>
444
+ <table>
445
+ <thead><tr><th>Field</th><th>Type</th><th>Description</th></tr></thead>
446
+ <tbody>
447
+ <tr><td><code>syncedAt</code></td><td><code>string</code></td><td>Server-stamped cursor — update <code>lastSyncedAt</code> after upsert.</td></tr>
448
+ <tr><td><code>messages</code></td><td><code>Message[]</code></td><td>All messages with <code>updatedAt &gt; since</code>. On fresh install (<code>since</code> omitted), returns all messages.</td></tr>
449
+ <tr><td><code>deletedForMe</code></td><td><code>SyncDeletedForMe[]</code></td><td>Messages deleted for this user — remove from local DB.</td></tr>
450
+ <tr><td><code>reactions</code></td><td><code>SyncReactions</code></td><td>Full current emoji counts for every message that had reaction activity since <code>since</code>. <strong>Always upsert as ground truth</strong> — reactions are hard-deleted so delta is not safe.</td></tr>
451
+ <tr><td><code>stars</code></td><td><code>SyncStarEntry[]</code></td><td><code>isActive: true</code> = starred, <code>isActive: false</code> = unstarred (soft-delete). Delete local star record when <code>false</code>.</td></tr>
452
+ <tr><td><code>participantChanges</code></td><td><code>SyncParticipantChange[]</code></td><td>Role changes, mutes, removals for this conversation.</td></tr>
453
+ <tr><td><code>readReceipts</code></td><td><code>SyncReadReceipt[]</code></td><td>Read receipts received since <code>since</code>.</td></tr>
454
+ </tbody>
455
+ </table>
456
+ <p><strong>Reactions are always returned as full current state</strong> for the affected messages — not as a delta. Because reaction removes are hard-deleted (no timestamp trace), a delta is impossible. Upsert the reaction counts returned and discard any locally cached state for those message IDs.</p>
457
+ <pre><code><span class="kw">import</span> { useConversationSync } <span class="kw">from</span> <span class="str">'@antzsoft/chat-rn-sdk'</span>;
458
+
459
+ <span class="cm">// In your conversation screen</span>
460
+ <span class="kw">const</span> { isSyncing, sync } = <span class="fn">useConversationSync</span>(conversationId);
461
+
462
+ <span class="cm">// Trigger when conversation is marked stale</span>
463
+ <span class="fn">useEffect</span>(() =&gt; {
464
+ <span class="kw">if</span> (needsRefresh) {
465
+ <span class="fn">sync</span>().then((data) =&gt; {
466
+ <span class="kw">if</span> (data) upsertConversationDeltaToSQLite(data);
467
+ clearNeedsRefresh(conversationId);
468
+ });
469
+ }
470
+ }, [conversationId, needsRefresh]);
471
+
472
+ <span class="cm">// Fresh install — omit since to get full history</span>
473
+ <span class="kw">const</span> { sync: fullSync } = <span class="fn">useConversationSync</span>(conversationId, { skipSince: <span class="kw">true</span> });
474
+
475
+ <span class="cm">// Or call syncApi directly</span>
476
+ <span class="kw">const</span> result = <span class="kw">await</span> syncApi.<span class="fn">pullConversation</span>(conversationId, lastSyncedAt);
477
+ <span class="kw">await</span> upsertConversationDeltaToSQLite(result);</code></pre>
478
+ <p><strong>Authorization</strong> — server verifies the caller is an active participant. Non-participants receive <code>403 Forbidden</code>.</p>
479
+ <p><strong>No integration changes required</strong> for web or existing RN users. RN apps that want offline sync should add <code>useSync</code> at the root provider level and <code>useConversationSync</code> in each conversation screen. See the <a href="#syncApi">Sync API reference</a> in the README for type signatures and full usage examples.</p>
480
+ </div>
481
+ </div>
482
+
483
+ </div>
484
+ </div><!-- /.wn-version -->
485
+
486
+ <!-- ── v1.2.6 ── -->
487
+ <div class="wn-version" id="wn-126">
488
+ <div class="wn-header" onclick="toggleVersion('wn-126')">
489
+ <div class="wn-title">
490
+ <span class="wn-ver">v1.2.6</span>
491
+ <span class="wn-date">June 2026</span>
492
+ </div>
493
+ <span class="wn-chevron">▼</span>
494
+ </div>
263
495
  <div class="wn-body">
264
496
 
265
497
  <div class="wn-item" id="wn-126-clearchat">
@@ -280,7 +512,7 @@ section.sec>h2:hover{color:#fff}
280
512
  </tbody>
281
513
  </table>
282
514
  <p><strong>Message history after re-add / reappear:</strong> Membership periods are wiped on "Delete Chat" and "Exit and Delete". When the caller is re-added to a group or the other party messages in a DM, only messages from that point onwards are visible — no prior history leaks through.</p>
283
- <p><strong>Socket event:</strong> <code>conversation_deleted</code> is emitted to the caller's own sockets when <code>delete()</code> is called — remove the conversation from local state and navigate away if it was open. Other participants do not receive this event.</p>
515
+ <p><strong>Socket event:</strong> <code>conversation_deleted</code> is emitted <strong>only to the acting user's own sockets</strong> in three cases: (1) <code>delete()</code> self-delete/hide a DM or group, (2) <code>leave(id, true)</code> exit and delete a group atomically, (3) <code>delete()</code> called on an already-exited group. In all cases, remove the conversation from local state and navigate away if it was open. Other participants never receive this event.</p>
284
516
  <pre><code><span class="kw">import</span> { conversationsApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
285
517
 
286
518
  <span class="cm">// ── DM: "Delete Chat" ─────────────────────────────────────────────────</span>
@@ -393,7 +625,7 @@ section.sec>h2:hover{color:#fff}
393
625
  <button class="wn-history-btn" onclick="openVersionHistory()">
394
626
  <span class="wn-history-icon">⏱</span>
395
627
  View version history
396
- <span class="wn-history-count">19 older versions</span>
628
+ <span class="wn-history-count">20 older versions</span>
397
629
  </button>
398
630
 
399
631
  </section>
@@ -3166,7 +3398,7 @@ socket.<span class="fn">on</span>(<span class="str">'message_deleted'</span>, (e
3166
3398
  </tr>
3167
3399
  <tr>
3168
3400
  <td><code>'conversation_deleted'</code></td>
3169
- <td>A conversation was deleted</td>
3401
+ <td>Emitted only to the acting user's own sockets on: self-delete (<code>delete()</code>), exit+delete (<code>leave(id, true)</code>), or hide-after-exit (<code>delete()</code> on an already-exited group). Other participants never receive this event.</td>
3170
3402
  <td><strong>App root / conversation list screen</strong> — remove the conversation from state and navigate away if it was open.</td>
3171
3403
  </tr>
3172
3404
  <tr>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antzsoft/chat-core",
3
- "version": "1.2.6",
3
+ "version": "1.2.8",
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",