@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.
@@ -0,0 +1,7 @@
1
+ import {
2
+ useChatStore
3
+ } from "./chunk-TB52RCSF.js";
4
+ export {
5
+ useChatStore
6
+ };
7
+ //# sourceMappingURL=chat.store-PY3YVYGN.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,54 @@
1
+ // src/stores/chat.store.ts
2
+ import { create } from "zustand";
3
+ var useChatStore = create((set) => ({
4
+ activeConversationId: null,
5
+ pendingTarget: null,
6
+ typingUsers: {},
7
+ onlineUsers: [],
8
+ lastRead: {},
9
+ lastSeen: {},
10
+ replyingTo: null,
11
+ editingMessage: null,
12
+ isSidebarOpen: true,
13
+ isGroupInfoOpen: false,
14
+ isStarredPanelOpen: false,
15
+ setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null }),
16
+ setPendingTarget: (target) => set({ pendingTarget: target }),
17
+ addTypingUser: (conversationId, user) => set((state) => {
18
+ const existing = state.typingUsers[conversationId] ?? [];
19
+ const deduped = existing.filter((u) => u.userId !== user.userId);
20
+ return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };
21
+ }),
22
+ removeTypingUser: (conversationId, userId) => set((state) => ({
23
+ typingUsers: {
24
+ ...state.typingUsers,
25
+ [conversationId]: (state.typingUsers[conversationId] ?? []).filter(
26
+ (u) => u.userId !== userId
27
+ )
28
+ }
29
+ })),
30
+ setUserOnline: (userId) => set((state) => ({
31
+ onlineUsers: state.onlineUsers.includes(userId) ? state.onlineUsers : [...state.onlineUsers, userId]
32
+ })),
33
+ setUserOffline: (userId) => set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),
34
+ setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),
35
+ setLastRead: (conversationId, messageId, readAt) => set((state) => ({
36
+ lastRead: { ...state.lastRead, [conversationId]: { messageId, readAt } }
37
+ })),
38
+ setLastSeen: (userId, lastSeenAt) => set((state) => ({
39
+ lastSeen: { ...state.lastSeen, [userId]: lastSeenAt }
40
+ })),
41
+ setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),
42
+ setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),
43
+ toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
44
+ setSidebarOpen: (open) => set({ isSidebarOpen: open }),
45
+ toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
46
+ setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),
47
+ toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),
48
+ setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open })
49
+ }));
50
+
51
+ export {
52
+ useChatStore
53
+ };
54
+ //# sourceMappingURL=chunk-TB52RCSF.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/stores/chat.store.ts"],"sourcesContent":["import { create } from 'zustand';\nimport type { Message } from '../types/index.js';\n\ninterface TypingUser {\n userId: string;\n displayName: string;\n avatarUrl?: string;\n}\n\nexport interface LastReadEntry {\n messageId: string;\n readAt: string;\n}\n\ninterface ChatState {\n activeConversationId: string | null;\n pendingTarget: { conversationId: string; messageId: string } | null;\n typingUsers: Record<string, TypingUser[]>;\n onlineUsers: string[];\n /** keyed by conversationId — current user's last read pointer per conversation */\n lastRead: Record<string, LastReadEntry>;\n /** keyed by userId — last seen timestamp for each user */\n lastSeen: Record<string, string>;\n replyingTo: Message | null;\n editingMessage: Message | null;\n isSidebarOpen: boolean;\n isGroupInfoOpen: boolean;\n isStarredPanelOpen: boolean;\n\n setActiveConversation: (id: string | null) => void;\n setPendingTarget: (target: { conversationId: string; messageId: string } | null) => void;\n addTypingUser: (conversationId: string, user: TypingUser) => void;\n removeTypingUser: (conversationId: string, userId: string) => void;\n setUserOnline: (userId: string) => void;\n setUserOffline: (userId: string) => void;\n setOnlineUsers: (userIds: string[]) => void;\n setLastRead: (conversationId: string, messageId: string, readAt: string) => void;\n setLastSeen: (userId: string, lastSeenAt: string) => void;\n setReplyingTo: (message: Message | null) => void;\n setEditingMessage: (message: Message | null) => void;\n toggleSidebar: () => void;\n setSidebarOpen: (open: boolean) => void;\n toggleGroupInfo: () => void;\n setGroupInfoOpen: (open: boolean) => void;\n toggleStarredPanel: () => void;\n setStarredPanelOpen: (open: boolean) => void;\n}\n\nexport const useChatStore = create<ChatState>((set) => ({\n activeConversationId: null,\n pendingTarget: null,\n typingUsers: {},\n onlineUsers: [],\n lastRead: {},\n lastSeen: {},\n replyingTo: null,\n editingMessage: null,\n isSidebarOpen: true,\n isGroupInfoOpen: false,\n isStarredPanelOpen: false,\n\n setActiveConversation: (id) =>\n set({ activeConversationId: id, replyingTo: null, editingMessage: null }),\n\n setPendingTarget: (target) => set({ pendingTarget: target }),\n\n addTypingUser: (conversationId, user) =>\n set((state) => {\n const existing = state.typingUsers[conversationId] ?? [];\n const deduped = existing.filter((u) => u.userId !== user.userId);\n return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };\n }),\n\n removeTypingUser: (conversationId, userId) =>\n set((state) => ({\n typingUsers: {\n ...state.typingUsers,\n [conversationId]: (state.typingUsers[conversationId] ?? []).filter(\n (u) => u.userId !== userId,\n ),\n },\n })),\n\n setUserOnline: (userId) =>\n set((state) => ({\n onlineUsers: state.onlineUsers.includes(userId)\n ? state.onlineUsers\n : [...state.onlineUsers, userId],\n })),\n\n setUserOffline: (userId) =>\n set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),\n\n setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),\n\n setLastRead: (conversationId, messageId, readAt) =>\n set((state) => ({\n lastRead: { ...state.lastRead, [conversationId]: { messageId, readAt } },\n })),\n\n setLastSeen: (userId, lastSeenAt) =>\n set((state) => ({\n lastSeen: { ...state.lastSeen, [userId]: lastSeenAt },\n })),\n\n setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),\n\n setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),\n\n toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),\n setSidebarOpen: (open) => set({ isSidebarOpen: open }),\n\n toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),\n setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),\n\n toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),\n setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open }),\n}));\n"],"mappings":";AAAA,SAAS,cAAc;AAgDhB,IAAM,eAAe,OAAkB,CAAC,SAAS;AAAA,EACtD,sBAAsB;AAAA,EACtB,eAAe;AAAA,EACf,aAAa,CAAC;AAAA,EACd,aAAa,CAAC;AAAA,EACd,UAAU,CAAC;AAAA,EACX,UAAU,CAAC;AAAA,EACX,YAAY;AAAA,EACZ,gBAAgB;AAAA,EAChB,eAAe;AAAA,EACf,iBAAiB;AAAA,EACjB,oBAAoB;AAAA,EAEpB,uBAAuB,CAAC,OACtB,IAAI,EAAE,sBAAsB,IAAI,YAAY,MAAM,gBAAgB,KAAK,CAAC;AAAA,EAE1E,kBAAkB,CAAC,WAAW,IAAI,EAAE,eAAe,OAAO,CAAC;AAAA,EAE3D,eAAe,CAAC,gBAAgB,SAC9B,IAAI,CAAC,UAAU;AACb,UAAM,WAAW,MAAM,YAAY,cAAc,KAAK,CAAC;AACvD,UAAM,UAAU,SAAS,OAAO,CAAC,MAAM,EAAE,WAAW,KAAK,MAAM;AAC/D,WAAO,EAAE,aAAa,EAAE,GAAG,MAAM,aAAa,CAAC,cAAc,GAAG,CAAC,GAAG,SAAS,IAAI,EAAE,EAAE;AAAA,EACvF,CAAC;AAAA,EAEH,kBAAkB,CAAC,gBAAgB,WACjC,IAAI,CAAC,WAAW;AAAA,IACd,aAAa;AAAA,MACX,GAAG,MAAM;AAAA,MACT,CAAC,cAAc,IAAI,MAAM,YAAY,cAAc,KAAK,CAAC,GAAG;AAAA,QAC1D,CAAC,MAAM,EAAE,WAAW;AAAA,MACtB;AAAA,IACF;AAAA,EACF,EAAE;AAAA,EAEJ,eAAe,CAAC,WACd,IAAI,CAAC,WAAW;AAAA,IACd,aAAa,MAAM,YAAY,SAAS,MAAM,IAC1C,MAAM,cACN,CAAC,GAAG,MAAM,aAAa,MAAM;AAAA,EACnC,EAAE;AAAA,EAEJ,gBAAgB,CAAC,WACf,IAAI,CAAC,WAAW,EAAE,aAAa,MAAM,YAAY,OAAO,CAAC,OAAO,OAAO,MAAM,EAAE,EAAE;AAAA,EAEnF,gBAAgB,CAAC,YAAY,IAAI,EAAE,aAAa,QAAQ,CAAC;AAAA,EAEzD,aAAa,CAAC,gBAAgB,WAAW,WACvC,IAAI,CAAC,WAAW;AAAA,IACd,UAAU,EAAE,GAAG,MAAM,UAAU,CAAC,cAAc,GAAG,EAAE,WAAW,OAAO,EAAE;AAAA,EACzE,EAAE;AAAA,EAEJ,aAAa,CAAC,QAAQ,eACpB,IAAI,CAAC,WAAW;AAAA,IACd,UAAU,EAAE,GAAG,MAAM,UAAU,CAAC,MAAM,GAAG,WAAW;AAAA,EACtD,EAAE;AAAA,EAEJ,eAAe,CAAC,YAAY,IAAI,EAAE,YAAY,SAAS,gBAAgB,KAAK,CAAC;AAAA,EAE7E,mBAAmB,CAAC,YAAY,IAAI,EAAE,gBAAgB,SAAS,YAAY,KAAK,CAAC;AAAA,EAEjF,eAAe,MAAM,IAAI,CAAC,WAAW,EAAE,eAAe,CAAC,MAAM,cAAc,EAAE;AAAA,EAC7E,gBAAgB,CAAC,SAAS,IAAI,EAAE,eAAe,KAAK,CAAC;AAAA,EAErD,iBAAiB,MAAM,IAAI,CAAC,WAAW,EAAE,iBAAiB,CAAC,MAAM,gBAAgB,EAAE;AAAA,EACnF,kBAAkB,CAAC,SAAS,IAAI,EAAE,iBAAiB,KAAK,CAAC;AAAA,EAEzD,oBAAoB,MAAM,IAAI,CAAC,WAAW,EAAE,oBAAoB,CAAC,MAAM,mBAAmB,EAAE;AAAA,EAC5F,qBAAqB,CAAC,SAAS,IAAI,EAAE,oBAAoB,KAAK,CAAC;AACjE,EAAE;","names":[]}
package/dist/index.cjs CHANGED
@@ -5,6 +5,9 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __esm = (fn, res) => function __init() {
9
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
10
+ };
8
11
  var __export = (target, all) => {
9
12
  for (var name in all)
10
13
  __defProp(target, name, { get: all[name], enumerable: true });
@@ -27,6 +30,66 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
27
30
  ));
