@droppii-org/chat-sdk 0.7.6 → 0.7.8

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.
Files changed (109) hide show
  1. package/dist/components/conversation/ThreadCard.d.ts +2 -1
  2. package/dist/components/conversation/ThreadCard.d.ts.map +1 -1
  3. package/dist/components/conversation/ThreadCard.js +2 -2
  4. package/dist/components/conversation-detail/ConversationDetailHeader.d.ts +5 -1
  5. package/dist/components/conversation-detail/ConversationDetailHeader.d.ts.map +1 -1
  6. package/dist/components/conversation-detail/ConversationDetailHeader.js +3 -3
  7. package/dist/components/conversation-detail/ConversationMoreOptionPanel.d.ts.map +1 -1
  8. package/dist/components/conversation-detail/ConversationMoreOptionPanel.js +3 -1
  9. package/dist/components/conversation-detail/ThreadProfilePanel.d.ts +5 -1
  10. package/dist/components/conversation-detail/ThreadProfilePanel.d.ts.map +1 -1
  11. package/dist/components/conversation-detail/ThreadProfilePanel.js +13 -3
  12. package/dist/components/conversation-detail/profile/PinThreadLimitModal.d.ts +7 -0
  13. package/dist/components/conversation-detail/profile/PinThreadLimitModal.d.ts.map +1 -0
  14. package/dist/components/conversation-detail/profile/PinThreadLimitModal.js +14 -0
  15. package/dist/components/conversation-detail/profile/ThreadProfileActionRows.d.ts +15 -0
  16. package/dist/components/conversation-detail/profile/ThreadProfileActionRows.d.ts.map +1 -0
  17. package/dist/components/conversation-detail/profile/ThreadProfileActionRows.js +20 -0
  18. package/dist/components/conversation-detail/profile/ThreadProfileQuickActions.d.ts +11 -0
  19. package/dist/components/conversation-detail/profile/ThreadProfileQuickActions.d.ts.map +1 -0
  20. package/dist/components/conversation-detail/profile/ThreadProfileQuickActions.js +36 -0
  21. package/dist/components/conversation-detail/thread-search/ThreadSearchContent.d.ts +13 -0
  22. package/dist/components/conversation-detail/thread-search/ThreadSearchContent.d.ts.map +1 -0
  23. package/dist/components/conversation-detail/thread-search/ThreadSearchContent.js +70 -0
  24. package/dist/components/conversation-detail/thread-search/ThreadSearchPanel.d.ts +11 -0
  25. package/dist/components/conversation-detail/thread-search/ThreadSearchPanel.d.ts.map +1 -0
  26. package/dist/components/conversation-detail/thread-search/ThreadSearchPanel.js +12 -0
  27. package/dist/components/conversation-detail/thread-search/ThreadSearchResultItem.d.ts +11 -0
  28. package/dist/components/conversation-detail/thread-search/ThreadSearchResultItem.d.ts.map +1 -0
  29. package/dist/components/conversation-detail/thread-search/ThreadSearchResultItem.js +24 -0
  30. package/dist/components/conversation-detail/thread-search/utils.d.ts +8 -0
  31. package/dist/components/conversation-detail/thread-search/utils.d.ts.map +1 -0
  32. package/dist/components/conversation-detail/thread-search/utils.js +17 -0
  33. package/dist/components/conversation-inbox/ConversationInboxItem.d.ts +3 -1
  34. package/dist/components/conversation-inbox/ConversationInboxItem.d.ts.map +1 -1
  35. package/dist/components/conversation-inbox/ConversationInboxItem.js +4 -6
  36. package/dist/components/conversation-inbox/ConversationInboxList.d.ts +3 -1
  37. package/dist/components/conversation-inbox/ConversationInboxList.d.ts.map +1 -1
  38. package/dist/components/conversation-inbox/ConversationInboxList.js +2 -2
  39. package/dist/components/conversation-inbox/ConversationThreadActions.d.ts +12 -0
  40. package/dist/components/conversation-inbox/ConversationThreadActions.d.ts.map +1 -0
  41. package/dist/components/conversation-inbox/ConversationThreadActions.js +58 -0
  42. package/dist/components/conversation-inbox/contact-search/ContactSearchCommunitySection.d.ts +10 -0
  43. package/dist/components/conversation-inbox/contact-search/ContactSearchCommunitySection.d.ts.map +1 -0
  44. package/dist/components/conversation-inbox/contact-search/ContactSearchCommunitySection.js +15 -0
  45. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts.map +1 -1
  46. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.js +39 -28
  47. package/dist/components/conversation-inbox/contact-search/constants.js +2 -2
  48. package/dist/components/conversation-inbox/index.d.ts +2 -2
  49. package/dist/components/conversation-inbox/index.d.ts.map +1 -1
  50. package/dist/components/conversation-inbox/index.js +1 -1
  51. package/dist/components/group/MemberPermissionPanel.d.ts.map +1 -1
  52. package/dist/components/group/MemberPermissionPanel.js +12 -23
  53. package/dist/components/message/toolbox/MessageToolboxMenu.d.ts +7 -0
  54. package/dist/components/message/toolbox/MessageToolboxMenu.d.ts.map +1 -1
  55. package/dist/components/message/toolbox/MessageToolboxMenu.js +1 -1
  56. package/dist/constants/moreOption.d.ts +1 -0
  57. package/dist/constants/moreOption.d.ts.map +1 -1
  58. package/dist/hooks/conversation/useBotFlow.d.ts.map +1 -1
  59. package/dist/hooks/conversation/useBotFlow.js +42 -23
  60. package/dist/hooks/conversation/useConversationTarget.d.ts +4 -0
  61. package/dist/hooks/conversation/useConversationTarget.d.ts.map +1 -0
  62. package/dist/hooks/conversation/useConversationTarget.js +23 -0
  63. package/dist/hooks/conversation/usePinConversation.d.ts +10 -0
  64. package/dist/hooks/conversation/usePinConversation.d.ts.map +1 -0
  65. package/dist/hooks/conversation/usePinConversation.js +52 -0
  66. package/dist/hooks/message/holdMessageHighlight.d.ts +6 -0
  67. package/dist/hooks/message/holdMessageHighlight.d.ts.map +1 -0
  68. package/dist/hooks/message/holdMessageHighlight.js +22 -0
  69. package/dist/hooks/message/useMessageListScroll.d.ts.map +1 -1
  70. package/dist/hooks/message/useMessageListScroll.js +13 -0
  71. package/dist/hooks/message/useSendMessage.d.ts +1 -1
  72. package/dist/hooks/message/useSendMessage.d.ts.map +1 -1
  73. package/dist/hooks/message/useSendMessage.js +358 -285
  74. package/dist/hooks/search/useSearchPublicGroups.d.ts +1 -0
  75. package/dist/hooks/search/useSearchPublicGroups.d.ts.map +1 -1
  76. package/dist/hooks/search/useSearchPublicGroups.js +6 -0
  77. package/dist/hooks/search/useSearchThreadMessages.d.ts +222 -0
  78. package/dist/hooks/search/useSearchThreadMessages.d.ts.map +1 -0
  79. package/dist/hooks/search/useSearchThreadMessages.js +60 -0
  80. package/dist/locales/vi/conversation-inbox.json +31 -0
  81. package/dist/screens/conversation-detail/index.d.ts +5 -1
  82. package/dist/screens/conversation-detail/index.d.ts.map +1 -1
  83. package/dist/screens/conversation-detail/index.js +17 -2
  84. package/dist/screens/conversation-inbox/index.d.ts +6 -0
  85. package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
  86. package/dist/screens/conversation-inbox/index.js +3 -3
  87. package/dist/services/query.d.ts +3 -0
  88. package/dist/services/query.d.ts.map +1 -1
  89. package/dist/services/query.js +3 -0
  90. package/dist/services/routes.d.ts +2 -0
  91. package/dist/services/routes.d.ts.map +1 -1
  92. package/dist/services/routes.js +2 -0
  93. package/dist/store/conversation.d.ts.map +1 -1
  94. package/dist/store/conversation.js +10 -6
  95. package/dist/store/type.d.ts +3 -0
  96. package/dist/store/type.d.ts.map +1 -1
  97. package/dist/styles/global.css +1 -1
  98. package/dist/types/conversation.d.ts +8 -0
  99. package/dist/types/conversation.d.ts.map +1 -1
  100. package/dist/utils/common.d.ts +7 -0
  101. package/dist/utils/common.d.ts.map +1 -1
  102. package/dist/utils/common.js +42 -13
  103. package/dist/utils/sendQueue.d.ts +2 -0
  104. package/dist/utils/sendQueue.d.ts.map +1 -0
  105. package/dist/utils/sendQueue.js +14 -0
  106. package/package.json +1 -1
  107. package/dist/components/conversation-inbox/ConversationMuteSwipeAction.d.ts +0 -8
  108. package/dist/components/conversation-inbox/ConversationMuteSwipeAction.d.ts.map +0 -1
  109. package/dist/components/conversation-inbox/ConversationMuteSwipeAction.js +0 -31
