@antzsoft/chat-core 1.0.0 → 1.0.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/dist/index.js CHANGED
@@ -1,6 +1,19 @@
1
+ import {
2
+ useChatStore
3
+ } from "./chunk-TB52RCSF.js";
4
+
1
5
  // src/config/types.ts
2
6
  function resolveConfig(config) {
3
7
  const socketUrl = config.socketUrl ?? config.apiUrl.replace(/\/api\/v\d+\/?$/, "").replace(/\/$/, "");
8
+ let socketOrigin = socketUrl;
9
+ let socketPath = "/socket.io";
10
+ try {
11
+ const parsed = new URL(socketUrl);
12
+ socketOrigin = parsed.origin;
13
+ const pathname = parsed.pathname.replace(/\/$/, "");
14
+ if (pathname && pathname !== "/") socketPath = `${pathname}/socket.io`;
15
+ } catch {
16
+ }
4
17
  const raw = config.upload?.maxFileSizeMB;
5
18
  let limits;
6
19
  if (typeof raw === "number") {
@@ -18,10 +31,13 @@ function resolveConfig(config) {
18
31
  return {
19
32
  apiUrl: config.apiUrl.replace(/\/$/, ""),
20
33
  socketUrl,
34
+ socketOrigin,
35
+ socketPath,
21
36
  authToken: config.authToken,
22
37
  authProvider: config.authProvider,
23
38
  userId: config.userId,
24
39
  tenantId: config.tenantId,
40
+ avatar: config.avatar,
25
41
  encryptionMode: config.encryptionMode ?? "none",
26
42
  upload: {
27
43
  maxFileSizeMB: limits,
@@ -31,7 +47,10 @@ function resolveConfig(config) {
31
47
  onProgress: config.upload?.onProgress
32
48
  },
33
49
  platformUploadFn: config.platformUploadFn,
34
- persistStorage: config.persistStorage
50
+ persistStorage: config.persistStorage,
51
+ messagePageSize: config.messagePageSize ?? 40,
52
+ starredMessagePageSize: config.starredMessagePageSize ?? 30,
53
+ searchPageSize: config.searchPageSize ?? 50
35
54
  };
36
55
  }
37
56
 
@@ -39,9 +58,11 @@ function resolveConfig(config) {
39
58
  import axios from "axios";
40
59
  var _tokenStore = null;
41
60
  var _config = null;
61
+ var _avatarSent = false;
42
62
  function initApiClient(config, tokenStore) {
43
63
  _config = config;
44
64
  _tokenStore = tokenStore;
65
+ _avatarSent = false;
45
66
  const client = axios.create({
46
67
  baseURL: config.apiUrl,
47
68
  headers: { "Content-Type": "application/json" }
@@ -51,6 +72,11 @@ function initApiClient(config, tokenStore) {
51
72
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
52
73
  if (_config?.userId) req.headers["x-user-id"] = _config.userId;
53
74
  if (_config?.tenantId) req.headers["X-Tenant-ID"] = _config.tenantId;
75
+ if (token && !_avatarSent && _config?.avatar) {
76
+ if (_config.avatar.base64) req.headers["x-avatar-base64"] = _config.avatar.base64;
77
+ else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
78
+ _avatarSent = true;
79
+ }
54
80
  return req;
55
81
  });
56
82
  let isRefreshing = false;
@@ -135,15 +161,37 @@ var authApi = {
135
161
  async getMe() {
136
162
  const { data } = await getApiClient().get("/users/me");
137
163
  return data;
164
+ },
165
+ // Upload a new avatar file (builtin mode — multipart)
166
+ async uploadAvatar(file, mimeType) {
167
+ const form = new FormData();
168
+ form.append("avatar", file instanceof File ? file : new File([file], "avatar.jpg", { type: mimeType ?? "image/jpeg" }));
169
+ const { data } = await getApiClient().put("/users/me/avatar", form, {
170
+ headers: { "Content-Type": "multipart/form-data" }
171
+ });
172
+ return data;
173
+ },
174
+ // Sync avatar from URL or base64 (non-builtin modes — or post-init update)
175
+ async syncAvatar(source) {
176
+ const headers = {};
177
+ if (source.base64) headers["x-avatar-base64"] = source.base64;
178
+ else if (source.url) headers["x-avatar-url"] = source.url;
179
+ const { data } = await getApiClient().post("/users/me/avatar/sync", {}, { headers });
180
+ return data;
138
181
  }
139
182
  };
140
183
 
141
184
  // src/api/messages.ts
142
185
  var messagesApi = {
143
186
  async list(conversationId, params = {}) {
187
+ const { cursor, direction, ...rest } = params;
188
+ const serverParams = { ...rest };
189
+ if (cursor) {
190
+ serverParams[direction === "after" ? "after" : "before"] = cursor;
191
+ }
144
192
  const { data } = await getApiClient().get(
145
193
  `/conversations/${conversationId}/messages`,
146
- { params }
194
+ { params: serverParams }
147
195
  );
148
196
  return data;
149
197
  },
@@ -165,6 +213,9 @@ var messagesApi = {
165
213
  async delete(messageId) {
166
214
  await getApiClient().delete(`/messages/${messageId}`);
167
215
  },
216
+ async deleteForMe(messageId) {
217
+ await getApiClient().delete(`/messages/${messageId}/for-me`);
218
+ },
168
219
  async addReaction(messageId, emoji) {
169
220
  const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
170
221
  return data;
@@ -189,6 +240,12 @@ var messagesApi = {
189
240
  const { data } = await getApiClient().get("/messages/search", { params });
190
241
  return data;
191
242
  },
243
+ async getLastRead(conversationId) {
244
+ const { data } = await getApiClient().get(
245
+ `/conversations/${conversationId}/read-receipt`
246
+ );
247
+ return data;
248
+ },
192
249
  async markAsRead(conversationId, messageId) {
193
250
  await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});
194
251
  },
@@ -213,6 +270,7 @@ function normalizeParticipant(p) {
213
270
  userId: p.userId,
214
271
  role: p.role,
215
272
  joinedAt: p.joinedAt,
273
+ isActive: p.isActive,
216
274
  user: hasUserDetails ? {
217
275
  id: p.userId,
218
276
  tenantId: "",
@@ -239,7 +297,7 @@ function normalizeLastMessage(lastMsg) {
239
297
  text: lastMsg.contentPreview
240
298
  },
241
299
  reactions: [],
242
- status: "sent",
300
+ status: lastMsg.status ?? "sent",
243
301
  isEdited: false,
244
302
  sentAt: lastMsg.sentAt ?? "",
245
303
  createdAt: lastMsg.sentAt ?? ""
@@ -316,9 +374,25 @@ var conversationsApi = {
316
374
  const { data } = await getApiClient().get(`/conversations/${conversationId}/participants`);
317
375
  return data;
318
376
  },
319
- async searchUsers(query) {
320
- const { data } = await getApiClient().get("/users/search", { params: { query } });
321
- return data.data;
377
+ /**
378
+ * Get unread message count for a single conversation.
379
+ * Use this after app foreground or socket reconnect to refresh a specific count.
380
+ */
381
+ async getUnreadCount(conversationId) {
382
+ const { data } = await getApiClient().get(
383
+ `/conversations/${conversationId}/unread`
384
+ );
385
+ return data;
386
+ },
387
+ /**
388
+ * Get total unread count across all conversations + per-conversation breakdown.
389
+ * Use on app cold start, foreground resume, or after socket reconnect.
390
+ * The socket keeps counts live while connected — this is the source of truth
391
+ * when the socket was down.
392
+ */
393
+ async getUnreadSummary() {
394
+ const { data } = await getApiClient().get("/conversations/unread");
395
+ return data;
322
396
  }
323
397
  };
324
398
 
@@ -423,11 +497,52 @@ var devicesApi = {
423
497
  }
424
498
  };
425
499
 
500
+ // src/api/users.ts
501
+ var usersApi = {
502
+ async list(params = {}) {
503
+ const { data } = await getApiClient().get("/users", { params });
504
+ return data;
505
+ },
506
+ async getById(userId) {
507
+ const { data } = await getApiClient().get(`/users/${userId}`);
508
+ return data;
509
+ },
510
+ async getLastSeen(userId) {
511
+ const { data } = await getApiClient().get(`/users/${userId}`);
512
+ return { lastSeenAt: data.lastSeenAt ?? null };
513
+ },
514
+ /**
515
+ * Update notification preferences for the current user.
516
+ * Partial update — only send fields you want to change.
517
+ * A prefs record is automatically created with defaults when a device
518
+ * token is first registered, so this never fails with "not found".
519
+ */
520
+ async updatePreferences(prefs) {
521
+ const { data } = await getApiClient().put("/users/me/preferences", prefs);
522
+ return data;
523
+ },
524
+ /**
525
+ * Fetch current notification preferences for the current user.
526
+ * Returns null if no prefs record exists yet (all defaults apply).
527
+ */
528
+ async getPreferences() {
529
+ try {
530
+ const { data } = await getApiClient().get("/users/me/preferences");
531
+ return data;
532
+ } catch {
533
+ return null;
534
+ }
535
+ }
536
+ };
537
+
426
538
  // src/socket/socket.ts
427
539
  import { io } from "socket.io-client";
428
540
  var _socket = null;
429
541
  var _status = "disconnected";
430
542
  var _statusListeners = /* @__PURE__ */ new Set();
543
+ var _getToken = null;
544
+ var _userId;
545
+ var _tenantId;
431
546
  function setStatus(s) {
432
547
  _status = s;
433
548
  _statusListeners.forEach((l) => l(s));
@@ -447,10 +562,23 @@ function onSocketStatus(listener) {
447
562
  return () => _statusListeners.delete(listener);
448
563
  }
449
564
  async function connectSocket(config, getToken) {
450
- if (_socket?.connected) return _socket;
565
+ if (_socket && !_socket.disconnected) return _socket;
566
+ _getToken = getToken;
567
+ _userId = config.userId;
568
+ _tenantId = config.tenantId;
451
569
  const token = getToken();
452
- _socket = io(`${config.socketUrl}/chat`, {
453
- auth: { token: token ? `Bearer ${token}` : "" },
570
+ _socket = io(`${config.socketOrigin}/chat`, {
571
+ auth: {
572
+ token: token ? `Bearer ${token}` : "",
573
+ ...config.userId && { userId: config.userId },
574
+ ...config.tenantId && { tenantId: config.tenantId },
575
+ ...config.avatar?.url && { avatarUrl: config.avatar.url },
576
+ ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
577
+ },
578
+ // path must match SOCKET_IO_PATH on the server (default '/socket.io').
579
+ // Set socketPath in SDK config when the server is behind a reverse proxy
580
+ // that adds a path prefix (e.g. '/chat-api/socket.io' for UAT).
581
+ path: config.socketPath,
454
582
  transports: ["websocket", "polling"],
455
583
  reconnection: true,
456
584
  reconnectionAttempts: 10,
@@ -461,9 +589,30 @@ async function connectSocket(config, getToken) {
461
589
  setStatus("connecting");
462
590
  _socket.on("connect", () => setStatus("connected"));
463
591
  _socket.on("disconnect", () => setStatus("disconnected"));
464
- _socket.on("connect_error", () => setStatus("error"));
592
+ _socket.on("connect_error", (err) => {
593
+ console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
594
+ setStatus("error");
595
+ });
465
596
  _socket.on("reconnecting", () => setStatus("reconnecting"));
466
597
  _socket.on("reconnect", () => setStatus("connected"));
598
+ _socket.on("read_receipt", (event) => {
599
+ import("./chat.store-PY3YVYGN.js").then(({ useChatStore: useChatStore2 }) => {
600
+ useChatStore2.getState().setLastRead(event.conversationId, event.messageId, event.readAt);
601
+ });
602
+ });
603
+ _socket.on("user_online", (event) => {
604
+ import("./chat.store-PY3YVYGN.js").then(({ useChatStore: useChatStore2 }) => {
605
+ const store = useChatStore2.getState();
606
+ store.setUserOnline(event.userId);
607
+ });
608
+ });
609
+ _socket.on("user_offline", (event) => {
610
+ import("./chat.store-PY3YVYGN.js").then(({ useChatStore: useChatStore2 }) => {
611
+ const store = useChatStore2.getState();
612
+ store.setUserOffline(event.userId);
613
+ if (event.lastSeenAt) store.setLastSeen(event.userId, event.lastSeenAt);
614
+ });
615
+ });
467
616
  return _socket;
468
617
  }
469
618
  function disconnectSocket() {
@@ -472,13 +621,31 @@ function disconnectSocket() {
472
621
  _socket = null;
473
622
  setStatus("disconnected");
474
623
  }
624
+ _getToken = null;
625
+ _userId = void 0;
626
+ _tenantId = void 0;
475
627
  }
476
- function reconnectSocket(token) {
628
+ function reconnectSocket(token, userId, tenantId) {
477
629
  if (_socket) {
478
- _socket.auth = { token: `Bearer ${token}` };
630
+ _socket.auth = {
631
+ token: `Bearer ${token}`,
632
+ ...userId && { userId },
633
+ ...tenantId && { tenantId }
634
+ };
479
635
  _socket.connect();
480
636
  }
481
637
  }
638
+ function refreshSocketAuth() {
639
+ if (!_socket || !_getToken) return false;
640
+ const fresh = _getToken();
641
+ if (!fresh) return false;
642
+ _socket.auth = {
643
+ token: `Bearer ${fresh}`,
644
+ ..._userId && { userId: _userId },
645
+ ..._tenantId && { tenantId: _tenantId }
646
+ };
647
+ return true;
648
+ }
482
649
 
483
650
  // src/socket/emitters.ts
484
651
  var ACK_TIMEOUT = 5e3;
@@ -514,6 +681,9 @@ var socketEmit = {
514
681
  deleteMessage(messageId) {
515
682
  return withAck("delete_message", { messageId });
516
683
  },
684
+ deleteMessageForMe(messageId) {
685
+ return withAck("delete_message_for_me", { messageId });
686
+ },
517
687
  addReaction(messageId, emoji) {
518
688
  return withAck("add_reaction", { messageId, emoji });
519
689
  },
@@ -643,48 +813,6 @@ function resetAuthStore() {
643
813
  _authStore = null;
644
814
  }
645
815
 
646
- // src/stores/chat.store.ts
647
- import { create as create2 } from "zustand";
648
- var useChatStore = create2((set) => ({
649
- activeConversationId: null,
650
- pendingTarget: null,
651
- typingUsers: {},
652
- onlineUsers: [],
653
- replyingTo: null,
654
- editingMessage: null,
655
- isSidebarOpen: true,
656
- isGroupInfoOpen: false,
657
- isStarredPanelOpen: false,
658
- setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null }),
659
- setPendingTarget: (target) => set({ pendingTarget: target }),
660
- addTypingUser: (conversationId, user) => set((state) => {
661
- const existing = state.typingUsers[conversationId] ?? [];
662
- const deduped = existing.filter((u) => u.userId !== user.userId);
663
- return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };
664
- }),
665
- removeTypingUser: (conversationId, userId) => set((state) => ({
666
- typingUsers: {
667
- ...state.typingUsers,
668
- [conversationId]: (state.typingUsers[conversationId] ?? []).filter(
669
- (u) => u.userId !== userId
670
- )
671
- }
672
- })),
673
- setUserOnline: (userId) => set((state) => ({
674
- onlineUsers: state.onlineUsers.includes(userId) ? state.onlineUsers : [...state.onlineUsers, userId]
675
- })),
676
- setUserOffline: (userId) => set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),
677
- setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),
678
- setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),
679
- setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),
680
- toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
681
- setSidebarOpen: (open) => set({ isSidebarOpen: open }),
682
- toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
683
- setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),
684
- toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),
685
- setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open })
686
- }));
687
-
688
816
  // src/client-facade.ts
689
817
  var AntzChatClient = class {
690
818
  constructor(rawConfig) {
@@ -692,6 +820,7 @@ var AntzChatClient = class {
692
820
  this.messages = messagesApi;
693
821
  this.conversations = conversationsApi;
694
822
  this.storage = storageApi;
823
+ this.users = usersApi;
695
824
  this.socket = {
696
825
  emit: socketEmit,
697
826
  on: (event, handler) => getSocket().on(event, handler),
@@ -738,6 +867,7 @@ export {
738
867
  normalizeConversation,
739
868
  onSocketStatus,
740
869
  reconnectSocket,
870
+ refreshSocketAuth,
741
871
  resetAuthStore,
742
872
  resolveConfig,
743
873
  setApiClientInstance,
@@ -745,6 +875,7 @@ export {
745
875
  storageApi,
746
876
  tryGetSocket,
747
877
  uploadBatch,
748
- useChatStore
878
+ useChatStore,
879
+ usersApi
749
880
  };
750
881
  //# sourceMappingURL=index.js.map