@antzsoft/chat-core 1.1.2 → 1.1.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 +69 -5
- package/dist/index.cjs +88 -25
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +37 -3
- package/dist/index.d.ts +37 -3
- package/dist/index.js +88 -25
- package/dist/index.js.map +1 -1
- package/docs/integration-guide.html +191 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -625,6 +625,13 @@ class AntzChatClient {
|
|
|
625
625
|
* Same presigned URL pipeline as message attachments — platformUploadFn is handled automatically.
|
|
626
626
|
*/
|
|
627
627
|
uploadIcon(conversationId: string, file: UploadableFile): Promise<Conversation>;
|
|
628
|
+
|
|
629
|
+
/**
|
|
630
|
+
* Remove the group icon (admin only).
|
|
631
|
+
* Deletes the asset from storage and clears iconMeta on the conversation.
|
|
632
|
+
* Returns the updated conversation with iconUrl: undefined.
|
|
633
|
+
*/
|
|
634
|
+
removeIcon(conversationId: string): Promise<Conversation>;
|
|
628
635
|
}
|
|
629
636
|
```
|
|
630
637
|
|
|
@@ -849,15 +856,16 @@ import { conversationsApi } from '@antzsoft/chat-core';
|
|
|
849
856
|
| `createDirect` | `(data: CreateDirectData) => Promise<Conversation>` | Start or retrieve a direct conversation with another user. |
|
|
850
857
|
| `update` | `(conversationId: string, data: UpdateConversationData) => Promise<Conversation>` | Update group name or description. |
|
|
851
858
|
| `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`. |
|
|
852
|
-
| `
|
|
853
|
-
| `
|
|
854
|
-
| `
|
|
859
|
+
| `removeIcon` | `(conversationId: string) => Promise<Conversation>` | Remove the group icon (admin only). Deletes the asset from storage, clears `iconMeta` on the conversation, and returns the updated conversation with `iconUrl: undefined`. Non-admins receive `403 Forbidden`. |
|
|
860
|
+
| `delete` | `(conversationId: string) => Promise<void>` | Hide a conversation from the caller's list. Works for any participant (active or inactive) on both DMs and groups — no admin role required. For DMs this is "Delete Chat"; for groups this is "Delete Group" (after already exiting). Other participants are completely unaffected. |
|
|
861
|
+
| `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. Message visibility on re-add depends on whether the user previously deleted the conversation (see [Message History & Re-add](#message-history--re-add)). |
|
|
862
|
+
| `removeParticipant` | `(conversationId: string, userId: string) => Promise<Conversation>` | Remove a participant (admin only). |
|
|
855
863
|
| `updateParticipantRole` | `(conversationId: string, userId: string, role: 'admin' \| 'member') => Promise<Conversation>` | Promote or demote a participant. |
|
|
856
864
|
| `mute` | `(conversationId: string, mutedUntil?: string) => Promise<void>` | Mute notifications. Pass an ISO date string to mute until a specific time. |
|
|
857
865
|
| `unmute` | `(conversationId: string) => Promise<void>` | Unmute a conversation. |
|
|
858
866
|
| `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. |
|
|
859
867
|
| `unpin` | `(conversationId: string) => Promise<void>` | Unpin a conversation. |
|
|
860
|
-
| `leave` | `(conversationId: string) => Promise<void>` | Leave a group conversation. |
|
|
868
|
+
| `leave` | `(conversationId: string, andDelete?: boolean) => Promise<void>` | Leave a group conversation. Pass `andDelete: true` to also hide it from the caller's list in one atomic operation ("Exit and Delete"). When the last admin calls `leave()`, the server automatically promotes the longest-standing active member to admin before completing the exit — no client action required. |
|
|
861
869
|
| `getMembers` | `(conversationId: string) => Promise<User[]>` | Fetch full user profiles for all participants. |
|
|
862
870
|
|
|
863
871
|
```typescript
|
|
@@ -974,6 +982,11 @@ console.log(updated.iconUrl); // fresh signed URL, regenerated on every response
|
|
|
974
982
|
// - Previous icon deleted from storage automatically on replace
|
|
975
983
|
// - Non-admins get 403 Forbidden
|
|
976
984
|
|
|
985
|
+
// Remove the group icon (admin only).
|
|
986
|
+
// Deletes the asset from storage and clears iconMeta. Returns conversation with iconUrl: undefined.
|
|
987
|
+
const noIcon = await conversationsApi.removeIcon(group.id);
|
|
988
|
+
// noIcon.iconUrl === undefined
|
|
989
|
+
|
|
977
990
|
// Add members (default role: member)
|
|
978
991
|
await conversationsApi.addParticipants(group.id, ['user-d', 'user-e']);
|
|
979
992
|
|
|
@@ -1539,6 +1552,9 @@ import { usersApi } from '@antzsoft/chat-core';
|
|
|
1539
1552
|
| `list` | `(params?: { query?: string; page?: number; limit?: number }) => Promise<PaginatedResponse<User>>` | List users, optionally filtered by a search query. Omit `page`/`limit` for all results. |
|
|
1540
1553
|
| `getById` | `(userId: string) => Promise<User>` | Fetch a single user by their chat system ID. |
|
|
1541
1554
|
| `getLastSeen` | `(userId: string) => Promise<{ lastSeenAt: string \| null }>` | Fetch a user's last-seen timestamp. Use on initial load; after that the store is kept live by `user_offline` socket events. |
|
|
1555
|
+
| `updateProfile` | `(payload: UpdateProfilePayload) => Promise<User>` | Update the current user's profile fields. Works in both builtin and non-builtin modes. |
|
|
1556
|
+
| `updatePreferences` | `(prefs: UserPreferences) => Promise<User>` | Partial update of notification preferences. |
|
|
1557
|
+
| `getPreferences` | `() => Promise<UserPreferences \| null>` | Fetch current notification preferences. Returns `null` if no record exists yet. |
|
|
1542
1558
|
|
|
1543
1559
|
All methods return `User` objects that include `externalId` for non-builtin modes.
|
|
1544
1560
|
|
|
@@ -1560,6 +1576,43 @@ const user = await usersApi.getById('64abc...');
|
|
|
1560
1576
|
console.log(user.displayName, user.externalId);
|
|
1561
1577
|
```
|
|
1562
1578
|
|
|
1579
|
+
#### `updateProfile` — immediate profile update
|
|
1580
|
+
|
|
1581
|
+
Use this to push a profile change to the chat server without waiting for the next sync cycle. Useful in **non-builtin (external/SSO) modes** when the host app knows a field just changed in the identity provider — e.g. the user updated their name in the main system.
|
|
1582
|
+
|
|
1583
|
+
```typescript
|
|
1584
|
+
import { usersApi, type UpdateProfilePayload } from '@antzsoft/chat-core';
|
|
1585
|
+
|
|
1586
|
+
await usersApi.updateProfile({
|
|
1587
|
+
firstName: 'Jane',
|
|
1588
|
+
lastName: 'Smith',
|
|
1589
|
+
displayName: 'Jane S.', // optional — auto-derived from firstName+lastName if omitted (builtin)
|
|
1590
|
+
email: 'jane@example.com',
|
|
1591
|
+
phone: '+919900000000',
|
|
1592
|
+
});
|
|
1593
|
+
```
|
|
1594
|
+
|
|
1595
|
+
**Behaviour by mode:**
|
|
1596
|
+
|
|
1597
|
+
| Field | Builtin | Non-builtin (external / SSO / WSO2) |
|
|
1598
|
+
|---|---|---|
|
|
1599
|
+
| `firstName` | Written directly | Written immediately; overwritten on next sync if user-service has a different value |
|
|
1600
|
+
| `lastName` | Written directly | Written immediately; overwritten on next sync if user-service has a different value |
|
|
1601
|
+
| `email` | Written directly | Written immediately; overwritten on next sync if user-service has a different value |
|
|
1602
|
+
| `displayName` | Written. Auto-derived from `firstName`+`lastName` if omitted | Written immediately |
|
|
1603
|
+
| `phone` | Written directly | Written immediately |
|
|
1604
|
+
| `username` | Not allowed | Not allowed |
|
|
1605
|
+
| `status` | Not allowed | Not allowed |
|
|
1606
|
+
|
|
1607
|
+
**Uniqueness constraints (both modes):**
|
|
1608
|
+
- `email` must be unique within the tenant → `409 Conflict` if another user holds it
|
|
1609
|
+
- `phone` must be unique within the tenant → `409 Conflict` if another user holds it
|
|
1610
|
+
|
|
1611
|
+
**Background sync in non-builtin mode:**
|
|
1612
|
+
- On `AntzChatClient` socket connect, if the user's shadow record is stale (> 6 hours since last sync), the server fires a single-user sync from the upstream user-service in the background.
|
|
1613
|
+
- The 2-hour bulk cron also keeps all shadow records up to date.
|
|
1614
|
+
- `updateProfile()` lets the host app push changes immediately rather than waiting for the next sync cycle.
|
|
1615
|
+
|
|
1563
1616
|
---
|
|
1564
1617
|
|
|
1565
1618
|
### App Config API (`appConfigApi`)
|
|
@@ -2398,7 +2451,18 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
|
|
|
2398
2451
|
|
|
2399
2452
|
## Changelog
|
|
2400
2453
|
|
|
2454
|
+
### v1.1.3
|
|
2455
|
+
- **New: `conversationsApi.leave(conversationId, andDelete?)` — `andDelete` param** — Pass `andDelete: true` to exit a group and hide it from the caller's list in a single atomic server call ("Exit and Delete"). Previously required two separate calls.
|
|
2456
|
+
- **New: Auto-promote on last-admin exit** — When the only admin leaves a group, the server now automatically promotes the longest-standing active member to admin before completing the exit. Previously the server returned `400 Bad Request` requiring the admin to manually promote someone first.
|
|
2457
|
+
- **Changed: `conversationsApi.delete()` — no longer admin-only** — Any participant (active or inactive) can call `delete()` to hide their own copy of a DM or group. For DMs this is "Delete Chat". For groups this is "Delete Group" (for already-exited members). Other participants are unaffected.
|
|
2458
|
+
- **New: Message visibility windows (`membershipPeriods`)** — Each participant now tracks an array of membership time windows. The message fetch API enforces these windows so gap messages (sent while the user was not a member) are always hidden. Previously re-added users could see all messages including those sent during their absence.
|
|
2459
|
+
- **New: DM reappear on new message** — After a user deletes a DM, if the other participant sends a new message the conversation automatically reappears in the deleted user's list. The user sees only messages from the reappearance point onwards.
|
|
2460
|
+
- **New: `useConversations()` returns `leaveGroup`, `leaveAndDeleteGroup`, `deleteGroup` / `deleteConversation` mutations** — Available in both the web SDK and RN SDK hooks.
|
|
2461
|
+
|
|
2401
2462
|
### v1.1.2
|
|
2463
|
+
- **New: `usersApi.updateProfile()`** — Update the current user's profile fields (`firstName`, `lastName`, `email`, `displayName`, `phone`) immediately without waiting for the next sync cycle. Works in both builtin and non-builtin modes. Email and phone are enforced unique per tenant (409 on conflict). In non-builtin modes, fields may be overwritten on the next background sync if user-service returns different values.
|
|
2464
|
+
- **New: On-connect per-user sync** — In non-builtin modes, `AntzChatClient.connect()` now triggers a background sync of the current user's profile from the upstream user-service if the shadow record is stale (> 6 hours). Previously only the 2-hour bulk cron kept shadow records updated.
|
|
2465
|
+
- **New: Phone number uniqueness enforced** — `phone` is now unique per tenant (schema index + pre-check on register and `updateProfile`). A `409 Conflict` is returned if another user in the same tenant already holds the phone number.
|
|
2402
2466
|
- **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
2467
|
- **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
2468
|
- **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()`.
|
|
@@ -2453,7 +2517,7 @@ document.querySelectorAll('[data-conv-id]').forEach((el) => {
|
|
|
2453
2517
|
- Fix: attachment last-message content preview
|
|
2454
2518
|
- Fix: avatar upload via `AntzChatClient`
|
|
2455
2519
|
- Fix: `client.connect()` on React Native
|
|
2456
|
-
- Group icon — create &
|
|
2520
|
+
- Group icon — create, update & remove
|
|
2457
2521
|
- Scroll to first unread message
|
|
2458
2522
|
- Push notifications — device token registration (RN & Web)
|
|
2459
2523
|
|
package/dist/index.cjs
CHANGED
|
@@ -301,45 +301,61 @@ function b64ToBuf(b64) {
|
|
|
301
301
|
}
|
|
302
302
|
|
|
303
303
|
// src/crypto/session.ts
|
|
304
|
-
var
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
304
|
+
var _KEY = /* @__PURE__ */ Symbol.for("__antz_chat_transit__");
|
|
305
|
+
function getState() {
|
|
306
|
+
const g = globalThis;
|
|
307
|
+
if (!g[_KEY]) {
|
|
308
|
+
g[_KEY] = {
|
|
309
|
+
session: null,
|
|
310
|
+
sessionEverEstablished: false,
|
|
311
|
+
readyResolve: null,
|
|
312
|
+
readyPromise: null,
|
|
313
|
+
transitConfigured: null
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
return g[_KEY];
|
|
317
|
+
}
|
|
308
318
|
function configureTransit(enabled) {
|
|
309
|
-
|
|
319
|
+
const s = getState();
|
|
320
|
+
s.transitConfigured = enabled;
|
|
310
321
|
if (!enabled) {
|
|
311
|
-
|
|
312
|
-
|
|
322
|
+
s.readyResolve?.();
|
|
323
|
+
s.readyResolve = null;
|
|
313
324
|
}
|
|
314
325
|
}
|
|
315
326
|
function waitForTransitReady() {
|
|
316
|
-
|
|
317
|
-
if (
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
327
|
+
const s = getState();
|
|
328
|
+
if (!s.transitConfigured) return Promise.resolve();
|
|
329
|
+
if (s.session) return Promise.resolve();
|
|
330
|
+
if (s.sessionEverEstablished) return Promise.resolve();
|
|
331
|
+
if (!s.readyPromise) {
|
|
332
|
+
s.readyPromise = new Promise((resolve) => {
|
|
333
|
+
s.readyResolve = resolve;
|
|
321
334
|
});
|
|
322
335
|
}
|
|
323
|
-
return
|
|
336
|
+
return s.readyPromise;
|
|
324
337
|
}
|
|
325
338
|
function setTransitSession(session) {
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
339
|
+
const s = getState();
|
|
340
|
+
s.session = session;
|
|
341
|
+
s.sessionEverEstablished = true;
|
|
342
|
+
s.readyResolve?.();
|
|
343
|
+
s.readyResolve = null;
|
|
329
344
|
}
|
|
330
345
|
function clearTransitSession() {
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
346
|
+
const s = getState();
|
|
347
|
+
s.session = null;
|
|
348
|
+
s.readyPromise = null;
|
|
349
|
+
s.readyResolve = null;
|
|
334
350
|
}
|
|
335
351
|
function isTransitEnabled() {
|
|
336
|
-
return
|
|
352
|
+
return getState().session?.enabled === true;
|
|
337
353
|
}
|
|
338
354
|
function getSessionKey() {
|
|
339
|
-
return
|
|
355
|
+
return getState().session?.sessionKey ?? null;
|
|
340
356
|
}
|
|
341
357
|
function getSessionId() {
|
|
342
|
-
return
|
|
358
|
+
return getState().session?.sessionId ?? null;
|
|
343
359
|
}
|
|
344
360
|
|
|
345
361
|
// src/api/client.ts
|
|
@@ -699,8 +715,9 @@ var conversationsApi = {
|
|
|
699
715
|
async unpin(conversationId) {
|
|
700
716
|
await getApiClient().delete(`/conversations/${conversationId}/pin`);
|
|
701
717
|
},
|
|
702
|
-
async leave(conversationId) {
|
|
703
|
-
|
|
718
|
+
async leave(conversationId, andDelete) {
|
|
719
|
+
const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
|
|
720
|
+
await getApiClient().delete(url);
|
|
704
721
|
},
|
|
705
722
|
async getMembers(conversationId, filter) {
|
|
706
723
|
const { data } = await getApiClient().get(
|
|
@@ -740,6 +757,12 @@ var conversationsApi = {
|
|
|
740
757
|
{ fileId }
|
|
741
758
|
);
|
|
742
759
|
return normalizeConversation(data);
|
|
760
|
+
},
|
|
761
|
+
async removeIcon(conversationId) {
|
|
762
|
+
const { data } = await getApiClient().delete(
|
|
763
|
+
`/conversations/${conversationId}/icon`
|
|
764
|
+
);
|
|
765
|
+
return normalizeConversation(data);
|
|
743
766
|
}
|
|
744
767
|
};
|
|
745
768
|
|
|
@@ -868,6 +891,16 @@ var usersApi = {
|
|
|
868
891
|
const { data } = await getApiClient().get(`/users/${userId}`);
|
|
869
892
|
return { lastSeenAt: data.lastSeenAt ?? null };
|
|
870
893
|
},
|
|
894
|
+
/**
|
|
895
|
+
* Update basic profile fields for the current user.
|
|
896
|
+
* Works in both builtin and non-builtin modes. Use this to push an immediate
|
|
897
|
+
* profile update to the chat server when the host app knows a change just
|
|
898
|
+
* happened — without waiting for the next 2-hour sync cycle.
|
|
899
|
+
*/
|
|
900
|
+
async updateProfile(payload) {
|
|
901
|
+
const { data } = await getApiClient().put("/users/me", payload);
|
|
902
|
+
return data;
|
|
903
|
+
},
|
|
871
904
|
/**
|
|
872
905
|
* Update notification preferences for the current user.
|
|
873
906
|
* Partial update — only send fields you want to change.
|
|
@@ -1219,6 +1252,36 @@ function refreshSocketAuth() {
|
|
|
1219
1252
|
// src/socket/emitters.ts
|
|
1220
1253
|
var ACK_TIMEOUT = 5e3;
|
|
1221
1254
|
var RECONNECT_WAIT_TIMEOUT = 15e3;
|
|
1255
|
+
var QUEUE_MAX_SIZE = 100;
|
|
1256
|
+
var QUEUE_ENTRY_TTL = 3e4;
|
|
1257
|
+
var sendQueue = [];
|
|
1258
|
+
var sendQueueRunning = false;
|
|
1259
|
+
async function drainSendQueue() {
|
|
1260
|
+
if (sendQueueRunning) return;
|
|
1261
|
+
sendQueueRunning = true;
|
|
1262
|
+
while (sendQueue.length > 0) {
|
|
1263
|
+
const entry = sendQueue.shift();
|
|
1264
|
+
if (Date.now() - entry.enqueuedAt > QUEUE_ENTRY_TTL) {
|
|
1265
|
+
entry.reject(new Error("[AntzChat] Message dropped: queued too long"));
|
|
1266
|
+
continue;
|
|
1267
|
+
}
|
|
1268
|
+
try {
|
|
1269
|
+
entry.resolve(await entry.run());
|
|
1270
|
+
} catch (e) {
|
|
1271
|
+
entry.reject(e);
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
sendQueueRunning = false;
|
|
1275
|
+
}
|
|
1276
|
+
function queueSendMessage(payload) {
|
|
1277
|
+
if (sendQueue.length >= QUEUE_MAX_SIZE) {
|
|
1278
|
+
return Promise.reject(new Error("[AntzChat] Send queue full: too many messages in flight"));
|
|
1279
|
+
}
|
|
1280
|
+
return new Promise((resolve, reject) => {
|
|
1281
|
+
sendQueue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
|
|
1282
|
+
drainSendQueue();
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1222
1285
|
function waitForReconnect() {
|
|
1223
1286
|
return new Promise((resolve, reject) => {
|
|
1224
1287
|
const timer = setTimeout(() => {
|
|
@@ -1266,7 +1329,7 @@ var socketEmit = {
|
|
|
1266
1329
|
fireAndForget("leave_room", { conversationId });
|
|
1267
1330
|
},
|
|
1268
1331
|
sendMessage(payload) {
|
|
1269
|
-
return
|
|
1332
|
+
return queueSendMessage(payload);
|
|
1270
1333
|
},
|
|
1271
1334
|
updateMessage(messageId, text) {
|
|
1272
1335
|
return withAck("update_message", { messageId, text });
|