@antzsoft/chat-core 1.0.3 → 1.0.4

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
@@ -787,7 +787,8 @@ import { conversationsApi } from '@antzsoft/chat-core';
787
787
  | `get` | `(conversationId: string) => Promise<Conversation>` | Fetch a single conversation. |
788
788
  | `createGroup` | `(data: CreateGroupData) => Promise<Conversation>` | Create a group conversation. |
789
789
  | `createDirect` | `(data: CreateDirectData) => Promise<Conversation>` | Start or retrieve a direct conversation with another user. |
790
- | `update` | `(conversationId: string, data: UpdateConversationData) => Promise<Conversation>` | Update group name, description, or icon. |
790
+ | `update` | `(conversationId: string, data: UpdateConversationData) => Promise<Conversation>` | Update group name or description. |
791
+ | `uploadIcon` | `(conversationId: string, file: File \| { uri: string; name: string; type: string }) => Promise<Conversation>` | Upload or replace the group icon (admin only). Deletes previous icon from storage. Returns conversation with fresh `iconUrl`. |
791
792
  | `delete` | `(conversationId: string) => Promise<void>` | Delete a conversation (admin only). |
792
793
  | `addParticipants` | `(conversationId: string, userIds: string[]) => Promise<Conversation>` | Add one or more participants. |
793
794
  | `removeParticipant` | `(conversationId: string, userId: string) => Promise<Conversation>` | Remove a participant. |
