@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.cjs CHANGED
@@ -108,6 +108,7 @@ __export(src_exports, {
108
108
  AntzChatPermissionError: () => AntzChatPermissionError,
109
109
  AntzChatServerError: () => AntzChatServerError,
110
110
  AntzChatValidationError: () => AntzChatValidationError,
111
+ HIGHLY_FORWARDED_DEPTH_THRESHOLD: () => HIGHLY_FORWARDED_DEPTH_THRESHOLD,
111
112
  MAX_FORWARD_TARGETS: () => MAX_FORWARD_TARGETS,
112
113
  MENTION_ALL_ID: () => MENTION_ALL_ID,
113
114
  appConfigApi: () => appConfigApi,
@@ -1012,530 +1013,16 @@ function resolveSystemMessageText(message, currentUserId) {
1012
1013
  }
1013
1014
  }
1014
1015
 
1015
- // src/api/messages.ts
1016
- var MAX_FORWARD_TARGETS = 5;
1017
- var messagesApi = {
1018
- async list(conversationId, params = {}) {
1019
- const { cursor, direction, ...rest } = params;
1020
- const serverParams = { ...rest };
1021
- if (cursor) {
1022
- serverParams[direction === "after" ? "after" : "before"] = cursor;
1023
- }
1024
- const { data } = await getApiClient().get(
1025
- `/conversations/${conversationId}/messages`,
1026
- { params: serverParams }
1027
- );
1028
- const currentUserId = getAuthStore().useAuthStore.getState().user?.id;
1029
- if (!currentUserId) return data;
1030
- return {
1031
- ...data,
1032
- data: data.data.map(
1033
- (m) => m.content.type === "system" ? { ...m, content: { ...m.content, text: resolveSystemMessageText(m, currentUserId) } } : m
1034
- )
1035
- };
1036
- },
1037
- async get(messageId) {
1038
- const { data } = await getApiClient().get(`/messages/${messageId}`);
1039
- return data;
1040
- },
1041
- async send(conversationId, payload) {
1042
- const { data } = await getApiClient().post(
1043
- `/conversations/${conversationId}/messages`,
1044
- payload
1045
- );
1046
- return data;
1047
- },
1048
- async update(messageId, text) {
1049
- const { data } = await getApiClient().post(`/messages/${messageId}/update`, { text });
1050
- return data;
1051
- },
1052
- async delete(messageId) {
1053
- await getApiClient().post(`/messages/${messageId}/delete`);
1054
- },
1055
- async deleteForMe(messageId) {
1056
- await getApiClient().post(`/messages/${messageId}/delete-for-me`);
1057
- },
1058
- async addReaction(messageId, emoji) {
1059
- const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
1060
- return data;
1061
- },
1062
- async removeReaction(messageId, emoji) {
1063
- const { data } = await getApiClient().post(
1064
- `/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`
1065
- );
1066
- return data;
1067
- },
1068
- async getReactions(messageId) {
1069
- const { data } = await getApiClient().post(`/messages/${messageId}/reactions/list`);
1070
- return data;
1071
- },
1072
- async star(messageId) {
1073
- await getApiClient().post(`/messages/${messageId}/star`);
1074
- },
1075
- async unstar(messageId) {
1076
- await getApiClient().post(`/messages/${messageId}/unstar`);
1077
- },
1078
- async getStarred(params = {}) {
1079
- const { data } = await getApiClient().get("/messages/starred", { params });
1080
- return data;
1081
- },
1082
- async search(params) {
1083
- const { data } = await getApiClient().get("/messages/search", { params });
1084
- return data;
1085
- },
1086
- async getLastRead(conversationId) {
1087
- const { data } = await getApiClient().get(
1088
- `/conversations/${conversationId}/read-receipt`
1089
- );
1090
- return data;
1091
- },
1092
- async markAsRead(conversationId, messageId) {
1093
- await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});
1094
- },
1095
- async pin(messageId) {
1096
- const { data } = await getApiClient().post(`/messages/${messageId}/pin`);
1097
- return data;
1098
- },
1099
- async unpin(messageId) {
1100
- const { data } = await getApiClient().post(`/messages/${messageId}/unpin`);
1101
- return data;
1102
- },
1103
- async getPinned(conversationId) {
1104
- const { data } = await getApiClient().get(`/conversations/${conversationId}/pinned-messages`);
1105
- return data;
1106
- },
1107
- async getReceipts(messageId) {
1108
- const { data } = await getApiClient().get(`/messages/${messageId}/receipts`);
1109
- return data;
1110
- },
1111
- /**
1112
- * Forwards a message into one or more target conversations (max MAX_FORWARD_TARGETS
1113
- * per call, also enforced server-side). Each target is independent — one failing
1114
- * (e.g. no longer a participant) does not block the others; check `success`/`error`
1115
- * per entry in the returned array.
1116
- */
1117
- async forward(messageId, targetConversationIds) {
1118
- const { data } = await getApiClient().post(
1119
- `/messages/${messageId}/forward`,
1120
- { targetConversationIds }
1121
- );
1122
- return data;
1123
- }
1124
- };
1125
-
1126
- // src/api/conversations.ts
1127
- function normalizeParticipant(p) {
1128
- const hasUserDetails = p.displayName || p.username || p.avatarUrl;
1129
- return {
1130
- userId: p.userId,
1131
- externalId: p.externalId ?? p.user?.externalId,
1132
- role: p.role,
1133
- joinedAt: p.joinedAt,
1134
- isActive: p.isActive,
1135
- user: hasUserDetails ? {
1136
- id: p.userId,
1137
- externalId: p.externalId,
1138
- tenantId: "",
1139
- email: "",
1140
- username: p.username ?? "",
1141
- displayName: p.displayName ?? p.username ?? "",
1142
- avatarUrl: p.avatarUrl,
1143
- status: "offline",
1144
- createdAt: p.joinedAt ?? "",
1145
- updatedAt: p.joinedAt ?? ""
1146
- } : p.user
1147
- };
1148
- }
1149
- function normalizeLastMessage(lastMsg) {
1150
- if (!lastMsg) return void 0;
1151
- if (lastMsg.content !== void 0) return lastMsg;
1152
- return {
1153
- id: lastMsg.messageId ?? "",
1154
- tenantId: "",
1155
- conversationId: "",
1156
- senderId: lastMsg.senderId ?? "",
1157
- content: {
1158
- type: lastMsg.hasAttachments ? "attachment" : "text",
1159
- text: lastMsg.contentPreview
1160
- },
1161
- reactions: [],
1162
- lastReaction: lastMsg.lastReaction ?? null,
1163
- status: lastMsg.status ?? "active",
1164
- deliveryStatus: lastMsg.deliveryStatus ?? "sent",
1165
- isEdited: false,
1166
- sentAt: lastMsg.sentAt ?? "",
1167
- createdAt: lastMsg.sentAt ?? "",
1168
- ...lastMsg.senderName && { senderName: lastMsg.senderName },
1169
- ...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
1170
- };
1171
- }
1172
- function normalizeConversation(conv) {
1173
- return {
1174
- ...conv,
1175
- id: conv.id ?? conv.conversationId,
1176
- participants: (conv.participants ?? []).map(normalizeParticipant),
1177
- lastMessage: normalizeLastMessage(conv.lastMessage)
1178
- };
1179
- }
1180
- var conversationsApi = {
1181
- async list(params = {}) {
1182
- const { data } = await getApiClient().get("/conversations", { params });
1183
- return { ...data, data: data.data.map(normalizeConversation) };
1184
- },
1185
- async get(conversationId) {
1186
- const { data } = await getApiClient().get(`/conversations/${conversationId}`);
1187
- return normalizeConversation(data);
1188
- },
1189
- async createGroup(payload) {
1190
- const { data } = await getApiClient().post("/conversations", payload);
1191
- return normalizeConversation(data);
1192
- },
1193
- async createDirect(payload) {
1194
- const { data } = await getApiClient().post("/conversations/direct", payload);
1195
- return normalizeConversation(data);
1196
- },
1197
- async update(conversationId, payload) {
1198
- const { data } = await getApiClient().post(`/conversations/${conversationId}/update`, payload);
1199
- return normalizeConversation(data);
1200
- },
1201
- async delete(conversationId) {
1202
- await getApiClient().post(`/conversations/${conversationId}/delete`);
1203
- },
1204
- async addParticipants(conversationId, userIds, role) {
1205
- const { data } = await getApiClient().post(
1206
- `/conversations/${conversationId}/participants`,
1207
- { userIds, ...role && { role } }
1208
- );
1209
- return normalizeConversation(data);
1210
- },
1211
- async removeParticipant(conversationId, userId) {
1212
- const { data } = await getApiClient().post(
1213
- `/conversations/${conversationId}/participants/${userId}/remove`
1214
- );
1215
- return normalizeConversation(data);
1216
- },
1217
- async updateParticipantRole(conversationId, userId, role) {
1218
- const { data } = await getApiClient().post(
1219
- `/conversations/${conversationId}/participants/${userId}/role`,
1220
- { role }
1221
- );
1222
- return normalizeConversation(data);
1223
- },
1224
- async mute(conversationId, mutedUntil) {
1225
- await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});
1226
- },
1227
- async unmute(conversationId) {
1228
- await getApiClient().post(`/conversations/${conversationId}/unmute`);
1229
- },
1230
- async pin(conversationId) {
1231
- await getApiClient().post(`/conversations/${conversationId}/pin`);
1232
- },
1233
- async unpin(conversationId) {
1234
- await getApiClient().post(`/conversations/${conversationId}/unpin`);
1235
- },
1236
- async markUnread(conversationId) {
1237
- await getApiClient().post(`/conversations/${conversationId}/unread`);
1238
- },
1239
- async markRead(conversationId) {
1240
- await getApiClient().post(`/conversations/${conversationId}/unread/clear`);
1241
- },
1242
- async leave(conversationId, andDelete) {
1243
- const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
1244
- await getApiClient().post(url);
1245
- },
1246
- async getMembers(conversationId, filter) {
1247
- const { data } = await getApiClient().get(
1248
- `/conversations/${conversationId}/participants`,
1249
- filter ? { params: { filter } } : void 0
1250
- );
1251
- return (data ?? []).map(normalizeParticipant);
1252
- },
1253
- /**
1254
- * Get unread message count for a single conversation.
1255
- * Use this after app foreground or socket reconnect to refresh a specific count.
1256
- */
1257
- async getUnreadCount(conversationId) {
1258
- const { data } = await getApiClient().get(
1259
- `/conversations/${conversationId}/unread`
1260
- );
1261
- return data;
1262
- },
1263
- /**
1264
- * Get total unread count across all conversations + per-conversation breakdown.
1265
- * Use on app cold start, foreground resume, or after socket reconnect.
1266
- * The socket keeps counts live while connected — this is the source of truth
1267
- * when the socket was down.
1268
- */
1269
- async getUnreadSummary() {
1270
- const { data } = await getApiClient().get("/conversations/unread");
1271
- return data;
1272
- },
1273
- /**
1274
- * Set the group icon from an already-uploaded file (admin only).
1275
- * The fileId comes from uploadBatch() / client.uploadFiles() — same as attachments.
1276
- * Server copies storageKey into conversation.iconMeta and deletes the chat_files record.
1277
- */
1278
- async uploadIcon(conversationId, fileId) {
1279
- const { data } = await getApiClient().post(
1280
- `/conversations/${conversationId}/icon`,
1281
- { fileId }
1282
- );
1283
- return normalizeConversation(data);
1284
- },
1285
- async removeIcon(conversationId) {
1286
- const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
1287
- return normalizeConversation(data);
1288
- },
1289
- async clearChat(conversationId) {
1290
- await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);
1291
- }
1292
- };
1293
-
1294
- // src/crypto/uuid.ts
1295
- function generateUUID() {
1296
- if (typeof globalThis.crypto?.randomUUID === "function") {
1297
- return globalThis.crypto.randomUUID();
1298
- }
1299
- return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1300
- const r = Math.random() * 16 | 0;
1301
- return (c === "x" ? r : r & 3 | 8).toString(16);
1302
- });
1303
- }
1304
-
1305
- // src/api/storage.ts
1306
- var storageApi = {
1307
- async requestPresignedUrl(payload) {
1308
- const { data } = await getApiClient().post("/storage/presigned-url", payload);
1309
- return data;
1310
- },
1311
- async requestPresignedUrlBatch(files) {
1312
- const { data } = await getApiClient().post("/storage/presigned-url/batch", { files });
1313
- return data;
1314
- },
1315
- async confirmUpload(fileId) {
1316
- const { data } = await getApiClient().post(`/storage/confirm/${fileId}`);
1317
- return data;
1318
- },
1319
- async getFile(fileId) {
1320
- const { data } = await getApiClient().get(`/storage/files/${fileId}`);
1321
- return data;
1322
- },
1323
- async getFileUrl(fileId, expiresIn) {
1324
- const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {
1325
- params: expiresIn ? { expiresIn } : {}
1326
- });
1327
- return data;
1328
- },
1329
- async deleteFile(fileId) {
1330
- await getApiClient().post(`/storage/files/${fileId}/delete`);
1331
- },
1332
- async completeMultipartUpload(fileId, uploadId, parts) {
1333
- const { data } = await getApiClient().post(
1334
- `/storage/multipart/complete/${fileId}`,
1335
- { uploadId, parts }
1336
- );
1337
- return data;
1338
- },
1339
- async getConversationFiles(conversationId, params = {}) {
1340
- const { data } = await getApiClient().get(
1341
- `/storage/conversations/${conversationId}/files`,
1342
- { params }
1343
- );
1344
- return data;
1345
- },
1346
- async getMyFiles(params = {}) {
1347
- const { data } = await getApiClient().get("/storage/my-files", { params });
1348
- return data;
1349
- }
1350
- };
1351
- async function runMultipartUpload(presigned, file, platformUploadPartFn, onProgress) {
1352
- const { multipart } = presigned;
1353
- if (!multipart) throw new Error("No multipart info on presigned response");
1354
- const CONCURRENCY = 3;
1355
- const completedParts = [];
1356
- const partProgress = {};
1357
- multipart.partUrls.forEach(({ partNumber }) => {
1358
- partProgress[partNumber] = 0;
1359
- });
1360
- const reportProgress = () => {
1361
- if (!onProgress) return;
1362
- const vals = Object.values(partProgress);
1363
- const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
1364
- onProgress(Math.round(avg * 0.95));
1365
- };
1366
- const uploadPart = async (partNumber, uploadUrl, method) => {
1367
- const offset = (partNumber - 1) * multipart.chunkSize;
1368
- const end = Math.min(offset + multipart.chunkSize, file.size);
1369
- const blob = await fetch(file.uri).then((r) => r.blob());
1370
- const slice = blob.slice(offset, end);
1371
- const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {
1372
- partProgress[partNumber] = pct;
1373
- reportProgress();
1374
- }, method);
1375
- completedParts.push({ partNumber, etag });
1376
- partProgress[partNumber] = 100;
1377
- reportProgress();
1378
- };
1379
- for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {
1380
- const batch = multipart.partUrls.slice(i, i + CONCURRENCY);
1381
- const results = await Promise.allSettled(
1382
- batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? "PUT"))
1383
- );
1384
- const failed = results.find((r) => r.status === "rejected");
1385
- if (failed) throw failed.reason;
1386
- }
1387
- completedParts.sort((a, b) => a.partNumber - b.partNumber);
1388
- const fileResponse = await storageApi.completeMultipartUpload(
1389
- presigned.fileId,
1390
- multipart.uploadId,
1391
- completedParts
1392
- );
1393
- onProgress?.(100);
1394
- return fileResponse;
1395
- }
1396
- async function runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
1397
- const compressedFiles = await Promise.all(
1398
- files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
1399
- );
1400
- const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));
1401
- const requests = slotted.map(({ file: f, clientIndex }) => ({
1402
- filename: f.name,
1403
- mimeType: f.type,
1404
- size: f.size,
1405
- conversationId,
1406
- clientIndex,
1407
- ...f.compressed && {
1408
- metadata: {
1409
- compressed: f.compressed,
1410
- originalSize: f.originalSize,
1411
- compressionAlgorithm: f.compressionAlgorithm
1412
- }
1413
- }
1414
- }));
1415
- const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
1416
- const failedSlotIds = /* @__PURE__ */ new Set();
1417
- const failed = requestErrors.map((e) => {
1418
- const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);
1419
- const slotId = slotted[idx]?.slotId;
1420
- if (slotId) failedSlotIds.add(slotId);
1421
- return { filename: e.filename, error: e.error };
1422
- });
1423
- const progressMap = {};
1424
- const reportProgress = () => {
1425
- if (!onProgress) return;
1426
- const vals = Object.values(progressMap);
1427
- const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
1428
- onProgress(Math.round(avg));
1429
- };
1430
- const successful = [];
1431
- const slotToFile = /* @__PURE__ */ new Map();
1432
- await Promise.all(
1433
- urls.map(async (presigned, idx) => {
1434
- const originalIdx = presigned.clientIndex ?? idx;
1435
- const { file, slotId } = slotted[originalIdx];
1436
- progressMap[originalIdx] = 0;
1437
- try {
1438
- let fileResponse;
1439
- if (presigned.multipart && platformUploadPartFn) {
1440
- fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {
1441
- progressMap[originalIdx] = pct;
1442
- reportProgress();
1443
- });
1444
- } else {
1445
- await platformUploadFn(presigned, file, (pct) => {
1446
- progressMap[originalIdx] = Math.round(pct * 0.9);
1447
- reportProgress();
1448
- });
1449
- fileResponse = await storageApi.confirmUpload(presigned.fileId);
1450
- }
1451
- progressMap[originalIdx] = 100;
1452
- reportProgress();
1453
- successful.push(fileResponse);
1454
- slotToFile.set(slotId, fileResponse);
1455
- } catch (err) {
1456
- failed.push({ filename: file.name, error: err.message });
1457
- }
1458
- })
1459
- );
1460
- return { result: { successful, failed }, slotToFile };
1461
- }
1462
- async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
1463
- const slotIds = files.map(() => generateUUID());
1464
- const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);
1465
- return result;
1466
- }
1467
-
1468
- // src/api/devices.ts
1469
- var devicesApi = {
1470
- /**
1471
- * Register or update a device push token with the chat server.
1472
- *
1473
- * Upserts by `deviceId` — calling this multiple times with the same deviceId
1474
- * simply refreshes the token value (tokens can rotate silently on some platforms).
1475
- *
1476
- * The SDK never calls this automatically. The parent app or the `tokenProvider`
1477
- * option in `pushNotifications` config is responsible for calling it after
1478
- * obtaining the token from the OS / browser.
1479
- */
1480
- async register(payload) {
1481
- await getApiClient().post("/users/me/devices", payload);
1482
- },
1483
- /**
1484
- * Remove a device token from the chat server.
1485
- * Call this on logout so the user stops receiving push notifications on this device.
1486
- */
1487
- async remove(deviceId) {
1488
- await getApiClient().post(`/users/me/devices/${deviceId}/remove`);
1489
- }
1490
- };
1491
-
1492
- // src/api/users.ts
1493
- var usersApi = {
1494
- async list(params = {}) {
1495
- const { data } = await getApiClient().get("/users", { params });
1496
- return data;
1497
- },
1498
- async getById(userId) {
1499
- const { data } = await getApiClient().get(`/users/${userId}`);
1500
- return data;
1501
- },
1502
- async getLastSeen(userId) {
1503
- const { data } = await getApiClient().get(`/users/${userId}`);
1504
- return { lastSeenAt: data.lastSeenAt ?? null };
1505
- },
1506
- /**
1507
- * Update basic profile fields for the current user.
1508
- * Works in both builtin and non-builtin modes. Use this to push an immediate
1509
- * profile update to the chat server when the host app knows a change just
1510
- * happened — without waiting for the next 2-hour sync cycle.
1511
- */
1512
- async updateProfile(payload) {
1513
- const { data } = await getApiClient().post("/users/me/update", payload);
1514
- return data;
1515
- },
1516
- /**
1517
- * Update notification preferences for the current user.
1518
- * Partial update — only send fields you want to change.
1519
- * A prefs record is automatically created with defaults when a device
1520
- * token is first registered, so this never fails with "not found".
1521
- */
1522
- async updatePreferences(prefs) {
1523
- const { data } = await getApiClient().post("/users/me/preferences", prefs);
1524
- return data;
1525
- },
1526
- /**
1527
- * Fetch current notification preferences for the current user.
1528
- * Returns null if no prefs record exists yet (all defaults apply).
1529
- */
1530
- async getPreferences() {
1531
- try {
1532
- const { data } = await getApiClient().get("/users/me/preferences");
1533
- return data;
1534
- } catch {
1535
- return null;
1536
- }
1016
+ // src/crypto/uuid.ts
1017
+ function generateUUID() {
1018
+ if (typeof globalThis.crypto?.randomUUID === "function") {
1019
+ return globalThis.crypto.randomUUID();
1537
1020
  }
1538
- };
1021
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1022
+ const r = Math.random() * 16 | 0;
1023
+ return (c === "x" ? r : r & 3 | 8).toString(16);
1024
+ });
1025
+ }
1539
1026
 