28
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
32
 
33
+ // src/stores/chat.store.ts
34
+ var chat_store_exports = {};
35
+ __export(chat_store_exports, {
36
+ useChatStore: () => useChatStore
37
+ });
38
+ var import_zustand, useChatStore;
39
+ var init_chat_store = __esm({
40
+ "src/stores/chat.store.ts"() {
41
+ "use strict";
42
+ import_zustand = require("zustand");
43
+ useChatStore = (0, import_zustand.create)((set) => ({
44
+ activeConversationId: null,
45
+ pendingTarget: null,
46
+ typingUsers: {},
47
+ onlineUsers: [],
48
+ lastRead: {},
49
+ lastSeen: {},
50
+ replyingTo: null,
51
+ editingMessage: null,
52
+ isSidebarOpen: true,
53
+ isGroupInfoOpen: false,
54
+ isStarredPanelOpen: false,
55
+ setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null }),
56
+ setPendingTarget: (target) => set({ pendingTarget: target }),
57
+ addTypingUser: (conversationId, user) => set((state) => {
58
+ const existing = state.typingUsers[conversationId] ?? [];
59
+ const deduped = existing.filter((u) => u.userId !== user.userId);
60
+ return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };
61
+ }),
62
+ removeTypingUser: (conversationId, userId) => set((state) => ({
63
+ typingUsers: {
64
+ ...state.typingUsers,
65
+ [conversationId]: (state.typingUsers[conversationId] ?? []).filter(
66
+ (u) => u.userId !== userId
67
+ )
68
+ }
69
+ })),
70
+ setUserOnline: (userId) => set((state) => ({
71
+ onlineUsers: state.onlineUsers.includes(userId) ? state.onlineUsers : [...state.onlineUsers, userId]
72
+ })),
73
+ setUserOffline: (userId) => set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),
74
+ setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),
75
+ setLastRead: (conversationId, messageId, readAt) => set((state) => ({
76
+ lastRead: { ...state.lastRead, [conversationId]: { messageId, readAt } }
77
+ })),
78
+ setLastSeen: (userId, lastSeenAt) => set((state) => ({
79
+ lastSeen: { ...state.lastSeen, [userId]: lastSeenAt }
80
+ })),
81
+ setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),
82
+ setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),
83
+ toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
84
+ setSidebarOpen: (open) => set({ isSidebarOpen: open }),
85
+ toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
86
+ setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),
87
+ toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),
88
+ setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open })
89
+ }));
90
+ }
91
+ });
92
+
30
93
  // src/index.ts
