@antzsoft/chat-core 1.2.2 → 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.
package/README.md CHANGED
@@ -740,6 +740,7 @@ import { messagesApi } from '@antzsoft/chat-core';
740
740
  | `search` | `(params: SearchParams) => Promise<PaginatedResponse<Message>>` | Full-text message search. |
741
741
  | `getLastRead` | `(conversationId: string) => Promise<{ lastReadMessageId: string \| null; lastReadAt: string \| null }>` | Fetch the current user's last-read pointer for a conversation. Use on initial load; after that the store is kept live by socket events. |
742
742
  | `markAsRead` | `(conversationId: string, messageId?: string) => Promise<void>` | Mark messages as read via REST. |
743
+ | `getReceipts` | `(messageId: string) => Promise<MessageReceiptsResponse>` | Fetch per-user read and delivery receipts for a single message, with resolved user profiles (name, avatar). Use as the initial load for a message info / "Read by" detail screen. |
743
744
  | `pin` | `(messageId: string) => Promise<Message>` | Pin a message. |
744
745
  | `unpin` | `(messageId: string) => Promise<Message>` | Unpin a message. |
745
746
  | `getPinned` | `(conversationId: string) => Promise<Message[]>` | List pinned messages in a conversation. |
@@ -1809,8 +1810,8 @@ Subscribe using `client.socket.on(event, handler)` (headless) or directly on the
1809
1810
  | `read_receipt` | `ReadReceiptEvent` | A user read messages in a conversation. | **Chat detail screen** (to update tick marks) + **app root** (to clear your own unread count when read on another device). |
1810
1811
  | `unread_count_changed` | `{ conversationId: string; unreadCount: number; userId: string }` | Your unread count changed for a conversation (fired to your personal room on all devices). | **App root / conversation list screen** — keep alive as long as the list is rendered. |
1811
1812
  | `message_ack` | `MessageAckEvent` | Server confirmation for a message you sent via socket (maps tempId to the real messageId). | **Chat detail screen** — add on mount, remove on unmount. |
1812
- | `message_delivered` | `MessageDeliveredEvent` | A single message you sent was delivered to all active recipients. | **Chat detail screen** — add on mount, remove on unmount. |
1813
- | `messages_delivered` | `MessagesDeliveredEvent` | Batch delivery catch-up — fired when a recipient comes online and your pending messages are delivered. | **Chat detail screen** — add on mount, remove on unmount. |
1813
+ | `message_delivered` | `MessageDeliveredEvent` | Fired to the sender each time a recipient receives a message one event per recipient. Fires immediately at send time for online recipients, and again per recipient as offline recipients reconnect. Payload: `{ messageId, conversationId, deliveredTo: { userId, deliveredAt } }` | **Chat detail screen** — add on mount, remove on unmount. |
1814
+ | `messages_delivered` | `MessagesDeliveredEvent` | Batch delivery catch-up — fired to the sender when an offline recipient reconnects and all pending undelivered messages are marked delivered in bulk. Payload: `{ conversationId, messageIds[], deliveredTo: string, deliveredAt }` | **Chat detail screen** — add on mount, remove on unmount. |
1814
1815
  | `conversation_created` | `Conversation` | A new conversation was created (or you were added to one). | **App root** — call `joinRoom` here for the new conversation. Keep for full session. |
1815
1816
  | `conversation_updated` | `Conversation` | A conversation's last message or metadata changed — use this to update the conversation list. | **App root** — keep for full session. The server emits this for every message across all conversations; a global listener keeps the in-memory conversation list and unread badge always in sync. |
1816
1817
  | `conversation_deleted` | `{ conversationId: string }` | A conversation was deleted. | **App root / conversation list screen** — remove from state and navigate away if it was open. |
