@antzsoft/chat-core 1.4.3 → 1.4.5
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 +26 -3
- package/dist/{chunk-XTQYF5HU.js → chunk-WUNH3UTE.js} +92 -22
- package/dist/chunk-WUNH3UTE.js.map +1 -0
- package/dist/index.cjs +944 -781
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +40 -5
- package/dist/index.d.ts +40 -5
- package/dist/index.js +557 -454
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs.map +1 -1
- package/dist/internal.d.cts +1 -1
- package/dist/internal.d.ts +1 -1
- package/dist/internal.js +1 -1
- package/dist/{storage-D7GPq-Mm.d.cts → storage-DlxfLVqx.d.cts} +43 -1
- package/dist/{storage-D7GPq-Mm.d.ts → storage-DlxfLVqx.d.ts} +43 -1
- package/docs/integration-guide.html +187 -3
- package/package.json +1 -1
- package/dist/chunk-XTQYF5HU.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -5,14 +5,17 @@ import {
|
|
|
5
5
|
AntzChatPermissionError,
|
|
6
6
|
AntzChatServerError,
|
|
7
7
|
AntzChatValidationError,
|
|
8
|
+
awaitTransitReadyOr,
|
|
8
9
|
clearTransitSession,
|
|
9
|
-
|
|
10
|
+
configureTransitIfUnset,
|
|
10
11
|
createRestTransitSession,
|
|
11
12
|
decryptPayload,
|
|
12
13
|
detectTransitAlgo,
|
|
13
14
|
encryptPayload,
|
|
15
|
+
ensureRestTransitHandshake,
|
|
14
16
|
fetchServerKeys,
|
|
15
17
|
generateEphemeralKey,
|
|
18
|
+
generateUUID,
|
|
16
19
|
getApiClient,
|
|
17
20
|
getCompressionStrategy,
|
|
18
21
|
getSessionId,
|
|
@@ -20,16 +23,20 @@ import {
|
|
|
20
23
|
getTransitHandshakePromise,
|
|
21
24
|
getTransitSession,
|
|
22
25
|
initApiClient,
|
|
26
|
+
isApiClientConfigured,
|
|
23
27
|
isTransitEnabled,
|
|
24
28
|
isTransitEnvelope,
|
|
29
|
+
isTransitRequired,
|
|
25
30
|
normalizeAxiosError,
|
|
31
|
+
onTransitReady,
|
|
26
32
|
performHandshake,
|
|
27
33
|
resetAlgoCache,
|
|
28
34
|
setApiClientInstance,
|
|
35
|
+
setAuthReadyPromise,
|
|
29
36
|
setTransitSession,
|
|
30
37
|
storageApi,
|
|
31
38
|
uploadBatch
|
|
32
|
-
} from "./chunk-
|
|
39
|
+
} from "./chunk-WUNH3UTE.js";
|
|
33
40
|
import {
|
|
34
41
|
useChatStore
|
|
35
42
|
} from "./chunk-UIYJAOGL.js";
|
|
@@ -88,6 +95,7 @@ function resolveConfig(config) {
|
|
|
88
95
|
compressDocuments: config.compression?.compressDocuments ?? true
|
|
89
96
|
},
|
|
90
97
|
persistStorage: config.persistStorage,
|
|
98
|
+
onSendError: config.onSendError,
|
|
91
99
|
messagePageSize: config.messagePageSize ?? 40,
|
|
92
100
|
starredMessagePageSize: config.starredMessagePageSize ?? 30,
|
|
93
101
|
searchPageSize: config.searchPageSize ?? 50
|
|
@@ -299,363 +307,6 @@ function resolveSystemMessageText(message, currentUserId) {
|
|
|
299
307
|
}
|
|
300
308
|
}
|
|
301
309
|
|
|
302
|
-
// src/api/messages.ts
|
|
303
|
-
var MAX_FORWARD_TARGETS = 5;
|
|
304
|
-
var HIGHLY_FORWARDED_DEPTH_THRESHOLD = 5;
|
|
305
|
-
var messagesApi = {
|
|
306
|
-
async list(conversationId, params = {}) {
|
|
307
|
-
const { cursor, direction, ...rest } = params;
|
|
308
|
-
const serverParams = { ...rest };
|
|
309
|
-
if (cursor) {
|
|
310
|
-
serverParams[direction === "after" ? "after" : "before"] = cursor;
|
|
311
|
-
}
|
|
312
|
-
const { data } = await getApiClient().get(
|
|
313
|
-
`/conversations/${conversationId}/messages`,
|
|
314
|
-
{ params: serverParams }
|
|
315
|
-
);
|
|
316
|
-
const currentUserId = getAuthStore().useAuthStore.getState().user?.id;
|
|
317
|
-
if (!currentUserId) return data;
|
|
318
|
-
return {
|
|
319
|
-
...data,
|
|
320
|
-
data: data.data.map(
|
|
321
|
-
(m) => m.content.type === "system" ? { ...m, content: { ...m.content, text: resolveSystemMessageText(m, currentUserId) } } : m
|
|
322
|
-
)
|
|
323
|
-
};
|
|
324
|
-
},
|
|
325
|
-
async get(messageId) {
|
|
326
|
-
const { data } = await getApiClient().get(`/messages/${messageId}`);
|
|
327
|
-
return data;
|
|
328
|
-
},
|
|
329
|
-
async send(conversationId, payload) {
|
|
330
|
-
const { data } = await getApiClient().post(
|
|
331
|
-
`/conversations/${conversationId}/messages`,
|
|
332
|
-
payload
|
|
333
|
-
);
|
|
334
|
-
return data;
|
|
335
|
-
},
|
|
336
|
-
async update(messageId, text) {
|
|
337
|
-
const { data } = await getApiClient().post(`/messages/${messageId}/update`, { text });
|
|
338
|
-
return data;
|
|
339
|
-
},
|
|
340
|
-
async delete(messageId) {
|
|
341
|
-
await getApiClient().post(`/messages/${messageId}/delete`);
|
|
342
|
-
},
|
|
343
|
-
async deleteForMe(messageId) {
|
|
344
|
-
await getApiClient().post(`/messages/${messageId}/delete-for-me`);
|
|
345
|
-
},
|
|
346
|
-
async addReaction(messageId, emoji) {
|
|
347
|
-
const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
|
|
348
|
-
return data;
|
|
349
|
-
},
|
|
350
|
-
async removeReaction(messageId, emoji) {
|
|
351
|
-
const { data } = await getApiClient().post(
|
|
352
|
-
`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`
|
|
353
|
-
);
|
|
354
|
-
return data;
|
|
355
|
-
},
|
|
356
|
-
async getReactions(messageId) {
|
|
357
|
-
const { data } = await getApiClient().post(`/messages/${messageId}/reactions/list`);
|
|
358
|
-
return data;
|
|
359
|
-
},
|
|
360
|
-
async star(messageId) {
|
|
361
|
-
await getApiClient().post(`/messages/${messageId}/star`);
|
|
362
|
-
},
|
|
363
|
-
async unstar(messageId) {
|
|
364
|
-
await getApiClient().post(`/messages/${messageId}/unstar`);
|
|
365
|
-
},
|
|
366
|
-
async getStarred(params = {}) {
|
|
367
|
-
const { data } = await getApiClient().get("/messages/starred", { params });
|
|
368
|
-
return data;
|
|
369
|
-
},
|
|
370
|
-
async search(params) {
|
|
371
|
-
const { data } = await getApiClient().get("/messages/search", { params });
|
|
372
|
-
return data;
|
|
373
|
-
},
|
|
374
|
-
async getLastRead(conversationId) {
|
|
375
|
-
const { data } = await getApiClient().get(
|
|
376
|
-
`/conversations/${conversationId}/read-receipt`
|
|
377
|
-
);
|
|
378
|
-
return data;
|
|
379
|
-
},
|
|
380
|
-
async markAsRead(conversationId, messageId) {
|
|
381
|
-
await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});
|
|
382
|
-
},
|
|
383
|
-
async pin(messageId) {
|
|
384
|
-
const { data } = await getApiClient().post(`/messages/${messageId}/pin`);
|
|
385
|
-
return data;
|
|
386
|
-
},
|
|
387
|
-
async unpin(messageId) {
|
|
388
|
-
const { data } = await getApiClient().post(`/messages/${messageId}/unpin`);
|
|
389
|
-
return data;
|
|
390
|
-
},
|
|
391
|
-
async getPinned(conversationId) {
|
|
392
|
-
const { data } = await getApiClient().get(`/conversations/${conversationId}/pinned-messages`);
|
|
393
|
-
return data;
|
|
394
|
-
},
|
|
395
|
-
async getReceipts(messageId) {
|
|
396
|
-
const { data } = await getApiClient().get(`/messages/${messageId}/receipts`);
|
|
397
|
-
return data;
|
|
398
|
-
},
|
|
399
|
-
/**
|
|
400
|
-
* Forwards a message into one or more target conversations (max MAX_FORWARD_TARGETS
|
|
401
|
-
* per call, also enforced server-side). Each target is independent — one failing
|
|
402
|
-
* (e.g. no longer a participant) does not block the others; check `success`/`error`
|
|
403
|
-
* per entry in the returned array.
|
|
404
|
-
*
|
|
405
|
-
* Pass `attachmentIds` to forward only a subset of the source message's attachments
|
|
406
|
-
* (e.g. one image out of a multi-image message) — omit it to forward the whole
|
|
407
|
-
* message, including all its attachments, unchanged. An ID not present on the
|
|
408
|
-
* source message is ignored server-side.
|
|
409
|
-
*/
|
|
410
|
-
async forward(messageId, targetConversationIds, attachmentIds) {
|
|
411
|
-
const { data } = await getApiClient().post(
|
|
412
|
-
`/messages/${messageId}/forward`,
|
|
413
|
-
{ targetConversationIds, ...attachmentIds ? { attachmentIds } : {} }
|
|
414
|
-
);
|
|
415
|
-
return data;
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
|
|
419
|
-
// src/api/conversations.ts
|
|
420
|
-
function normalizeParticipant(p) {
|
|
421
|
-
const hasUserDetails = p.displayName || p.username || p.avatarUrl;
|
|
422
|
-
return {
|
|
423
|
-
userId: p.userId,
|
|
424
|
-
externalId: p.externalId ?? p.user?.externalId,
|
|
425
|
-
role: p.role,
|
|
426
|
-
joinedAt: p.joinedAt,
|
|
427
|
-
isActive: p.isActive,
|
|
428
|
-
user: hasUserDetails ? {
|
|
429
|
-
id: p.userId,
|
|
430
|
-
externalId: p.externalId,
|
|
431
|
-
tenantId: "",
|
|
432
|
-
email: "",
|
|
433
|
-
username: p.username ?? "",
|
|
434
|
-
displayName: p.displayName ?? p.username ?? "",
|
|
435
|
-
avatarUrl: p.avatarUrl,
|
|
436
|
-
status: "offline",
|
|
437
|
-
createdAt: p.joinedAt ?? "",
|
|
438
|
-
updatedAt: p.joinedAt ?? ""
|
|
439
|
-
} : p.user
|
|
440
|
-
};
|
|
441
|
-
}
|
|
442
|
-
function normalizeLastMessage(lastMsg) {
|
|
443
|
-
if (!lastMsg) return void 0;
|
|
444
|
-
if (lastMsg.content !== void 0) return lastMsg;
|
|
445
|
-
return {
|
|
446
|
-
id: lastMsg.messageId ?? "",
|
|
447
|
-
tenantId: "",
|
|
448
|
-
conversationId: "",
|
|
449
|
-
senderId: lastMsg.senderId ?? "",
|
|
450
|
-
content: {
|
|
451
|
-
type: lastMsg.hasAttachments ? "attachment" : "text",
|
|
452
|
-
text: lastMsg.contentPreview
|
|
453
|
-
},
|
|
454
|
-
reactions: [],
|
|
455
|
-
lastReaction: lastMsg.lastReaction ?? null,
|
|
456
|
-
status: lastMsg.status ?? "active",
|
|
457
|
-
deliveryStatus: lastMsg.deliveryStatus ?? "sent",
|
|
458
|
-
isEdited: false,
|
|
459
|
-
sentAt: lastMsg.sentAt ?? "",
|
|
460
|
-
createdAt: lastMsg.sentAt ?? "",
|
|
461
|
-
...lastMsg.senderName && { senderName: lastMsg.senderName },
|
|
462
|
-
...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
function normalizeConversation(conv) {
|
|
466
|
-
return {
|
|
467
|
-
...conv,
|
|
468
|
-
id: conv.id ?? conv.conversationId,
|
|
469
|
-
participants: (conv.participants ?? []).map(normalizeParticipant),
|
|
470
|
-
lastMessage: normalizeLastMessage(conv.lastMessage)
|
|
471
|
-
};
|
|
472
|
-
}
|
|
473
|
-
var conversationsApi = {
|
|
474
|
-
async list(params = {}) {
|
|
475
|
-
const { data } = await getApiClient().get("/conversations", { params });
|
|
476
|
-
return { ...data, data: data.data.map(normalizeConversation) };
|
|
477
|
-
},
|
|
478
|
-
async get(conversationId) {
|
|
479
|
-
const { data } = await getApiClient().get(`/conversations/${conversationId}`);
|
|
480
|
-
return normalizeConversation(data);
|
|
481
|
-
},
|
|
482
|
-
async createGroup(payload) {
|
|
483
|
-
const { data } = await getApiClient().post("/conversations", payload);
|
|
484
|
-
return normalizeConversation(data);
|
|
485
|
-
},
|
|
486
|
-
async createDirect(payload) {
|
|
487
|
-
const { data } = await getApiClient().post("/conversations/direct", payload);
|
|
488
|
-
return normalizeConversation(data);
|
|
489
|
-
},
|
|
490
|
-
async update(conversationId, payload) {
|
|
491
|
-
const { data } = await getApiClient().post(`/conversations/${conversationId}/update`, payload);
|
|
492
|
-
return normalizeConversation(data);
|
|
493
|
-
},
|
|
494
|
-
async delete(conversationId) {
|
|
495
|
-
await getApiClient().post(`/conversations/${conversationId}/delete`);
|
|
496
|
-
},
|
|
497
|
-
async addParticipants(conversationId, userIds, role) {
|
|
498
|
-
const { data } = await getApiClient().post(
|
|
499
|
-
`/conversations/${conversationId}/participants`,
|
|
500
|
-
{ userIds, ...role && { role } }
|
|
501
|
-
);
|
|
502
|
-
return normalizeConversation(data);
|
|
503
|
-
},
|
|
504
|
-
async removeParticipant(conversationId, userId) {
|
|
505
|
-
const { data } = await getApiClient().post(
|
|
506
|
-
`/conversations/${conversationId}/participants/${userId}/remove`
|
|
507
|
-
);
|
|
508
|
-
return normalizeConversation(data);
|
|
509
|
-
},
|
|
510
|
-
async updateParticipantRole(conversationId, userId, role) {
|
|
511
|
-
const { data } = await getApiClient().post(
|
|
512
|
-
`/conversations/${conversationId}/participants/${userId}/role`,
|
|
513
|
-
{ role }
|
|
514
|
-
);
|
|
515
|
-
return normalizeConversation(data);
|
|
516
|
-
},
|
|
517
|
-
async mute(conversationId, mutedUntil) {
|
|
518
|
-
await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});
|
|
519
|
-
},
|
|
520
|
-
async unmute(conversationId) {
|
|
521
|
-
await getApiClient().post(`/conversations/${conversationId}/unmute`);
|
|
522
|
-
},
|
|
523
|
-
async pin(conversationId) {
|
|
524
|
-
await getApiClient().post(`/conversations/${conversationId}/pin`);
|
|
525
|
-
},
|
|
526
|
-
async unpin(conversationId) {
|
|
527
|
-
await getApiClient().post(`/conversations/${conversationId}/unpin`);
|
|
528
|
-
},
|
|
529
|
-
async markUnread(conversationId) {
|
|
530
|
-
await getApiClient().post(`/conversations/${conversationId}/unread`);
|
|
531
|
-
},
|
|
532
|
-
async markRead(conversationId) {
|
|
533
|
-
await getApiClient().post(`/conversations/${conversationId}/unread/clear`);
|
|
534
|
-
},
|
|
535
|
-
async leave(conversationId, andDelete) {
|
|
536
|
-
const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
|
|
537
|
-
await getApiClient().post(url);
|
|
538
|
-
},
|
|
539
|
-
async getMembers(conversationId, filter) {
|
|
540
|
-
const { data } = await getApiClient().get(
|
|
541
|
-
`/conversations/${conversationId}/participants`,
|
|
542
|
-
filter ? { params: { filter } } : void 0
|
|
543
|
-
);
|
|
544
|
-
return (data ?? []).map(normalizeParticipant);
|
|
545
|
-
},
|
|
546
|
-
/**
|
|
547
|
-
* Get unread message count for a single conversation.
|
|
548
|
-
* Use this after app foreground or socket reconnect to refresh a specific count.
|
|
549
|
-
*/
|
|
550
|
-
async getUnreadCount(conversationId) {
|
|
551
|
-
const { data } = await getApiClient().get(
|
|
552
|
-
`/conversations/${conversationId}/unread`
|
|
553
|
-
);
|
|
554
|
-
return data;
|
|
555
|
-
},
|
|
556
|
-
/**
|
|
557
|
-
* Get total unread count across all conversations + per-conversation breakdown.
|
|
558
|
-
* Use on app cold start, foreground resume, or after socket reconnect.
|
|
559
|
-
* The socket keeps counts live while connected — this is the source of truth
|
|
560
|
-
* when the socket was down.
|
|
561
|
-
*/
|
|
562
|
-
async getUnreadSummary() {
|
|
563
|
-
const { data } = await getApiClient().get("/conversations/unread");
|
|
564
|
-
return data;
|
|
565
|
-
},
|
|
566
|
-
/**
|
|
567
|
-
* Set the group icon from an already-uploaded file (admin only).
|
|
568
|
-
* The fileId comes from uploadBatch() / client.uploadFiles() — same as attachments.
|
|
569
|
-
* Server copies storageKey into conversation.iconMeta and deletes the chat_files record.
|
|
570
|
-
*/
|
|
571
|
-
async uploadIcon(conversationId, fileId) {
|
|
572
|
-
const { data } = await getApiClient().post(
|
|
573
|
-
`/conversations/${conversationId}/icon`,
|
|
574
|
-
{ fileId }
|
|
575
|
-
);
|
|
576
|
-
return normalizeConversation(data);
|
|
577
|
-
},
|
|
578
|
-
async removeIcon(conversationId) {
|
|
579
|
-
const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
|
|
580
|
-
return normalizeConversation(data);
|
|
581
|
-
},
|
|
582
|
-
async clearChat(conversationId) {
|
|
583
|
-
await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);
|
|
584
|
-
}
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
// src/api/devices.ts
|
|
588
|
-
var devicesApi = {
|
|
589
|
-
/**
|
|
590
|
-
* Register or update a device push token with the chat server.
|
|
591
|
-
*
|
|
592
|
-
* Upserts by `deviceId` — calling this multiple times with the same deviceId
|
|
593
|
-
* simply refreshes the token value (tokens can rotate silently on some platforms).
|
|
594
|
-
*
|
|
595
|
-
* The SDK never calls this automatically. The parent app or the `tokenProvider`
|
|
596
|
-
* option in `pushNotifications` config is responsible for calling it after
|
|
597
|
-
* obtaining the token from the OS / browser.
|
|
598
|
-
*/
|
|
599
|
-
async register(payload) {
|
|
600
|
-
await getApiClient().post("/users/me/devices", payload);
|
|
601
|
-
},
|
|
602
|
-
/**
|
|
603
|
-
* Remove a device token from the chat server.
|
|
604
|
-
* Call this on logout so the user stops receiving push notifications on this device.
|
|
605
|
-
*/
|
|
606
|
-
async remove(deviceId) {
|
|
607
|
-
await getApiClient().post(`/users/me/devices/${deviceId}/remove`);
|
|
608
|
-
}
|
|
609
|
-
};
|
|
610
|
-
|
|
611
|
-
// src/api/users.ts
|
|
612
|
-
var usersApi = {
|
|
613
|
-
async list(params = {}) {
|
|
614
|
-
const { data } = await getApiClient().get("/users", { params });
|
|
615
|
-
return data;
|
|
616
|
-
},
|
|
617
|
-
async getById(userId) {
|
|
618
|
-
const { data } = await getApiClient().get(`/users/${userId}`);
|
|
619
|
-
return data;
|
|
620
|
-
},
|
|
621
|
-
async getLastSeen(userId) {
|
|
622
|
-
const { data } = await getApiClient().get(`/users/${userId}`);
|
|
623
|
-
return { lastSeenAt: data.lastSeenAt ?? null };
|
|
624
|
-
},
|
|
625
|
-
/**
|
|
626
|
-
* Update basic profile fields for the current user.
|
|
627
|
-
* Works in both builtin and non-builtin modes. Use this to push an immediate
|
|
628
|
-
* profile update to the chat server when the host app knows a change just
|
|
629
|
-
* happened — without waiting for the next 2-hour sync cycle.
|
|
630
|
-
*/
|
|
631
|
-
async updateProfile(payload) {
|
|
632
|
-
const { data } = await getApiClient().post("/users/me/update", payload);
|
|
633
|
-
return data;
|
|
634
|
-
},
|
|
635
|
-
/**
|
|
636
|
-
* Update notification preferences for the current user.
|
|
637
|
-
* Partial update — only send fields you want to change.
|
|
638
|
-
* A prefs record is automatically created with defaults when a device
|
|
639
|
-
* token is first registered, so this never fails with "not found".
|
|
640
|
-
*/
|
|
641
|
-
async updatePreferences(prefs) {
|
|
642
|
-
const { data } = await getApiClient().post("/users/me/preferences", prefs);
|
|
643
|
-
return data;
|
|
644
|
-
},
|
|
645
|
-
/**
|
|
646
|
-
* Fetch current notification preferences for the current user.
|
|
647
|
-
* Returns null if no prefs record exists yet (all defaults apply).
|
|
648
|
-
*/
|
|
649
|
-
async getPreferences() {
|
|
650
|
-
try {
|
|
651
|
-
const { data } = await getApiClient().get("/users/me/preferences");
|
|
652
|
-
return data;
|
|
653
|
-
} catch {
|
|
654
|
-
return null;
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
};
|
|
658
|
-
|
|
659
310
|
// src/socket/socket.ts
|
|
660
311
|
import { io } from "socket.io-client";
|
|
661
312
|
var _socket = null;
|
|
@@ -667,7 +318,6 @@ var _getToken = null;
|
|
|
667
318
|
var _userId;
|
|
668
319
|
var _tenantId;
|
|
669
320
|
var _config = null;
|
|
670
|
-
var _preservingSession = false;
|
|
671
321
|
function setStatus(s) {
|
|
672
322
|
_status = s;
|
|
673
323
|
_statusListeners.forEach((l) => l(s));
|
|
@@ -686,29 +336,48 @@ function onSocketStatus(listener) {
|
|
|
686
336
|
_statusListeners.add(listener);
|
|
687
337
|
return () => _statusListeners.delete(listener);
|
|
688
338
|
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
ack(decrypted);
|
|
700
|
-
} catch {
|
|
701
|
-
ack(encryptedResponse);
|
|
702
|
-
}
|
|
703
|
-
} else {
|
|
704
|
-
ack(encryptedResponse);
|
|
705
|
-
}
|
|
706
|
-
});
|
|
339
|
+
var SECURE_EMIT_WAIT_MS = 4e3;
|
|
340
|
+
function emitEncrypted(socket, event, envelope, key, ack) {
|
|
341
|
+
if (ack) {
|
|
342
|
+
socket.emit(event, envelope, async (encryptedResponse) => {
|
|
343
|
+
if (isTransitEnvelope(encryptedResponse)) {
|
|
344
|
+
try {
|
|
345
|
+
ack(await decryptPayload(encryptedResponse, key));
|
|
346
|
+
} catch {
|
|
347
|
+
ack(encryptedResponse);
|
|
348
|
+
}
|
|
707
349
|
} else {
|
|
708
|
-
|
|
350
|
+
ack(encryptedResponse);
|
|
709
351
|
}
|
|
352
|
+
});
|
|
353
|
+
} else {
|
|
354
|
+
socket.emit(event, envelope);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async function secureEmit(socket, event, payload, ack) {
|
|
358
|
+
let key = getSessionKey();
|
|
359
|
+
if (key && isTransitEnabled()) {
|
|
360
|
+
emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (isTransitRequired()) {
|
|
364
|
+
try {
|
|
365
|
+
ensureRestTransitHandshake();
|
|
366
|
+
} catch {
|
|
367
|
+
}
|
|
368
|
+
const ok = await awaitTransitReadyOr(SECURE_EMIT_WAIT_MS);
|
|
369
|
+
key = getSessionKey();
|
|
370
|
+
if (ok && key && isTransitEnabled()) {
|
|
371
|
+
emitEncrypted(socket, event, await encryptPayload(payload, key), key, ack);
|
|
710
372
|
return;
|
|
711
373
|
}
|
|
374
|
+
if (isTransitRequired()) {
|
|
375
|
+
throw new AntzChatNetworkError(
|
|
376
|
+
`Transit encryption required but no session established \u2014 "${event}" not sent`,
|
|
377
|
+
"TRANSIT_NOT_READY",
|
|
378
|
+
{ event }
|
|
379
|
+
);
|
|
380
|
+
}
|
|
712
381
|
}
|
|
713
382
|
if (ack) {
|
|
714
383
|
socket.emit(event, payload, ack);
|
|
@@ -734,6 +403,28 @@ function secureOn(socket, event, handler) {
|
|
|
734
403
|
handler(raw);
|
|
735
404
|
});
|
|
736
405
|
}
|
|
406
|
+
var _joinedRooms = /* @__PURE__ */ new Set();
|
|
407
|
+
function trackRoomJoin(conversationId) {
|
|
408
|
+
_joinedRooms.add(conversationId);
|
|
409
|
+
}
|
|
410
|
+
function trackRoomLeave(conversationId) {
|
|
411
|
+
_joinedRooms.delete(conversationId);
|
|
412
|
+
}
|
|
413
|
+
function resetTrackedRooms() {
|
|
414
|
+
_joinedRooms.clear();
|
|
415
|
+
}
|
|
416
|
+
function flushJoinedRooms() {
|
|
417
|
+
const socket = tryGetSocket();
|
|
418
|
+
if (!socket || _joinedRooms.size === 0) return;
|
|
419
|
+
for (const conversationId of _joinedRooms) {
|
|
420
|
+
void secureEmit(socket, "join_room", { conversationId }).catch(() => {
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
onTransitReady(flushJoinedRooms);
|
|
425
|
+
onSocketStatus((status) => {
|
|
426
|
+
if (status === "connected") flushJoinedRooms();
|
|
427
|
+
});
|
|
737
428
|
async function connectSocket(config, getToken) {
|
|
738
429
|
if (_socket && !_socket.disconnected) return _socket;
|
|
739
430
|
if (_connectingPromise) return _connectingPromise;
|
|
@@ -755,6 +446,7 @@ async function _doConnect(config, getToken) {
|
|
|
755
446
|
...config.avatar?.url && { avatarUrl: config.avatar.url },
|
|
756
447
|
...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
|
|
757
448
|
};
|
|
449
|
+
configureTransitIfUnset(config.transitEncryption);
|
|
758
450
|
let boundDeriveSessionKey = null;
|
|
759
451
|
if (config.transitEncryption) {
|
|
760
452
|
const inFlight = getTransitHandshakePromise();
|
|
@@ -819,7 +511,6 @@ async function _doConnect(config, getToken) {
|
|
|
819
511
|
_socket.on("connect", () => setStatus("connected"));
|
|
820
512
|
_socket.on("disconnect", () => {
|
|
821
513
|
setStatus("disconnected");
|
|
822
|
-
if (!_preservingSession) clearTransitSession();
|
|
823
514
|
});
|
|
824
515
|
_socket.on("connect_error", (err) => {
|
|
825
516
|
console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
|
|
@@ -834,8 +525,9 @@ async function _doConnect(config, getToken) {
|
|
|
834
525
|
resolve();
|
|
835
526
|
};
|
|
836
527
|
const timeout = setTimeout(() => {
|
|
837
|
-
console.
|
|
838
|
-
|
|
528
|
+
console.error(
|
|
529
|
+
"[AntzChat] transit_session did not arrive within 5s. Transit stays REQUIRED \u2014 chat requests are gated (not downgraded to plaintext) until the handshake succeeds via REST retry or socket reconnect. Persistent failure = check POST /crypto/session (rate limit / 5xx) and the socket transit middleware."
|
|
530
|
+
);
|
|
839
531
|
done();
|
|
840
532
|
}, 5e3);
|
|
841
533
|
_socket.on("transit_session", async ({ sessionId }) => {
|
|
@@ -922,13 +614,8 @@ function reconnectSocket(token, userId, tenantId) {
|
|
|
922
614
|
...tenantId && { tenantId },
|
|
923
615
|
transitSessionId: existing.sessionId
|
|
924
616
|
};
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (_socket.connected) _socket.disconnect();
|
|
928
|
-
_socket.connect();
|
|
929
|
-
} finally {
|
|
930
|
-
_preservingSession = false;
|
|
931
|
-
}
|
|
617
|
+
if (_socket.connected) _socket.disconnect();
|
|
618
|
+
_socket.connect();
|
|
932
619
|
return;
|
|
933
620
|
}
|
|
934
621
|
if (_getToken) {
|
|
@@ -973,7 +660,15 @@ async function drainSendQueue(conversationId) {
|
|
|
973
660
|
entry.reject(new AntzChatNetworkError("Message dropped: queued too long", "MESSAGE_DROPPED"));
|
|
974
661
|
continue;
|
|
975
662
|
}
|
|
976
|
-
|
|
663
|
+
let acked;
|
|
664
|
+
try {
|
|
665
|
+
({ acked } = await emitWithAck("send_message", entry.payload));
|
|
666
|
+
} catch (err) {
|
|
667
|
+
entry.reject(err);
|
|
668
|
+
while (queue.length > 0) queue.shift().reject(err);
|
|
669
|
+
break;
|
|
670
|
+
}
|
|
671
|
+
acked.then(entry.resolve).catch(entry.reject);
|
|
977
672
|
}
|
|
978
673
|
sendQueues.delete(conversationId);
|
|
979
674
|
sendQueueRunning.delete(conversationId);
|
|
@@ -986,7 +681,7 @@ function queueSendMessage(payload) {
|
|
|
986
681
|
return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
|
|
987
682
|
}
|
|
988
683
|
return new Promise((resolve, reject) => {
|
|
989
|
-
queue.push({
|
|
684
|
+
queue.push({ payload, resolve, reject, enqueuedAt: Date.now() });
|
|
990
685
|
drainSendQueue(conversationId);
|
|
991
686
|
});
|
|
992
687
|
}
|
|
@@ -1009,91 +704,496 @@ function waitForReconnect() {
|
|
|
1009
704
|
});
|
|
1010
705
|
});
|
|
1011
706
|
}
|
|
1012
|
-
async function
|
|
707
|
+
async function emitWithAck(event, payload) {
|
|
1013
708
|
let socket = tryGetSocket();
|
|
1014
709
|
if (!socket) {
|
|
1015
710
|
await waitForReconnect();
|
|
1016
711
|
socket = tryGetSocket();
|
|
1017
712
|
}
|
|
1018
|
-
if (!socket)
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
713
|
+
if (!socket) throw new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event });
|
|
714
|
+
let resolveAck;
|
|
715
|
+
let rejectAck;
|
|
716
|
+
const acked = new Promise((res, rej) => {
|
|
717
|
+
resolveAck = res;
|
|
718
|
+
rejectAck = rej;
|
|
719
|
+
});
|
|
720
|
+
await secureEmit(socket, event, payload, (response) => resolveAck(response));
|
|
721
|
+
const timer = setTimeout(
|
|
722
|
+
() => rejectAck(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })),
|
|
723
|
+
ACK_TIMEOUT
|
|
724
|
+
);
|
|
725
|
+
void acked.then(() => clearTimeout(timer), () => clearTimeout(timer));
|
|
726
|
+
return { acked };
|
|
727
|
+
}
|
|
728
|
+
async function withAck(event, payload) {
|
|
729
|
+
const { acked } = await emitWithAck(event, payload);
|
|
730
|
+
return acked;
|
|
731
|
+
}
|
|
732
|
+
function fireAndForget(event, payload) {
|
|
733
|
+
const socket = tryGetSocket();
|
|
734
|
+
if (!socket) return;
|
|
735
|
+
secureEmit(socket, event, payload).catch(() => {
|
|
1027
736
|
});
|
|
1028
737
|
}
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
738
|
+
var socketEmit = {
|
|
739
|
+
joinRoom(conversationId) {
|
|
740
|
+
trackRoomJoin(conversationId);
|
|
741
|
+
fireAndForget("join_room", { conversationId });
|
|
742
|
+
},
|
|
743
|
+
leaveRoom(conversationId) {
|
|
744
|
+
trackRoomLeave(conversationId);
|
|
745
|
+
fireAndForget("leave_room", { conversationId });
|
|
746
|
+
},
|
|
747
|
+
sendMessage(payload) {
|
|
748
|
+
return queueSendMessage({ ...payload, sentAt: Date.now() });
|
|
749
|
+
},
|
|
750
|
+
// Not queued like sendMessage — forward targets are independent conversations,
|
|
751
|
+
// not the single conversation ordering that queueSendMessage protects, and each
|
|
752
|
+
// target's own new_message/push already fires server-side via MessagesService.forward()'s
|
|
753
|
+
// internal create() calls, so there's nothing here that needs in-order draining.
|
|
754
|
+
forwardMessage(payload) {
|
|
755
|
+
return withAck("forward_message", payload);
|
|
756
|
+
},
|
|
757
|
+
updateMessage(messageId, text) {
|
|
758
|
+
return withAck("update_message", { messageId, text });
|
|
759
|
+
},
|
|
760
|
+
deleteMessage(messageId) {
|
|
761
|
+
return withAck("delete_message", { messageId });
|
|
762
|
+
},
|
|
763
|
+
deleteMessageForMe(messageId) {
|
|
764
|
+
return withAck("delete_message_for_me", { messageId });
|
|
765
|
+
},
|
|
766
|
+
clearChat(conversationId) {
|
|
767
|
+
return withAck("clear_chat_for_me", { conversationId });
|
|
768
|
+
},
|
|
769
|
+
addReaction(messageId, emoji) {
|
|
770
|
+
return withAck("add_reaction", { messageId, emoji });
|
|
771
|
+
},
|
|
772
|
+
removeReaction(messageId, emoji) {
|
|
773
|
+
return withAck("remove_reaction", { messageId, emoji });
|
|
774
|
+
},
|
|
775
|
+
pinMessage(messageId) {
|
|
776
|
+
return withAck("pin_message", { messageId });
|
|
777
|
+
},
|
|
778
|
+
unpinMessage(messageId) {
|
|
779
|
+
return withAck("unpin_message", { messageId });
|
|
780
|
+
},
|
|
781
|
+
// markRead and typing are best-effort — silently dropped if socket not ready
|
|
782
|
+
typing(conversationId, isTyping) {
|
|
783
|
+
fireAndForget("typing", { conversationId, isTyping });
|
|
784
|
+
},
|
|
785
|
+
markRead(conversationId, messageId) {
|
|
786
|
+
fireAndForget("mark_read", { conversationId, ...messageId ? { messageId } : {} });
|
|
787
|
+
},
|
|
788
|
+
getOnlineUsers(userIds) {
|
|
789
|
+
const socket = tryGetSocket();
|
|
790
|
+
if (!socket) return Promise.resolve([]);
|
|
791
|
+
return new Promise((resolve, reject) => {
|
|
792
|
+
let timer;
|
|
793
|
+
secureEmit(socket, "get_online_users", { userIds }, (response) => {
|
|
794
|
+
clearTimeout(timer);
|
|
795
|
+
if (response && typeof response === "object" && "onlineStatus" in response) {
|
|
796
|
+
const status = response.onlineStatus;
|
|
797
|
+
resolve(Object.entries(status).filter(([, v]) => v).map(([k]) => k));
|
|
798
|
+
} else if (Array.isArray(response)) {
|
|
799
|
+
resolve(response);
|
|
800
|
+
} else {
|
|
801
|
+
resolve([]);
|
|
802
|
+
}
|
|
803
|
+
}).then(() => {
|
|
804
|
+
timer = setTimeout(() => reject(new AntzChatNetworkError("Socket ack timeout: get_online_users", "SOCKET_TIMEOUT", { event: "get_online_users" })), ACK_TIMEOUT);
|
|
805
|
+
}).catch(reject);
|
|
806
|
+
});
|
|
807
|
+
},
|
|
808
|
+
getTypingUsers(conversationId) {
|
|
809
|
+
return withAck("get_typing_users", { conversationId });
|
|
810
|
+
}
|
|
811
|
+
};
|
|
812
|
+
|
|
813
|
+
// src/api/messages.ts
|
|
814
|
+
var MAX_FORWARD_TARGETS = 5;
|
|
815
|
+
var HIGHLY_FORWARDED_DEPTH_THRESHOLD = 5;
|
|
816
|
+
var messagesApi = {
|
|
817
|
+
async list(conversationId, params = {}) {
|
|
818
|
+
const { cursor, direction, ...rest } = params;
|
|
819
|
+
const serverParams = { ...rest };
|
|
820
|
+
if (cursor) {
|
|
821
|
+
serverParams[direction === "after" ? "after" : "before"] = cursor;
|
|
822
|
+
}
|
|
823
|
+
const { data } = await getApiClient().get(
|
|
824
|
+
`/conversations/${conversationId}/messages`,
|
|
825
|
+
{ params: serverParams }
|
|
826
|
+
);
|
|
827
|
+
const currentUserId = getAuthStore().useAuthStore.getState().user?.id;
|
|
828
|
+
if (!currentUserId) return data;
|
|
829
|
+
return {
|
|
830
|
+
...data,
|
|
831
|
+
data: data.data.map(
|
|
832
|
+
(m) => m.content.type === "system" ? { ...m, content: { ...m.content, text: resolveSystemMessageText(m, currentUserId) } } : m
|
|
833
|
+
)
|
|
834
|
+
};
|
|
835
|
+
},
|
|
836
|
+
async get(messageId) {
|
|
837
|
+
const { data } = await getApiClient().get(`/messages/${messageId}`);
|
|
838
|
+
return data;
|
|
839
|
+
},
|
|
840
|
+
/**
|
|
841
|
+
* Sends a message via REST. For real-time delivery use `socketEmit.sendMessage`
|
|
842
|
+
* instead — this is a lower-level entry point (used e.g. by the socket path's
|
|
843
|
+
* REST-mirror flows). Pass `payload.tempId` and reuse the SAME value on retry to
|
|
844
|
+
* make a retry-after-timeout safe — see `SendData.tempId`.
|
|
845
|
+
*/
|
|
846
|
+
async send(conversationId, payload) {
|
|
847
|
+
const { data } = await getApiClient().post(
|
|
848
|
+
`/conversations/${conversationId}/messages`,
|
|
849
|
+
payload
|
|
850
|
+
);
|
|
851
|
+
return data;
|
|
852
|
+
},
|
|
853
|
+
async update(messageId, text) {
|
|
854
|
+
const { data } = await getApiClient().post(`/messages/${messageId}/update`, { text });
|
|
855
|
+
return data;
|
|
856
|
+
},
|
|
857
|
+
async delete(messageId) {
|
|
858
|
+
await getApiClient().post(`/messages/${messageId}/delete`);
|
|
859
|
+
},
|
|
860
|
+
async deleteForMe(messageId) {
|
|
861
|
+
await getApiClient().post(`/messages/${messageId}/delete-for-me`);
|
|
862
|
+
},
|
|
863
|
+
async addReaction(messageId, emoji) {
|
|
864
|
+
const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
|
|
865
|
+
return data;
|
|
866
|
+
},
|
|
867
|
+
async removeReaction(messageId, emoji) {
|
|
868
|
+
const { data } = await getApiClient().post(
|
|
869
|
+
`/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`
|
|
870
|
+
);
|
|
871
|
+
return data;
|
|
872
|
+
},
|
|
873
|
+
async getReactions(messageId) {
|
|
874
|
+
const { data } = await getApiClient().post(`/messages/${messageId}/reactions/list`);
|
|
875
|
+
return data;
|
|
876
|
+
},
|
|
877
|
+
async star(messageId) {
|
|
878
|
+
await getApiClient().post(`/messages/${messageId}/star`);
|
|
879
|
+
},
|
|
880
|
+
async unstar(messageId) {
|
|
881
|
+
await getApiClient().post(`/messages/${messageId}/unstar`);
|
|
882
|
+
},
|
|
883
|
+
async getStarred(params = {}) {
|
|
884
|
+
const { data } = await getApiClient().get("/messages/starred", { params });
|
|
885
|
+
return data;
|
|
886
|
+
},
|
|
887
|
+
async search(params) {
|
|
888
|
+
const { data } = await getApiClient().get("/messages/search", { params });
|
|
889
|
+
return data;
|
|
890
|
+
},
|
|
891
|
+
async getLastRead(conversationId) {
|
|
892
|
+
const { data } = await getApiClient().get(
|
|
893
|
+
`/conversations/${conversationId}/read-receipt`
|
|
894
|
+
);
|
|
895
|
+
return data;
|
|
896
|
+
},
|
|
897
|
+
async markAsRead(conversationId, messageId) {
|
|
898
|
+
await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});
|
|
899
|
+
},
|
|
900
|
+
async pin(messageId) {
|
|
901
|
+
const { data } = await getApiClient().post(`/messages/${messageId}/pin`);
|
|
902
|
+
return data;
|
|
903
|
+
},
|
|
904
|
+
async unpin(messageId) {
|
|
905
|
+
const { data } = await getApiClient().post(`/messages/${messageId}/unpin`);
|
|
906
|
+
return data;
|
|
907
|
+
},
|
|
908
|
+
async getPinned(conversationId) {
|
|
909
|
+
const { data } = await getApiClient().get(`/conversations/${conversationId}/pinned-messages`);
|
|
910
|
+
return data;
|
|
911
|
+
},
|
|
912
|
+
async getReceipts(messageId) {
|
|
913
|
+
const { data } = await getApiClient().get(`/messages/${messageId}/receipts`);
|
|
914
|
+
return data;
|
|
915
|
+
},
|
|
916
|
+
/**
|
|
917
|
+
* Forwards a message into one or more target conversations (max MAX_FORWARD_TARGETS
|
|
918
|
+
* per call, also enforced server-side). Each target is independent — one failing
|
|
919
|
+
* (e.g. no longer a participant) does not block the others; check `success`/`error`
|
|
920
|
+
* per entry in the returned array.
|
|
921
|
+
*
|
|
922
|
+
* Pass `attachmentIds` to forward only a subset of the source message's attachments
|
|
923
|
+
* (e.g. one image out of a multi-image message) — omit it to forward the whole
|
|
924
|
+
* message, including all its attachments, unchanged. An ID not present on the
|
|
925
|
+
* source message is ignored server-side.
|
|
926
|
+
*
|
|
927
|
+
* Idempotency: pass `tempId` — generate ONE value per forward action (e.g. via
|
|
928
|
+
* `generateUUID` from '@antzsoft/chat-core/internal') and reuse that SAME value if
|
|
929
|
+
* you retry this exact forward (e.g. after a client-side timeout). The server then
|
|
930
|
+
* recognizes the retry per target and returns the already-created message instead
|
|
931
|
+
* of creating a duplicate. Never mint a new tempId for a retry — only when the user
|
|
932
|
+
* initiates a genuinely new forward action. If omitted, a fresh one is generated
|
|
933
|
+
* per call, which means a retry without an explicit tempId gets NO dedup protection.
|
|
934
|
+
*
|
|
935
|
+
* Transport: uses the 'forward_message' socket event when a socket is connected
|
|
936
|
+
* (lower latency, same server-side MessagesService.forward() path, so broadcast/
|
|
937
|
+
* push notification behavior is identical either way) and transparently falls back
|
|
938
|
+
* to the REST endpoint when no socket is available.
|
|
939
|
+
*/
|
|
940
|
+
async forward(messageId, targetConversationIds, attachmentIds, tempId) {
|
|
941
|
+
const resolvedTempId = tempId ?? generateUUID();
|
|
942
|
+
if (tryGetSocket()) {
|
|
943
|
+
const ack = await socketEmit.forwardMessage({
|
|
944
|
+
messageId,
|
|
945
|
+
targetConversationIds,
|
|
946
|
+
...attachmentIds ? { attachmentIds } : {},
|
|
947
|
+
tempId: resolvedTempId
|
|
948
|
+
});
|
|
949
|
+
if (ack.error) throw new Error(ack.error);
|
|
950
|
+
return ack.results;
|
|
951
|
+
}
|
|
952
|
+
const { data } = await getApiClient().post(
|
|
953
|
+
`/messages/${messageId}/forward`,
|
|
954
|
+
{ targetConversationIds, ...attachmentIds ? { attachmentIds } : {}, tempId: resolvedTempId }
|
|
955
|
+
);
|
|
956
|
+
return data;
|
|
957
|
+
}
|
|
958
|
+
};
|
|
959
|
+
|
|
960
|
+
// src/api/conversations.ts
|
|
961
|
+
function normalizeParticipant(p) {
|
|
962
|
+
const hasUserDetails = p.displayName || p.username || p.avatarUrl;
|
|
963
|
+
return {
|
|
964
|
+
userId: p.userId,
|
|
965
|
+
externalId: p.externalId ?? p.user?.externalId,
|
|
966
|
+
role: p.role,
|
|
967
|
+
joinedAt: p.joinedAt,
|
|
968
|
+
isActive: p.isActive,
|
|
969
|
+
user: hasUserDetails ? {
|
|
970
|
+
id: p.userId,
|
|
971
|
+
externalId: p.externalId,
|
|
972
|
+
tenantId: "",
|
|
973
|
+
email: "",
|
|
974
|
+
username: p.username ?? "",
|
|
975
|
+
displayName: p.displayName ?? p.username ?? "",
|
|
976
|
+
avatarUrl: p.avatarUrl,
|
|
977
|
+
status: "offline",
|
|
978
|
+
createdAt: p.joinedAt ?? "",
|
|
979
|
+
updatedAt: p.joinedAt ?? ""
|
|
980
|
+
} : p.user
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
function normalizeLastMessage(lastMsg) {
|
|
984
|
+
if (!lastMsg) return void 0;
|
|
985
|
+
if (lastMsg.content !== void 0) return lastMsg;
|
|
986
|
+
return {
|
|
987
|
+
id: lastMsg.messageId ?? "",
|
|
988
|
+
tenantId: "",
|
|
989
|
+
conversationId: "",
|
|
990
|
+
senderId: lastMsg.senderId ?? "",
|
|
991
|
+
content: {
|
|
992
|
+
type: lastMsg.hasAttachments ? "attachment" : "text",
|
|
993
|
+
text: lastMsg.contentPreview
|
|
994
|
+
},
|
|
995
|
+
reactions: [],
|
|
996
|
+
lastReaction: lastMsg.lastReaction ?? null,
|
|
997
|
+
status: lastMsg.status ?? "active",
|
|
998
|
+
deliveryStatus: lastMsg.deliveryStatus ?? "sent",
|
|
999
|
+
isEdited: false,
|
|
1000
|
+
sentAt: lastMsg.sentAt ?? "",
|
|
1001
|
+
createdAt: lastMsg.sentAt ?? "",
|
|
1002
|
+
...lastMsg.senderName && { senderName: lastMsg.senderName },
|
|
1003
|
+
...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
|
|
1004
|
+
};
|
|
1033
1005
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1006
|
+
function normalizeConversation(conv) {
|
|
1007
|
+
return {
|
|
1008
|
+
...conv,
|
|
1009
|
+
id: conv.id ?? conv.conversationId,
|
|
1010
|
+
participants: (conv.participants ?? []).map(normalizeParticipant),
|
|
1011
|
+
lastMessage: normalizeLastMessage(conv.lastMessage)
|
|
1012
|
+
};
|
|
1013
|
+
}
|
|
1014
|
+
var conversationsApi = {
|
|
1015
|
+
async list(params = {}) {
|
|
1016
|
+
const { data } = await getApiClient().get("/conversations", { params });
|
|
1017
|
+
return { ...data, data: data.data.map(normalizeConversation) };
|
|
1037
1018
|
},
|
|
1038
|
-
|
|
1039
|
-
|
|
1019
|
+
async get(conversationId) {
|
|
1020
|
+
const { data } = await getApiClient().get(`/conversations/${conversationId}`);
|
|
1021
|
+
return normalizeConversation(data);
|
|
1040
1022
|
},
|
|
1041
|
-
|
|
1042
|
-
|
|
1023
|
+
async createGroup(payload) {
|
|
1024
|
+
const { data } = await getApiClient().post("/conversations", payload);
|
|
1025
|
+
return normalizeConversation(data);
|
|
1043
1026
|
},
|
|
1044
|
-
|
|
1045
|
-
|
|
1027
|
+
async createDirect(payload) {
|
|
1028
|
+
const { data } = await getApiClient().post("/conversations/direct", payload);
|
|
1029
|
+
return normalizeConversation(data);
|
|
1046
1030
|
},
|
|
1047
|
-
|
|
1048
|
-
|
|
1031
|
+
async update(conversationId, payload) {
|
|
1032
|
+
const { data } = await getApiClient().post(`/conversations/${conversationId}/update`, payload);
|
|
1033
|
+
return normalizeConversation(data);
|
|
1049
1034
|
},
|
|
1050
|
-
|
|
1051
|
-
|
|
1035
|
+
async delete(conversationId) {
|
|
1036
|
+
await getApiClient().post(`/conversations/${conversationId}/delete`);
|
|
1052
1037
|
},
|
|
1053
|
-
|
|
1054
|
-
|
|
1038
|
+
async addParticipants(conversationId, userIds, role) {
|
|
1039
|
+
const { data } = await getApiClient().post(
|
|
1040
|
+
`/conversations/${conversationId}/participants`,
|
|
1041
|
+
{ userIds, ...role && { role } }
|
|
1042
|
+
);
|
|
1043
|
+
return normalizeConversation(data);
|
|
1055
1044
|
},
|
|
1056
|
-
|
|
1057
|
-
|
|
1045
|
+
async removeParticipant(conversationId, userId) {
|
|
1046
|
+
const { data } = await getApiClient().post(
|
|
1047
|
+
`/conversations/${conversationId}/participants/${userId}/remove`
|
|
1048
|
+
);
|
|
1049
|
+
return normalizeConversation(data);
|
|
1058
1050
|
},
|
|
1059
|
-
|
|
1060
|
-
|
|
1051
|
+
async updateParticipantRole(conversationId, userId, role) {
|
|
1052
|
+
const { data } = await getApiClient().post(
|
|
1053
|
+
`/conversations/${conversationId}/participants/${userId}/role`,
|
|
1054
|
+
{ role }
|
|
1055
|
+
);
|
|
1056
|
+
return normalizeConversation(data);
|
|
1061
1057
|
},
|
|
1062
|
-
|
|
1063
|
-
|
|
1058
|
+
async mute(conversationId, mutedUntil) {
|
|
1059
|
+
await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});
|
|
1064
1060
|
},
|
|
1065
|
-
|
|
1066
|
-
|
|
1061
|
+
async unmute(conversationId) {
|
|
1062
|
+
await getApiClient().post(`/conversations/${conversationId}/unmute`);
|
|
1067
1063
|
},
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
fireAndForget("typing", { conversationId, isTyping });
|
|
1064
|
+
async pin(conversationId) {
|
|
1065
|
+
await getApiClient().post(`/conversations/${conversationId}/pin`);
|
|
1071
1066
|
},
|
|
1072
|
-
|
|
1073
|
-
|
|
1067
|
+
async unpin(conversationId) {
|
|
1068
|
+
await getApiClient().post(`/conversations/${conversationId}/unpin`);
|
|
1074
1069
|
},
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
if (!socket) return Promise.resolve([]);
|
|
1078
|
-
return new Promise((resolve, reject) => {
|
|
1079
|
-
let timer;
|
|
1080
|
-
secureEmit(socket, "get_online_users", { userIds }, (response) => {
|
|
1081
|
-
clearTimeout(timer);
|
|
1082
|
-
if (response && typeof response === "object" && "onlineStatus" in response) {
|
|
1083
|
-
const status = response.onlineStatus;
|
|
1084
|
-
resolve(Object.entries(status).filter(([, v]) => v).map(([k]) => k));
|
|
1085
|
-
} else if (Array.isArray(response)) {
|
|
1086
|
-
resolve(response);
|
|
1087
|
-
} else {
|
|
1088
|
-
resolve([]);
|
|
1089
|
-
}
|
|
1090
|
-
}).then(() => {
|
|
1091
|
-
timer = setTimeout(() => reject(new AntzChatNetworkError("Socket ack timeout: get_online_users", "SOCKET_TIMEOUT", { event: "get_online_users" })), ACK_TIMEOUT);
|
|
1092
|
-
}).catch(reject);
|
|
1093
|
-
});
|
|
1070
|
+
async markUnread(conversationId) {
|
|
1071
|
+
await getApiClient().post(`/conversations/${conversationId}/unread`);
|
|
1094
1072
|
},
|
|
1095
|
-
|
|
1096
|
-
|
|
1073
|
+
async markRead(conversationId) {
|
|
1074
|
+
await getApiClient().post(`/conversations/${conversationId}/unread/clear`);
|
|
1075
|
+
},
|
|
1076
|
+
async leave(conversationId, andDelete) {
|
|
1077
|
+
const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
|
|
1078
|
+
await getApiClient().post(url);
|
|
1079
|
+
},
|
|
1080
|
+
async getMembers(conversationId, filter) {
|
|
1081
|
+
const { data } = await getApiClient().get(
|
|
1082
|
+
`/conversations/${conversationId}/participants`,
|
|
1083
|
+
filter ? { params: { filter } } : void 0
|
|
1084
|
+
);
|
|
1085
|
+
return (data ?? []).map(normalizeParticipant);
|
|
1086
|
+
},
|
|
1087
|
+
/**
|
|
1088
|
+
* Get unread message count for a single conversation.
|
|
1089
|
+
* Use this after app foreground or socket reconnect to refresh a specific count.
|
|
1090
|
+
*/
|
|
1091
|
+
async getUnreadCount(conversationId) {
|
|
1092
|
+
const { data } = await getApiClient().get(
|
|
1093
|
+
`/conversations/${conversationId}/unread`
|
|
1094
|
+
);
|
|
1095
|
+
return data;
|
|
1096
|
+
},
|
|
1097
|
+
/**
|
|
1098
|
+
* Get total unread count across all conversations + per-conversation breakdown.
|
|
1099
|
+
* Use on app cold start, foreground resume, or after socket reconnect.
|
|
1100
|
+
* The socket keeps counts live while connected — this is the source of truth
|
|
1101
|
+
* when the socket was down.
|
|
1102
|
+
*/
|
|
1103
|
+
async getUnreadSummary() {
|
|
1104
|
+
const { data } = await getApiClient().get("/conversations/unread");
|
|
1105
|
+
return data;
|
|
1106
|
+
},
|
|
1107
|
+
/**
|
|
1108
|
+
* Set the group icon from an already-uploaded file (admin only).
|
|
1109
|
+
* The fileId comes from uploadBatch() / client.uploadFiles() — same as attachments.
|
|
1110
|
+
* Server copies storageKey into conversation.iconMeta and deletes the chat_files record.
|
|
1111
|
+
*/
|
|
1112
|
+
async uploadIcon(conversationId, fileId) {
|
|
1113
|
+
const { data } = await getApiClient().post(
|
|
1114
|
+
`/conversations/${conversationId}/icon`,
|
|
1115
|
+
{ fileId }
|
|
1116
|
+
);
|
|
1117
|
+
return normalizeConversation(data);
|
|
1118
|
+
},
|
|
1119
|
+
async removeIcon(conversationId) {
|
|
1120
|
+
const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
|
|
1121
|
+
return normalizeConversation(data);
|
|
1122
|
+
},
|
|
1123
|
+
async clearChat(conversationId) {
|
|
1124
|
+
await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);
|
|
1125
|
+
}
|
|
1126
|
+
};
|
|
1127
|
+
|
|
1128
|
+
// src/api/devices.ts
|
|
1129
|
+
var devicesApi = {
|
|
1130
|
+
/**
|
|
1131
|
+
* Register or update a device push token with the chat server.
|
|
1132
|
+
*
|
|
1133
|
+
* Upserts by `deviceId` — calling this multiple times with the same deviceId
|
|
1134
|
+
* simply refreshes the token value (tokens can rotate silently on some platforms).
|
|
1135
|
+
*
|
|
1136
|
+
* The SDK never calls this automatically. The parent app or the `tokenProvider`
|
|
1137
|
+
* option in `pushNotifications` config is responsible for calling it after
|
|
1138
|
+
* obtaining the token from the OS / browser.
|
|
1139
|
+
*/
|
|
1140
|
+
async register(payload) {
|
|
1141
|
+
await getApiClient().post("/users/me/devices", payload);
|
|
1142
|
+
},
|
|
1143
|
+
/**
|
|
1144
|
+
* Remove a device token from the chat server.
|
|
1145
|
+
* Call this on logout so the user stops receiving push notifications on this device.
|
|
1146
|
+
*/
|
|
1147
|
+
async remove(deviceId) {
|
|
1148
|
+
await getApiClient().post(`/users/me/devices/${deviceId}/remove`);
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
|
|
1152
|
+
// src/api/users.ts
|
|
1153
|
+
var usersApi = {
|
|
1154
|
+
async list(params = {}) {
|
|
1155
|
+
const { data } = await getApiClient().get("/users", { params });
|
|
1156
|
+
return data;
|
|
1157
|
+
},
|
|
1158
|
+
async getById(userId) {
|
|
1159
|
+
const { data } = await getApiClient().get(`/users/${userId}`);
|
|
1160
|
+
return data;
|
|
1161
|
+
},
|
|
1162
|
+
async getLastSeen(userId) {
|
|
1163
|
+
const { data } = await getApiClient().get(`/users/${userId}`);
|
|
1164
|
+
return { lastSeenAt: data.lastSeenAt ?? null };
|
|
1165
|
+
},
|
|
1166
|
+
/**
|
|
1167
|
+
* Update basic profile fields for the current user.
|
|
1168
|
+
* Works in both builtin and non-builtin modes. Use this to push an immediate
|
|
1169
|
+
* profile update to the chat server when the host app knows a change just
|
|
1170
|
+
* happened — without waiting for the next 2-hour sync cycle.
|
|
1171
|
+
*/
|
|
1172
|
+
async updateProfile(payload) {
|
|
1173
|
+
const { data } = await getApiClient().post("/users/me/update", payload);
|
|
1174
|
+
return data;
|
|
1175
|
+
},
|
|
1176
|
+
/**
|
|
1177
|
+
* Update notification preferences for the current user.
|
|
1178
|
+
* Partial update — only send fields you want to change.
|
|
1179
|
+
* A prefs record is automatically created with defaults when a device
|
|
1180
|
+
* token is first registered, so this never fails with "not found".
|
|
1181
|
+
*/
|
|
1182
|
+
async updatePreferences(prefs) {
|
|
1183
|
+
const { data } = await getApiClient().post("/users/me/preferences", prefs);
|
|
1184
|
+
return data;
|
|
1185
|
+
},
|
|
1186
|
+
/**
|
|
1187
|
+
* Fetch current notification preferences for the current user.
|
|
1188
|
+
* Returns null if no prefs record exists yet (all defaults apply).
|
|
1189
|
+
*/
|
|
1190
|
+
async getPreferences() {
|
|
1191
|
+
try {
|
|
1192
|
+
const { data } = await getApiClient().get("/users/me/preferences");
|
|
1193
|
+
return data;
|
|
1194
|
+
} catch {
|
|
1195
|
+
return null;
|
|
1196
|
+
}
|
|
1097
1197
|
}
|
|
1098
1198
|
};
|
|
1099
1199
|
|
|
@@ -1254,6 +1354,7 @@ export {
|
|
|
1254
1354
|
getSocketStatus,
|
|
1255
1355
|
initApiClient,
|
|
1256
1356
|
initAuthStore,
|
|
1357
|
+
isApiClientConfigured,
|
|
1257
1358
|
isMentionAll,
|
|
1258
1359
|
isTransitEnvelope,
|
|
1259
1360
|
messagesApi,
|
|
@@ -1266,9 +1367,11 @@ export {
|
|
|
1266
1367
|
refreshSocketAuth,
|
|
1267
1368
|
renderMentionParts,
|
|
1268
1369
|
resetAuthStore,
|
|
1370
|
+
resetTrackedRooms,
|
|
1269
1371
|
resolveConfig,
|
|
1270
1372
|
resolveSystemMessageText,
|
|
1271
1373
|
setApiClientInstance,
|
|
1374
|
+
setAuthReadyPromise,
|
|
1272
1375
|
setTransitSession,
|
|
1273
1376
|
socketEmit,
|
|
1274
1377
|
storageApi,
|