@antzsoft/chat-core 1.0.9 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -733,7 +733,7 @@ import { messagesApi } from '@antzsoft/chat-core';
733
733
  | `unpin` | `(messageId: string) => Promise<Message>` | Unpin a message. |
734
734
  | `getPinned` | `(conversationId: string) => Promise<Message[]>` | List pinned messages in a conversation. |
735
735
 
736
- > **Delete permissions** — `delete()` (for everyone) requires either: the message belongs to the current user AND was sent within the delete window, OR the current user is a group admin. The server default window is **1800 s (30 min)** when `conversation.settings.messageConfig.deleteWindowSeconds` is not set. DMs have no admin role — only the sender can delete for everyone in a DM. `deleteForMe()` is always allowed for any message. See the [integration guide — Edit & Delete section](docs/integration-guide.html#step-edit) for a ready-to-use `getDeleteOptions()` helper.
736
+ > **Delete permissions** — `delete()` (for everyone) requires either: the message belongs to the current user AND was sent within the delete window, OR the current user is a group admin. The server default window is **216,000 s (60 hours)** when `conversation.settings.messageConfig.deleteWindowSeconds` is not set. DMs have no admin role — only the sender can delete for everyone in a DM. `deleteForMe()` is always allowed for any message. See the [integration guide — Edit & Delete section](docs/integration-guide.html#step-edit) for a ready-to-use `getDeleteOptions()` helper.
737
737
 
738
738
  ```typescript
739
739
  interface ListMessagesParams {
@@ -847,12 +847,12 @@ import { conversationsApi } from '@antzsoft/chat-core';
847
847
  | `update` | `(conversationId: string, data: UpdateConversationData) => Promise<Conversation>` | Update group name or description. |
848
848
  | `uploadIcon` | `(conversationId: string, fileId: string) => Promise<Conversation>` | Set the group icon from an already-uploaded file (admin only). Call `client.uploadFiles()` first to get the `fileId`, then pass it here. Server copies `storageKey` into `conversation.iconMeta`, deletes the `chat_files` record, and returns the conversation with a fresh `iconUrl`. |
849
849
  | `delete` | `(conversationId: string) => Promise<void>` | Delete a conversation (admin only). |
850
- | `addParticipants` | `(conversationId: string, userIds: string[]) => Promise<Conversation>` | Add one or more participants. |
850
+ | `addParticipants` | `(conversationId: string, userIds: string[], role?: 'admin' \| 'member') => Promise<Conversation>` | Add one or more participants. `role` defaults to `'member'`. Previously removed members who are re-added always receive the specified role — a former admin re-added without `role: 'admin'` comes back as a member. |
851
851
  | `removeParticipant` | `(conversationId: string, userId: string) => Promise<Conversation>` | Remove a participant. |
852
852
  | `updateParticipantRole` | `(conversationId: string, userId: string, role: 'admin' \| 'member') => Promise<Conversation>` | Promote or demote a participant. |
853
853
  | `mute` | `(conversationId: string, mutedUntil?: string) => Promise<void>` | Mute notifications. Pass an ISO date string to mute until a specific time. |
854
854
  | `unmute` | `(conversationId: string) => Promise<void>` | Unmute a conversation. |
855
- | `pin` | `(conversationId: string) => Promise<void>` | Pin a conversation to the top of the list. |
855
+ | `pin` | `(conversationId: string) => Promise<void>` | Pin a conversation to the top of the list. Max 5 pins — server returns `400` if the limit is reached. |
856
856
  | `unpin` | `(conversationId: string) => Promise<void>` | Unpin a conversation. |
857
857
  | `leave` | `(conversationId: string) => Promise<void>` | Leave a group conversation. |
858
858
  | `getMembers` | `(conversationId: string) => Promise<User[]>` | Fetch full user profiles for all participants. |
@@ -971,9 +971,12 @@ console.log(updated.iconUrl); // fresh signed URL, regenerated on every response
971
971
  // - Previous icon deleted from storage automatically on replace
972
972
  // - Non-admins get 403 Forbidden
973
973
 
974
- // Add members
974
+ // Add members (default role: member)
975
975
  await conversationsApi.addParticipants(group.id, ['user-d', 'user-e']);
976
976
 
977
+ // Add members as admins
978
+ await conversationsApi.addParticipants(group.id, ['user-f'], 'admin');
979
+
977
980
  // Mute for 8 hours
978
981
  const mutedUntil = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString();
979
982
  await conversationsApi.mute(group.id, mutedUntil);
@@ -1554,6 +1557,33 @@ console.log(user.displayName, user.externalId);
1554
1557
 
1555
1558
  ---
1556
1559
 
1560
+ ### App Config API (`appConfigApi`)
1561
+
1562
+ Returns server-side configuration constants. Call once on init and cache the result.
1563
+
1564
+ ```typescript
1565
+ import { appConfigApi } from '@antzsoft/chat-core';
1566
+ ```
1567
+
1568
+ | Method | Signature | Description |
1569
+ |---|---|---|
1570
+ | `get` | `() => Promise<AppConfig>` | Fetch app-level config from the server. |
1571
+
1572
+ ```typescript
1573
+ interface AppConfig {
1574
+ maxPinnedConversations: number; // currently 5
1575
+ }
1576
+ ```
1577
+
1578
+ ```typescript
1579
+ const config = await appConfigApi.get();
1580
+ // Use config.maxPinnedConversations to gate the pin UI
1581
+ ```
1582
+
1583
+ > The SDK caches this under the `['app-config']` React Query key with `staleTime: Infinity` — it is only fetched once per session. `useConversations()` exposes `maxPinnedConversations` directly.
1584
+
1585
+ ---
1586
+
1557
1587
  ### Socket
1558
1588
 
1559
1589
  #### Connection management
@@ -1606,12 +1636,14 @@ This means:
1606
1636
 
1607
1637
  **When to call `joinRoom`:**
1608
1638
 
1609
- 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.
1639
+ When the **user is added to a conversation while their socket is already connected**, the server now automatically joins their active sockets into the new room and emits `conversation_created` to their personal room. No `joinRoom` call is required the user will receive `new_message`, `typing_indicator`, and all other room events immediately.
1640
+
1641
+ Calling `joinRoom` on `conversation_created` is still safe (idempotent) and can be kept for defensive compatibility, but it is no longer necessary:
1610
1642
 
1611
1643
  ```typescript
1644
+ // Optional — server already handles this automatically
1612
1645
  client.socket.on('conversation_created', (conv) => {
1613
- // Fires when you're added to a new or existing conversation at runtime
1614
- client.socket.emit.joinRoom(conv.id);
1646
+ client.socket.emit.joinRoom(conv.id); // safe no-op if already in room
1615
1647
  });
1616
1648
  ```
1617
1649
 
@@ -1663,7 +1695,7 @@ All emit methods that have server responses use a 5-second ack timeout and retur
1663
1695
  | `leaveRoom` | `(conversationId: string) => void` | Leave a conversation room. Fire-and-forget. |
1664
1696
  | `sendMessage` | `(payload: SendMessagePayload) => Promise<unknown>` | Send a message. Ack-based. |
1665
1697
  | `updateMessage` | `(messageId: string, text: string) => Promise<unknown>` | Edit a message. Ack-based. |
1666
- | `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. |
1698
+ | `deleteMessage` | `(messageId: string) => Promise<unknown>` | Delete a message for everyone. Own messages must be within the delete window (default 60 hours); group admins can delete any message with no time restriction. Ack-based. |
1667
1699
  | `deleteMessageForMe` | `(messageId: string) => Promise<unknown>` | Hide a message for the current user only. Ack-based. |
1668
1700
  | `addReaction` | `(messageId: string, emoji: string) => Promise<unknown>` | Add a reaction. Ack-based. |
1669
1701
  | `removeReaction` | `(messageId: string, emoji: string) => Promise<unknown>` | Remove a reaction. Ack-based. |
@@ -2045,10 +2077,21 @@ interface MessageReaction {
2045
2077
  ### `MessageReplyReference`
2046
2078
 
2047
2079
  ```typescript
2080
+ interface ReplyAttachmentSnapshot {
2081
+ type: FileType; // 'image' | 'video' | 'document' | 'audio'
2082
+ filename: string;
2083
+ mimeType: string;
2084
+ size: number;
2085
+ duration?: number; // seconds — audio/video only
2086
+ dimensions?: { width: number; height: number };
2087
+ url: string; // signed URL for the first attachment of the quoted message
2088
+ }
2089
+
2048
2090
  interface MessageReplyReference {
2049
2091
  messageId?: string;
2050
- contentPreview?: string;
2092
+ contentPreview?: string; // text snippet, filename, or "3 Photos" / "3 Attachments" etc.
2051
2093
  senderName?: string;
2094
+ attachmentSnapshot?: ReplyAttachmentSnapshot; // present only when quoted message had attachments
2052
2095
  // Present on optimistic messages before server confirmation
2053
2096
  id?: string;
2054
2097
  content?: MessageContent;
@@ -2056,6 +2099,8 @@ interface MessageReplyReference {
2056
2099
  }
2057
2100
  ```
2058
2101
 
2102
+ `attachmentSnapshot` is a denormalized copy of the first attachment from the quoted message, stored at write time. This means the reply bubble can render an attachment preview (thumbnail, filename, type icon) without fetching the original message — even if it has scrolled far out of the paginated window. The `url` is a signed URL regenerated on every response, identical to how `content.attachments[].url` works.
2103
+
2059
2104
  ### `Conversation`
2060
2105
 
2061
2106
  ```typescript
@@ -2356,19 +2401,35 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
2356
2401
 
2357
2402
  ## Changelog
2358
2403
 
2404
+ ### v1.1.1
2405
+ - **New: `attachmentSnapshot` on reply messages** — `replyTo` now includes a snapshot of the first attachment from the quoted message (`type`, `filename`, `mimeType`, `size`, `duration`, `dimensions`, signed `url`). Stored at write time so reply bubbles render without fetching the original message.
2406
+ - **Fix: `contentPreview` for multi-attachment replies** — Now reflects attachment count: `"photo.jpg +2 more"`, `"3 Photos"`, `"3 Attachments"` (mixed types). Previously always showed only the first filename.
2407
+ - **New: exported `ReplyAttachmentSnapshot` type.**
2408
+
2409
+ ### v1.1.0
2410
+ - **Fix: 500 error on device token registration** — Registering a push token that was previously registered under a different user or device ID (e.g. after app reinstall, account switch, or UUID rotation) now succeeds. The stale token record is removed before the upsert, preventing a duplicate key violation on the global `token_unique` index.
2411
+ - **Fix: 500 error on remove participant** — Removing a participant no longer errors for the removed user's subsequent actions. Message stops and socket events now correctly target only remaining active members.
2412
+ - **Fix: Leave group now works for removed members** — Calling `conversationsApi.leave()` after being removed from a group (kicked by admin) now hides the conversation from the user's list instead of returning a 403. Previously, removed members (`isActive: false`) were blocked by the active-participant guard.
2413
+ - **Fix: Group auto-disbands when last member leaves or is removed** — When the last active participant leaves or is removed, the conversation is now marked inactive (`isActive: false`) automatically. Previously the group persisted as an orphan with zero members.
2414
+ - **Fix: `lastMessage` content not updating after message edit** — Editing a message now correctly updates `conversation.lastMessage.contentPreview` if the edited message is the current last message.
2415
+ - **Fix: Reply attachment preview shows `[Attachment]`** — When replying to a message that contains an attachment, the quoted preview now returns the filename if available, otherwise the file type (e.g. `image`, `video`). Clients should use the parent message's attachment data to render a visual preview for previewable types.
2416
+ - **Fix: Remove reaction returns error** — Removing a reaction that does not exist no longer throws a 500. The operation is now idempotent.
2417
+ - **New: Pin limit — max 5 pinned conversations** — Server enforces a limit of 5 pinned conversations per user and returns `400` if exceeded. A new `GET /app/config` endpoint and `appConfigApi.get()` return `{ maxPinnedConversations: 5 }` so clients can gate the UI before hitting the API. `useConversations()` (Web + RN) fetches config automatically, blocks the pin mutation if the limit is reached, and exposes `maxPinnedConversations`. RN `ConversationList` now supports long-press to pin/unpin.
2418
+
2359
2419
  ### v1.0.9
2360
- - **Socket reconnect resilience for `sendMessage`** — On Android, the OS suspends idle WebSocket connections during long audio recordings. `sendMessage` now waits up to 15 seconds for Socket.IO to auto-reconnect before sending, instead of immediately failing with "Socket not connected". Audio messages go through without error after upload. **No API change.**
2361
- - **`settings` and `participantCount` now returned in conversation responses** — All conversation API responses and socket events (`conversation_created`, `conversation_updated`, etc.) now include `settings` (`onlyAdminsCanMessage`, `onlyAdminsCanAddMembers`, `messageConfig.editWindowSeconds`, `messageConfig.deleteWindowSeconds`) and `participantCount`. Previously these were server-side only. **No SDK change required — fields were already in the `Conversation` type as optional.**
2362
- - **Fix: 500 error when leaving a group** Leaving a group conversation returned an internal server error (leave still succeeded) for groups with no `messageId` in their stored `lastMessage`. The response builder now safely skips `lastMessage` when its `messageId` is missing. **No SDK change required.**
2363
- - **Fix: empty `text: ""` stored on attachment-only messages** Sending a message with an attachment and `text: ""` was persisting an empty string instead of `undefined`, causing the conversation-list preview to show blank instead of the filename or "Attachment". Both the HTTP and socket paths now trim and treat empty strings as absent before saving. **No SDK change required.**
2364
- - **Fix: `participant_left` not received by users outside the open conversation** — The `participant_left` socket event was only broadcast to the `conversation:<id>` room, which users only join when they open that specific conversation. Users on the conversation-list screen never received it and saw stale participant counts. The server now also emits directly to each remaining participant's personal `user:<tenantId>:<userId>` room (joined on connect), skipping users already in the conversation room to avoid double-delivery. **No SDK change required.**
2420
+ - **Socket reconnect resilience for `sendMessage`** — On Android, the OS suspends idle WebSocket connections during long audio recordings. `sendMessage` now waits up to 15 seconds for Socket.IO to auto-reconnect before sending, instead of immediately failing with "Socket not connected". Audio messages go through without error after upload.
2421
+ - **`settings` and `participantCount` now returned in conversation responses** — All conversation API responses and socket events (`conversation_created`, `conversation_updated`, etc.) now include `settings` (`onlyAdminsCanMessage`, `onlyAdminsCanAddMembers`, `messageConfig.editWindowSeconds`, `messageConfig.deleteWindowSeconds`) and `participantCount`. Previously these were server-side only. Fields were already in the `Conversation` type as optional.
2422
+ - **Fix: `participant_left` not received by users outside the open conversation** The event was only sent to users who had the conversation room joined. Users on the conversation-list screen now receive it via their personal socket room. **No client action required.**
2423
+ - **Fix: Removed user could re-enter conversation room via `joinRoom`** The socket access check was missing `isActive`, letting removed users slip back into the room and receive messages. Fixed server-side. **No client action required.**
2424
+ - **Fix: Re-added user's conversation missing from list after deletion** — If a user deleted a conversation and was re-added by an admin, it never reappeared. The server now resets `isHidden`, auto-joins the socket room, and pushes `conversation_created` to the user's personal room. **No client action required.** Existing `conversation_created joinRoom` listeners are safe (idempotent).
2425
+ - **New: `role` parameter on `addParticipants`** — Optional third argument (`'admin' | 'member'`, default `'member'`). Re-added users always receive the specified role — a former admin re-added without `role: 'admin'` comes back as a member. **Backward compatible.**
2365
2426
 
2366
2427
  ### v1.0.8
2367
2428
  - **`duration` field in `SendMessageAttachment`** — Pass `duration` (seconds) when sending audio or video. Server now stores and returns it in `new_message` and message list responses. **Action required (RN):** Omitting it on React Native can crash native audio player libraries on the receiver side; always pass it for audio/video. Web is unaffected.
2368
2429
  - **File compression** — `uploadBatch` now accepts optional `platformCompressFn` + `compressionConfig` args. Fully backward compatible — existing callers unchanged. Web/RN SDKs wire this in automatically; Node.js users can supply `nodeCompressFn` manually. No action required unless opting in on Node.
2369
- - **Removed members keep read-only access** — Server behavior change. Removed participants stay in their conversation list and can read history but cannot write. Socket room membership ends immediately on removal. **No SDK change required.**
2370
- - **Admin delete is hide-only** — Server behavior change. Deleting a conversation sets `isHidden` for the requester only; other participants are unaffected. **No SDK change required.**
2371
- - **Fix: `lastMessage.status` stuck as `deleted`** — Server now explicitly resets `status: 'active'` on both REST and WebSocket paths when a new message is sent. **No client action required.**
2430
+ - **Removed members keep read-only access** — Server behavior change. Removed participants stay in their conversation list and can read history but cannot write. Socket room membership ends immediately on removal.
2431
+ - **Admin delete is hide-only** — Server behavior change. Deleting a conversation sets `isHidden` for the requester only; other participants are unaffected.
2432
+ - **Fix: `lastMessage.status` stuck as `deleted`** — Server now explicitly resets `status: 'active'` on both REST and WebSocket paths when a new message is sent.
2372
2433
  - **Fix: `duration` stored from sender payload** — `SendMessageAttachment.duration` is now persisted and echoed back. The `SendMessageAttachment` interface in this package is unchanged (field was already present as optional).
2373
2434
 
2374
2435
  ### v1.0.7
package/dist/index.cjs CHANGED
@@ -94,6 +94,7 @@ var init_chat_store = __esm({
94
94
  var src_exports = {};
95
95
  __export(src_exports, {
96
96
  AntzChatClient: () => AntzChatClient,
97
+ appConfigApi: () => appConfigApi,
97
98
  authApi: () => authApi,
98
99
  connectSocket: () => connectSocket,
99
100
  conversationsApi: () => conversationsApi,
@@ -372,6 +373,14 @@ var authApi = {
372
373
  }
373
374
  };
374
375
 
376
+ // src/api/app-config.ts
377
+ var appConfigApi = {
378
+ async get() {
379
+ const { data } = await getApiClient().get("/app/config");
380
+ return data;
381
+ }
382
+ };
383
+
375
384
  // src/api/messages.ts
376
385
  var messagesApi = {
377
386
  async list(conversationId, params = {}) {
@@ -526,10 +535,10 @@ var conversationsApi = {
526
535
  async delete(conversationId) {
527
536
  await getApiClient().delete(`/conversations/${conversationId}`);
528
537
  },
529
- async addParticipants(conversationId, userIds) {
538
+ async addParticipants(conversationId, userIds, role) {
530
539
  const { data } = await getApiClient().post(
531
540
  `/conversations/${conversationId}/participants`,
532
- { userIds }
541
+ { userIds, ...role && { role } }
533
542
  );
534
543
  return normalizeConversation(data);
535
544
  },
@@ -649,14 +658,7 @@ async function uploadBatch(files, platformUploadFn, conversationId, onProgress,
649
658
  filename: f.name,
650
659
  mimeType: f.type,
651
660
  size: f.size,
652
- conversationId,
653
- ...f.compressed && {
654
- metadata: {
655
- compressed: true,
656
- originalSize: f.originalSize,
657
- compressionAlgorithm: f.compressionAlgorithm
658
- }
659
- }
661
+ conversationId
660
662
  }));
661
663
  const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
662
664
  const progressMap = {};
@@ -1108,6 +1110,7 @@ var AntzChatClient = class {
1108
1110
  // Annotate the CommonJS export names for ESM import in node:
1109
1111
  0 && (module.exports = {
1110
1112
  AntzChatClient,
1113
+ appConfigApi,
1111
1114
  authApi,
1112
1115
  connectSocket,
1113
1116
  conversationsApi,