@@ -2090,6 +2091,7 @@ interface Message {
2090
2091
  conversationId: string;
2091
2092
  senderId: string;
2092
2093
  content: MessageContent;
2094
+ metadata?: MessageMetadata;
2093
2095
  replyTo?: MessageReplyReference;
2094
2096
  reactions: MessageReaction[];
2095
2097
  status: 'sent' | 'delivered' | 'read' | 'failed' | 'deleted';
@@ -2109,6 +2111,80 @@ interface Message {
2109
2111
  }
2110
2112
  ```
2111
2113
 
2114
+ ### `MessageMetadata`
2115
+
2116
+ Present on all messages. For system messages, the actor/target fields power viewer-aware text resolution inside the SDK — `content.text` is already resolved by the time the message reaches your code.
2117
+
2118
+ ```typescript
2119
+ interface MessageMetadata {
2120
+ type: string;
2121
+ hasAttachments: boolean;
2122
+ containsLink: boolean;
2123
+ // System messages only
2124
+ systemOperationType?: string; // 'group_created' | 'user_added' | 'user_removed' | 'user_left' | 'admin_promoted' | 'admin_demoted' | 'group_updated'
2125
+ actorUserId?: string; // ID of the user who performed the action
2126
+ actorUserName?: string; // Display name of the actor
2127
+ targetUserId?: string; // ID of the user the action was performed on
2128
+ targetUserName?: string; // Display name of the target
2129
+ }
2130
+ ```
2131
+
2132
+ ### `resolveSystemMessageText`
2133
+
2134
+ A pure utility function exported from the package for advanced use cases. The SDK calls this automatically inside `messagesApi.list()` — you only need it if you process raw messages outside the SDK pipeline (e.g. a custom socket handler with `AntzChatClient`).
2135
+
2136
+ ```typescript
2137
+ import { resolveSystemMessageText } from '@antzsoft/chat-core';
2138
+
2139
+ client.socket.on('new_message', (event: NewMessageEvent) => {
2140
+ const message = event.message;
2141
+ if (message.content.type === 'system') {
2142
+ const currentUserId = getAuthStore().useAuthStore.getState().user?.id ?? '';
2143
+ message.content.text = resolveSystemMessageText(message, currentUserId);
2144
+ }
2145
+ appendMessageToView(message);
2146
+ });
2147
+ ```
2148
+
2149
+ **Viewer-aware output for `user_removed` (currentUserId = Anil's ID):**
2150
+
2151
+ | Viewer | `content.text` after resolution |
2152
+ |--------|--------------------------------|
2153
+ | Anil (actor) | `"You removed Ajay Antony"` |
2154
+ | Ajay (target) | `"Anil Rathod removed you"` |
2155
+ | Saket (bystander) | `"Anil Rathod removed Ajay Antony"` |
2156
+
2157
+ The function falls back to the raw `content.text` from the server if `metadata.actorUserId` is absent (old server compatibility).
2158
+
2159
+ ### `MessageReceiptsResponse`
2160
+
2161
+ Returned by `messagesApi.getReceipts()`. Use as the initial load for a "Read by / Delivered to" detail screen. User profiles (name, avatar) are resolved server-side so no secondary lookup is needed.
2162
+
2163
+ ```typescript
2164
+ interface MessageReceiptEntry {
2165
+ userId: string;
2166
+ displayName: string;
2167
+ avatarUrl?: string;
2168
+ }
2169
+
2170
+ interface MessageReceiptsResponse {
2171
+ messageId: string;
2172
+ readBy: Array<MessageReceiptEntry & { readAt: string }>;
2173
+ deliveredTo: Array<MessageReceiptEntry & { deliveredAt: string }>;
2174
+ }
2175
+ ```
2176
+
2177
+ ```typescript
2178
+ // Initial load for the detail screen
2179
+ const receipts = await messagesApi.getReceipts(messageId);
2180
+
2181
+ // receipts.readBy → participants who have read the message (with timestamps)
2182
+ // receipts.deliveredTo → participants who received it but haven't read it yet
2183
+ // "Sent to" bucket → conversation.participants minus the above two sets
2184
+ ```
2185
+
2186
+ ---
2187
+
2112
2188
  ### `MessageContent`
2113
2189
 
2114
2190
  ```typescript
@@ -2451,6 +2527,10 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
2451
2527
 
2452
2528
  ## Changelog
2453
2529
 
2530
+ ### v1.2.3
2531
+ - **New: `messagesApi.getReceipts(messageId)` — message info screen API** — New `GET /messages/:id/receipts` endpoint returns per-user read and delivery receipts for a single message with resolved user profiles (name, avatar) included. Use as the initial data load for a "Read by / Delivered to" detail screen — no secondary user lookup needed. Returns `{ messageId, readBy: [{ userId, displayName, avatarUrl, readAt }], deliveredTo: [{ userId, displayName, avatarUrl, deliveredAt }] }`. New exported types: `MessageReceiptsResponse`, `MessageReceiptEntry`.
2532
+ - **Fix: `message_delivered` now fires per recipient, not all-or-nothing** — Previously the server only emitted `message_delivered` to the sender when every recipient was online simultaneously at send time. It now fires once per online recipient at send time. The `deliveredTo` field is now a single `{ userId, deliveredAt }` object (previously an array of all recipients). This matches how `read_receipt` works and enables the delivery section of a message info screen to update live one entry at a time. **Backward compatible** — existing handlers only read `event.messageId` to update the tick mark and are unaffected.
2533
+
2454
2534
  ### v1.2.0
2455
2535
  - **Fix: `getSocket()` and `tryGetSocket()` now return the transit-aware proxy** — Previously these functions returned the raw Socket.IO socket, bypassing the `secureOn` decryption wrapper. Any integrator calling `getSocket().on('new_message', ...)` or `tryGetSocket().on(...)` directly would receive raw encrypted envelopes (`{v:1, iv:..., ct:...}`) instead of decrypted payloads. Both functions now return the same secure proxy that `connectSocket()` returns — all `.on()` calls automatically decrypt via transit when enabled. **No integration changes required — existing code that uses `getSocket()` or `tryGetSocket()` will now transparently receive decrypted data.**
2456
2536
 
package/dist/index.cjs CHANGED
@@ -35,12 +35,12 @@ var chat_store_exports = {};
35
35
  __export(chat_store_exports, {
36
36
  useChatStore: () => useChatStore
37
37
  });
38
- var import_zustand, useChatStore;
38
+ var import_zustand2, useChatStore;
39
39
  var init_chat_store = __esm({
40
40
  "src/stores/chat.store.ts"() {
41
41
  "use strict";
42
- import_zustand = require("zustand");
43
- useChatStore = (0, import_zustand.create)((set) => ({
42
+ import_zustand2 = require("zustand");
43
+ useChatStore = (0, import_zustand2.create)((set) => ({
44
44
  activeConversationId: null,
45
45
  pendingTarget: null,
46
46
  typingUsers: {},
@@ -119,6 +119,7 @@ __export(src_exports, {
119
119
  refreshSocketAuth: () => refreshSocketAuth,
120
120
  resetAuthStore: () => resetAuthStore,
121
121
  resolveConfig: () => resolveConfig,
122
+ resolveSystemMessageText: () => resolveSystemMessageText,
122
123
  setApiClientInstance: () => setApiClientInstance,
123
124
  setTransitSession: () => setTransitSession,
124
125
  socketEmit: () => socketEmit,
@@ -574,6 +575,123 @@ var appConfigApi = {
574
575
  }
575
576
  };
576
577
 
578
+ // src/stores/auth.store.ts
579
+ var import_zustand = require("zustand");
580
+ var import_middleware = require("zustand/middleware");
581
+ function createAuthStore(storage) {
582
+ if (!storage) throw new Error("[AntzChat] createAuthStore requires a valid PersistStorage \u2014 received undefined. Make sure the SDK config is fully resolved before initializing the store.");
583
+ const ref = { store: null };
584
+ const store = (0, import_zustand.create)()(
585
+ (0, import_middleware.persist)(
586
+ (set) => ({
587
+ user: null,
588
+ tokens: null,
589
+ isAuthenticated: false,
590
+ isLoading: false,
591
+ isHydrated: false,
592
+ setAuth: (user, tokens) => set({ user, tokens, isAuthenticated: true, isLoading: false }),
593
+ setTokens: (tokens) => set({ tokens }),
594
+ setUser: (user) => set({ user }),
595
+ logout: () => set({ user: null, tokens: null, isAuthenticated: false, isLoading: false }),
596
+ setLoading: (isLoading) => set({ isLoading }),
597
+ setHydrated: (isHydrated) => set({ isHydrated })
598
+ }),
599
+ {
600
+ name: "antz-chat-auth",
601
+ storage: {
602
+ getItem: (name) => {
603
+ const result = storage.getItem(name);
604
+ if (result instanceof Promise) {
605
+ return result.then((str) => str ? JSON.parse(str) : null);
606
+ }
607
+ return result ? JSON.parse(result) : null;
608
+ },
609
+ setItem: (name, value) => {
610
+ storage.setItem(name, JSON.stringify(value));
611
+ },
612
+ removeItem: (name) => storage.removeItem(name)
613
+ },
614
+ partialize: (state) => ({
615
+ user: state.user,
616
+ tokens: state.tokens,
617
+ isAuthenticated: state.isAuthenticated
618
+ }),
619
+ onRehydrateStorage: () => (state, error) => {
620
+ if (error) {
621
+ console.warn("[AntzChat] Auth store rehydration failed:", error);
622
+ }
623
+ ref.store?.setState({ isHydrated: true });
624
+ }
625
+ }
626
+ )
627
+ );
628
+ ref.store = store;
629
+ if (!store.getState().isHydrated) {
630
+ const hydrationTimeout = setTimeout(() => {
631
+ if (!store.getState().isHydrated) {
632
+ store.setState({ isHydrated: true });
633
+ }
634
+ }, 3e3);
635
+ const unsub = store.subscribe((s) => {
636
+ if (s.isHydrated) {
637
+ clearTimeout(hydrationTimeout);
638
+ unsub();
639
+ }
640
+ });
641
+ }
642
+ const tokenStore = {
643
+ getAccessToken: () => store.getState().tokens?.accessToken,
644
+ getRefreshToken: () => store.getState().tokens?.refreshToken,
645
+ setTokens: (tokens) => store.getState().setTokens(tokens),
646
+ clearTokens: () => store.getState().logout()
647
+ };
648
+ return { useAuthStore: store, authTokenStore: tokenStore };
649
+ }
650
+ var _authStore = null;
651
+ function initAuthStore(storage) {
652
+ if (!_authStore) {
653
+ _authStore = createAuthStore(storage);
654
+ }
655
+ return _authStore;
656
+ }
657
+ function getAuthStore() {
658
+ if (!_authStore) throw new Error("[AntzChat] Auth store not initialized. Call initAuthStore first.");
659
+ return _authStore;
660
+ }
661
+ function resetAuthStore() {
662
+ _authStore = null;
663
+ }
664
+
665
+ // src/utils/resolveSystemMessageText.ts
666
+ function resolveSystemMessageText(message, currentUserId) {
667
+ const m = message.metadata;
668
+ if (!m?.systemOperationType || !m.actorUserId) {
669
+ return message.content.text ?? "";
670
+ }
671
+ const iActor = m.actorUserId === currentUserId;
672
+ const iTarget = m.targetUserId === currentUserId;
673
+ const actor = iActor ? "You" : m.actorUserName ?? "Someone";
674
+ const target = iTarget ? "you" : m.targetUserName ?? "a member";
675
+ switch (m.systemOperationType) {
676
+ case "group_created":
677
+ return iActor ? "You created the group" : `${actor} created the group`;
678
+ case "user_added":
679
+ return iActor ? `You added ${target}` : `${actor} added ${target}`;
680
+ case "user_removed":
681
+ return iActor ? `You removed ${target}` : `${actor} removed ${target}`;
682
+ case "user_left":
683
+ return iActor ? "You left the group" : `${actor} left the group`;
684
+ case "admin_promoted":
685
+ return iActor ? `You made ${target} an admin` : `${actor} made ${target} an admin`;
686
+ case "admin_demoted":
687
+ return iActor ? `You removed ${target} as admin` : `${actor} removed ${target} as admin`;
688
+ case "group_updated":
689
+ return iActor ? "You updated the group" : `${actor} updated the group`;
690
+ default:
691
+ return message.content.text ?? "";
692
+ }
693
+ }
694
+
577
695
  // src/api/messages.ts
578
696
  var messagesApi = {
579
697
  async list(conversationId, params = {}) {
@@ -586,7 +704,14 @@ var messagesApi = {
586
704
  `/conversations/${conversationId}/messages`,
587
705
  { params: serverParams }
588
706
  );
589
- return data;
707
+ const currentUserId = getAuthStore().useAuthStore.getState().user?.id;
708
+ if (!currentUserId) return data;
709
+ return {
710
+ ...data,
711
+ data: data.data.map(
712
+ (m) => m.content.type === "system" ? { ...m, content: { ...m.content, text: resolveSystemMessageText(m, currentUserId) } } : m
713
+ )
714
+ };
590
715
  },
591
716
  async get(messageId) {
592
717
  const { data } = await getApiClient().get(`/messages/${messageId}`);
@@ -653,6 +778,10 @@ var messagesApi = {
653
778
  async getPinned(conversationId) {
654
779
  const { data } = await getApiClient().get(`/conversations/${conversationId}/pinned-messages`);
655
780
  return data;
781
+ },
782
+ async getReceipts(messageId) {
783
+ const { data } = await getApiClient().get(`/messages/${messageId}/receipts`);
784
+ return data;
656
785
  }
657
786
  };
658
787
 
@@ -1500,93 +1629,6 @@ var socketEmit = {
1500
1629
  }
1501
1630
  };
1502
1631
 
1503
- // src/stores/auth.store.ts
1504
- var import_zustand2 = require("zustand");
1505
- var import_middleware = require("zustand/middleware");
1506
- function createAuthStore(storage) {
1507
- if (!storage) throw new Error("[AntzChat] createAuthStore requires a valid PersistStorage \u2014 received undefined. Make sure the SDK config is fully resolved before initializing the store.");
1508
- const ref = { store: null };
1509
- const store = (0, import_zustand2.create)()(
1510
- (0, import_middleware.persist)(
1511
- (set) => ({
1512
- user: null,
1513
- tokens: null,
1514
- isAuthenticated: false,
1515
- isLoading: false,
1516
- isHydrated: false,
1517
- setAuth: (user, tokens) => set({ user, tokens, isAuthenticated: true, isLoading: false }),
1518
- setTokens: (tokens) => set({ tokens }),
1519
- setUser: (user) => set({ user }),
1520
- logout: () => set({ user: null, tokens: null, isAuthenticated: false, isLoading: false }),
1521
- setLoading: (isLoading) => set({ isLoading }),
1522
- setHydrated: (isHydrated) => set({ isHydrated })
1523
- }),
1524
- {
1525
- name: "antz-chat-auth",
1526
- storage: {
1527
- getItem: (name) => {
1528
- const result = storage.getItem(name);
1529
- if (result instanceof Promise) {
1530
- return result.then((str) => str ? JSON.parse(str) : null);
1531
- }
1532
- return result ? JSON.parse(result) : null;
1533
- },
1534
- setItem: (name, value) => {
1535
- storage.setItem(name, JSON.stringify(value));
1536
- },
1537
- removeItem: (name) => storage.removeItem(name)
1538
- },
1539
- partialize: (state) => ({
1540
- user: state.user,
1541
- tokens: state.tokens,
1542
- isAuthenticated: state.isAuthenticated
1543
- }),
1544
- onRehydrateStorage: () => (state, error) => {
1545
- if (error) {
1546
- console.warn("[AntzChat] Auth store rehydration failed:", error);
1547
- }
1548
- ref.store?.setState({ isHydrated: true });
1549
- }
1550
- }
1551
- )
1552
- );
1553
- ref.store = store;
1554
- if (!store.getState().isHydrated) {
1555
- const hydrationTimeout = setTimeout(() => {
1556
- if (!store.getState().isHydrated) {
1557
- store.setState({ isHydrated: true });
1558
- }
1559
- }, 3e3);
1560
- const unsub = store.subscribe((s) => {
1561
- if (s.isHydrated) {
1562
- clearTimeout(hydrationTimeout);
1563
- unsub();
1564
- }
1565
- });
1566
- }
1567
- const tokenStore = {
1568
- getAccessToken: () => store.getState().tokens?.accessToken,
1569
- getRefreshToken: () => store.getState().tokens?.refreshToken,
1570
- setTokens: (tokens) => store.getState().setTokens(tokens),
1571
- clearTokens: () => store.getState().logout()
1572
- };
1573
- return { useAuthStore: store, authTokenStore: tokenStore };
1574
- }
1575
- var _authStore = null;
1576
- function initAuthStore(storage) {
1577
- if (!_authStore) {
1578
- _authStore = createAuthStore(storage);
1579
- }
1580
- return _authStore;
1581
- }
1582
- function getAuthStore() {
1583
- if (!_authStore) throw new Error("[AntzChat] Auth store not initialized. Call initAuthStore first.");
1584
- return _authStore;
1585
- }
1586
- function resetAuthStore() {
1587
- _authStore = null;
1588
- }
1589
-
1590
1632
  // src/index.ts
1591
1633
  init_chat_store();
1592
1634
 
@@ -1663,6 +1705,7 @@ var AntzChatClient = class {
1663
1705
  refreshSocketAuth,
1664
1706
  resetAuthStore,
1665
1707
  resolveConfig,
1708
+ resolveSystemMessageText,
1666
1709
  setApiClientInstance,
1667
1710
  setTransitSession,
1668
1711
  socketEmit,