@@ -829,7 +830,6 @@ interface ConversationListParams {
829
830
  interface CreateGroupData {
830
831
  name: string;
831
832
  description?: string;
832
- icon?: string; // Emoji or short string used as the group avatar
833
833
  participantIds: string[];
834
834
  }
835
835
 
@@ -840,7 +840,6 @@ interface CreateDirectData {
840
840
  interface UpdateConversationData {
841
841
  name?: string;
842
842
  description?: string;
843
- icon?: string;
844
843
  }
845
844
  ```
846
845
 
@@ -935,6 +934,48 @@ socket?.on('unread_count_changed', ({ conversationId, unreadCount }) => {
935
934
 
936
935
  Both events are emitted to the user's **private room** (`user:{tenantId}:{userId}`) so every connected device of the same user receives them simultaneously. This is how reading on your phone automatically clears the badge on your browser tab.
937
936
 
937
+ #### Chat icon badge — complete pattern (headless / custom UI)
938
+
939
+ When building a custom UI using `chat-core` directly (no web/RN SDK), maintain your own unread state and update it on socket events:
940
+
941
+ ```typescript
942
+ import { conversationsApi, tryGetSocket } from '@antzsoft/chat-core';
943
+
944
+ // 1. Cold start — fetch accurate counts from DB
945
+ const summary = await conversationsApi.getUnreadSummary();
946
+ let totalUnread = summary.totalUnread;
947
+ updateBadge(totalUnread); // your UI function
948
+
949
+ // 2. While socket is connected — update on every event
950
+ const socket = tryGetSocket();
951
+
952
+ socket?.on('conversation_updated', ({ conversationId, unreadCount }) => {
953
+ // Server sends accurate DB count per conversation — recalculate total
954
+ summary.byConversation = summary.byConversation
955
+ .filter(c => c.conversationId !== conversationId)
956
+ .concat({ conversationId, unreadCount });
957
+ totalUnread = summary.byConversation.reduce((s, c) => s + c.unreadCount, 0);
958
+ updateBadge(totalUnread);
959
+ });
960
+
961
+ socket?.on('unread_count_changed', ({ conversationId }) => {
962
+ // User read a conversation — clear it from the map
963
+ summary.byConversation = summary.byConversation
964
+ .filter(c => c.conversationId !== conversationId);
965
+ totalUnread = summary.byConversation.reduce((s, c) => s + c.unreadCount, 0);
966
+ updateBadge(totalUnread);
967
+ });
968
+
969
+ // 3. On foreground / socket reconnect — resync from DB
970
+ async function onForeground() {
971
+ const fresh = await conversationsApi.getUnreadSummary();
972
+ totalUnread = fresh.totalUnread;
973
+ updateBadge(totalUnread);
974
+ }
975
+ ```
976
+
977
+ > **Using `@antzsoft/chat-web-sdk` or `@antzsoft/chat-rn-sdk`?** You don't need any of this — `useConversations()` handles socket subscriptions internally. Just sum `conversations.reduce((s, c) => s + (c.unreadCount ?? 0), 0)` and it updates automatically.
978
+
938
979
  ---
939
980
 
940
981
  ### Storage API (`storageApi` and `uploadBatch`)
@@ -1306,6 +1347,60 @@ unsubscribe();
1306
1347
  disconnectSocket();
1307
1348
  ```
1308
1349
 
1350
+ #### Room membership — auto-join and `joinRoom`
1351
+
1352
+ **On every socket connection, the server automatically joins the user into all their existing conversation rooms.** No client action is needed. The moment `client.connect()` resolves, the user is already subscribed to real-time events for every conversation they belong to.
1353
+
1354
+ This means:
1355
+ - You do **not** need to call `joinRoom` when opening a chat screen.
1356
+ - `new_message`, `typing_indicator`, `read_receipt`, and all other conversation events are received for all conversations from the moment the socket connects.
1357
+ - Calling `joinRoom` on an already-joined room is safe (idempotent) but causes an unnecessary DB access check — avoid it in hot paths.
1358
+
1359
+ **When to call `joinRoom`:**
1360
+
1361
+ There is only one real use case — when the **user is added to a conversation while their socket is already connected** (either someone created a new group and added them, or they were added to an existing group). The server does not auto-join the user's socket to the new room — it only emits `conversation_created` to the user's personal room. The client must call `joinRoom` in response, otherwise the socket will not receive any `new_message`, `typing_indicator`, or other room events for that conversation.
1362
+
1363
+ ```typescript
1364
+ client.socket.on('conversation_created', (conv) => {
1365
+ // Fires when you're added to a new or existing conversation at runtime
1366
+ client.socket.emit.joinRoom(conv.id);
1367
+ });
1368
+ ```
1369
+
1370
+ **Where to add the `new_message` listener:**
1371
+
1372
+ Register it **once at app root level**, right after `client.connect()`. A single listener handles messages from all conversations — use `message.conversationId` to route to the right place. Never add it inside a screen or component.
1373
+
1374
+ ```typescript
1375
+ await client.connect();
1376
+
1377
+ client.socket.on('new_message', (event: NewMessageEvent) => {
1378
+ const { message } = event;
1379
+
1380
+ // Update the open chat view if this conversation is active
1381
+ if (message.conversationId === activeConversationId) {
1382
+ appendMessageToView(message);
1383
+ }
1384
+
1385
+ // Always update the conversation list (last message + unread badge)
1386
+ updateConversationList(message.conversationId, {
1387
+ lastMessage: message,
1388
+ // Don't increment unread if the user sent it or is currently viewing that chat
1389
+ incrementUnread: message.conversationId !== activeConversationId
1390
+ && message.senderId !== currentUserId,
1391
+ });
1392
+ });
1393
+
1394
+ // Only case where joinRoom is needed — new conversation created at runtime
1395
+ client.socket.on('conversation_created', (conv) => {
1396
+ client.socket.emit.joinRoom(conv.id);
1397
+ });
1398
+ ```
1399
+
1400
+ **`leaveRoom`** 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.
1401
+
1402
+ ---
1403
+
1309
1404
  #### `socketEmit` — outbound events
1310
1405
 
1311
1406
  ```typescript
@@ -1320,7 +1415,7 @@ All emit methods that have server responses use a 5-second ack timeout and retur
1320
1415
  | `leaveRoom` | `(conversationId: string) => void` | Leave a conversation room. Fire-and-forget. |
1321
1416
  | `sendMessage` | `(payload: SendMessagePayload) => Promise<unknown>` | Send a message. Ack-based. |
1322
1417
  | `updateMessage` | `(messageId: string, text: string) => Promise<unknown>` | Edit a message. Ack-based. |
1323
- | `deleteMessage` | `(messageId: string) => Promise<unknown>` | Delete a message for everyone (own message within window, or admin). Ack-based. |
1418
+ | `deleteMessage` | `(messageId: string) => Promise<unknown>` | Delete a message for everyone. Own messages must be within the delete window (default 30 min); group admins can delete any message with no time restriction. Ack-based. |
1324
1419
  | `deleteMessageForMe` | `(messageId: string) => Promise<unknown>` | Hide a message for the current user only. Ack-based. |
1325
1420
  | `addReaction` | `(messageId: string, emoji: string) => Promise<unknown>` | Add a reaction. Ack-based. |
1326
1421
  | `removeReaction` | `(messageId: string, emoji: string) => Promise<unknown>` | Remove a reaction. Ack-based. |
@@ -1360,21 +1455,29 @@ console.log('Online:', onlineIds); // ['user-a', 'user-c']
1360
1455
 
1361
1456
  Subscribe using `client.socket.on(event, handler)` (headless) or directly on the Socket.IO socket via `getSocket().on(event, handler)`.
1362
1457
 
1363
- | Event | Payload type | Description |
1364
- |---|---|---|
1365
- | `new_message` | `NewMessageEvent` | A new message was sent to a room you've joined. |
1366
- | `message_updated` | `MessageUpdatedEvent` | A message was edited. |
1367
- | `message_deleted` | `MessageDeletedEvent` | A message was deleted for everyone. |
1368
- | `message_deleted_for_me` | `{ messageId: string }` | A message was hidden for the current user only (fired only to that user). |
1369
- | `reaction_updated` | `ReactionUpdatedEvent` | Reactions on a message changed (full reaction array). |
1370
- | `typing_indicator` | `TypingIndicatorEvent` | A user started or stopped typing. |
1371
- | `user_status` | `UserStatusEvent` | A user's online/offline/away status changed. |
1372
- | `read_receipt` | `ReadReceiptEvent` | A user read messages in a conversation. |
1373
- | `message_ack` | `MessageAckEvent` | Server confirmation for a message you sent via socket (maps tempId to the real messageId). |
1374
- | `messages_delivered` | `MessagesDeliveredEvent` | Messages you sent were delivered to a recipient. |
1375
- | `conversation_created` | `Conversation` | A new conversation was created (or you were added to one). |
1376
- | `conversation_updated` | `Conversation` | A conversation's metadata was changed. |
1377
- | `conversation_deleted` | `{ conversationId: string }` | A conversation was deleted. |
1458
+ | Event | Payload type | Description | Where to listen / unlisten |
1459
+ |---|---|---|---|
1460
+ | `new_message` | `NewMessageEvent` | New message in any of the user's conversations — all rooms are auto-joined on connect. | **App root** after `client.connect()`. Never remove — must stay alive for the full app session to keep the conversation list and unread badges up to date. |
1461
+ | `message_updated` | `MessageUpdatedEvent` | A message was edited. | **Chat detail screen** — add on mount, remove on unmount. |
1462
+ | `message_deleted` | `MessageDeletedEvent` | A message was deleted for everyone. | **Chat detail screen** — add on mount, remove on unmount. |
1463
+ | `message_deleted_for_me` | `{ messageId: string; conversationId: string }` | A message was hidden for the current user only (fired only to that user's socket). | **Chat detail screen** — add on mount, remove on unmount. |
1464
+ | `reaction_updated` | `ReactionUpdatedEvent` | Reactions on a message changed (full reaction array). | **Chat detail screen** — add on mount, remove on unmount. |
1465
+ | `message_pin_updated` | `{ messageId: string; conversationId: string; isPinned: boolean; pinnedBy?: string; pinnedAt?: string }` | A message was pinned or unpinned. | **Chat detail screen** — add on mount, remove on unmount. |
1466
+ | `typing_indicator` | `TypingIndicatorEvent` | A user started or stopped typing. | **Chat detail screen** — add on mount, remove on unmount. |
1467
+ | `user_online` | `{ userId: string }` | A participant came online auto-updates `useChatStore.onlineUsers`. | **App root** — drives online indicators everywhere. Keep for full session. |
1468
+ | `user_offline` | `{ userId: string; lastSeen: string }` | A participant went offline auto-updates `useChatStore.lastSeen`. | **App root** drives online indicators and last-seen everywhere. Keep for full session. |
1469
+ | `user_status` | `UserStatusEvent` | Global presence broadcast (online/offline/away) to all connected clients. | **App root** — keep for full session. |
1470
+ | `online_users` | `string[]` | Full list of online user IDs sent on initial room join. | **App root** — keep for full session. |
1471
+ | `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). |
1472
+ | `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. |
1473
+ | `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. |
1474
+ | `message_delivered` | `{ messageId: string; conversationId: string; deliveredAt: string }` | A single message you sent was delivered to all active recipients. | **Chat detail screen** — add on mount, remove on unmount. |
1475
+ | `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. |
1476
+ | `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. |
1477
+ | `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. |
1478
+ | `conversation_deleted` | `{ conversationId: string }` | A conversation was deleted. | **App root / conversation list screen** — remove from state and navigate away if it was open. |
1479
+ | `participant_joined` | `{ conversationId: string; userId: string; displayName: string; addedBy: string }` | A new participant was added to a group conversation. | **Chat detail screen** — add on mount, remove on unmount. |
1480
+ | `participant_left` | `{ conversationId: string; userId: string; displayName: string; removedBy?: string }` | A participant left or was removed from a group conversation. | **Chat detail screen** — add on mount, remove on unmount. |
1378
1481
 
1379
1482
  ```typescript
1380
1483
  import type {
@@ -1713,8 +1816,7 @@ interface Conversation {
1713
1816
  conversationType: 'direct' | 'group';
1714
1817
  name?: string;
1715
1818
  description?: string;
1716
- icon?: string;
1717
- iconUrl?: string;
1819
+ iconUrl?: string; // Fresh signed URL generated on each response — never stored directly
1718
1820
  participants: Participant[];
1719
1821
  participantCount?: number;
1720
1822
  settings?: ConversationSettings;
package/dist/index.cjs CHANGED
@@ -514,6 +514,26 @@ var conversationsApi = {
514
514
  async getUnreadSummary() {
515
515
  const { data } = await getApiClient().get("/conversations/unread");
516
516
  return data;
517
+ },
518
+ /**
519
+ * Upload or replace the group icon (admin only).
520
+ * Accepts a File (web) or { uri, name, type } object (React Native).
521
+ * The server stores the image in its own storage, deletes the previous icon,
522
+ * and returns a fresh signed iconUrl on every subsequent response.
523
+ */
524
+ async uploadIcon(conversationId, file) {
525
+ const formData = new FormData();
526
+ if (file instanceof File) {
527
+ formData.append("icon", file);
528
+ } else {
529
+ formData.append("icon", { uri: file.uri, name: file.name, type: file.type });
530
+ }
531
+ const { data } = await getApiClient().put(
532
+ `/conversations/${conversationId}/icon`,
533
+ formData,
534
+ { headers: { "Content-Type": "multipart/form-data" } }
535
+ );
536
+ return normalizeConversation(data);
517
537
  }
518
538
  };
519
539