1540
1027
  // src/socket/socket.ts
1541
1028
  var import_socket = require("socket.io-client");
@@ -1854,127 +1341,684 @@ async function drainSendQueue(conversationId) {
1854
1341
  entry.reject(new AntzChatNetworkError("Message dropped: queued too long", "MESSAGE_DROPPED"));
1855
1342
  continue;
1856
1343
  }
1857
- entry.run().then(entry.resolve).catch(entry.reject);
1344
+ entry.run().then(entry.resolve).catch(entry.reject);
1345
+ }
1346
+ sendQueues.delete(conversationId);
1347
+ sendQueueRunning.delete(conversationId);
1348
+ }
1349
+ function queueSendMessage(payload) {
1350
+ const conversationId = payload.conversationId;
1351
+ if (!sendQueues.has(conversationId)) sendQueues.set(conversationId, []);
1352
+ const queue = sendQueues.get(conversationId);
1353
+ if (queue.length >= QUEUE_MAX_SIZE) {
1354
+ return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
1355
+ }
1356
+ return new Promise((resolve, reject) => {
1357
+ queue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
1358
+ drainSendQueue(conversationId);
1359
+ });
1360
+ }
1361
+ function waitForReconnect() {
1362
+ return new Promise((resolve, reject) => {
1363
+ const timer = setTimeout(() => {
1364
+ unsubscribe();
1365
+ reject(new AntzChatNetworkError("Socket reconnect timeout", "SOCKET_TIMEOUT"));
1366
+ }, RECONNECT_WAIT_TIMEOUT);
1367
+ const unsubscribe = onSocketStatus((status) => {
1368
+ if (status === "connected") {
1369
+ clearTimeout(timer);
1370
+ unsubscribe();
1371
+ resolve();
1372
+ } else if (status === "error") {
1373
+ clearTimeout(timer);
1374
+ unsubscribe();
1375
+ reject(new AntzChatNetworkError("Socket reconnect failed", "SOCKET_NOT_CONNECTED"));
1376
+ }
1377
+ });
1378
+ });
1379
+ }
1380
+ async function withAck(event, payload) {
1381
+ let socket = tryGetSocket();
1382
+ if (!socket) {
1383
+ await waitForReconnect();
1384
+ socket = tryGetSocket();
1385
+ }
1386
+ if (!socket) return Promise.reject(new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event }));
1387
+ return new Promise((resolve, reject) => {
1388
+ let timer;
1389
+ secureEmit(socket, event, payload, (response) => {
1390
+ clearTimeout(timer);
1391
+ resolve(response);
1392
+ }).then(() => {
1393
+ timer = setTimeout(() => reject(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })), ACK_TIMEOUT);
1394
+ }).catch(reject);
1395
+ });
1396
+ }
1397
+ function fireAndForget(event, payload) {
1398
+ const socket = tryGetSocket();
1399
+ if (!socket) return;
1400
+ secureEmit(socket, event, payload);
1401
+ }
1402
+ var socketEmit = {
1403
+ joinRoom(conversationId) {
1404
+ fireAndForget("join_room", { conversationId });
1405
+ },
1406
+ leaveRoom(conversationId) {
1407
+ fireAndForget("leave_room", { conversationId });
1408
+ },
1409
+ sendMessage(payload) {
1410
+ return queueSendMessage({ ...payload, sentAt: Date.now() });
1411
+ },
1412
+ // Not queued like sendMessage — forward targets are independent conversations,
1413
+ // not the single conversation ordering that queueSendMessage protects, and each
1414
+ // target's own new_message/push already fires server-side via MessagesService.forward()'s
1415
+ // internal create() calls, so there's nothing here that needs in-order draining.
1416
+ forwardMessage(payload) {
1417
+ return withAck("forward_message", payload);
1418
+ },
1419
+ updateMessage(messageId, text) {
1420
+ return withAck("update_message", { messageId, text });
1421
+ },
1422
+ deleteMessage(messageId) {
1423
+ return withAck("delete_message", { messageId });
1424
+ },
1425
+ deleteMessageForMe(messageId) {
1426
+ return withAck("delete_message_for_me", { messageId });
1427
+ },
1428
+ clearChat(conversationId) {
1429
+ return withAck("clear_chat_for_me", { conversationId });
1430
+ },
1431
+ addReaction(messageId, emoji) {
1432
+ return withAck("add_reaction", { messageId, emoji });
1433
+ },
1434
+ removeReaction(messageId, emoji) {
1435
+ return withAck("remove_reaction", { messageId, emoji });
1436
+ },
1437
+ pinMessage(messageId) {
1438
+ return withAck("pin_message", { messageId });
1439
+ },
1440
+ unpinMessage(messageId) {
1441
+ return withAck("unpin_message", { messageId });
1442
+ },
1443
+ // markRead and typing are best-effort — silently dropped if socket not ready
1444
+ typing(conversationId, isTyping) {
1445
+ fireAndForget("typing", { conversationId, isTyping });
1446
+ },
1447
+ markRead(conversationId, messageId) {
1448
+ fireAndForget("mark_read", { conversationId, ...messageId ? { messageId } : {} });
1449
+ },
1450
+ getOnlineUsers(userIds) {
1451
+ const socket = tryGetSocket();
1452
+ if (!socket) return Promise.resolve([]);
1453
+ return new Promise((resolve, reject) => {
1454
+ let timer;
1455
+ secureEmit(socket, "get_online_users", { userIds }, (response) => {
1456
+ clearTimeout(timer);
1457
+ if (response && typeof response === "object" && "onlineStatus" in response) {
1458
+ const status = response.onlineStatus;
1459
+ resolve(Object.entries(status).filter(([, v]) => v).map(([k]) => k));
1460
+ } else if (Array.isArray(response)) {
1461
+ resolve(response);
1462
+ } else {
1463
+ resolve([]);
1464
+ }
1465
+ }).then(() => {
1466
+ timer = setTimeout(() => reject(new AntzChatNetworkError("Socket ack timeout: get_online_users", "SOCKET_TIMEOUT", { event: "get_online_users" })), ACK_TIMEOUT);
1467
+ }).catch(reject);
1468
+ });
1469
+ },
1470
+ getTypingUsers(conversationId) {
1471
+ return withAck("get_typing_users", { conversationId });
1472
+ }
1473
+ };
1474
+
1475
+ // src/api/messages.ts
1476
+ var MAX_FORWARD_TARGETS = 5;
1477
+ var HIGHLY_FORWARDED_DEPTH_THRESHOLD = 5;
1478
+ var messagesApi = {
1479
+ async list(conversationId, params = {}) {
1480
+ const { cursor, direction, ...rest } = params;
1481
+ const serverParams = { ...rest };
1482
+ if (cursor) {
1483
+ serverParams[direction === "after" ? "after" : "before"] = cursor;
1484
+ }
1485
+ const { data } = await getApiClient().get(
1486
+ `/conversations/${conversationId}/messages`,
1487
+ { params: serverParams }
1488
+ );
1489
+ const currentUserId = getAuthStore().useAuthStore.getState().user?.id;
1490
+ if (!currentUserId) return data;
1491
+ return {
1492
+ ...data,
1493
+ data: data.data.map(
1494
+ (m) => m.content.type === "system" ? { ...m, content: { ...m.content, text: resolveSystemMessageText(m, currentUserId) } } : m
1495
+ )
1496
+ };
1497
+ },
1498
+ async get(messageId) {
1499
+ const { data } = await getApiClient().get(`/messages/${messageId}`);
1500
+ return data;
1501
+ },
1502
+ /**
1503
+ * Sends a message via REST. For real-time delivery use `socketEmit.sendMessage`
1504
+ * instead — this is a lower-level entry point (used e.g. by the socket path's
1505
+ * REST-mirror flows). Pass `payload.tempId` and reuse the SAME value on retry to
1506
+ * make a retry-after-timeout safe — see `SendData.tempId`.
1507
+ */
1508
+ async send(conversationId, payload) {
1509
+ const { data } = await getApiClient().post(
1510
+ `/conversations/${conversationId}/messages`,
1511
+ payload
1512
+ );
1513
+ return data;
1514
+ },
1515
+ async update(messageId, text) {
1516
+ const { data } = await getApiClient().post(`/messages/${messageId}/update`, { text });
1517
+ return data;
1518
+ },
1519
+ async delete(messageId) {
1520
+ await getApiClient().post(`/messages/${messageId}/delete`);
1521
+ },
1522
+ async deleteForMe(messageId) {
1523
+ await getApiClient().post(`/messages/${messageId}/delete-for-me`);
1524
+ },
1525
+ async addReaction(messageId, emoji) {
1526
+ const { data } = await getApiClient().post(`/messages/${messageId}/reactions`, { emoji });
1527
+ return data;
1528
+ },
1529
+ async removeReaction(messageId, emoji) {
1530
+ const { data } = await getApiClient().post(
1531
+ `/messages/${messageId}/reactions/${encodeURIComponent(emoji)}/remove`
1532
+ );
1533
+ return data;
1534
+ },
1535
+ async getReactions(messageId) {
1536
+ const { data } = await getApiClient().post(`/messages/${messageId}/reactions/list`);
1537
+ return data;
1538
+ },
1539
+ async star(messageId) {
1540
+ await getApiClient().post(`/messages/${messageId}/star`);
1541
+ },
1542
+ async unstar(messageId) {
1543
+ await getApiClient().post(`/messages/${messageId}/unstar`);
1544
+ },
1545
+ async getStarred(params = {}) {
1546
+ const { data } = await getApiClient().get("/messages/starred", { params });
1547
+ return data;
1548
+ },
1549
+ async search(params) {
1550
+ const { data } = await getApiClient().get("/messages/search", { params });
1551
+ return data;
1552
+ },
1553
+ async getLastRead(conversationId) {
1554
+ const { data } = await getApiClient().get(
1555
+ `/conversations/${conversationId}/read-receipt`
1556
+ );
1557
+ return data;
1558
+ },
1559
+ async markAsRead(conversationId, messageId) {
1560
+ await getApiClient().post(`/conversations/${conversationId}/read`, messageId ? { messageId } : {});
1561
+ },
1562
+ async pin(messageId) {
1563
+ const { data } = await getApiClient().post(`/messages/${messageId}/pin`);
1564
+ return data;
1565
+ },
1566
+ async unpin(messageId) {
1567
+ const { data } = await getApiClient().post(`/messages/${messageId}/unpin`);
1568
+ return data;
1569
+ },
1570
+ async getPinned(conversationId) {
1571
+ const { data } = await getApiClient().get(`/conversations/${conversationId}/pinned-messages`);
1572
+ return data;
1573
+ },
1574
+ async getReceipts(messageId) {
1575
+ const { data } = await getApiClient().get(`/messages/${messageId}/receipts`);
1576
+ return data;
1577
+ },
1578
+ /**
1579
+ * Forwards a message into one or more target conversations (max MAX_FORWARD_TARGETS
1580
+ * per call, also enforced server-side). Each target is independent — one failing
1581
+ * (e.g. no longer a participant) does not block the others; check `success`/`error`
1582
+ * per entry in the returned array.
1583
+ *
1584
+ * Pass `attachmentIds` to forward only a subset of the source message's attachments
1585
+ * (e.g. one image out of a multi-image message) — omit it to forward the whole
1586
+ * message, including all its attachments, unchanged. An ID not present on the
1587
+ * source message is ignored server-side.
1588
+ *
1589
+ * Idempotency: pass `tempId` — generate ONE value per forward action (e.g. via
1590
+ * `generateUUID` from '@antzsoft/chat-core/internal') and reuse that SAME value if
1591
+ * you retry this exact forward (e.g. after a client-side timeout). The server then
1592
+ * recognizes the retry per target and returns the already-created message instead
1593
+ * of creating a duplicate. Never mint a new tempId for a retry — only when the user
1594
+ * initiates a genuinely new forward action. If omitted, a fresh one is generated
1595
+ * per call, which means a retry without an explicit tempId gets NO dedup protection.
1596
+ *
1597
+ * Transport: uses the 'forward_message' socket event when a socket is connected
1598
+ * (lower latency, same server-side MessagesService.forward() path, so broadcast/
1599
+ * push notification behavior is identical either way) and transparently falls back
1600
+ * to the REST endpoint when no socket is available.
1601
+ */
1602
+ async forward(messageId, targetConversationIds, attachmentIds, tempId) {
1603
+ const resolvedTempId = tempId ?? generateUUID();
1604
+ if (tryGetSocket()) {
1605
+ const ack = await socketEmit.forwardMessage({
1606
+ messageId,
1607
+ targetConversationIds,
1608
+ ...attachmentIds ? { attachmentIds } : {},
1609
+ tempId: resolvedTempId
1610
+ });
1611
+ if (ack.error) throw new Error(ack.error);
1612
+ return ack.results;
1613
+ }
1614
+ const { data } = await getApiClient().post(
1615
+ `/messages/${messageId}/forward`,
1616
+ { targetConversationIds, ...attachmentIds ? { attachmentIds } : {}, tempId: resolvedTempId }
1617
+ );
1618
+ return data;
1858
1619
  }
1859
- sendQueues.delete(conversationId);
1860
- sendQueueRunning.delete(conversationId);
1620
+ };
1621
+
1622
+ // src/api/conversations.ts
1623
+ function normalizeParticipant(p) {
1624
+ const hasUserDetails = p.displayName || p.username || p.avatarUrl;
1625
+ return {
1626
+ userId: p.userId,
1627
+ externalId: p.externalId ?? p.user?.externalId,
1628
+ role: p.role,
1629
+ joinedAt: p.joinedAt,
1630
+ isActive: p.isActive,
1631
+ user: hasUserDetails ? {
1632
+ id: p.userId,
1633
+ externalId: p.externalId,
1634
+ tenantId: "",
1635
+ email: "",
1636
+ username: p.username ?? "",
1637
+ displayName: p.displayName ?? p.username ?? "",
1638
+ avatarUrl: p.avatarUrl,
1639
+ status: "offline",
1640
+ createdAt: p.joinedAt ?? "",
1641
+ updatedAt: p.joinedAt ?? ""
1642
+ } : p.user
1643
+ };
1861
1644
  }
