@antzsoft/chat-core 1.4.2 → 1.4.4

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