@@ -18,6 +18,7 @@ import { updateSession } from "../../hooks/session/useUpdateSession";
18
18
  import { invalidateSessionQueries } from "../../utils/queryHelpers";
19
19
  import { resolveConversationBotId } from "../../utils/conversation";
20
20
  import { QUERY_KEYS } from "../../services/query";
21
+ import { enqueue } from "../../utils/sendQueue";
21
22
  const messageFileStore = new Map();
22
23
  const MEDIA_CONTENT_TYPES = new Set([
23
24
  MessageType.PictureMessage,
@@ -149,6 +150,120 @@ export const createQuoteMessage = async (text, quotedMessage, urls) => {
149
150
  });
150
151
  return textMessage;
151
152
  };
153
+ // Rebuilds a failed message under a new clientMsgID, re-attaching its stored files.
154
+ const recreateFailedMessage = async (failedMessage) => {
155
+ var _a, _b;
156
+ const storedFiles = messageFileStore.get(failedMessage.clientMsgID);
157
+ if (!storedFiles) {
158
+ const textContent = ((_a = failedMessage.textElem) === null || _a === void 0 ? void 0 : _a.content) || failedMessage.content || "";
159
+ if (failedMessage.contentType === MessageType.CustomMessage) {
160
+ return failedMessage.customElem
161
+ ? createCustomMessageByParams({
162
+ data: failedMessage.customElem.data,
163
+ extension: failedMessage.customElem.extension,
164
+ description: failedMessage.customElem.description,
165
+ })
166
+ : null;
167
+ }
168
+ if (failedMessage.contentType === MessageType.UrlTextMessage &&
169
+ failedMessage.urlTextElem) {
170
+ return createUrlTextMessage(textContent, failedMessage.urlTextElem.urls);
171
+ }
172
+ if (failedMessage.contentType === MessageType.StickerMessage) {
173
+ return ((_b = failedMessage.stickerElem) === null || _b === void 0 ? void 0 : _b.content)
174
+ ? createStickerMessage(failedMessage.stickerElem.content)
175
+ : null;
176
+ }
177
+ return createTextMessage(textContent);
178
+ }
179
+ let newPayload = null;
180
+ if (storedFiles.type === "image" && failedMessage.pictureElem) {
181
+ const { sourcePicture, bigPicture, snapshotPicture, sourcePath } = failedMessage.pictureElem;
182
+ newPayload = await createImageMessageByFile({
183
+ sourcePicture,
184
+ bigPicture,
185
+ snapshotPicture,
186
+ sourcePath,
187
+ file: storedFiles.file,
188
+ });
189
+ }
190
+ else if (storedFiles.type === "video" && failedMessage.videoElem) {
191
+ const elem = failedMessage.videoElem;
192
+ newPayload = await createVideoMessageByFile({
193
+ videoPath: elem.videoPath,
194
+ duration: elem.duration,
195
+ videoType: elem.videoType,
196
+ snapshotPath: elem.snapshotPath,
197
+ videoUUID: elem.videoUUID,
198
+ videoUrl: elem.videoUrl,
199
+ videoSize: elem.videoSize,
200
+ snapshotUUID: elem.snapshotUUID,
201
+ snapshotSize: elem.snapshotSize,
202
+ snapshotUrl: elem.snapshotUrl,
203
+ snapshotWidth: elem.snapshotWidth,
204
+ snapshotHeight: elem.snapshotHeight,
205
+ videoFile: storedFiles.videoFile,
206
+ snapshotFile: storedFiles.snapshotFile,
207
+ });
208
+ }
209
+ else if (storedFiles.type === "file" && failedMessage.fileElem) {
210
+ const elem = failedMessage.fileElem;
211
+ newPayload = await createFileMessageByFile({
212
+ filePath: elem.filePath,
213
+ fileName: elem.fileName,
214
+ uuid: elem.uuid,
215
+ sourceUrl: elem.sourceUrl,
216
+ fileSize: elem.fileSize,
217
+ file: storedFiles.file,
218
+ });
219
+ }
220
+ if (newPayload) {
221
+ messageFileStore.delete(failedMessage.clientMsgID);
222
+ messageFileStore.set(newPayload.clientMsgID, storedFiles);
223
+ }
224
+ return newPayload;
225
+ };
226
+ const SEND_HOLD_BASE_MS = 60000;
227
+ // About a 2 Mbps upload.
228
+ const SEND_HOLD_MIN_BYTES_PER_MS = 250;
229
+ const storedFileBytes = (clientMsgID) => {
230
+ const stored = messageFileStore.get(clientMsgID);
231
+ if (!stored)
232
+ return 0;
233
+ return stored.type === "video"
234
+ ? stored.videoFile.size + stored.snapshotFile.size
235
+ : stored.file.size;
236
+ };
237
+ // How long a send may hold back the next queued message. Past this the next one goes
238
+ // anyway, so a stalled upload cannot block the thread for good.
239
+ const sendHoldLimitMs = (message) => {
240
+ var _a, _b;
241
+ const items = (_b = (_a = message.mergeElem) === null || _a === void 0 ? void 0 : _a.multiMessage) !== null && _b !== void 0 ? _b : [message];
242
+ const bytes = items.reduce((sum, item) => sum + storedFileBytes(item.clientMsgID), 0);
243
+ return SEND_HOLD_BASE_MS + bytes / SEND_HOLD_MIN_BYTES_PER_MS;
244
+ };
245
+ const settleOrTimeout = (promise, ms) => new Promise((resolve) => {
246
+ const timer = setTimeout(resolve, ms);
247
+ void promise.finally(() => {
248
+ clearTimeout(timer);
249
+ resolve();
250
+ });
251
+ });
252
+ let unsentCount = 0;
253
+ const warnUnsent = (event) => {
254
+ event.preventDefault();
255
+ event.returnValue = "";
256
+ };
257
+ // Queued messages live only in memory until the SDK takes them, so a reload would drop them.
258
+ const trackUnsent = (sending) => {
259
+ if (unsentCount++ === 0)
260
+ window.addEventListener("beforeunload", warnUnsent);
261
+ return sending.finally(() => {
262
+ if (--unsentCount === 0) {
263
+ window.removeEventListener("beforeunload", warnUnsent);
264
+ }
265
+ });
266
+ };
152
267
  export const useSendMessage = (options) => {
153
268
  var _a;
154
269
  const { t } = useTranslation("common");
@@ -185,7 +300,7 @@ export const useSendMessage = (options) => {
185
300
  console.error("syncSessionAfterPublicMessage", error);
186
301
  }
187
302
  }, [queryClient]);