1862
- function queueSendMessage(payload) {
1863
- const conversationId = payload.conversationId;
1864
- if (!sendQueues.has(conversationId)) sendQueues.set(conversationId, []);
1865
- const queue = sendQueues.get(conversationId);
1866
- if (queue.length >= QUEUE_MAX_SIZE) {
1867
- return Promise.reject(new AntzChatNetworkError("Send queue full: too many messages in flight", "SEND_QUEUE_FULL", { conversationId }));
1868
- }
1869
- return new Promise((resolve, reject) => {
1870
- queue.push({ run: () => withAck("send_message", payload), resolve, reject, enqueuedAt: Date.now() });
1871
- drainSendQueue(conversationId);
1872
- });
1645
+ function normalizeLastMessage(lastMsg) {
1646
+ if (!lastMsg) return void 0;
1647
+ if (lastMsg.content !== void 0) return lastMsg;
1648
+ return {
1649
+ id: lastMsg.messageId ?? "",
1650
+ tenantId: "",
1651
+ conversationId: "",
1652
+ senderId: lastMsg.senderId ?? "",
1653
+ content: {
1654
+ type: lastMsg.hasAttachments ? "attachment" : "text",
1655
+ text: lastMsg.contentPreview
1656
+ },
1657
+ reactions: [],
1658
+ lastReaction: lastMsg.lastReaction ?? null,
1659
+ status: lastMsg.status ?? "active",
1660
+ deliveryStatus: lastMsg.deliveryStatus ?? "sent",
1661
+ isEdited: false,
1662
+ sentAt: lastMsg.sentAt ?? "",
1663
+ createdAt: lastMsg.sentAt ?? "",
1664
+ ...lastMsg.senderName && { senderName: lastMsg.senderName },
1665
+ ...lastMsg.attachmentType && { attachmentType: lastMsg.attachmentType }
1666
+ };
1873
1667
  }
1874
- function waitForReconnect() {
1875
- return new Promise((resolve, reject) => {
1876
- const timer = setTimeout(() => {
1877
- unsubscribe();
1878
- reject(new AntzChatNetworkError("Socket reconnect timeout", "SOCKET_TIMEOUT"));
1879
- }, RECONNECT_WAIT_TIMEOUT);
1880
- const unsubscribe = onSocketStatus((status) => {
1881
- if (status === "connected") {
1882
- clearTimeout(timer);
1883
- unsubscribe();
1884
- resolve();
1885
- } else if (status === "error") {
1886
- clearTimeout(timer);
1887
- unsubscribe();
1888
- reject(new AntzChatNetworkError("Socket reconnect failed", "SOCKET_NOT_CONNECTED"));
1889
- }
1890
- });
1891
- });
1668
+ function normalizeConversation(conv) {
1669
+ return {
1670
+ ...conv,
1671
+ id: conv.id ?? conv.conversationId,
1672
+ participants: (conv.participants ?? []).map(normalizeParticipant),
1673
+ lastMessage: normalizeLastMessage(conv.lastMessage)
1674
+ };
1892
1675
  }
1893
- async function withAck(event, payload) {
1894
- let socket = tryGetSocket();
1895
- if (!socket) {
1896
- await waitForReconnect();
1897
- socket = tryGetSocket();
1676
+ var conversationsApi = {
1677
+ async list(params = {}) {
1678
+ const { data } = await getApiClient().get("/conversations", { params });
1679
+ return { ...data, data: data.data.map(normalizeConversation) };
1680
+ },
1681
+ async get(conversationId) {
1682
+ const { data } = await getApiClient().get(`/conversations/${conversationId}`);
1683
+ return normalizeConversation(data);
1684
+ },
1685
+ async createGroup(payload) {
1686
+ const { data } = await getApiClient().post("/conversations", payload);
1687
+ return normalizeConversation(data);
1688
+ },
1689
+ async createDirect(payload) {
1690
+ const { data } = await getApiClient().post("/conversations/direct", payload);
1691
+ return normalizeConversation(data);
1692
+ },
1693
+ async update(conversationId, payload) {
1694
+ const { data } = await getApiClient().post(`/conversations/${conversationId}/update`, payload);
1695
+ return normalizeConversation(data);
1696
+ },
1697
+ async delete(conversationId) {
1698
+ await getApiClient().post(`/conversations/${conversationId}/delete`);
1699
+ },
1700
+ async addParticipants(conversationId, userIds, role) {
1701
+ const { data } = await getApiClient().post(
1702
+ `/conversations/${conversationId}/participants`,
1703
+ { userIds, ...role && { role } }
1704
+ );
1705
+ return normalizeConversation(data);
1706
+ },
1707
+ async removeParticipant(conversationId, userId) {
1708
+ const { data } = await getApiClient().post(
1709
+ `/conversations/${conversationId}/participants/${userId}/remove`
1710
+ );
1711
+ return normalizeConversation(data);
1712
+ },
1713
+ async updateParticipantRole(conversationId, userId, role) {
1714
+ const { data } = await getApiClient().post(
1715
+ `/conversations/${conversationId}/participants/${userId}/role`,
1716
+ { role }
1717
+ );
1718
+ return normalizeConversation(data);
1719
+ },
1720
+ async mute(conversationId, mutedUntil) {
1721
+ await getApiClient().post(`/conversations/${conversationId}/mute`, mutedUntil ? { mutedUntil } : {});
1722
+ },
1723
+ async unmute(conversationId) {
1724
+ await getApiClient().post(`/conversations/${conversationId}/unmute`);
1725
+ },
1726
+ async pin(conversationId) {
1727
+ await getApiClient().post(`/conversations/${conversationId}/pin`);
1728
+ },
1729
+ async unpin(conversationId) {
1730
+ await getApiClient().post(`/conversations/${conversationId}/unpin`);
1731
+ },
1732
+ async markUnread(conversationId) {
1733
+ await getApiClient().post(`/conversations/${conversationId}/unread`);
1734
+ },
1735
+ async markRead(conversationId) {
1736
+ await getApiClient().post(`/conversations/${conversationId}/unread/clear`);
1737
+ },
1738
+ async leave(conversationId, andDelete) {
1739
+ const url = andDelete ? `/conversations/${conversationId}/leave?delete=true` : `/conversations/${conversationId}/leave`;
1740
+ await getApiClient().post(url);
1741
+ },
1742
+ async getMembers(conversationId, filter) {
1743
+ const { data } = await getApiClient().get(
1744
+ `/conversations/${conversationId}/participants`,
1745
+ filter ? { params: { filter } } : void 0
1746
+ );
1747
+ return (data ?? []).map(normalizeParticipant);
1748
+ },
1749
+ /**
1750
+ * Get unread message count for a single conversation.
1751
+ * Use this after app foreground or socket reconnect to refresh a specific count.
1752
+ */
1753
+ async getUnreadCount(conversationId) {
1754
+ const { data } = await getApiClient().get(
1755
+ `/conversations/${conversationId}/unread`
1756
+ );
1757
+ return data;
1758
+ },
1759
+ /**
1760
+ * Get total unread count across all conversations + per-conversation breakdown.
1761
+ * Use on app cold start, foreground resume, or after socket reconnect.
1762
+ * The socket keeps counts live while connected — this is the source of truth
1763
+ * when the socket was down.
1764
+ */
1765
+ async getUnreadSummary() {
1766
+ const { data } = await getApiClient().get("/conversations/unread");
1767
+ return data;
1768
+ },
1769
+ /**
1770
+ * Set the group icon from an already-uploaded file (admin only).
1771
+ * The fileId comes from uploadBatch() / client.uploadFiles() — same as attachments.
1772
+ * Server copies storageKey into conversation.iconMeta and deletes the chat_files record.
1773
+ */
1774
+ async uploadIcon(conversationId, fileId) {
1775
+ const { data } = await getApiClient().post(
1776
+ `/conversations/${conversationId}/icon`,
1777
+ { fileId }
1778
+ );
1779
+ return normalizeConversation(data);
1780
+ },
1781
+ async removeIcon(conversationId) {
1782
+ const { data } = await getApiClient().post(`/conversations/${conversationId}/icon/remove`);
1783
+ return normalizeConversation(data);
1784
+ },
1785
+ async clearChat(conversationId) {
1786
+ await getApiClient().post(`/conversations/${conversationId}/clear-for-me`);
1898
1787
  }
1899
- if (!socket) return Promise.reject(new AntzChatNetworkError(`Socket not connected (event: ${event})`, "SOCKET_NOT_CONNECTED", { event }));
1900
- return new Promise((resolve, reject) => {
1901
- let timer;
1902
- secureEmit(socket, event, payload, (response) => {
1903
- clearTimeout(timer);
1904
- resolve(response);
1905
- }).then(() => {
1906
- timer = setTimeout(() => reject(new AntzChatNetworkError(`Socket ack timeout: ${event}`, "SOCKET_TIMEOUT", { event })), ACK_TIMEOUT);
1907
- }).catch(reject);
1908
- });
1909
- }
1910
- function fireAndForget(event, payload) {
1911
- const socket = tryGetSocket();
1912
- if (!socket) return;
1913
- secureEmit(socket, event, payload);
1914
- }
1915
- var socketEmit = {
1916
- joinRoom(conversationId) {
1917
- fireAndForget("join_room", { conversationId });
1788
+ };
1789
+
1790
+ // src/api/storage.ts
1791
+ var storageApi = {
1792
+ async requestPresignedUrl(payload) {
1793
+ const { data } = await getApiClient().post("/storage/presigned-url", payload);
1794
+ return data;
1918
1795
  },
1919
- leaveRoom(conversationId) {
1920
- fireAndForget("leave_room", { conversationId });
1796
+ async requestPresignedUrlBatch(files) {
1797
+ const { data } = await getApiClient().post("/storage/presigned-url/batch", { files });
1798
+ return data;
1921
1799
  },
1922
- sendMessage(payload) {
1923
- return queueSendMessage({ ...payload, sentAt: Date.now() });
1800
+ async confirmUpload(fileId) {
1801
+ const { data } = await getApiClient().post(`/storage/confirm/${fileId}`);
1802
+ return data;
1924
1803
  },
1925
- updateMessage(messageId, text) {
1926
- return withAck("update_message", { messageId, text });
1804
+ async getFile(fileId) {
1805
+ const { data } = await getApiClient().get(`/storage/files/${fileId}`);
1806
+ return data;
1927
1807
  },
1928
- deleteMessage(messageId) {
1929
- return withAck("delete_message", { messageId });
1808
+ async getFileUrl(fileId, expiresIn) {
1809
+ const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {
1810
+ params: expiresIn ? { expiresIn } : {}
1811
+ });
1812
+ return data;
1930
1813
  },
1931
- deleteMessageForMe(messageId) {
1932
- return withAck("delete_message_for_me", { messageId });
1814
+ async deleteFile(fileId) {
1815
+ await getApiClient().post(`/storage/files/${fileId}/delete`);
1933
1816
  },
1934
- clearChat(conversationId) {
1935
- return withAck("clear_chat_for_me", { conversationId });
1817
+ async completeMultipartUpload(fileId, uploadId, parts) {
1818
+ const { data } = await getApiClient().post(
1819
+ `/storage/multipart/complete/${fileId}`,
1820
+ { uploadId, parts }
1821
+ );
1822
+ return data;
1936
1823
  },
1937
- addReaction(messageId, emoji) {
1938
- return withAck("add_reaction", { messageId, emoji });
1824
+ async getConversationFiles(conversationId, params = {}) {
1825
+ const { data } = await getApiClient().get(
1826
+ `/storage/conversations/${conversationId}/files`,
1827
+ { params }
1828
+ );
1829
+ return data;
1939
1830
  },
1940
- removeReaction(messageId, emoji) {
1941
- return withAck("remove_reaction", { messageId, emoji });
1831
+ async getMyFiles(params = {}) {
1832
+ const { data } = await getApiClient().get("/storage/my-files", { params });
1833
+ return data;
1834
+ }
1835
+ };
1836
+ async function runMultipartUpload(presigned, file, platformUploadPartFn, onProgress) {
1837
+ const { multipart } = presigned;
1838
+ if (!multipart) throw new Error("No multipart info on presigned response");
1839
+ const CONCURRENCY = 3;
1840
+ const completedParts = [];
1841
+ const partProgress = {};
1842
+ multipart.partUrls.forEach(({ partNumber }) => {
1843
+ partProgress[partNumber] = 0;
1844
+ });
1845
+ const reportProgress = () => {
1846
+ if (!onProgress) return;
1847
+ const vals = Object.values(partProgress);
1848
+ const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
1849
+ onProgress(Math.round(avg * 0.95));
1850
+ };
1851
+ const uploadPart = async (partNumber, uploadUrl, method) => {
1852
+ const offset = (partNumber - 1) * multipart.chunkSize;
1853
+ const end = Math.min(offset + multipart.chunkSize, file.size);
1854
+ const blob = await fetch(file.uri).then((r) => r.blob());
1855
+ const slice = blob.slice(offset, end);
1856
+ const etag = await platformUploadPartFn(uploadUrl, slice, (pct) => {
1857
+ partProgress[partNumber] = pct;
1858
+ reportProgress();
1859
+ }, method);
1860
+ completedParts.push({ partNumber, etag });
1861
+ partProgress[partNumber] = 100;
1862
+ reportProgress();
1863
+ };
1864
+ for (let i = 0; i < multipart.partUrls.length; i += CONCURRENCY) {
1865
+ const batch = multipart.partUrls.slice(i, i + CONCURRENCY);
1866
+ const results = await Promise.allSettled(
1867
+ batch.map(({ partNumber, uploadUrl, method }) => uploadPart(partNumber, uploadUrl, method ?? "PUT"))
1868
+ );
1869
+ const failed = results.find((r) => r.status === "rejected");
1870
+ if (failed) throw failed.reason;
1871
+ }
1872
+ completedParts.sort((a, b) => a.partNumber - b.partNumber);
1873
+ const fileResponse = await storageApi.completeMultipartUpload(
1874
+ presigned.fileId,
1875
+ multipart.uploadId,
1876
+ completedParts
1877
+ );
1878
+ onProgress?.(100);
1879
+ return fileResponse;
1880
+ }
1881
+ async function runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
1882
+ const compressedFiles = await Promise.all(
1883
+ files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
1884
+ );
1885
+ const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i], clientIndex: i }));
1886
+ const requests = slotted.map(({ file: f, clientIndex }) => ({
1887
+ filename: f.name,
1888
+ mimeType: f.type,
1889
+ size: f.size,
1890
+ conversationId,
1891
+ clientIndex,
1892
+ ...f.compressed && {
1893
+ metadata: {
1894
+ compressed: f.compressed,
1895
+ originalSize: f.originalSize,
1896
+ compressionAlgorithm: f.compressionAlgorithm
1897
+ }
1898
+ }
1899
+ }));
1900
+ const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
1901
+ const failedSlotIds = /* @__PURE__ */ new Set();
1902
+ const failed = requestErrors.map((e) => {
1903
+ const idx = e.clientIndex ?? slotted.findIndex((s) => s.file.name === e.filename);
1904
+ const slotId = slotted[idx]?.slotId;
1905
+ if (slotId) failedSlotIds.add(slotId);
1906
+ return { filename: e.filename, error: e.error };
1907
+ });
1908
+ const progressMap = {};
1909
+ const reportProgress = () => {
1910
+ if (!onProgress) return;
1911
+ const vals = Object.values(progressMap);
1912
+ const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
1913
+ onProgress(Math.round(avg));
1914
+ };
1915
+ const successful = [];
1916
+ const slotToFile = /* @__PURE__ */ new Map();
1917
+ await Promise.all(
1918
+ urls.map(async (presigned, idx) => {
1919
+ const originalIdx = presigned.clientIndex ?? idx;
1920
+ const { file, slotId } = slotted[originalIdx];
1921
+ progressMap[originalIdx] = 0;
1922
+ try {
1923
+ let fileResponse;
1924
+ if (presigned.multipart && platformUploadPartFn) {
1925
+ fileResponse = await runMultipartUpload(presigned, file, platformUploadPartFn, (pct) => {
1926
+ progressMap[originalIdx] = pct;
1927
+ reportProgress();
1928
+ });
1929
+ } else {
1930
+ await platformUploadFn(presigned, file, (pct) => {
1931
+ progressMap[originalIdx] = Math.round(pct * 0.9);
1932
+ reportProgress();
1933
+ });
1934
+ fileResponse = await storageApi.confirmUpload(presigned.fileId);
1935
+ }
1936
+ progressMap[originalIdx] = 100;
1937
+ reportProgress();
1938
+ successful.push(fileResponse);
1939
+ slotToFile.set(slotId, fileResponse);
1940
+ } catch (err) {
1941
+ failed.push({ filename: file.name, error: err.message });
1942
+ }
1943
+ })
1944
+ );
1945
+ return { result: { successful, failed }, slotToFile };
1946
+ }
1947
+ async function uploadBatch(files, platformUploadFn, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn) {
1948
+ const slotIds = files.map(() => generateUUID());
1949
+ const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig, platformUploadPartFn);
1950
+ return result;
1951
+ }
1952
+
1953
+ // src/api/devices.ts
1954
+ var devicesApi = {
1955
+ /**
1956
+ * Register or update a device push token with the chat server.
1957
+ *
1958
+ * Upserts by `deviceId` — calling this multiple times with the same deviceId
1959
+ * simply refreshes the token value (tokens can rotate silently on some platforms).
1960
+ *
1961
+ * The SDK never calls this automatically. The parent app or the `tokenProvider`
1962
+ * option in `pushNotifications` config is responsible for calling it after
1963
+ * obtaining the token from the OS / browser.
1964
+ */
1965
+ async register(payload) {
1966
+ await getApiClient().post("/users/me/devices", payload);
1942
1967
  },
1943
- pinMessage(messageId) {
1944
- return withAck("pin_message", { messageId });
1968
+ /**
1969
+ * Remove a device token from the chat server.
1970
+ * Call this on logout so the user stops receiving push notifications on this device.
1971
+ */
1972
+ async remove(deviceId) {
1973
+ await getApiClient().post(`/users/me/devices/${deviceId}/remove`);
1974
+ }
1975
+ };
1976
+
1977
+ // src/api/users.ts
1978
+ var usersApi = {
1979
+ async list(params = {}) {
1980
+ const { data } = await getApiClient().get("/users", { params });
1981
+ return data;
1945
1982
  },
1946
- unpinMessage(messageId) {
1947
- return withAck("unpin_message", { messageId });
1983
+ async getById(userId) {
1984
+ const { data } = await getApiClient().get(`/users/${userId}`);
1985
+ return data;
1948
1986
  },
1949
- // markRead and typing are best-effort — silently dropped if socket not ready
1950
- typing(conversationId, isTyping) {
1951
- fireAndForget("typing", { conversationId, isTyping });
1987
+ async getLastSeen(userId) {
1988
+ const { data } = await getApiClient().get(`/users/${userId}`);
1989
+ return { lastSeenAt: data.lastSeenAt ?? null };
1952
1990
  },
1953
- markRead(conversationId, messageId) {
1954
- fireAndForget("mark_read", { conversationId, ...messageId ? { messageId } : {} });
1991
+ /**
1992
+ * Update basic profile fields for the current user.
1993
+ * Works in both builtin and non-builtin modes. Use this to push an immediate
1994
+ * profile update to the chat server when the host app knows a change just
1995
+ * happened — without waiting for the next 2-hour sync cycle.
1996
+ */
1997
+ async updateProfile(payload) {
1998
+ const { data } = await getApiClient().post("/users/me/update", payload);
1999
+ return data;
1955
2000
  },
1956
- getOnlineUsers(userIds) {
1957
- const socket = tryGetSocket();
1958
- if (!socket) return Promise.resolve([]);
1959
- return new Promise((resolve, reject) => {
1960
- let timer;
1961
- secureEmit(socket, "get_online_users", { userIds }, (response) => {
1962
- clearTimeout(timer);
1963
- if (response && typeof response === "object" && "onlineStatus" in response) {
1964
- const status = response.onlineStatus;
1965
- resolve(Object.entries(status).filter(([, v]) => v).map(([k]) => k));
1966
- } else if (Array.isArray(response)) {
1967
- resolve(response);
1968
- } else {
1969
- resolve([]);
1970
- }
1971
- }).then(() => {
1972
- timer = setTimeout(() => reject(new AntzChatNetworkError("Socket ack timeout: get_online_users", "SOCKET_TIMEOUT", { event: "get_online_users" })), ACK_TIMEOUT);
1973
- }).catch(reject);
1974
- });
2001
+ /**
2002
+ * Update notification preferences for the current user.
2003
+ * Partial update — only send fields you want to change.
2004
+ * A prefs record is automatically created with defaults when a device
2005
+ * token is first registered, so this never fails with "not found".
2006
+ */
2007
+ async updatePreferences(prefs) {
2008
+ const { data } = await getApiClient().post("/users/me/preferences", prefs);
2009
+ return data;
1975
2010
  },
1976
- getTypingUsers(conversationId) {
1977
- return withAck("get_typing_users", { conversationId });
2011
+ /**
2012
+ * Fetch current notification preferences for the current user.
2013
+ * Returns null if no prefs record exists yet (all defaults apply).
2014
+ */
2015
+ async getPreferences() {
2016
+ try {
2017
+ const { data } = await getApiClient().get("/users/me/preferences");
2018
+ return data;
2019
+ } catch {
2020
+ return null;
2021
+ }
1978
2022
  }
1979
2023
  };
1980
2024
 
@@ -2113,6 +2157,7 @@ var AntzChatClient = class {
2113
2157
  AntzChatPermissionError,
2114
2158
  AntzChatServerError,
2115
2159
  AntzChatValidationError,
2160
+ HIGHLY_FORWARDED_DEPTH_THRESHOLD,
2116
2161
  MAX_FORWARD_TARGETS,
2117
2162
  MENTION_ALL_ID,
2118
2163
  appConfigApi,