@antzsoft/chat-core 1.2.1 → 1.2.3

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.2.0 Release Notes</a></li>
158
+ <li><a href="#whats-new">v1.2.3 Release Notes</a></li>
159
159
  </ul>
160
160
 
161
161
  <div class="section-label">Getting Started</div>
@@ -192,6 +192,7 @@ section.sec>h2:hover{color:#fff}
192
192
  <li><a href="#step-realtime">10. Real-time Events</a></li>
193
193
  <li><a href="#step-typing">11. Typing Indicators</a></li>
194
194
  <li><a href="#step-read">12. Read Receipts &amp; Last Seen</a></li>
195
+ <li><a href="#step-message-info">12b. Message Info Screen</a></li>
195
196
  <li><a href="#step-edit">13. Edit &amp; Delete</a></li>
196
197
  <li><a href="#step-search">14. Search Messages</a></li>
197
198
  <li><a href="#step-reactions">15. Reactions, Pin, Star</a></li>
@@ -242,16 +243,107 @@ section.sec>h2:hover{color:#fff}
242
243
  <h2>What's New</h2>
243
244
  <p style="color:var(--muted);font-size:13px;margin-bottom:20px">Version history and release notes. Click a version to expand.</p>
244
245
 
245
- <!-- ── v1.2.0 (current) ── -->
246
- <div class="wn-version open" id="wn-120">
247
- <div class="wn-header" onclick="toggleVersion('wn-120')">
246
+ <!-- ── v1.2.3 (current) ── -->
247
+ <div class="wn-version open" id="wn-123">
248
+ <div class="wn-header" onclick="toggleVersion('wn-123')">
248
249
  <div class="wn-title">
249
- <span class="wn-ver">v1.2.0</span>
250
+ <span class="wn-ver">v1.2.3</span>
250
251
  <span class="wn-badge current">Current</span>
251
252
  <span class="wn-date">May 2026</span>
252
253
  </div>
253
254
  <span class="wn-chevron">▲</span>
254
255
  </div>
256
+ <div class="wn-body">
257
+
258
+ <div class="wn-item" id="wn-123-1">
259
+ <div class="wn-item-header" onclick="toggleItem('wn-123-1')">
260
+ <span class="wn-tag new">New</span>
261
+ <span class="wn-item-title"><code>messagesApi.getReceipts(messageId)</code> — per-user read &amp; delivery receipt detail</span>
262
+ <span class="wn-chevron-sm">▾</span>
263
+ </div>
264
+ <div class="wn-item-body">
265
+ <p>New endpoint <code>GET /messages/:id/receipts</code> returns the full read and delivery receipt list for a single message, with user profiles (name, avatar) resolved server-side. Designed as the initial load for a "Read by / Delivered to" message info screen.</p>
266
+ <p>Response shape:</p>
267
+ <pre><code>{
268
+ messageId: string,
269
+ readBy: [{ userId, displayName, avatarUrl, readAt }],
270
+ deliveredTo: [{ userId, displayName, avatarUrl, deliveredAt }]
271
+ }</code></pre>
272
+ <p>No secondary user lookup needed — profiles are included. The "Sent to" bucket (not yet delivered) is derived client-side by subtracting <code>readBy</code> and <code>deliveredTo</code> user IDs from the conversation participant list.</p>
273
+ <p class="wn-ref">→ <a href="#step-message-info">Step 12b — Message Info Screen</a> for the complete pattern including live updates.</p>
274
+ </div>
275
+ </div>
276
+
277
+ <div class="wn-item" id="wn-123-2">
278
+ <div class="wn-item-header" onclick="toggleItem('wn-123-2')">
279
+ <span class="wn-tag fix">Fix</span>
280
+ <span class="wn-item-title"><code>message_delivered</code> now fires per recipient, not all-or-nothing</span>
281
+ <span class="wn-chevron-sm">▾</span>
282
+ </div>
283
+ <div class="wn-item-body">
284
+ <p>Previously <code>message_delivered</code> only fired to the sender when <em>all</em> recipients were online at the moment of sending — if even one recipient was offline, the sender received no delivery event at all until every recipient came online simultaneously (which often never happens in large groups).</p>
285
+ <p>The server now fires <code>message_delivered</code> once per online recipient at send time. The payload's <code>deliveredTo</code> field is now a single object <code>{ userId, deliveredAt }</code> (previously an array) — one event per person, matching how <code>read_receipt</code> works.</p>
286
+ <p>This means the delivery section of a message info screen builds up live one entry at a time, exactly as WhatsApp does.</p>
287
+ <p><strong>Backward compatible</strong> — existing <code>message_delivered</code> handlers in both the web and RN SDKs only read <code>event.messageId</code> to update the tick mark and are unaffected by the <code>deliveredTo</code> shape change. The event fires more frequently (good — more accurate delivery feedback) but the existing handler logic is identical.</p>
288
+ <p class="wn-ref">→ <a href="#step-message-info">Step 12b — Message Info Screen</a> · <a href="#step-realtime">Step 10 — Real-time Events</a></p>
289
+ </div>
290
+ </div>
291
+
292
+ <div class="wn-item" id="wn-123-3">
293
+ <div class="wn-item-header" onclick="toggleItem('wn-123-3')">
294
+ <span class="wn-tag new">New</span>
295
+ <span class="wn-item-title">Viewer-aware system message text — SDK resolves "You", "you", or full names automatically</span>
296
+ <span class="wn-chevron-sm">▾</span>
297
+ </div>
298
+ <div class="wn-item-body">
299
+ <p>System messages (member added/removed, admin promoted/demoted, group created/updated) now render differently depending on who is reading them — matching WhatsApp behaviour.</p>
300
+ <table>
301
+ <thead><tr><th>Viewer</th><th>Rendered text</th></tr></thead>
302
+ <tbody>
303
+ <tr><td>Actor (Anil removed Ajay)</td><td>"You removed Ajay Antony"</td></tr>
304
+ <tr><td>Target (Ajay was removed)</td><td>"Anil Rathod removed you"</td></tr>
305
+ <tr><td>Bystander (Saket)</td><td>"Anil Rathod removed Ajay Antony"</td></tr>
306
+ </tbody>
307
+ </table>
308
+ <p><strong>How it works:</strong> The server stores the neutral third-person form in <code>content.text</code> (used for push notifications and exports) and sends structured <code>actorUserId</code>, <code>actorUserName</code>, <code>targetUserId</code>, <code>targetUserName</code> fields in the message <code>metadata</code>. The SDK's <code>messagesApi.list()</code> applies <code>resolveSystemMessageText()</code> in-memory before returning messages — <code>content.text</code> is already correct by the time it reaches your UI. No frontend logic changes needed.</p>
309
+ <p><strong>Socket path:</strong> For raw <code>new_message</code> socket events handled outside the SDK pipeline (e.g. <code>AntzChatClient</code> headless usage), call <code>resolveSystemMessageText(message, currentUserId)</code> manually:</p>
310
+ <pre><code>import { resolveSystemMessageText, getAuthStore } from '@antzsoft/chat-core';
311
+
312
+ client.socket.on('new_message', (event) => {
313
+ const message = event.message;
314
+ if (message.content.type === 'system') {
315
+ const uid = getAuthStore().useAuthStore.getState().user?.id ?? '';
316
+ message.content.text = resolveSystemMessageText(message, uid);
317
+ }
318
+ appendMessageToView(message);
319
+ });</code></pre>
320
+ <p><strong>Backward compatible:</strong></p>
321
+ <ul>
322
+ <li>Old SDK + new server: <code>actorUserId</code> fields ignored, raw <code>content.text</code> displayed — degrades gracefully.</li>
323
+ <li>New SDK + old server: <code>actorUserId</code> absent, guard fires, raw <code>content.text</code> returned unchanged.</li>
324
+ </ul>
325
+ <p>Applies to: <code>group_created</code>, <code>user_added</code>, <code>user_removed</code>, <code>user_left</code>, <code>admin_promoted</code>, <code>admin_demoted</code>, <code>group_updated</code>.</p>
326
+ <p><strong>Also fixed in this release:</strong></p>
327
+ <ul>
328
+ <li><code>group_updated</code> system message text now includes the actor name ("Anil changed the group name to 'X'" instead of the previous "Group name changed to 'X'").</li>
329
+ <li>Push notification body for <code>admin_demoted</code> corrected from "made you member" to "removed you as admin".</li>
330
+ <li>Removed users who are offline at the time of removal now receive a push notification ("Anil Rathod removed you from the group") since the socket event cannot reach them.</li>
331
+ </ul>
332
+ </div>
333
+ </div>
334
+
335
+ </div>
336
+ </div><!-- /.wn-version -->
337
+
338
+ <!-- ── v1.2.0 ── -->
339
+ <div class="wn-version" id="wn-120">
340
+ <div class="wn-header" onclick="toggleVersion('wn-120')">
341
+ <div class="wn-title">
342
+ <span class="wn-ver">v1.2.0</span>
343
+ <span class="wn-date">May 2026</span>
344
+ </div>
345
+ <span class="wn-chevron">▾</span>
346
+ </div>
255
347
  <div class="wn-body">
256
348
 
257
349
  <div class="wn-item" id="wn-120-1">
@@ -2641,12 +2733,12 @@ socket.<span class="fn">on</span>(<span class="str">'message_deleted'</span>, (e
2641
2733
  </tr>
2642
2734
  <tr>
2643
2735
  <td><code>'message_delivered'</code></td>
2644
- <td>A single message you sent was delivered to all active recipients. Payload: <code>{ messageId, conversationId, deliveredTo: Array&lt;{ userId, deliveredAt }&gt; }</code></td>
2645
- <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
2736
+ <td>Fired to the sender each time a recipient receives a message <strong>one event per recipient</strong>. Fires at send time for online recipients, and again per recipient as offline recipients reconnect. Payload: <code>{ messageId, conversationId, deliveredTo: { userId, deliveredAt } }</code></td>
2737
+ <td><strong>Chat detail screen / message info screen</strong> — add on mount, remove on unmount.</td>
2646
2738
  </tr>
2647
2739
  <tr>
2648
2740
  <td><code>'messages_delivered'</code></td>
2649
- <td>Batch delivery catch-up when a recipient comes online. Payload: <code>{ conversationId, messageIds[], deliveredTo: string, deliveredAt }</code></td>
2741
+ <td>Batch delivery catch-up fired to the sender when an offline recipient reconnects and all pending messages are marked delivered in bulk. Payload: <code>{ conversationId, messageIds[], deliveredTo: string, deliveredAt }</code></td>
2650
2742
  <td><strong>Chat detail screen</strong> — add on mount, remove on unmount.</td>
2651
2743
  </tr>
2652
2744
  <tr>
@@ -2848,6 +2940,79 @@ socket?.<span class="fn">on</span>(<span class="str">'read_receipt'</span>, ({ c
2848
2940
  </table>
2849
2941
  </section>
2850
2942
 
2943
+ <!-- ─── STEP 12b: MESSAGE INFO SCREEN ────────────────────────────────── -->
2944
+ <section id="step-message-info">
2945
+ <h2><span class="step">STEP 12b</span> Message Info Screen (Read by / Delivered to)</h2>
2946
+
2947
+ <p>The message info screen shows all group participants split into three buckets: <strong>Read by</strong>, <strong>Delivered to</strong>, and <strong>Sent to</strong> (not yet received). The list updates live as participants read or receive the message.</p>
2948
+
2949
+ <h3>Initial load</h3>
2950
+ <p>Call <code>messagesApi.getReceipts(messageId)</code> on open. This returns <code>readBy[]</code> and <code>deliveredTo[]</code> with resolved user profiles — no secondary lookup needed.</p>
2951
+
2952
+ <pre><code><span class="kw">import</span> { messagesApi, conversationsApi } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
2953
+ <span class="kw">import</span> <span class="tp">{ MessageReceiptsResponse }</span> <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
2954
+
2955
+ <span class="cm">// Fetch receipts + participants in parallel</span>
2956
+ <span class="kw">const</span> [receipts, conversation] = <span class="kw">await</span> Promise.<span class="fn">all</span>([
2957
+ messagesApi.<span class="fn">getReceipts</span>(messageId),
2958
+ conversationsApi.<span class="fn">get</span>(conversationId),
2959
+ ]);
2960
+
2961
+ <span class="kw">const</span> readByIds = <span class="kw">new</span> <span class="fn">Set</span>(receipts.readBy.<span class="fn">map</span>(r => r.userId));
2962
+ <span class="kw">const</span> deliveredIds = <span class="kw">new</span> <span class="fn">Set</span>(receipts.deliveredTo.<span class="fn">map</span>(d => d.userId));
2963
+
2964
+ <span class="kw">const</span> sentTo = conversation.participants
2965
+ .<span class="fn">filter</span>(p => !readByIds.<span class="fn">has</span>(p.userId) && !deliveredIds.<span class="fn">has</span>(p.userId) && p.userId !== senderId)
2966
+ .<span class="fn">map</span>(p => p.user);</code></pre>
2967
+
2968
+ <h3>Live updates</h3>
2969
+ <p>Subscribe to <code>read_receipt</code> and <code>message_delivered</code> while the screen is open. Both fire per-person so the buckets update one entry at a time.</p>
2970
+
2971
+ <pre><code><span class="kw">import</span> { tryGetSocket } <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
2972
+ <span class="kw">import</span> <span class="tp">{ ReadReceiptEvent, MessageDeliveredEvent }</span> <span class="kw">from</span> <span class="str">'@antzsoft/chat-core'</span>;
2973
+
2974
+ <span class="kw">const</span> socket = <span class="fn">tryGetSocket</span>();
2975
+
2976
+ <span class="kw">function</span> <span class="fn">onReadReceipt</span>(evt: <span class="tp">ReadReceiptEvent</span>) {
2977
+ <span class="kw">if</span> (!evt.updatedMessageIds?.<span class="fn">includes</span>(messageId)) <span class="kw">return</span>;
2978
+ <span class="cm">// Move evt.userId from deliveredTo / sentTo → readBy</span>
2979
+ <span class="fn">addToReadBy</span>({ userId: evt.userId, readAt: evt.readAt });
2980
+ <span class="fn">removeFromDeliveredTo</span>(evt.userId);
2981
+ <span class="fn">removeFromSentTo</span>(evt.userId);
2982
+ }
2983
+
2984
+ <span class="kw">function</span> <span class="fn">onMessageDelivered</span>(evt: <span class="tp">MessageDeliveredEvent</span>) {
2985
+ <span class="kw">if</span> (evt.messageId !== messageId) <span class="kw">return</span>;
2986
+ <span class="cm">// Move evt.deliveredTo.userId from sentTo → deliveredTo</span>
2987
+ <span class="fn">addToDeliveredTo</span>(evt.deliveredTo);
2988
+ <span class="fn">removeFromSentTo</span>(evt.deliveredTo.userId);
2989
+ }
2990
+
2991
+ socket?.<span class="fn">on</span>(<span class="str">'read_receipt'</span>, onReadReceipt);
2992
+ socket?.<span class="fn">on</span>(<span class="str">'message_delivered'</span>, onMessageDelivered);
2993
+
2994
+ <span class="cm">// Clean up on screen close</span>
2995
+ <span class="kw">return</span> () => {
2996
+ socket?.<span class="fn">off</span>(<span class="str">'read_receipt'</span>, onReadReceipt);
2997
+ socket?.<span class="fn">off</span>(<span class="str">'message_delivered'</span>, onMessageDelivered);
2998
+ };</code></pre>
2999
+
3000
+ <div class="callout tip">
3001
+ <strong>Three-bucket logic</strong> — All group participants minus the sender form the full list. <code>readBy</code> takes priority: once someone is in <code>readBy</code>, remove them from <code>deliveredTo</code> and <code>sentTo</code>. <code>deliveredTo</code> takes priority over <code>sentTo</code>. The <code>sentTo</code> bucket is always derived as the remainder.
3002
+ </div>
3003
+
3004
+ <h4>Summary — what is automatic vs. what you do</h4>
3005
+ <table>
3006
+ <thead><tr><th>Action</th><th>Automatic?</th><th>What you do</th></tr></thead>
3007
+ <tbody>
3008
+ <tr><td>Initial receipt list with user profiles</td><td>❌ Manual</td><td>Call <code>messagesApi.getReceipts(messageId)</code> on screen open</td></tr>
3009
+ <tr><td>Live read updates</td><td>❌ Manual</td><td>Listen to <code>read_receipt</code>, check <code>updatedMessageIds</code></td></tr>
3010
+ <tr><td>Live delivery updates</td><td>❌ Manual</td><td>Listen to <code>message_delivered</code>, check <code>messageId</code></td></tr>
3011
+ <tr><td>Participant list for "Sent to" bucket</td><td>❌ Manual</td><td>Fetch via <code>conversationsApi.get(conversationId)</code></td></tr>
3012
+ </tbody>
3013
+ </table>
3014
+ </section>
3015
+
2851
3016
  <!-- ─── STEP 13: EDIT & DELETE ────────────────────────────────────────── -->
2852
3017
  <section id="step-edit">
2853
3018
  <h2><span class="step">STEP 13</span> Edit &amp; Delete Messages</h2>
@@ -3432,6 +3597,133 @@ self.<span class="fn">addEventListener</span>(<span class="str">'notificationcli
3432
3597
  simultaneously when a notification is triggered. Removing a token only deactivates that specific
3433
3598
  device; other devices continue to receive notifications.
3434
3599
  </p>
3600
+
3601
+ <!-- ── Notification payload + channel/category setup (RN only) ──────── -->
3602
+ <div data-p="rn">
3603
+ <h3>Handling incoming notifications</h3>
3604
+
3605
+ <h4>Notification payload</h4>
3606
+ <p>
3607
+ Every push notification the server sends includes <code>channelId</code> and <code>categoryId</code>
3608
+ at the top level (read by the OS / Expo SDK) and mirrored inside the <code>data</code> object
3609
+ (readable by your JS notification handler):
3610
+ </p>
3611
+ <pre><code>{
3612
+ <span class="str">"title"</span>: <span class="str">"Alice replied to you"</span>,
3613
+ <span class="str">"body"</span>: <span class="str">"Sounds good!"</span>,
3614
+ <span class="str">"priority"</span>: <span class="str">"high"</span>,
3615
+ <span class="str">"channelId"</span>: <span class="str">"reply"</span>,
3616
+ <span class="str">"categoryId"</span>: <span class="str">"reply"</span>,
3617
+ <span class="str">"data"</span>: {
3618
+ <span class="str">"event"</span>: <span class="str">"reply"</span>,
3619
+ <span class="str">"channelId"</span>: <span class="str">"reply"</span>,
3620
+ <span class="str">"categoryId"</span>: <span class="str">"reply"</span>,
3621
+ <span class="str">"conversation_id"</span>: <span class="str">"..."</span>,
3622
+ <span class="str">"message_id"</span>: <span class="str">"..."</span>,
3623
+ <span class="str">"original_message_id"</span>: <span class="str">"..."</span>,
3624
+ <span class="str">"sender_id"</span>: <span class="str">"..."</span>,
3625
+ <span class="str">"sender_name"</span>: <span class="str">"Alice"</span>
3626
+ }
3627
+ }</code></pre>
3628
+
3629
+ <h4>Per-event channelId and categoryId</h4>
3630
+ <table style="width:100%;border-collapse:collapse;font-size:13px;margin:12px 0">
3631
+ <thead><tr style="border-bottom:1px solid var(--border)">
3632
+ <th style="text-align:left;padding:6px 10px;color:var(--muted)">Event</th>
3633
+ <th style="text-align:left;padding:6px 10px;color:var(--muted)">channelId</th>
3634
+ <th style="text-align:left;padding:6px 10px;color:var(--muted)">categoryId</th>
3635
+ <th style="text-align:left;padding:6px 10px;color:var(--muted)">Notes</th>
3636
+ </tr></thead>
3637
+ <tbody>
3638
+ <tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>dm_message</code></td><td style="padding:6px 10px"><code>messages</code></td><td style="padding:6px 10px"><code>message</code></td><td style="padding:6px 10px">Direct message</td></tr>
3639
+ <tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>group_message</code></td><td style="padding:6px 10px"><code>messages</code></td><td style="padding:6px 10px"><code>message</code></td><td style="padding:6px 10px">Group message</td></tr>
3640
+ <tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>mention</code></td><td style="padding:6px 10px"><code>messages</code></td><td style="padding:6px 10px"><code>message</code></td><td style="padding:6px 10px">Mentioned in a group</td></tr>
3641
+ <tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>reply</code></td><td style="padding:6px 10px"><code>reply</code></td><td style="padding:6px 10px"><code>reply</code></td><td style="padding:6px 10px">Reply to your message</td></tr>
3642
+ <tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>dm_reaction</code> / <code>group_reaction</code></td><td style="padding:6px 10px"><code>reactions</code></td><td style="padding:6px 10px"><code>reaction</code></td><td style="padding:6px 10px">Emoji reaction</td></tr>
3643
+ <tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>group_invite</code></td><td style="padding:6px 10px"><code>group_invites</code></td><td style="padding:6px 10px"><code>invite</code></td><td style="padding:6px 10px">Added to a group</td></tr>
3644
+ <tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>group_removed</code></td><td style="padding:6px 10px"><code>group_updates</code></td><td style="padding:6px 10px">—</td><td style="padding:6px 10px">Removed from a group</td></tr>
3645
+ <tr style="border-bottom:1px solid var(--border)"><td style="padding:6px 10px"><code>group_role_changed</code></td><td style="padding:6px 10px"><code>group_updates</code></td><td style="padding:6px 10px">—</td><td style="padding:6px 10px">Role changed in a group</td></tr>
3646
+ <tr><td style="padding:6px 10px"><code>message_edited</code> / <code>message_deleted</code></td><td style="padding:6px 10px"><code>silent</code></td><td style="padding:6px 10px">—</td><td style="padding:6px 10px">Silent background sync, no sound</td></tr>
3647
+ </tbody>
3648
+ </table>
3649
+ <div class="callout tip">
3650
+ <strong>channelId</strong> is Android-only (Android 8+) — controls sound, importance, and DND behaviour per channel.<br>
3651
+ <strong>categoryId</strong> is iOS-only — links to interactive action buttons registered with <code>setNotificationCategoryAsync</code>.<br>
3652
+ If the channel/category is not registered yet on the client, the OS falls back to default behaviour — notifications still arrive normally.
3653
+ </div>
3654
+
3655
+ <h4>Android — register channels at app startup</h4>
3656
+ <p>Create all channels unconditionally on every app launch, before the first notification arrives:</p>
3657
+ <pre><code><span class="kw">import</span> * <span class="kw">as</span> Notifications <span class="kw">from</span> <span class="str">'expo-notifications'</span>;
3658
+
3659
+ <span class="kw">async function</span> <span class="fn">setupNotificationChannels</span>() {
3660
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'messages'</span>, {
3661
+ name: <span class="str">'Messages'</span>,
3662
+ importance: Notifications.AndroidImportance.HIGH,
3663
+ sound: <span class="str">'default'</span>,
3664
+ });
3665
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'reply'</span>, {
3666
+ name: <span class="str">'Replies'</span>,
3667
+ importance: Notifications.AndroidImportance.HIGH,
3668
+ sound: <span class="str">'default'</span>,
3669
+ });
3670
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'reactions'</span>, {
3671
+ name: <span class="str">'Reactions'</span>,
3672
+ importance: Notifications.AndroidImportance.DEFAULT,
3673
+ });
3674
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'group_invites'</span>, {
3675
+ name: <span class="str">'Group Invites'</span>,
3676
+ importance: Notifications.AndroidImportance.HIGH,
3677
+ sound: <span class="str">'default'</span>,
3678
+ });
3679
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'group_updates'</span>, {
3680
+ name: <span class="str">'Group Updates'</span>,
3681
+ importance: Notifications.AndroidImportance.LOW,
3682
+ });
3683
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationChannelAsync</span>(<span class="str">'silent'</span>, {
3684
+ name: <span class="str">'Background Sync'</span>,
3685
+ importance: Notifications.AndroidImportance.MIN,
3686
+ sound: <span class="kw">undefined</span>,
3687
+ enableVibrate: <span class="kw">false</span>,
3688
+ });
3689
+ }</code></pre>
3690
+
3691
+ <h4>iOS — register categories with interactive actions</h4>
3692
+ <p>Register categories before requesting push permission. The <code>reply</code> category adds a Reply button when the app is killed:</p>
3693
+ <pre><code><span class="kw">import</span> * <span class="kw">as</span> Notifications <span class="kw">from</span> <span class="str">'expo-notifications'</span>;
3694
+
3695
+ <span class="kw">async function</span> <span class="fn">setupNotificationCategories</span>() {
3696
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationCategoryAsync</span>(<span class="str">'reply'</span>, [
3697
+ {
3698
+ identifier: <span class="str">'REPLY_ACTION'</span>,
3699
+ buttonTitle: <span class="str">'Reply'</span>,
3700
+ textInput: { submitButtonTitle: <span class="str">'Send'</span>, placeholder: <span class="str">'Type a reply...'</span> },
3701
+ },
3702
+ ]);
3703
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationCategoryAsync</span>(<span class="str">'message'</span>, [
3704
+ {
3705
+ identifier: <span class="str">'REPLY_ACTION'</span>,
3706
+ buttonTitle: <span class="str">'Reply'</span>,
3707
+ textInput: { submitButtonTitle: <span class="str">'Send'</span>, placeholder: <span class="str">'Type a reply...'</span> },
3708
+ },
3709
+ ]);
3710
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationCategoryAsync</span>(<span class="str">'reaction'</span>, []);
3711
+ <span class="kw">await</span> Notifications.<span class="fn">setNotificationCategoryAsync</span>(<span class="str">'invite'</span>, []);
3712
+ }</code></pre>
3713
+
3714
+ <h4>Reading data in your notification handler</h4>
3715
+ <p>Use <code>data.conversation_id</code> for deep-linking and <code>data.event</code> to distinguish notification types:</p>
3716
+ <pre><code><span class="kw">import</span> * <span class="kw">as</span> Notifications <span class="kw">from</span> <span class="str">'expo-notifications'</span>;
3717
+
3718
+ Notifications.<span class="fn">addNotificationResponseReceivedListener</span>((response) => {
3719
+ <span class="kw">const</span> { event, conversation_id, channelId } =
3720
+ response.notification.request.content.data;
3721
+
3722
+ <span class="kw">if</span> (conversation_id) {
3723
+ navigation.<span class="fn">navigate</span>(<span class="str">'Chat'</span>, { conversationId: conversation_id });
3724
+ }
3725
+ });</code></pre>
3726
+ </div>
3435
3727
  </section>
3436
3728
 
3437
3729
  <!-- ─── STEP 17.5: NOTIFICATION PREFERENCES ──────────────────────────── -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@antzsoft/chat-core",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
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",