188
- const dispatchMessage = useCallback(async (message, currentSession) => {
303
+ const dispatchMessage = useCallback(async (message) => {
189
304
  var _a, _b, _c, _d, _e, _f;
190
305
  const plainText = ((_a = message === null || message === void 0 ? void 0 : message.textElem) === null || _a === void 0 ? void 0 : _a.content) ||
191
306
  ((_b = message === null || message === void 0 ? void 0 : message.quoteElem) === null || _b === void 0 ? void 0 : _b.text) ||
@@ -225,14 +340,10 @@ export const useSendMessage = (options) => {
225
340
  },
226
341
  });
227
342
  updateOneMessage(Object.assign(Object.assign({}, successMessage), { ex: successMessage.ex || message.ex || "{}", isInternal: (_f = successMessage.isInternal) !== null && _f !== void 0 ? _f : message.isInternal }));
228
- if (!message.isInternal) {
229
- await syncSessionAfterPublicMessage(currentSession);
230
- }
231
343
  messageFileStore.delete(message.clientMsgID);
232
344
  }, [
233
345
  groupID,
234
346
  recvID,
235
- syncSessionAfterPublicMessage,
236
347
  t,
237
348
  conversationData === null || conversationData === void 0 ? void 0 : conversationData.showName,
238
349
  conversationData === null || conversationData === void 0 ? void 0 : conversationData.faceURL,
@@ -240,334 +351,290 @@ export const useSendMessage = (options) => {
240
351
  conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationType,
241
352
  userId,
242
353
  ]);
243
- const sendMessage = useCallback(async (message, currentSession) => {
244
- pushNewMessage(message);
245
- emit(scrollEvent);
246
- try {
247
- await dispatchMessage(message, currentSession);
248
- return true;
249
- }
250
- catch (error) {
251
- console.error("sendMessage", error);
252
- updateOneMessage(Object.assign(Object.assign({}, message), { status: MessageStatus.Failed }));
253
- return false;
254
- }
255
- }, [dispatchMessage, scrollEvent]);
256
- const resendMessage = useCallback(async (failedMessage) => {
257
- var _a, _b;
258
- const storedFiles = messageFileStore.get(failedMessage.clientMsgID);
259
- if (!storedFiles) {
260
- let newTextPayload = null;
261
- const textContent = ((_a = failedMessage.textElem) === null || _a === void 0 ? void 0 : _a.content) || failedMessage.content || "";
262
- if (failedMessage.contentType === MessageType.CustomMessage) {
263
- newTextPayload = failedMessage.customElem
264
- ? await createCustomMessageByParams({
265
- data: failedMessage.customElem.data,
266
- extension: failedMessage.customElem.extension,
267
- description: failedMessage.customElem.description,
268
- })
269
- : null;
270
- }
271
- else if (failedMessage.contentType === MessageType.UrlTextMessage &&
272
- failedMessage.urlTextElem) {
273
- newTextPayload = await createUrlTextMessage(textContent, failedMessage.urlTextElem.urls);
274
- }
275
- else if (failedMessage.contentType === MessageType.StickerMessage) {
276
- newTextPayload = ((_b = failedMessage.stickerElem) === null || _b === void 0 ? void 0 : _b.content)
277
- ? await createStickerMessage(failedMessage.stickerElem.content)
278
- : null;
279
- }
280
- else {
281
- newTextPayload = await createTextMessage(textContent);
282
- }
283
- if (!newTextPayload) {
284
- updateOneMessage(Object.assign(Object.assign({}, failedMessage), { status: MessageStatus.Failed }));
285
- return;
286
- }
287
- const conversationID = (conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID) || "";
288
- await DChatSDK.deleteMessageFromLocalStorage({
289
- conversationID,
290
- clientMsgID: failedMessage.clientMsgID,
291
- });
292
- removeOneMessage(failedMessage.clientMsgID);
293
- await sendMessage(Object.assign(Object.assign({}, newTextPayload), { ex: failedMessage.ex, isInternal: failedMessage.isInternal }));
294
- return;
295
- }
296
- let newPayload = null;
297
- if (storedFiles.type === "image" && failedMessage.pictureElem) {
298
- const { sourcePicture, bigPicture, snapshotPicture, sourcePath } = failedMessage.pictureElem;
299
- newPayload = await createImageMessageByFile({
300
- sourcePicture,
301
- bigPicture,
302
- snapshotPicture,
303
- sourcePath,
304
- file: storedFiles.file,
305
- });
306
- }
307
- else if (storedFiles.type === "video" && failedMessage.videoElem) {
308
- const elem = failedMessage.videoElem;
309
- newPayload = await createVideoMessageByFile({
310
- videoPath: elem.videoPath,
311
- duration: elem.duration,
312
- videoType: elem.videoType,
313
- snapshotPath: elem.snapshotPath,
314
- videoUUID: elem.videoUUID,
315
- videoUrl: elem.videoUrl,
316
- videoSize: elem.videoSize,
317
- snapshotUUID: elem.snapshotUUID,
318
- snapshotSize: elem.snapshotSize,
319
- snapshotUrl: elem.snapshotUrl,
320
- snapshotWidth: elem.snapshotWidth,
321
- snapshotHeight: elem.snapshotHeight,
322
- videoFile: storedFiles.videoFile,
323
- snapshotFile: storedFiles.snapshotFile,
324
- });
325
- }
326
- else if (storedFiles.type === "file" && failedMessage.fileElem) {
327
- const elem = failedMessage.fileElem;
328
- newPayload = await createFileMessageByFile({
329
- filePath: elem.filePath,
330
- fileName: elem.fileName,
331
- uuid: elem.uuid,
332
- sourceUrl: elem.sourceUrl,
333
- fileSize: elem.fileSize,
334
- file: storedFiles.file,
354
+ const sendQueueKey = (conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID) || "";
355
+ // Bubbles show in call order and reach the server one at a time in that same order,
356
+ // so a text sent right after a large video cannot overtake the video's upload.
357
+ const queueSend = useCallback((build, currentSession) => {
358
+ const shown = enqueue(`show:${sendQueueKey}`, async () => {
359
+ const messages = await build();
360
+ if (!(messages === null || messages === void 0 ? void 0 : messages.length))
361
+ return null;
362
+ messages.forEach((message) => pushNewMessage(message));
363
+ emit(scrollEvent);
364
+ return messages;
365
+ });
366
+ const outcomes = [];
367
+ let isSessionSynced = false;
368
+ const queued = enqueue(`send:${sendQueueKey}`, async () => {
369
+ const messages = await shown.catch((error) => {
370
+ console.error("sendMessage", error);
371
+ return null;
335
372
  });
336
- }
373
+ for (const message of messages !== null && messages !== void 0 ? messages : []) {
374
+ const holdLimitMs = sendHoldLimitMs(message);
375
+ const outcome = dispatchMessage(message).then(() => {
376
+ if (!message.isInternal && !isSessionSynced) {
377
+ isSessionSynced = true;
378
+ void syncSessionAfterPublicMessage(currentSession);
379
+ }
380
+ return true;
381
+ }, (error) => {
382
+ console.error("sendMessage", error);
383
+ updateOneMessage(Object.assign(Object.assign({}, message), { status: MessageStatus.Failed }));
384
+ return false;
385
+ });
386
+ outcomes.push(outcome);
387
+ await settleOrTimeout(outcome, holdLimitMs);
388
+ }
389
+ });
390
+ return trackUnsent(queued
391
+ .then(() => Promise.all(outcomes))
392
+ .then((results) => results.length > 0 && results.every(Boolean)));
393
+ }, [sendQueueKey, dispatchMessage, scrollEvent, syncSessionAfterPublicMessage]);
394
+ const resendMessage = useCallback((failedMessage) => queueSend(async () => {
395
+ const newPayload = await recreateFailedMessage(failedMessage);
337
396
  if (!newPayload) {
338
397
  updateOneMessage(Object.assign(Object.assign({}, failedMessage), { status: MessageStatus.Failed }));
339
- return;
398
+ return null;
340
399
  }
341
- messageFileStore.delete(failedMessage.clientMsgID);
342
- messageFileStore.set(newPayload.clientMsgID, storedFiles);
343
- const conversationID = (conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID) || "";
344
400
  await DChatSDK.deleteMessageFromLocalStorage({
345
- conversationID,
401
+ conversationID: sendQueueKey,
346
402
  clientMsgID: failedMessage.clientMsgID,
347
403
  });
348
404
  removeOneMessage(failedMessage.clientMsgID);
349
- await sendMessage(Object.assign(Object.assign({}, newPayload), { ex: failedMessage.ex }));
350
- }, [sendMessage]);
405
+ return [
406
+ Object.assign(Object.assign({}, newPayload), { ex: failedMessage.ex, isInternal: failedMessage.isInternal }),
407
+ ];
408
+ }), [queueSend, sendQueueKey]);
351
409
  const sendCustomMessage = useCallback(async (payload, sendOptions) => {
352
410
  if (!conversationData || (!recvID && !groupID))
353
411
  return false;
354
- const customMessage = await createCustomMessage(payload);
355
- if (!customMessage)
356
- return false;
357
412
  const extendMessageInfo = generateExtendMessageInfo({
358
413
  currentSession: sendOptions === null || sendOptions === void 0 ? void 0 : sendOptions.currentSession,
359
414
  });
360
- return sendMessage(Object.assign(Object.assign({}, customMessage), { ex: JSON.stringify(extendMessageInfo) || "{}", isInternal: false }), sendOptions === null || sendOptions === void 0 ? void 0 : sendOptions.currentSession);
361
- }, [conversationData, recvID, groupID, sendMessage]);
415
+ return queueSend(async () => {
416
+ const customMessage = await createCustomMessage(payload);
417
+ if (!customMessage)
418
+ return null;
419
+ return [
420
+ Object.assign(Object.assign({}, customMessage), { ex: JSON.stringify(extendMessageInfo) || "{}", isInternal: false }),
421
+ ];
422
+ }, sendOptions === null || sendOptions === void 0 ? void 0 : sendOptions.currentSession);
423
+ }, [conversationData, recvID, groupID, queueSend]);
362
424
  const sendStickerMessage = useCallback(async (sticker, sendOptions) => {
363
425
  var _a;
364
426
  if (!conversationData || (!recvID && !groupID))
365
427
  return false;
366
- if (!((_a = sticker.file) === null || _a === void 0 ? void 0 : _a.fullUrl))
367
- return false;
368
- const stickerMessage = await createStickerMessage(sticker.file.fullUrl);
369
- if (!stickerMessage)
428
+ const stickerUrl = (_a = sticker.file) === null || _a === void 0 ? void 0 : _a.fullUrl;
429
+ if (!stickerUrl)
370
430
  return false;
371
431
  // BE records "recent stickers" only from ex.stickerId on the saved message.
372
432
  const extendMessageInfo = generateExtendMessageInfo({
373
433
  currentSession: sendOptions === null || sendOptions === void 0 ? void 0 : sendOptions.currentSession,
374
434
  stickerId: sticker.id,
375
435
  });
376
- const isSent = await sendMessage(Object.assign(Object.assign({}, stickerMessage), { ex: JSON.stringify(extendMessageInfo) || "{}", isInternal: false }), sendOptions === null || sendOptions === void 0 ? void 0 : sendOptions.currentSession);
436
+ const isSent = await queueSend(async () => {
437
+ const stickerMessage = await createStickerMessage(stickerUrl);
438
+ if (!stickerMessage)
439
+ return null;
440
+ return [
441
+ Object.assign(Object.assign({}, stickerMessage), { ex: JSON.stringify(extendMessageInfo) || "{}", isInternal: false }),
442
+ ];
443
+ }, sendOptions === null || sendOptions === void 0 ? void 0 : sendOptions.currentSession);
377
444
  if (isSent) {
378
445
  queryClient.invalidateQueries({
379
446
  queryKey: [QUERY_KEYS.GET_STICKER_HISTORY],
380
447
  });
381
448
  }
382
449
  return isSent;
383
- }, [conversationData, recvID, groupID, sendMessage, queryClient]);
450
+ }, [conversationData, recvID, groupID, queueSend, queryClient]);
384
451
  const sendMergeMessage = useCallback(async ({ richText, plainText, files = [], currentSession, isInternal, urlMetadata, flow, customMessage, mentions, }) => {
385
452
  if (!conversationData || (!recvID && !groupID))
386
453
  return false;
387
- const messageList = [];
388
- const imageFileWrappers = files.filter((f) => { var _a; return (_a = f.originFileObj) === null || _a === void 0 ? void 0 : _a.type.startsWith("image/"); });
389
- const videoFileWrappers = files.filter((f) => { var _a; return (_a = f.originFileObj) === null || _a === void 0 ? void 0 : _a.type.startsWith("video/"); });
390
- const documentFileWrappers = files.filter((f) => {
391
- var _a;
392
- return (_a = f.originFileObj) === null || _a === void 0 ? void 0 : _a.type.startsWith("application/");
393
- });
394
- const imageMessages = [];
395
- for (const fileWrapper of imageFileWrappers) {
396
- const file = fileWrapper.originFileObj;
397
- if (!file)
398
- continue;
399
- try {
400
- const picInfo = await createPicBaseInfoFromFile(file);
401
- const baseInfo = {
402
- uuid: uuidv4(),
403
- type: file.type,
404
- size: file.size,
405
- width: picInfo.width,
406
- height: picInfo.height,
407
- url: URL.createObjectURL(file),
408
- };
409
- const imageMessage = await createImageMessageByFile({
410
- sourcePicture: baseInfo,
411
- bigPicture: baseInfo,
412
- snapshotPicture: baseInfo,
413
- sourcePath: "",
414
- file,
415
- });
416
- if (!imageMessage)
417
- continue;
418
- imageMessages.push(imageMessage);
419
- }
420
- catch (err) {
421
- console.error("Lỗi xử lý ảnh:", err);
422
- }
423
- }
424
- if (imageMessages.length === 1) {
425
- messageFileStore.set(imageMessages[0].clientMsgID, {
426
- type: "image",
427
- file: imageFileWrappers[0].originFileObj,
454
+ const hasText = !!plainText && plainText.trim() !== "";
455
+ // Clear the reply bar now rather than when earlier queued sends finish building.
456
+ const quoted = (quotedMessage === null || quotedMessage === void 0 ? void 0 : quotedMessage.clientMsgID) ? quotedMessage : undefined;
457
+ if (hasText && quotedMessage)
458
+ setQuotedMessage(null);
459
+ // Every bubble (image or album → videos → files → text) is built first and shown
460
+ // together, the same order the mobile app uses.
461
+ const buildMessages = async () => {
462
+ const messageList = [];
463
+ const imageFileWrappers = files.filter((f) => { var _a; return (_a = f.originFileObj) === null || _a === void 0 ? void 0 : _a.type.startsWith("image/"); });
464
+ const videoFileWrappers = files.filter((f) => { var _a; return (_a = f.originFileObj) === null || _a === void 0 ? void 0 : _a.type.startsWith("video/"); });
465
+ const documentFileWrappers = files.filter((f) => {
466
+ var _a;
467
+ return (_a = f.originFileObj) === null || _a === void 0 ? void 0 : _a.type.startsWith("application/");
428
468
  });
429
- messageList.push(imageMessages[0]);
430
- }
431
- else if (imageMessages.length > 1) {
432
- imageMessages.forEach((imageMsg, index) => {
433
- messageFileStore.set(imageMsg.clientMsgID, {
434
- type: "image",
435
- file: imageFileWrappers[index].originFileObj,
436
- });
437
- });
438
- const mergeMsg = await createMergerMessage({
439
- messageList: imageMessages,
440
- title: "Album ảnh",
441
- summaryList: imageMessages.map(() => "[Hình ảnh]"),
442
- });
443
- if (mergeMsg)
444
- messageList.push(mergeMsg);
445
- }
446
- for (const fileWrapper of videoFileWrappers) {
447
- const file = fileWrapper.originFileObj;
448
- if (!file)
449
- continue;
450
- try {
451
- const videoBaseInfo = await createVideoInfoWithThumbnail(file);
452
- const thumbFile = new File([videoBaseInfo.thumbnail], file.name + "-thumb.jpg", { type: "image/jpeg" });
453
- const videoMessage = await createVideoMessageByFile({
454
- videoPath: "",
455
- duration: videoBaseInfo.duration,
456
- videoType: file.type,
457
- snapshotPath: "",
458
- videoUUID: uuidv4(),
459
- videoUrl: URL.createObjectURL(file),
460
- videoSize: file.size,
461
- snapshotUUID: "",
462
- snapshotSize: file.size,
463
- snapshotUrl: URL.createObjectURL(thumbFile),
464
- snapshotWidth: videoBaseInfo.width,
465
- snapshotHeight: videoBaseInfo.height,
466
- snapShotType: file.type,
467
- videoFile: file,
468
- snapshotFile: thumbFile,
469
- });
470
- if (!videoMessage)
469
+ const imageMessages = [];
470
+ for (const fileWrapper of imageFileWrappers) {
471
+ const file = fileWrapper.originFileObj;
472
+ if (!file)
471
473
  continue;
472
- messageFileStore.set(videoMessage.clientMsgID, {
473
- type: "video",
474
- videoFile: file,
475
- snapshotFile: thumbFile,
476
- });
477
- messageList.push(videoMessage);
474
+ try {
475
+ const picInfo = await createPicBaseInfoFromFile(file);
476
+ const baseInfo = {
477
+ uuid: uuidv4(),
478
+ type: file.type,
479
+ size: file.size,
480
+ width: picInfo.width,
481
+ height: picInfo.height,
482
+ url: URL.createObjectURL(file),
483
+ };
484
+ const imageMessage = await createImageMessageByFile({
485
+ sourcePicture: baseInfo,
486
+ bigPicture: baseInfo,
487
+ snapshotPicture: baseInfo,
488
+ sourcePath: "",
489
+ file,
490
+ });
491
+ if (!imageMessage)
492
+ continue;
493
+ messageFileStore.set(imageMessage.clientMsgID, {
494
+ type: "image",
495
+ file,
496
+ });
497
+ imageMessages.push(imageMessage);
498
+ }
499
+ catch (err) {
500
+ console.error("Lỗi xử lý ảnh:", err);
501
+ }
478
502
  }
479
- catch (err) {
480
- console.error("Lỗi xử lý video:", err);
503
+ if (imageMessages.length === 1) {
504
+ messageList.push(imageMessages[0]);
481
505
  }
482
- }
483
- for (const fileWrapper of documentFileWrappers) {
484
- const file = fileWrapper.originFileObj;
485
- if (!file)
486
- continue;
487
- try {
488
- const fileParams = {
489
- filePath: "",
490
- fileName: file.name,
491
- uuid: uuidv4(),
492
- sourceUrl: URL.createObjectURL(file),
493
- fileSize: file.size,
494
- fileType: file.type,
495
- file: file,
496
- };
497
- const fileMessage = await createFileMessageByFile(fileParams);
498
- if (!fileMessage)
499
- continue;
500
- messageFileStore.set(fileMessage.clientMsgID, {
501
- type: "file",
502
- file: file,
506
+ else if (imageMessages.length > 1) {
507
+ const mergeMsg = await createMergerMessage({
508
+ messageList: imageMessages,
509
+ title: "Album ảnh",
510
+ summaryList: imageMessages.map(() => "[Hình ảnh]"),
503
511
  });
504
- messageList.push(fileMessage);
505
- }
506
- catch (err) {
507
- console.error("Lỗi xử lý tin nhắn:", err);
512
+ if (mergeMsg)
513
+ messageList.push(mergeMsg);
508
514
  }
509
- }
510
- const hasMentions = !!mentions && (mentions.atUserIDList.length > 0 || mentions.isAtAll);
511
- if (!!plainText && plainText.trim() !== "") {
512
- const urls = extractLinkUrls(plainText);
513
- let textMessage = null;
514
- if (hasMentions && mentions) {
515
- // OpenIM SDK convention: "tag all" is signaled by the sentinel
516
- // "AtAllTag" inside atUserIDList, not a separate flag/param.
517
- textMessage = await createAtTextMessage({
518
- text: plainText,
519
- atUserIDList: mentions.isAtAll
520
- ? [AT_ALL_TAG, ...mentions.atUserIDList]
521
- : mentions.atUserIDList,
522
- atUsersInfo: mentions.atUsersInfo,
523
- message: (quotedMessage === null || quotedMessage === void 0 ? void 0 : quotedMessage.clientMsgID) ? quotedMessage : undefined,
524
- urls: urls || undefined,
525
- });
526
- setQuotedMessage(null);
515
+ for (const fileWrapper of videoFileWrappers) {
516
+ const file = fileWrapper.originFileObj;
517
+ if (!file)
518
+ continue;
519
+ try {
520
+ const videoBaseInfo = await createVideoInfoWithThumbnail(file);
521
+ const thumbFile = new File([videoBaseInfo.thumbnail], file.name + "-thumb.jpg", { type: "image/jpeg" });
522
+ const videoMessage = await createVideoMessageByFile({
523
+ videoPath: "",
524
+ duration: videoBaseInfo.duration,
525
+ videoType: file.type,
526
+ snapshotPath: "",
527
+ videoUUID: uuidv4(),
528
+ videoUrl: URL.createObjectURL(file),
529
+ videoSize: file.size,
530
+ snapshotUUID: "",
531
+ snapshotSize: file.size,
532
+ snapshotUrl: URL.createObjectURL(thumbFile),
533
+ snapshotWidth: videoBaseInfo.width,
534
+ snapshotHeight: videoBaseInfo.height,
535
+ snapShotType: file.type,
536
+ videoFile: file,
537
+ snapshotFile: thumbFile,
538
+ });
539
+ if (!videoMessage)
540
+ continue;
541
+ messageFileStore.set(videoMessage.clientMsgID, {
542
+ type: "video",
543
+ videoFile: file,
544
+ snapshotFile: thumbFile,
545
+ });
546
+ messageList.push(videoMessage);
547
+ }
548
+ catch (err) {
549
+ console.error("Lỗi xử lý video:", err);
550
+ }
527
551
  }
528
- else if (quotedMessage === null || quotedMessage === void 0 ? void 0 : quotedMessage.clientMsgID) {
529
- textMessage = await createQuoteMessage(plainText, quotedMessage, urls || undefined);
530
- setQuotedMessage(null);
552
+ for (const fileWrapper of documentFileWrappers) {
553
+ const file = fileWrapper.originFileObj;
554
+ if (!file)
555
+ continue;
556
+ try {
557
+ const fileParams = {
558
+ filePath: "",
559
+ fileName: file.name,
560
+ uuid: uuidv4(),
561
+ sourceUrl: URL.createObjectURL(file),
562
+ fileSize: file.size,
563
+ fileType: file.type,
564
+ file: file,
565
+ };
566
+ const fileMessage = await createFileMessageByFile(fileParams);
567
+ if (!fileMessage)
568
+ continue;
569
+ messageFileStore.set(fileMessage.clientMsgID, {
570
+ type: "file",
571
+ file: file,
572
+ });
573
+ messageList.push(fileMessage);
574
+ }
575
+ catch (err) {
576
+ console.error("Lỗi xử lý tin nhắn:", err);
577
+ }
531
578
  }
532
- else if (urls.length > 0) {
533
- textMessage = await createUrlTextMessage(plainText, urls);
579
+ const hasMentions = !!mentions && (mentions.atUserIDList.length > 0 || mentions.isAtAll);
580
+ if (hasText) {
581
+ const urls = extractLinkUrls(plainText);
582
+ let textMessage = null;
583
+ if (hasMentions && mentions) {
584
+ // OpenIM SDK convention: "tag all" is signaled by the sentinel
585
+ // "AtAllTag" inside atUserIDList, not a separate flag/param.
586
+ textMessage = await createAtTextMessage({
587
+ text: plainText,
588
+ atUserIDList: mentions.isAtAll
589
+ ? [AT_ALL_TAG, ...mentions.atUserIDList]
590
+ : mentions.atUserIDList,
591
+ atUsersInfo: mentions.atUsersInfo,
592
+ message: quoted,
593
+ urls: urls || undefined,
594
+ });
595
+ }
596
+ else if (quoted) {
597
+ textMessage = await createQuoteMessage(plainText, quoted, urls || undefined);
598
+ }
599
+ else if (urls.length > 0) {
600
+ textMessage = await createUrlTextMessage(plainText, urls);
601
+ }
602
+ else {
603
+ textMessage = await createTextMessage(plainText);
604
+ }
605
+ if (!textMessage)
606
+ return null;
607
+ messageList.push(textMessage);
534
608
  }
535
- else {
536
- textMessage = await createTextMessage(plainText);
609
+ if (customMessage) {
610
+ const customMessageItem = await createCustomMessage(customMessage);
611
+ if (!customMessageItem)
612
+ return null;
613
+ messageList.push(customMessageItem);
537
614
  }
538
- if (!textMessage)
539
- return false;
540
- messageList.push(textMessage);
541
- }
542
- if (customMessage) {
543
- const customMessageItem = await createCustomMessage(customMessage);
544
- if (!customMessageItem)
545
- return false;
546
- messageList.push(customMessageItem);
547
- }
548
- for (const message of messageList) {
549
- const isTextLikeMessage = (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.TextMessage ||
550
- (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.UrlTextMessage ||
551
- (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.QuoteMessage ||
552
- (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.AtTextMessage;
553
- const extendMessageInfo = generateExtendMessageInfo({
554
- richText: isTextLikeMessage ? richText : "",
555
- urlMetadata: urlMetadata || undefined,
556
- currentSession,
557
- flow: isTextLikeMessage ? flow : undefined,
558
- mentionOffsets: (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.AtTextMessage
559
- ? mentions === null || mentions === void 0 ? void 0 : mentions.mentionOffsets
560
- : undefined,
615
+ return messageList.map((message) => {
616
+ const isTextLikeMessage = (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.TextMessage ||
617
+ (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.UrlTextMessage ||
618
+ (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.QuoteMessage ||
619
+ (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.AtTextMessage;
620
+ const extendMessageInfo = generateExtendMessageInfo({
621
+ richText: isTextLikeMessage ? richText : "",
622
+ urlMetadata: urlMetadata || undefined,
623
+ currentSession,
624
+ flow: isTextLikeMessage ? flow : undefined,
625
+ mentionOffsets: (message === null || message === void 0 ? void 0 : message.contentType) === MessageType.AtTextMessage
626
+ ? mentions === null || mentions === void 0 ? void 0 : mentions.mentionOffsets
627
+ : undefined,
628
+ });
629
+ return Object.assign(Object.assign({}, message), { ex: JSON.stringify(extendMessageInfo) || "{}", isInternal });
561
630
  });
562
- const mMessage = Object.assign(Object.assign({}, message), { ex: JSON.stringify(extendMessageInfo) || "{}", isInternal });
563
- await sendMessage(mMessage, currentSession);
564
- }
565
- return true;
631
+ };
632
+ return queueSend(buildMessages, currentSession);
566
633
  }, [
567
634
  conversationData,
568
635
  recvID,
569
636
  groupID,
570
- sendMessage,
637
+ queueSend,
571
638
  quotedMessage,
572
639
  setQuotedMessage,
573
640
  ]);
@@ -614,10 +681,15 @@ const createPicBaseInfoFromFile = (file) => new Promise((resolve, reject) => {
614
681
  img.onload = function () {
615
682
  resolve(img);
616
683
  };
684
+ // Without this a broken image would hang its send and every send queued after it.
685
+ img.onerror = reject;
617
686
  img.src = _URL.createObjectURL(file);
618
687
  });
688
+ const VIDEO_INFO_TIMEOUT_MS = 15000;
619
689
  function createVideoInfoWithThumbnail(file) {
620
690
  return new Promise((resolve, reject) => {
691
+ // A video the browser never seeks would hang its send and every send queued after it.
692
+ const timeout = setTimeout(() => reject(new Error("Video thumbnail timed out")), VIDEO_INFO_TIMEOUT_MS);
621
693
  try {
622
694
  const video = document.createElement("video");
623
695
  video.preload = "metadata";
@@ -640,6 +712,7 @@ function createVideoInfoWithThumbnail(file) {
640
712
  canvas.toBlob((blob) => {
641
713
  if (!blob)
642
714
  return reject("Thumbnail capture failed");
715
+ clearTimeout(timeout);
643
716
  resolve({
644
717
  duration: Math.floor(video.duration * 1000),
645
718
  width: video.videoWidth,