31
94
  var src_exports = {};
32
95
  __export(src_exports, {
@@ -47,6 +110,7 @@ __export(src_exports, {
47
110
  normalizeConversation: () => normalizeConversation,
48
111
  onSocketStatus: () => onSocketStatus,
49
112
  reconnectSocket: () => reconnectSocket,
113
+ refreshSocketAuth: () => refreshSocketAuth,
50
114
  resetAuthStore: () => resetAuthStore,
51
115
  resolveConfig: () => resolveConfig,
52
116
  setApiClientInstance: () => setApiClientInstance,
@@ -54,13 +118,23 @@ __export(src_exports, {
54
118
  storageApi: () => storageApi,
55
119
  tryGetSocket: () => tryGetSocket,
56
120
  uploadBatch: () => uploadBatch,
57
- useChatStore: () => useChatStore
121
+ useChatStore: () => useChatStore,
122
+ usersApi: () => usersApi
58
123
  });
59
124
  module.exports = __toCommonJS(src_exports);
60
125
 
61
126
  // src/config/types.ts
62
127
  function resolveConfig(config) {
63
128
  const socketUrl = config.socketUrl ?? config.apiUrl.replace(/\/api\/v\d+\/?$/, "").replace(/\/$/, "");
129
+ let socketOrigin = socketUrl;
130
+ let socketPath = "/socket.io";
131
+ try {
132
+ const parsed = new URL(socketUrl);
133
+ socketOrigin = parsed.origin;
134
+ const pathname = parsed.pathname.replace(/\/$/, "");
135
+ if (pathname && pathname !== "/") socketPath = `${pathname}/socket.io`;
136
+ } catch {
137
+ }
64
138
  const raw = config.upload?.maxFileSizeMB;
65
139
  let limits;
66
140
  if (typeof raw === "number") {
@@ -78,10 +152,13 @@ function resolveConfig(config) {
78
152
  return {
79
153
  apiUrl: config.apiUrl.replace(/\/$/, ""),
80
154
  socketUrl,
155
+ socketOrigin,
156
+ socketPath,
81
157
  authToken: config.authToken,
82
158
  authProvider: config.authProvider,
83
159
  userId: config.userId,
84
160
  tenantId: config.tenantId,
161
+ avatar: config.avatar,
85
162
  encryptionMode: config.encryptionMode ?? "none",
86
163
  upload: {
87
164
  maxFileSizeMB: limits,
@@ -91,7 +168,10 @@ function resolveConfig(config) {
91
168
  onProgress: config.upload?.onProgress
92
169
  },
93
170
  platformUploadFn: config.platformUploadFn,
94
- persistStorage: config.persistStorage
171
+ persistStorage: config.persistStorage,
172
+ messagePageSize: config.messagePageSize ?? 40,
173
+ starredMessagePageSize: config.starredMessagePageSize ?? 30,
174
+ searchPageSize: config.searchPageSize ?? 50
95
175
  };
96
176
  }
97
177
 
@@ -99,9 +179,11 @@ function resolveConfig(config) {
99
179
  var import_axios = __toESM(require("axios"), 1);
100
180
  var _tokenStore = null;
101
181
  var _config = null;
182
+ var _avatarSent = false;
102
183
  function initApiClient(config, tokenStore) {
103
184
  _config = config;
104
185
  _tokenStore = tokenStore;
186
+ _avatarSent = false;
105
187
  const client = import_axios.default.create({
106
188
  baseURL: config.apiUrl,
107
189
  headers: { "Content-Type": "application/json" }
@@ -111,6 +193,11 @@ function initApiClient(config, tokenStore) {
111
193
  if (token) req.headers["Authorization"] = `Bearer ${token}`;
112
194
  if (_config?.userId) req.headers["x-user-id"] = _config.userId;
113
195
  if (_config?.tenantId) req.headers["X-Tenant-ID"] = _config.tenantId;
196
+ if (token && !_avatarSent && _config?.avatar) {
197
+ if (_config.avatar.base64) req.headers["x-avatar-base64"] = _config.avatar.base64;
198
+ else if (_config.avatar.url) req.headers["x-avatar-url"] = _config.avatar.url;
199
+ _avatarSent = true;
200
+ }
114
201
  return req;
115
202
  });
116
203
  let isRefreshing = false;
@@ -195,15 +282,37 @@ var authApi = {
195
282
  async getMe() {
196
283
  const { data } = await getApiClient().get("/users/me");
197
284
  return data;
285
+ },
286
+ // Upload a new avatar file (builtin mode — multipart)
287
+ async uploadAvatar(file, mimeType) {
288
+ const form = new FormData();
289
+ form.append("avatar", file instanceof File ? file : new File([file], "avatar.jpg", { type: mimeType ?? "image/jpeg" }));
290
+ const { data } = await getApiClient().put("/users/me/avatar", form, {
291
+ headers: { "Content-Type": "multipart/form-data" }
292
+ });
293
+ return data;
294
+ },
295
+ // Sync avatar from URL or base64 (non-builtin modes — or post-init update)
296
+ async syncAvatar(source) {
297
+ const headers = {};
298
+ if (source.base64) headers["x-avatar-base64"] = source.base64;
299
+ else if (source.url) headers["x-avatar-url"] = source.url;
300
+ const { data } = await getApiClient().post("/users/me/avatar/sync", {}, { headers });
301
+ return data;
198
302
  }
199
303
  };
200
304
 
201
305
  // src/api/messages.ts
202
306
  var messagesApi = {
203
307
  async list(conversationId, params = {}) {
308
+ const { cursor, direction, ...rest } = params;
309
+ const serverParams = { ...rest };
310
+ if (cursor) {
311
+ serverParams[direction === "after" ? "after" : "before"] = cursor;
312
+ }
204
313
  const { data } = await getApiClient().get(
205
314
  `/conversations/${conversationId}/messages`,
206
- { params }
315
+ { params: serverParams }
207
316
  );
208
317
  return data;
209
318
  },
@@ -225,6 +334,9 @@ var messagesApi = {
225
334
  async delete(messageId) {
226
335
  await getApiClient().delete(`/messages/${messageId}`);
227
336
  },
337
+ async deleteForMe(messageId) {
338
+ await getApiClient().delete(`/messages/${messageId}/for-me`);
339
+ },
228
340
  async addReaction(messageId, emoji) {
229
341
  const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
230
342
  return data;
@@ -249,6 +361,12 @@ var messagesApi = {
249
361
  const { data } = await getApiClient().get("/messages/search", { params });
250
362
  return data;
251
363
  },
364
+ async getLastRead(conversationId) {
365
+ const { data } = await getApiClient().get(
366
+ `/conversations/${conversationId}/read-receipt`
367
+ );
368
+ return data;
369
+ },
252
370
  async markAsRead(conversationId, messageId) {
253
371
  await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});
254
372
  },
@@ -273,6 +391,7 @@ function normalizeParticipant(p) {
273
391
  userId: p.userId,
274
392
  role: p.role,
275
393
  joinedAt: p.joinedAt,
394
+ isActive: p.isActive,
276
395
  user: hasUserDetails ? {
277
396
  id: p.userId,
278
397
  tenantId: "",
@@ -299,7 +418,7 @@ function normalizeLastMessage(lastMsg) {
299
418
  text: lastMsg.contentPreview
300
419
  },
301
420
  reactions: [],
302
- status: "sent",
421
+ status: lastMsg.status ?? "sent",
303
422
  isEdited: false,
304
423
  sentAt: lastMsg.sentAt ?? "",
305
424
  createdAt: lastMsg.sentAt ?? ""
@@ -376,9 +495,25 @@ var conversationsApi = {
376
495
  const { data } = await getApiClient().get(`/conversations/${conversationId}/participants`);
377
496
  return data;
378
497
  },
379
- async searchUsers(query) {
380
- const { data } = await getApiClient().get("/users/search", { params: { query } });
381
- return data.data;
498
+ /**
499
+ * Get unread message count for a single conversation.
500
+ * Use this after app foreground or socket reconnect to refresh a specific count.
501
+ */
502
+ async getUnreadCount(conversationId) {
503
+ const { data } = await getApiClient().get(
504
+ `/conversations/${conversationId}/unread`
505
+ );
506
+ return data;
507
+ },
508
+ /**
509
+ * Get total unread count across all conversations + per-conversation breakdown.
510
+ * Use on app cold start, foreground resume, or after socket reconnect.
511
+ * The socket keeps counts live while connected — this is the source of truth
512
+ * when the socket was down.
513
+ */
514
+ async getUnreadSummary() {
515
+ const { data } = await getApiClient().get("/conversations/unread");
516
+ return data;
382
517
  }
383
518
  };
384
519
 
@@ -483,11 +618,52 @@ var devicesApi = {
483
618
  }
484
619
  };
485
620
 
621
+ // src/api/users.ts
622
+ var usersApi = {
623
+ async list(params = {}) {
624
+ const { data } = await getApiClient().get("/users", { params });
625
+ return data;
626
+ },
627
+ async getById(userId) {
628
+ const { data } = await getApiClient().get(`/users/${userId}`);
629
+ return data;
630
+ },
631
+ async getLastSeen(userId) {
632
+ const { data } = await getApiClient().get(`/users/${userId}`);
633
+ return { lastSeenAt: data.lastSeenAt ?? null };
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().put("/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
+
486
659
  // src/socket/socket.ts
487
660
  var import_socket = require("socket.io-client");
488
661
  var _socket = null;
489
662
  var _status = "disconnected";
490
663
  var _statusListeners = /* @__PURE__ */ new Set();
664
+ var _getToken = null;
665
+ var _userId;
666
+ var _tenantId;
491
667
  function setStatus(s) {
492
668
  _status = s;
493
669
  _statusListeners.forEach((l) => l(s));
@@ -507,10 +683,23 @@ function onSocketStatus(listener) {
507
683
  return () => _statusListeners.delete(listener);
508
684
  }
509
685
  async function connectSocket(config, getToken) {
510
- if (_socket?.connected) return _socket;
686
+ if (_socket && !_socket.disconnected) return _socket;
687
+ _getToken = getToken;
688
+ _userId = config.userId;
689
+ _tenantId = config.tenantId;
511
690
  const token = getToken();
512
- _socket = (0, import_socket.io)(`${config.socketUrl}/chat`, {
513
- auth: { token: token ? `Bearer ${token}` : "" },
691
+ _socket = (0, import_socket.io)(`${config.socketOrigin}/chat`, {
692
+ auth: {
693
+ token: token ? `Bearer ${token}` : "",
694
+ ...config.userId && { userId: config.userId },
695
+ ...config.tenantId && { tenantId: config.tenantId },
696
+ ...config.avatar?.url && { avatarUrl: config.avatar.url },
697
+ ...config.avatar?.base64 && { avatarBase64: config.avatar.base64 }
698
+ },
699
+ // path must match SOCKET_IO_PATH on the server (default '/socket.io').
700
+ // Set socketPath in SDK config when the server is behind a reverse proxy
701
+ // that adds a path prefix (e.g. '/chat-api/socket.io' for UAT).
702
+ path: config.socketPath,
514
703
  transports: ["websocket", "polling"],
515
704
  reconnection: true,
516
705
  reconnectionAttempts: 10,
@@ -521,9 +710,30 @@ async function connectSocket(config, getToken) {
521
710
  setStatus("connecting");
522
711
  _socket.on("connect", () => setStatus("connected"));
523
712
  _socket.on("disconnect", () => setStatus("disconnected"));
524
- _socket.on("connect_error", () => setStatus("error"));
713
+ _socket.on("connect_error", (err) => {
714
+ console.error("[AntzChat] Socket connect_error:", err?.message, err?.data);
715
+ setStatus("error");
716
+ });
525
717
  _socket.on("reconnecting", () => setStatus("reconnecting"));
526
718
  _socket.on("reconnect", () => setStatus("connected"));
719
+ _socket.on("read_receipt", (event) => {
720
+ Promise.resolve().then(() => (init_chat_store(), chat_store_exports)).then(({ useChatStore: useChatStore2 }) => {
721
+ useChatStore2.getState().setLastRead(event.conversationId, event.messageId, event.readAt);
722
+ });
723
+ });
724
+ _socket.on("user_online", (event) => {
725
+ Promise.resolve().then(() => (init_chat_store(), chat_store_exports)).then(({ useChatStore: useChatStore2 }) => {
726
+ const store = useChatStore2.getState();
727
+ store.setUserOnline(event.userId);
728
+ });
729
+ });
730
+ _socket.on("user_offline", (event) => {
731
+ Promise.resolve().then(() => (init_chat_store(), chat_store_exports)).then(({ useChatStore: useChatStore2 }) => {
732
+ const store = useChatStore2.getState();
733
+ store.setUserOffline(event.userId);
734
+ if (event.lastSeenAt) store.setLastSeen(event.userId, event.lastSeenAt);
735
+ });
736
+ });
527
737
  return _socket;
528
738
  }
529
739
  function disconnectSocket() {
@@ -532,13 +742,31 @@ function disconnectSocket() {
532
742
  _socket = null;
533
743
  setStatus("disconnected");
534
744
  }
745
+ _getToken = null;
746
+ _userId = void 0;
747
+ _tenantId = void 0;
535
748
  }
536
- function reconnectSocket(token) {
749
+ function reconnectSocket(token, userId, tenantId) {
537
750
  if (_socket) {
538
- _socket.auth = { token: `Bearer ${token}` };
751
+ _socket.auth = {
752
+ token: `Bearer ${token}`,
753
+ ...userId && { userId },
754
+ ...tenantId && { tenantId }
755
+ };
539
756
  _socket.connect();
540
757
  }
541
758
  }
759
+ function refreshSocketAuth() {
760
+ if (!_socket || !_getToken) return false;
761
+ const fresh = _getToken();
762
+ if (!fresh) return false;
763
+ _socket.auth = {
764
+ token: `Bearer ${fresh}`,
765
+ ..._userId && { userId: _userId },
766
+ ..._tenantId && { tenantId: _tenantId }
767
+ };
768
+ return true;
769
+ }
542
770
 
543
771
  // src/socket/emitters.ts
544
772
  var ACK_TIMEOUT = 5e3;
@@ -574,6 +802,9 @@ var socketEmit = {
574
802
  deleteMessage(messageId) {
575
803
  return withAck("delete_message", { messageId });
576
804
  },
805
+ deleteMessageForMe(messageId) {
806
+ return withAck("delete_message_for_me", { messageId });
807
+ },
577
808
  addReaction(messageId, emoji) {
578
809
  return withAck("add_reaction", { messageId, emoji });
579
810
  },
@@ -617,12 +848,12 @@ var socketEmit = {
617
848
  };
618
849
 
619
850
  // src/stores/auth.store.ts
620
- var import_zustand = require("zustand");
851
+ var import_zustand2 = require("zustand");
621
852
  var import_middleware = require("zustand/middleware");
622
853
  function createAuthStore(storage) {
623
854
  if (!storage) throw new Error("[AntzChat] createAuthStore requires a valid PersistStorage \u2014 received undefined. Make sure the SDK config is fully resolved before initializing the store.");
624
855
  const ref = { store: null };
625
- const store = (0, import_zustand.create)()(
856
+ const store = (0, import_zustand2.create)()(
626
857
  (0, import_middleware.persist)(
627
858
  (set) => ({
628
859
  user: null,
@@ -703,47 +934,8 @@ function resetAuthStore() {
703
934
  _authStore = null;
704
935
  }
705
936
 
706
- // src/stores/chat.store.ts
707
- var import_zustand2 = require("zustand");
708
- var useChatStore = (0, import_zustand2.create)((set) => ({
709
- activeConversationId: null,
710
- pendingTarget: null,
711
- typingUsers: {},
712
- onlineUsers: [],
713
- replyingTo: null,
714
- editingMessage: null,
715
- isSidebarOpen: true,
716
- isGroupInfoOpen: false,
717
- isStarredPanelOpen: false,
718
- setActiveConversation: (id) => set({ activeConversationId: id, replyingTo: null, editingMessage: null }),
719
- setPendingTarget: (target) => set({ pendingTarget: target }),
720
- addTypingUser: (conversationId, user) => set((state) => {
721
- const existing = state.typingUsers[conversationId] ?? [];
722
- const deduped = existing.filter((u) => u.userId !== user.userId);
723
- return { typingUsers: { ...state.typingUsers, [conversationId]: [...deduped, user] } };
724
- }),
725
- removeTypingUser: (conversationId, userId) => set((state) => ({
726
- typingUsers: {
727
- ...state.typingUsers,
728
- [conversationId]: (state.typingUsers[conversationId] ?? []).filter(
729
- (u) => u.userId !== userId
730
- )
731
- }
732
- })),
733
- setUserOnline: (userId) => set((state) => ({
734
- onlineUsers: state.onlineUsers.includes(userId) ? state.onlineUsers : [...state.onlineUsers, userId]
735
- })),
736
- setUserOffline: (userId) => set((state) => ({ onlineUsers: state.onlineUsers.filter((id) => id !== userId) })),
737
- setOnlineUsers: (userIds) => set({ onlineUsers: userIds }),
738
- setReplyingTo: (message) => set({ replyingTo: message, editingMessage: null }),
739
- setEditingMessage: (message) => set({ editingMessage: message, replyingTo: null }),
740
- toggleSidebar: () => set((state) => ({ isSidebarOpen: !state.isSidebarOpen })),
741
- setSidebarOpen: (open) => set({ isSidebarOpen: open }),
742
- toggleGroupInfo: () => set((state) => ({ isGroupInfoOpen: !state.isGroupInfoOpen })),
743
- setGroupInfoOpen: (open) => set({ isGroupInfoOpen: open }),
744
- toggleStarredPanel: () => set((state) => ({ isStarredPanelOpen: !state.isStarredPanelOpen })),
745
- setStarredPanelOpen: (open) => set({ isStarredPanelOpen: open })
746
- }));
937
+ // src/index.ts
938
+ init_chat_store();
747
939
 
748
940
  // src/client-facade.ts
749
941
  var AntzChatClient = class {
@@ -752,6 +944,7 @@ var AntzChatClient = class {
752
944
  this.messages = messagesApi;
753
945
  this.conversations = conversationsApi;
754
946
  this.storage = storageApi;
947
+ this.users = usersApi;
755
948
  this.socket = {
756
949
  emit: socketEmit,
757
950
  on: (event, handler) => getSocket().on(event, handler),
@@ -799,6 +992,7 @@ var AntzChatClient = class {
799
992
  normalizeConversation,
800
993
  onSocketStatus,
801
994
  reconnectSocket,
995
+ refreshSocketAuth,
802
996
  resetAuthStore,
803
997
  resolveConfig,
804
998
  setApiClientInstance,
@@ -806,6 +1000,7 @@ var AntzChatClient = class {
806
1000
  storageApi,
807
1001
  tryGetSocket,
808
1002
  uploadBatch,
809
- useChatStore
1003
+ useChatStore,
1004
+ usersApi
810
1005
  });
811
1006
  //# sourceMappingURL=index.cjs.map