@antzsoft/chat-core 1.0.8 → 1.1.0

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 {
@@ -852,7 +852,7 @@ import { conversationsApi } from '@antzsoft/chat-core';
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. |
@@ -1554,6 +1554,33 @@ console.log(user.displayName, user.externalId);
1554
1554
 
1555
1555
  ---
1556
1556
 
1557
+ ### App Config API (`appConfigApi`)
1558
+
1559
+ Returns server-side configuration constants. Call once on init and cache the result.
1560
+
1561
+ ```typescript
1562
+ import { appConfigApi } from '@antzsoft/chat-core';
1563
+ ```
1564
+
1565
+ | Method | Signature | Description |
1566
+ |---|---|---|
1567
+ | `get` | `() => Promise<AppConfig>` | Fetch app-level config from the server. |
1568
+
1569
+ ```typescript
1570
+ interface AppConfig {
1571
+ maxPinnedConversations: number; // currently 5
1572
+ }
1573
+ ```
1574
+
1575
+ ```typescript
1576
+ const config = await appConfigApi.get();
1577
+ // Use config.maxPinnedConversations to gate the pin UI
1578
+ ```
1579
+
1580
+ > 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.
1581
+
1582
+ ---
1583
+
1557
1584
  ### Socket
1558
1585
 
1559
1586
  #### Connection management
@@ -1663,7 +1690,7 @@ All emit methods that have server responses use a 5-second ack timeout and retur
1663
1690
  | `leaveRoom` | `(conversationId: string) => void` | Leave a conversation room. Fire-and-forget. |
1664
1691
  | `sendMessage` | `(payload: SendMessagePayload) => Promise<unknown>` | Send a message. Ack-based. |
1665
1692
  | `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. |
1693
+ | `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
1694
  | `deleteMessageForMe` | `(messageId: string) => Promise<unknown>` | Hide a message for the current user only. Ack-based. |
1668
1695
  | `addReaction` | `(messageId: string, emoji: string) => Promise<unknown>` | Add a reaction. Ack-based. |
1669
1696
  | `removeReaction` | `(messageId: string, emoji: string) => Promise<unknown>` | Remove a reaction. Ack-based. |
@@ -2086,6 +2113,12 @@ interface Conversation {
2086
2113
  interface ConversationSettings {
2087
2114
  onlyAdminsCanMessage?: boolean;
2088
2115
  onlyAdminsCanAddMembers?: boolean;
2116
+ messageConfig?: MessageConfig;
2117
+ }
2118
+
2119
+ interface MessageConfig {
2120
+ editWindowSeconds?: number;
2121
+ deleteWindowSeconds?: number;
2089
2122
  }
2090
2123
  ```
2091
2124
 
@@ -2350,12 +2383,26 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
2350
2383
 
2351
2384
  ## Changelog
2352
2385
 
2386
+ ### v1.1.0
2387
+ - **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.
2388
+ - **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.
2389
+ - **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.
2390
+ - **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.
2391
+ - **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.
2392
+ - **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.
2393
+ - **Fix: Remove reaction returns error** — Removing a reaction that does not exist no longer throws a 500. The operation is now idempotent.
2394
+ - **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.
2395
+
2396
+ ### v1.0.9
2397
+ - **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.
2398
+ - **`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.
2399
+
2353
2400
  ### v1.0.8
2354
2401
  - **`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.
2355
2402
  - **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.
2356
- - **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.**
2357
- - **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.**
2358
- - **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.**
2403
+ - **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.
2404
+ - **Admin delete is hide-only** — Server behavior change. Deleting a conversation sets `isHidden` for the requester only; other participants are unaffected.
2405
+ - **Fix: `lastMessage.status` stuck as `deleted`** — Server now explicitly resets `status: 'active'` on both REST and WebSocket paths when a new message is sent.
2359
2406
  - **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).
2360
2407
 
2361
2408
  ### 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 = {}) {
@@ -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 = {};
@@ -865,8 +867,32 @@ function refreshSocketAuth() {
865
867
 
866
868
  // src/socket/emitters.ts
867
869
  var ACK_TIMEOUT = 5e3;
868
- function withAck(event, payload) {
869
- const socket = tryGetSocket();
870
+ var RECONNECT_WAIT_TIMEOUT = 15e3;
871
+ function waitForReconnect() {
872
+ return new Promise((resolve, reject) => {
873
+ const timer = setTimeout(() => {
874
+ unsubscribe();
875
+ reject(new Error("[AntzChat] Socket reconnect timeout"));
876
+ }, RECONNECT_WAIT_TIMEOUT);
877
+ const unsubscribe = onSocketStatus((status) => {
878
+ if (status === "connected") {
879
+ clearTimeout(timer);
880
+ unsubscribe();
881
+ resolve();
882
+ } else if (status === "error") {
883
+ clearTimeout(timer);
884
+ unsubscribe();
885
+ reject(new Error("[AntzChat] Socket reconnect failed"));
886
+ }
887
+ });
888
+ });
889
+ }
890
+ async function withAck(event, payload) {
891
+ let socket = tryGetSocket();
892
+ if (!socket) {
893
+ await waitForReconnect();
894
+ socket = tryGetSocket();
895
+ }
870
896
  if (!socket) return Promise.reject(new Error(`[AntzChat] Socket not connected (event: ${event})`));
871
897
  return new Promise((resolve, reject) => {
872
898
  const timer = setTimeout(() => reject(new Error(`Socket ack timeout: ${event}`)), ACK_TIMEOUT);
@@ -1084,6 +1110,7 @@ var AntzChatClient = class {
1084
1110
  // Annotate the CommonJS export names for ESM import in node:
1085
1111
  0 && (module.exports = {
1086
1112
  AntzChatClient,
1113
+ appConfigApi,
1087
1114
  authApi,
1088
1115
  connectSocket,
1089
1116
  conversationsApi,