@antzsoft/chat-core 1.1.0 → 1.1.2

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
@@ -320,10 +320,14 @@ interface AntzChatConfig {
320
320
  tenantId?: string;
321
321
 
322
322
  /**
323
- * Encryption mode. Must match the server's ENCRYPTION_MODE env var.
324
- * Default: 'none'.
323
+ * Enable payload-level transit encryption for all HTTP and socket traffic.
324
+ * Uses ECDH key exchange (X25519/P-256) + AES-256-GCM to encrypt every
325
+ * request, response, and socket event on the wire — independent of TLS.
326
+ * Server must have TRANSIT_ENCRYPTION_ENABLED=true (default).
327
+ * Default: true. Set false only for local development or debugging.
328
+ * Safe to toggle anytime — no data migration needed (wire-only, never stored).
325
329
  */
326
- encryptionMode?: 'none' | 'server';
330
+ transitEncryption?: boolean;
327
331
 
328
332
  /**
329
333
  * The user's ID in the external auth system.
@@ -747,7 +751,6 @@ interface SendData {
747
751
  attachments?: SendMessageAttachment[];
748
752
  replyTo?: string; // messageId of the message being replied to
749
753
  tempId?: string; // Client-generated ID for optimistic UI
750
- isEncrypted?: boolean;
751
754
  }
752
755
 
753
756
  interface SearchParams {
@@ -847,7 +850,7 @@ import { conversationsApi } from '@antzsoft/chat-core';
847
850
  | `update` | `(conversationId: string, data: UpdateConversationData) => Promise<Conversation>` | Update group name or description. |
848
851
  | `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
852
  | `delete` | `(conversationId: string) => Promise<void>` | Delete a conversation (admin only). |
850
- | `addParticipants` | `(conversationId: string, userIds: string[]) => Promise<Conversation>` | Add one or more participants. |
853
+ | `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
854
  | `removeParticipant` | `(conversationId: string, userId: string) => Promise<Conversation>` | Remove a participant. |
852
855
  | `updateParticipantRole` | `(conversationId: string, userId: string, role: 'admin' \| 'member') => Promise<Conversation>` | Promote or demote a participant. |
853
856
  | `mute` | `(conversationId: string, mutedUntil?: string) => Promise<void>` | Mute notifications. Pass an ISO date string to mute until a specific time. |
@@ -971,9 +974,12 @@ console.log(updated.iconUrl); // fresh signed URL, regenerated on every response
971
974
  // - Previous icon deleted from storage automatically on replace
972
975
  // - Non-admins get 403 Forbidden
973
976
 
974
- // Add members
977
+ // Add members (default role: member)
975
978
  await conversationsApi.addParticipants(group.id, ['user-d', 'user-e']);
976
979
 
980
+ // Add members as admins
981
+ await conversationsApi.addParticipants(group.id, ['user-f'], 'admin');
982
+
977
983
  // Mute for 8 hours
978
984
  const mutedUntil = new Date(Date.now() + 8 * 60 * 60 * 1000).toISOString();
979
985
  await conversationsApi.mute(group.id, mutedUntil);
@@ -1105,6 +1111,8 @@ const presigned = await storageApi.requestPresignedUrl({
1105
1111
  mimeType: 'application/pdf',
1106
1112
  size: 512000,
1107
1113
  conversationId: 'conv-abc',
1114
+ // optional — stored on the server file record
1115
+ metadata: { compressed: true, originalSize: 900000, compressionAlgorithm: 'gzip' },
1108
1116
  });
1109
1117
  await platformUploadFn(presigned, file, (pct) => console.log(`${pct * 100}%`));
1110
1118
  const fileRecord = await storageApi.confirmUpload(presigned.fileId);
@@ -1133,9 +1141,9 @@ When `platformCompressFn` is provided and `compression.enabled` is `true` (the d
1133
1141
  1. Determine strategy per file (`image` → WebP/JPEG resize+encode, `gzip` → text/doc compression, `skip` → no-op)
1134
1142
  2. Run `platformCompressFn(file, compressionConfig)` — returns a `CompressedFile`
1135
1143
  3. If compressed result is **larger** than the original, the original is used instead (automatic fallback)
1136
- 4. Request presigned URL with the compressed size and MIME type
1144
+ 4. Request presigned URL with the compressed size, MIME type, and compression metadata (`compressed`, `originalSize`, `compressionAlgorithm`)
1137
1145
  5. Upload the compressed bytes
1138
- 6. Store `metadata.compressed`, `metadata.originalSize`, `metadata.compressionAlgorithm` on the file record
1146
+ 6. Server persists `metadata.compressed`, `metadata.originalSize`, `metadata.compressionAlgorithm` on the file record alongside system fields (`userId`, `tenantId`)
1139
1147
 
1140
1148
  ### Strategy by file type
1141
1149
 
@@ -1633,12 +1641,14 @@ This means:
1633
1641
 
1634
1642
  **When to call `joinRoom`:**
1635
1643
 
1636
- 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.
1644
+ 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.
1645
+
1646
+ Calling `joinRoom` on `conversation_created` is still safe (idempotent) and can be kept for defensive compatibility, but it is no longer necessary:
1637
1647
 
1638
1648
  ```typescript
1649
+ // Optional — server already handles this automatically
1639
1650
  client.socket.on('conversation_created', (conv) => {
1640
- // Fires when you're added to a new or existing conversation at runtime
1641
- client.socket.emit.joinRoom(conv.id);
1651
+ client.socket.emit.joinRoom(conv.id); // safe no-op if already in room
1642
1652
  });
1643
1653
  ```
1644
1654
 
@@ -2043,9 +2053,6 @@ interface Message {
2043
2053
  sender?: User;
2044
2054
  readBy?: Array<{ userId: string; readAt: string }>;
2045
2055
  deliveredTo?: Array<{ userId: string; deliveredAt: string }>;
2046
- isEncrypted?: boolean;
2047
- encryptionMode?: 'none' | 'server' | 'e2ee';
2048
- encryptedContent?: EncryptedContent;
2049
2056
  }
2050
2057
  ```
2051
2058
 
@@ -2072,10 +2079,21 @@ interface MessageReaction {
2072
2079
  ### `MessageReplyReference`
2073
2080
 
2074
2081
  ```typescript
2082
+ interface ReplyAttachmentSnapshot {
2083
+ type: FileType; // 'image' | 'video' | 'document' | 'audio'
2084
+ filename: string;
2085
+ mimeType: string;
2086
+ size: number;
2087
+ duration?: number; // seconds — audio/video only
2088
+ dimensions?: { width: number; height: number };
2089
+ url: string; // signed URL for the first attachment of the quoted message
2090
+ }
2091
+
2075
2092
  interface MessageReplyReference {
2076
2093
  messageId?: string;
2077
- contentPreview?: string;
2094
+ contentPreview?: string; // text snippet, filename, or "3 Photos" / "3 Attachments" etc.
2078
2095
  senderName?: string;
2096
+ attachmentSnapshot?: ReplyAttachmentSnapshot; // present only when quoted message had attachments
2079
2097
  // Present on optimistic messages before server confirmation
2080
2098
  id?: string;
2081
2099
  content?: MessageContent;
@@ -2083,6 +2101,8 @@ interface MessageReplyReference {
2083
2101
  }
2084
2102
  ```
2085
2103
 
2104
+ `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.
2105
+
2086
2106
  ### `Conversation`
2087
2107
 
2088
2108
  ```typescript
@@ -2105,9 +2125,6 @@ interface Conversation {
2105
2125
  isPinned?: boolean;
2106
2126
  isMuted?: boolean;
2107
2127
  mutedUntil?: string;
2108
- encryptionMode?: 'none' | 'server' | 'e2ee';
2109
- isEncryptionEnabled?: boolean;
2110
- encryptionKey?: string;
2111
2128
  }
2112
2129
 
2113
2130
  interface ConversationSettings {
@@ -2174,8 +2191,6 @@ interface SendMessagePayload {
2174
2191
  attachments?: SendMessageAttachment[];
2175
2192
  replyTo?: string; // messageId
2176
2193
  tempId: string; // client-generated; echoed back in message_ack
2177
- encryptedContent?: EncryptedContent;
2178
- isEncrypted?: boolean;
2179
2194
  }
2180
2195
 
2181
2196
  interface SendMessageAttachment {
@@ -2383,6 +2398,22 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
2383
2398
 
2384
2399
  ## Changelog
2385
2400
 
2401
+ ### v1.1.2
2402
+ - **New: Participants stored in a dedicated collection** — Participants are no longer embedded inside conversation documents. Each participant is now its own document in a separate collection, removing the MongoDB 16 MB document size ceiling for large groups and eliminating write contention on high-membership conversations. No client API changes.
2403
+ - **New: Transit encryption** — All socket payloads are now encrypted end-to-end using a per-session symmetric key negotiated on connect. Transparent to callers — no SDK API changes required.
2404
+ - **Fix: Disbanded groups now visible in read-only mode for removed members** — Previously removed members' views of disbanded groups were inconsistent. Removed participants now always see the conversation in read-only mode until they explicitly call `leave()`.
2405
+ - **Fix: Removed participant access hardened** — Removed users can no longer view messages sent after their removal via the REST API. Message history is now capped at the exact moment of removal. Removed users were also incorrectly able to search messages, view starred messages, and fetch unread counts from conversations they were removed from — all three are now blocked.
2406
+ - **Fix: Unread badge drops to zero immediately on removal** — When an admin removes a user from a group, all unread messages in that conversation are automatically marked as read for the removed user at the moment of removal. Previously the badge stayed non-zero until the user manually opened the conversation.
2407
+ - **Fix: Compression metadata now stored on the server** — When a compressed image is uploaded, the file record now correctly saves `compressed`, `originalSize`, and `compressionAlgorithm`. Previously this metadata was silently dropped by DTO validation. No changes needed on the client — handled automatically by `uploadBatch`.
2408
+ - **Fix: Delete for me also removes the message from your starred list** — Calling `deleteForMe` on a starred message now removes the star. Previously the message remained in the starred list even after being hidden.
2409
+
2410
+ ### v1.1.1
2411
+ - **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.
2412
+ - **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.
2413
+ - **New: exported `ReplyAttachmentSnapshot` type.**
2414
+ - **Fix: Pin, unpin, and unmute operations** — Server-side issues causing pin, unpin, and unmute to fail in certain states are resolved.
2415
+ - **Fix: Various backend fixes** — Stability and correctness improvements across multiple server-side paths.
2416
+
2386
2417
  ### v1.1.0
2387
2418
  - **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
2419
  - **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.
@@ -2396,6 +2427,10 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
2396
2427
  ### v1.0.9
2397
2428
  - **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
2429
  - **`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.
2430
+ - **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.**
2431
+ - **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.**
2432
+ - **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).
2433
+ - **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.**
2399
2434
 
2400
2435
  ### v1.0.8
2401
2436
  - **`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.