@droppii-org/chat-sdk 0.8.1 → 0.8.2

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 (165) hide show
  1. package/dist/components/canned-response/cannedTrigger.d.ts +2 -0
  2. package/dist/components/canned-response/cannedTrigger.d.ts.map +1 -0
  3. package/dist/components/canned-response/cannedTrigger.js +3 -0
  4. package/dist/components/canned-response/insertCannedContent.js +2 -2
  5. package/dist/components/conversation/ConversationByGroupItem.js +1 -1
  6. package/dist/components/conversation/ConversationBySessionItem.js +4 -4
  7. package/dist/components/conversation/internalNotePreview.d.ts +1 -1
  8. package/dist/components/conversation/internalNotePreview.d.ts.map +1 -1
  9. package/dist/components/conversation/internalNotePreview.js +22 -3
  10. package/dist/components/conversation/rowTime.d.ts +2 -2
  11. package/dist/components/conversation/rowTime.d.ts.map +1 -1
  12. package/dist/components/conversation/rowTime.js +2 -4
  13. package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
  14. package/dist/components/conversation-detail/ConversationDetailInput.js +8 -3
  15. package/dist/components/conversation-detail/ConversationMessageList.d.ts.map +1 -1
  16. package/dist/components/conversation-detail/ConversationMessageList.js +6 -3
  17. package/dist/components/conversation-detail/bot/BotMenuSheet.d.ts.map +1 -1
  18. package/dist/components/conversation-detail/bot/BotMenuSheet.js +1 -1
  19. package/dist/components/conversation-inbox/ConversationInboxList.d.ts.map +1 -1
  20. package/dist/components/conversation-inbox/ConversationInboxList.js +4 -0
  21. package/dist/components/conversation-inbox/InboxBootStatus.d.ts.map +1 -1
  22. package/dist/components/conversation-inbox/InboxBootStatus.js +10 -2
  23. package/dist/components/group/CreateGroupForm.d.ts.map +1 -1
  24. package/dist/components/group/CreateGroupForm.js +2 -1
  25. package/dist/components/group/CreateGroupFormCrmView.d.ts.map +1 -1
  26. package/dist/components/group/CreateGroupFormCrmView.js +1 -1
  27. package/dist/components/group/GroupMemberPickerCrm.d.ts.map +1 -1
  28. package/dist/components/group/GroupMemberPickerCrm.js +1 -1
  29. package/dist/components/media-collection/FileCollection.d.ts.map +1 -1
  30. package/dist/components/media-collection/FileCollection.js +2 -1
  31. package/dist/components/media-collection/ImageCollection.d.ts.map +1 -1
  32. package/dist/components/media-collection/ImageCollection.js +2 -1
  33. package/dist/components/media-collection/LinkCollection.d.ts.map +1 -1
  34. package/dist/components/media-collection/LinkCollection.js +14 -26
  35. package/dist/components/media-collection/VideoCollection.d.ts.map +1 -1
  36. package/dist/components/media-collection/VideoCollection.js +2 -1
  37. package/dist/components/media-resources/ResourceScrollContainer.d.ts.map +1 -1
  38. package/dist/components/media-resources/ResourceScrollContainer.js +34 -2
  39. package/dist/components/message/GroupMembersDrawer.d.ts.map +1 -1
  40. package/dist/components/message/GroupMembersDrawer.js +2 -3
  41. package/dist/components/message/MessageList.d.ts.map +1 -1
  42. package/dist/components/message/MessageList.js +15 -4
  43. package/dist/components/message/footer/ActionBar.d.ts.map +1 -1
  44. package/dist/components/message/footer/ActionBar.js +10 -44
  45. package/dist/components/message/footer/CannedResponsePlugin.d.ts.map +1 -1
  46. package/dist/components/message/footer/CannedResponsePlugin.js +3 -2
  47. package/dist/components/message/footer/EnterHandler.d.ts.map +1 -1
  48. package/dist/components/message/footer/EnterHandler.js +10 -47
  49. package/dist/components/message/footer/FilePreview.d.ts.map +1 -1
  50. package/dist/components/message/footer/FilePreview.js +2 -1
  51. package/dist/components/message/footer/LinkInputPreview.d.ts +1 -1
  52. package/dist/components/message/footer/LinkInputPreview.d.ts.map +1 -1
  53. package/dist/components/message/footer/LinkInputPreview.js +8 -1
  54. package/dist/components/message/footer/MediaUploadButtons.d.ts.map +1 -1
  55. package/dist/components/message/footer/MediaUploadButtons.js +4 -7
  56. package/dist/components/message/footer/index.d.ts.map +1 -1
  57. package/dist/components/message/footer/index.js +57 -16
  58. package/dist/components/message/footer/mention/useMentionOptions.d.ts.map +1 -1
  59. package/dist/components/message/footer/mention/useMentionOptions.js +2 -6
  60. package/dist/components/message/footer/resetEditorAfterSend.d.ts +7 -0
  61. package/dist/components/message/footer/resetEditorAfterSend.d.ts.map +1 -0
  62. package/dist/components/message/footer/resetEditorAfterSend.js +62 -0
  63. package/dist/components/message/groupMemberMenuCrm.d.ts +4 -4
  64. package/dist/components/message/groupMemberMenuCrm.d.ts.map +1 -1
  65. package/dist/components/message/groupMemberMenuCrm.js +73 -60
  66. package/dist/components/message/item/SystemLogMessage.d.ts.map +1 -1
  67. package/dist/components/message/item/SystemLogMessage.js +5 -2
  68. package/dist/components/search-conversation/SearchConversationAsGroups.js +1 -1
  69. package/dist/components/search-conversation/SearchConversationAsMessages.js +1 -1
  70. package/dist/components/search-conversation/SearchConversationMyInbox.js +1 -1
  71. package/dist/components/thread/GroupInfoBodyCrm.d.ts.map +1 -1
  72. package/dist/components/thread/GroupInfoBodyCrm.js +2 -2
  73. package/dist/components/thread/GroupThreadInfo.d.ts.map +1 -1
  74. package/dist/components/thread/GroupThreadInfo.js +2 -6
  75. package/dist/components/thread/ManualAssignPopover.d.ts.map +1 -1
  76. package/dist/components/thread/ManualAssignPopover.js +6 -8
  77. package/dist/components/thread/SessionDetailCard.d.ts.map +1 -1
  78. package/dist/components/thread/SessionDetailCard.js +3 -3
  79. package/dist/components/thread/SessionSection.d.ts.map +1 -1
  80. package/dist/components/thread/SessionSection.js +3 -9
  81. package/dist/components/thread/SessionTagPicker.d.ts +3 -1
  82. package/dist/components/thread/SessionTagPicker.d.ts.map +1 -1
  83. package/dist/components/thread/SessionTagPicker.js +2 -2
  84. package/dist/components/thread/sessionDateFormat.d.ts +2 -0
  85. package/dist/components/thread/sessionDateFormat.d.ts.map +1 -1
  86. package/dist/components/thread/sessionDateFormat.js +6 -0
  87. package/dist/constants/dateFormat.d.ts +2 -0
  88. package/dist/constants/dateFormat.d.ts.map +1 -0
  89. package/dist/constants/dateFormat.js +2 -0
  90. package/dist/hooks/common/useFetchUrlMetadata.d.ts +1 -0
  91. package/dist/hooks/common/useFetchUrlMetadata.d.ts.map +1 -1
  92. package/dist/hooks/conversation/useBotFlow.d.ts.map +1 -1
  93. package/dist/hooks/conversation/useBotFlow.js +64 -12
  94. package/dist/hooks/conversation/useCloseBotSession.d.ts +2 -0
  95. package/dist/hooks/conversation/useCloseBotSession.d.ts.map +1 -1
  96. package/dist/hooks/conversation/useCloseBotSession.js +17 -1
  97. package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
  98. package/dist/hooks/global/useGlobalEvent.js +148 -16
  99. package/dist/hooks/global/useLoginHealth.d.ts +3 -0
  100. package/dist/hooks/global/useLoginHealth.d.ts.map +1 -0
  101. package/dist/hooks/global/useLoginHealth.js +55 -0
  102. package/dist/hooks/group/useCreateGroup.d.ts.map +1 -1
  103. package/dist/hooks/group/useCreateGroup.js +75 -6
  104. package/dist/hooks/init/useChatBootPhase.d.ts +2 -0
  105. package/dist/hooks/init/useChatBootPhase.d.ts.map +1 -1
  106. package/dist/hooks/init/useChatBootPhase.js +11 -6
  107. package/dist/hooks/message/useMessage.d.ts.map +1 -1
  108. package/dist/hooks/message/useMessage.js +97 -28
  109. package/dist/hooks/message/usePinnedMessages.d.ts.map +1 -1
  110. package/dist/hooks/message/usePinnedMessages.js +13 -2
  111. package/dist/hooks/message/useRevokeMessage.d.ts.map +1 -1
  112. package/dist/hooks/message/useRevokeMessage.js +25 -0
  113. package/dist/hooks/message/useSendMessage.d.ts +15 -12
  114. package/dist/hooks/message/useSendMessage.d.ts.map +1 -1
  115. package/dist/hooks/message/useSendMessage.js +199 -133
  116. package/dist/hooks/search/useSearchMessage.d.ts.map +1 -1
  117. package/dist/hooks/search/useSearchMessage.js +57 -3
  118. package/dist/hooks/session/useConversationSessionState.d.ts.map +1 -1
  119. package/dist/hooks/session/useConversationSessionState.js +4 -1
  120. package/dist/hooks/session/useSessionLatestPreview.d.ts.map +1 -1
  121. package/dist/hooks/session/useSessionLatestPreview.js +34 -4
  122. package/dist/hooks/user/useAuth.d.ts +1 -1
  123. package/dist/hooks/user/useAuth.d.ts.map +1 -1
  124. package/dist/hooks/user/useAuth.js +31 -15
  125. package/dist/index.d.ts +1 -0
  126. package/dist/index.d.ts.map +1 -1
  127. package/dist/index.js +1 -0
  128. package/dist/locales/vi/boot.json +5 -1
  129. package/dist/locales/vi/common.json +34 -15
  130. package/dist/locales/vi/conversation-inbox.json +2 -1
  131. package/dist/screens/conversation-detail/index.d.ts.map +1 -1
  132. package/dist/screens/conversation-detail/index.js +6 -0
  133. package/dist/screens/desk-message/DeskBootCard.d.ts.map +1 -1
  134. package/dist/screens/desk-message/DeskBootCard.js +6 -4
  135. package/dist/store/boot.d.ts +5 -0
  136. package/dist/store/boot.d.ts.map +1 -1
  137. package/dist/store/boot.js +11 -1
  138. package/dist/store/conversation.d.ts.map +1 -1
  139. package/dist/store/conversation.js +5 -1
  140. package/dist/store/type.d.ts +3 -1
  141. package/dist/store/type.d.ts.map +1 -1
  142. package/dist/styles/global.css +1 -1
  143. package/dist/types/chat.d.ts +1 -1
  144. package/dist/types/chat.d.ts.map +1 -1
  145. package/dist/utils/common.d.ts.map +1 -1
  146. package/dist/utils/common.js +4 -1
  147. package/dist/utils/fileValidation.d.ts +7 -0
  148. package/dist/utils/fileValidation.d.ts.map +1 -1
  149. package/dist/utils/fileValidation.js +37 -13
  150. package/dist/utils/latestMessagePreviewable.d.ts +2 -0
  151. package/dist/utils/latestMessagePreviewable.d.ts.map +1 -0
  152. package/dist/utils/latestMessagePreviewable.js +18 -0
  153. package/dist/utils/mentionMembers.d.ts +3 -0
  154. package/dist/utils/mentionMembers.d.ts.map +1 -0
  155. package/dist/utils/mentionMembers.js +29 -0
  156. package/dist/utils/messageLog.d.ts +2 -2
  157. package/dist/utils/messageLog.d.ts.map +1 -1
  158. package/dist/utils/messageLog.js +129 -40
  159. package/dist/utils/notifiableMessage.d.ts +7 -0
  160. package/dist/utils/notifiableMessage.d.ts.map +1 -0
  161. package/dist/utils/notifiableMessage.js +26 -0
  162. package/dist/utils/withSdkTimeout.d.ts +11 -0
  163. package/dist/utils/withSdkTimeout.d.ts.map +1 -0
  164. package/dist/utils/withSdkTimeout.js +30 -0
  165. package/package.json +1 -1
@@ -250,7 +250,7 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
250
250
  });
251
251
  message.success(member.isAdmin
252
252
  ? t("remove_from_admin_success")
253
- : t("assign_as_admin_success"));
253
+ : t("assign_as_admin_success", { name: member.nickname }));
254
254
  // Optimistically patch the role in place instead of refetching the
255
255
  // whole list — a full refetch replaces every member's object
256
256
  // identity and re-sorts, which visibly re-renders/reorders rows that
@@ -283,7 +283,7 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
283
283
  groupID,
284
284
  newOwnerUserID: confirmMemberAction.member.userID,
285
285
  });
286
- message.success(t("transfer_owner_success"));
286
+ message.success(t("transfer_owner_success", { name: confirmMemberAction.member.nickname }));
287
287
  void refetch();
288
288
  }
289
289
  catch (error) {
@@ -314,7 +314,6 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
314
314
  }, [groupID]);
315
315
  const renderCrmPanel = (closeOnEscape) => (_jsx(GroupMembersPanelCrm, { totalCount: totalCount, members: sortedMembers.map((member) => isRealGroup ? member : Object.assign(Object.assign({}, member), { roleLabel: "" })), isRealGroup: isRealGroup, isLoading: isLoading || (isSearchActive && isSearching), isLoadingMore: isLoadingMore && !isSearchActive, onListScroll: handleListScroll, error: error, isSearchActive: isSearchActive, search: search, onSearchChange: setSearch, canAddMember: canAddMember, onAddMember: () => setShowAddMember(true), onClose: handleCloseDrawer, closeOnEscape: closeOnEscape, buildMenu: (member) => buildCrmMemberMenu({
316
316
  member,
317
- isRealGroup,
318
317
  permissions,
319
318
  actingIsOwner,
320
319
  leaveBlockedReason,
@@ -1 +1 @@
1
- {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageList.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAIxD,UAAU,gBAAgB;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qBAAqB,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAChE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAyb3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageList.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAIxD,UAAU,gBAAgB;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qBAAqB,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAChE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAoc3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -86,6 +86,7 @@ const MessageList = (props) => {
86
86
  const { revokeMessage, loading: isRevoking } = useRevokeMessage();
87
87
  const { pinMessage, unpinMessage } = usePinMessage();
88
88
  const pinnedTotalCount = useConversationStore((state) => state.pinnedTotalCount);
89
+ const pinnedBackendTotalCount = useConversationStore((state) => state.pinnedBackendTotalCount);
89
90
  const pinnedRemainingCount = useConversationStore((state) => state.pinnedRemainingCount);
90
91
  const [pinLimitModalOpen, setPinLimitModalOpen] = useState(false);
91
92
  const { rawGroupId, latestConversationSession, refetchConversationSessions } = useConversationSessionState({
@@ -136,13 +137,23 @@ const MessageList = (props) => {
136
137
  closeConfirmRevoke();
137
138
  }, [closeConfirmRevoke]);
138
139
  const onRevokeMessage = useCallback(async () => {
139
- await revokeMessage((conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID) || "", selectedItem || "");
140
- handleCloseRevoke();
140
+ try {
141
+ await revokeMessage((conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID) || "", selectedItem || "");
142
+ }
143
+ catch (error) {
144
+ // Keep the cause visible: the toast alone hides why core rejected it.
145
+ console.warn("revokeMessage failed", error);
146
+ toastMessage.error(t("revoke_message_failed"));
147
+ }
148
+ finally {
149
+ handleCloseRevoke();
150
+ }
141
151
  }, [
142
152
  conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID,
143
153
  selectedItem,
144
154
  revokeMessage,
145
155
  handleCloseRevoke,
156
+ t,
146
157
  ]);
147
158
  const onPressQuoteMessage = useCallback((clientMsgID) => {
148
159
  const hasMessage = loadState.messageList.some((msg) => msg.clientMsgID === clientMsgID);
@@ -160,7 +171,7 @@ const MessageList = (props) => {
160
171
  onPressQuoteMessage(firstUnreadMessageId);
161
172
  }, [firstUnreadMessageId, markDividerSeen, onPressQuoteMessage]);
162
173
  const handlePinMessage = useCallback(async (message) => {
163
- if (pinnedRemainingCount <= 0 && pinnedTotalCount > 0) {
174
+ if (pinnedRemainingCount <= 0 && pinnedBackendTotalCount > 0) {
164
175
  setPinLimitModalOpen(true);
165
176
  return;
166
177
  }
@@ -171,7 +182,7 @@ const MessageList = (props) => {
171
182
  catch (_a) {
172
183
  toastMessage.error(t("pin_message_failed"));
173
184
  }
174
- }, [conversationId, pinMessage, pinnedRemainingCount, pinnedTotalCount, t]);
185
+ }, [conversationId, pinMessage, pinnedRemainingCount, pinnedBackendTotalCount, t]);
175
186
  const handleUnpinMessage = useCallback(async (message) => {
176
187
  try {
177
188
  await unpinMessage(conversationId, message.clientMsgID);
@@ -1 +1 @@
1
- {"version":3,"file":"ActionBar.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/ActionBar.tsx"],"names":[],"mappings":"AA4BA,UAAU,cAAc;IAEtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,QAAA,MAAM,SAAS,GAAI,gCAAwC,cAAc,4CAmKxE,CAAC;AAEF,eAAe,SAAS,CAAC"}
1
+ {"version":3,"file":"ActionBar.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/ActionBar.tsx"],"names":[],"mappings":"AAiBA,UAAU,cAAc;IAEtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B;AAED,QAAA,MAAM,SAAS,GAAI,gCAAwC,cAAc,4CA2HxE,CAAC;AAEF,eAAe,SAAS,CAAC"}
@@ -8,13 +8,11 @@ import { useTranslation } from "react-i18next";
8
8
  import { Icon } from "../../../components/icon";
9
9
  import useAuthStore from "../../../store/auth";
10
10
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
11
- import { $createParagraphNode, $getRoot, $getSelection, $isRangeSelection, } from "lexical";
11
+ import { $getRoot } from "lexical";
12
12
  import { $generateHtmlFromNodes } from "@lexical/html";
13
- import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, } from "@lexical/list";
14
- import { $isQuoteNode } from "@lexical/rich-text";
15
13
  import { useMessageFooterContext } from ".";
16
14
  import MediaActions from "./MediaActions";
17
- import { clearComposerHistoryAfterUpdate } from "./clearComposerHistory";
15
+ import { resetEditorAfterSend, resetEditorIfUnchanged } from "./resetEditorAfterSend";
18
16
  import useConversationStore from "../../../store/conversation";
19
17
  import { extractMentions } from "./mention/extractMentions";
20
18
  const ActionBar = ({ formatOpen = false, onToggleFormat }) => {
@@ -39,53 +37,21 @@ const ActionBar = ({ formatOpen = false, onToggleFormat }) => {
39
37
  });
40
38
  if (canSend) {
41
39
  const mentions = extractMentions(editor);
42
- const sent = onSendMessage({
40
+ const sending = onSendMessage({
43
41
  plainText,
44
42
  richText,
45
43
  type: listUploadFiles.length > 0 ? "file" : "text",
46
44
  isInternal,
47
45
  mentions,
48
46
  });
49
- if (!sent) {
50
- return;
51
- }
47
+ // Clear only once the message is built; a failed build keeps the draft.
48
+ void Promise.resolve(sending).then((sent) => {
49
+ if (sent)
50
+ resetEditorIfUnchanged(editor, { plainText, richText });
51
+ });
52
+ return;
52
53
  }
53
- clearComposerHistoryAfterUpdate(editor);
54
- editor.update(() => {
55
- const root = $getRoot();
56
- root.clear();
57
- const paragraph = $createParagraphNode();
58
- root.append(paragraph);
59
- paragraph.select();
60
- const selection = $getSelection();
61
- if ($isRangeSelection(selection)) {
62
- const anchorNode = selection.anchor.getNode();
63
- const topLevelNode = anchorNode.getTopLevelElementOrThrow();
64
- const isListNode = $isListNode(topLevelNode);
65
- const isQuoteNode = $isQuoteNode(topLevelNode);
66
- const listType = isListNode ? topLevelNode.getListType() : undefined;
67
- if (selection.hasFormat("bold")) {
68
- selection.formatText("bold");
69
- }
70
- if (selection.hasFormat("italic")) {
71
- selection.formatText("italic");
72
- }
73
- if (selection.hasFormat("strikethrough")) {
74
- selection.formatText("strikethrough");
75
- }
76
- if (isQuoteNode) {
77
- const paragraph = $createParagraphNode();
78
- const children = topLevelNode.getChildren();
79
- children.forEach((child) => paragraph.append(child));
80
- topLevelNode.replace(paragraph);
81
- }
82
- if (isListNode && listType) {
83
- editor.dispatchCommand(listType === "number"
84
- ? INSERT_ORDERED_LIST_COMMAND
85
- : INSERT_UNORDERED_LIST_COMMAND, undefined);
86
- }
87
- }
88
- });
54
+ resetEditorAfterSend(editor);
89
55
  }, [editor, onSendMessage, canSend, isInternal, listUploadFiles.length]);
90
56
  useEffect(() => {
91
57
  return editor.registerUpdateListener(({ editorState }) => {
@@ -1 +1 @@
1
- {"version":3,"file":"CannedResponsePlugin.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/CannedResponsePlugin.tsx"],"names":[],"mappings":"AAYA,KAAK,KAAK,GAAG;IACX,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,KAAK,QAwGrE"}
1
+ {"version":3,"file":"CannedResponsePlugin.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/CannedResponsePlugin.tsx"],"names":[],"mappings":"AAaA,KAAK,KAAK,GAAG;IACX,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB,CAAC;AAEF,wBAAgB,2BAA2B,CAAC,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,KAAK,QAwGrE"}
@@ -2,6 +2,7 @@ import { useEffect, useRef } from "react";
2
2
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
3
3
  import { $getSelection, $isRangeSelection, COMMAND_PRIORITY_LOW, KEY_ARROW_DOWN_COMMAND, KEY_ARROW_UP_COMMAND, KEY_ESCAPE_COMMAND, } from "lexical";
4
4
  import { useMessageFooterContext } from ".";
5
+ import { CANNED_TRIGGER_BEFORE_CURSOR } from "../../../components/canned-response/cannedTrigger";
5
6
  export function CannedResponseTriggerPlugin({ onOpen, onClose }) {
6
7
  const [editor] = useLexicalComposerContext();
7
8
  const { activeCannedIndex, setActiveCannedIndex, isOpenCanned, maxActiveCannedIndex, } = useMessageFooterContext();
@@ -21,8 +22,8 @@ export function CannedResponseTriggerPlugin({ onOpen, onClose }) {
21
22
  const text = node.getTextContent();
22
23
  const offset = anchor.offset;
23
24
  const before = text.slice(0, offset);
24
- // match #abc (KHÔNG có space phía sau)
25
- const match = before.match(/(^|\s)#(\w*)$/);
25
+ // match #abc or #abc-def (no space after the cursor)
26
+ const match = before.match(CANNED_TRIGGER_BEFORE_CURSOR);
26
27
  lastBeforeRef.current = before;
27
28
  if (match && dismissedRef.current !== before) {
28
29
  onOpen(match[2] || "");
@@ -1 +1 @@
1
- {"version":3,"file":"EnterHandler.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/EnterHandler.tsx"],"names":[],"mappings":"AA2BA,MAAM,CAAC,OAAO,UAAU,YAAY,SA+JnC"}
1
+ {"version":3,"file":"EnterHandler.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/EnterHandler.tsx"],"names":[],"mappings":"AAoBA,MAAM,CAAC,OAAO,UAAU,YAAY,SAkHnC"}
@@ -1,11 +1,10 @@
1
1
  "use client";
2
2
  import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
3
- import { KEY_ENTER_COMMAND, COMMAND_PRIORITY_NORMAL, $getSelection, $isRangeSelection, $createParagraphNode, $getRoot, } from "lexical";
3
+ import { KEY_ENTER_COMMAND, COMMAND_PRIORITY_NORMAL, $createParagraphNode, $getRoot, } from "lexical";
4
4
  import { clearComposerHistoryAfterUpdate } from "./clearComposerHistory";
5
+ import { resetEditorAfterSend, resetEditorIfUnchanged } from "./resetEditorAfterSend";
5
6
  import { useEffect, useRef } from "react";
6
7
  import { $generateHtmlFromNodes } from "@lexical/html";
7
- import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, } from "@lexical/list";
8
- import { $isQuoteNode } from "@lexical/rich-text";
9
8
  import { useMessageFooterContext } from ".";
10
9
  import useConversationStore from "../../../store/conversation";
11
10
  import emitter, { emit } from "../../../utils/events";
@@ -56,57 +55,21 @@ export default function EnterHandler() {
56
55
  const canSend = hasQuote ? hasText : hasText || hasFiles;
57
56
  if (canSend) {
58
57
  const mentions = extractMentions(editor);
59
- const sent = onSendMessage({
58
+ const sending = onSendMessage({
60
59
  plainText,
61
60
  richText,
62
61
  type: hasFiles ? "file" : "text",
63
62
  isInternal,
64
63
  mentions,
65
64
  });
66
- if (!sent) {
67
- return true;
68
- }
65
+ // Clear only once the message is built; a failed build keeps the draft.
66
+ void Promise.resolve(sending).then((sent) => {
67
+ if (sent)
68
+ resetEditorIfUnchanged(editor, { plainText, richText });
69
+ });
70
+ return true;
69
71
  }
70
- clearComposerHistoryAfterUpdate(editor);
71
- editor.update(() => {
72
- const root = $getRoot();
73
- root.clear();
74
- const paragraph = $createParagraphNode();
75
- root.append(paragraph);
76
- paragraph.select();
77
- const selection = $getSelection();
78
- if ($isRangeSelection(selection)) {
79
- const anchorNode = selection.anchor.getNode();
80
- const topLevelNode = anchorNode.getTopLevelElementOrThrow();
81
- const isListNode = $isListNode(topLevelNode);
82
- const isQuoteNode = $isQuoteNode(topLevelNode);
83
- const listType = isListNode
84
- ? topLevelNode.getListType()
85
- : undefined;
86
- if (selection.hasFormat("bold")) {
87
- selection.formatText("bold");
88
- }
89
- if (selection.hasFormat("italic")) {
90
- selection.formatText("italic");
91
- }
92
- if (selection.hasFormat("strikethrough")) {
93
- selection.formatText("strikethrough");
94
- }
95
- if (isQuoteNode) {
96
- // Nếu đang là QuoteNode → chuyển về Paragraph (bình thường)
97
- const paragraph = $createParagraphNode();
98
- // copy con của quote sang paragraph
99
- const children = topLevelNode.getChildren();
100
- children.forEach((child) => paragraph.append(child));
101
- topLevelNode.replace(paragraph);
102
- }
103
- if (isListNode && listType) {
104
- editor.dispatchCommand(listType === "number"
105
- ? INSERT_ORDERED_LIST_COMMAND
106
- : INSERT_UNORDERED_LIST_COMMAND, undefined);
107
- }
108
- }
109
- });
72
+ resetEditorAfterSend(editor);
110
73
  return true;
111
74
  }
112
75
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"FilePreview.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/FilePreview.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AA8IlC,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,YAAY,GACb,EAAE;IACD,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,kDAsBA;AAED,QAAA,MAAM,WAAW,+CAQhB,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"FilePreview.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/FilePreview.tsx"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,MAAM,MAAM,CAAC;AA+IlC,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,YAAY,GACb,EAAE;IACD,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,kDAsBA;AAED,QAAA,MAAM,WAAW,+CAQhB,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -45,7 +45,8 @@ const MediaThumb = ({ file, onRemove, onVideoReady }) => {
45
45
  return () => URL.revokeObjectURL(url);
46
46
  }, [file.originFileObj, file.url]);
47
47
  const handleLoadedMetadata = (event) => {
48
- setDuration(event.currentTarget.duration);
48
+ const { duration: videoDuration } = event.currentTarget;
49
+ setDuration(Number.isFinite(videoDuration) ? videoDuration : null);
49
50
  onVideoReady === null || onVideoReady === void 0 ? void 0 : onVideoReady(file.uid);
50
51
  };
51
52
  return (_jsxs("div", { "data-cp-name": "MediaThumb", className: "relative h-12 w-12 shrink-0", children: [isVideo ? (_jsx("video", { src: src, className: "h-12 w-12 rounded-lg object-cover", preload: "metadata", muted: true, onLoadedMetadata: handleLoadedMetadata })) : (_jsx("img", { src: src, alt: file.name, className: "h-12 w-12 rounded-lg object-cover" })), isLoading && (_jsx("div", { className: "absolute inset-0 flex items-center justify-center rounded-lg bg-black/60", children: _jsxs("span", { className: "text-xs font-semibold text-white", children: [percent, "%"] }) })), isVideo && duration !== null && (_jsx("div", { className: "absolute bottom-1 left-1/2 flex w-9 -translate-x-1/2 items-center justify-center rounded-xl bg-black/70 px-1 text-center text-[10px] font-normal leading-4 text-white", children: formatDuration(duration) })), _jsx(RemoveButton, { file: file, onRemove: onRemove })] }));
@@ -2,6 +2,6 @@ export interface LinkInputPreviewProps {
2
2
  url: string;
3
3
  onRemove: () => void;
4
4
  }
5
- declare const LinkInputPreview: ({ url, onRemove }: LinkInputPreviewProps) => import("react/jsx-runtime").JSX.Element;
5
+ declare const LinkInputPreview: ({ url, onRemove }: LinkInputPreviewProps) => import("react/jsx-runtime").JSX.Element | null;
6
6
  export default LinkInputPreview;
7
7
  //# sourceMappingURL=LinkInputPreview.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"LinkInputPreview.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/LinkInputPreview.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,QAAA,MAAM,gBAAgB,GAAI,mBAAmB,qBAAqB,4CAqEjE,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"LinkInputPreview.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/LinkInputPreview.tsx"],"names":[],"mappings":"AAQA,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,QAAA,MAAM,gBAAgB,GAAI,mBAAmB,qBAAqB,mDAwEjE,CAAC;AAEF,eAAe,gBAAgB,CAAC"}
@@ -8,9 +8,16 @@ import { useTranslation } from "react-i18next";
8
8
  const LinkInputPreview = ({ url, onRemove }) => {
9
9
  const { t } = useTranslation("conversation-inbox");
10
10
  const { data, isLoading, isError } = useFetchUrlMetadata(url);
11
+ // A link whose preview cannot be loaded is shown as a plain link only.
12
+ const hasNoPreview = isError ||
13
+ !!(data === null || data === void 0 ? void 0 : data.isEmpty) ||
14
+ (!!data && !data.title && !data.description && !data.image) ||
15
+ data === null;
16
+ if (!isLoading && hasNoPreview)
17
+ return null;
11
18
  const handleOpen = () => {
12
19
  openLink(url, t);
13
20
  };
14
- return (_jsxs("div", { "data-testid": "link-input-preview", "data-cp-name": "LinkInputPreview", className: "flex w-full shrink-0 items-center gap-2 overflow-hidden bg-blue-25 pr-2", children: [_jsxs("button", { type: "button", onClick: handleOpen, className: "flex min-w-0 flex-1 items-center gap-2 py-0 pl-0 text-left", children: [_jsx("div", { className: "flex h-[51px] w-[51px] shrink-0 items-center justify-center overflow-hidden bg-blue-25", children: isLoading ? (_jsx("div", { className: "h-5 w-5 animate-spin rounded-full border-2 border-[var(--dc-color-primary)] border-t-transparent" })) : (data === null || data === void 0 ? void 0 : data.image) ? (_jsx("img", { src: data.image, alt: "", className: "h-full w-full object-cover" })) : (_jsx(AdaptiveIcon, { stroke: "link", font: "link-alt-b", size: 24, className: "text-[var(--dc-color-primary)]" })) }), _jsxs("div", { className: "min-w-0 flex-1", children: [isLoading ? (_jsx("p", { className: "truncate text-[14px] font-medium leading-[1.6] text-gray-900", children: t("link_preview.loading") })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "truncate text-[14px] font-medium leading-[1.6] text-gray-900", children: (data === null || data === void 0 ? void 0 : data.title) || getHostLabel(url) }), ((data === null || data === void 0 ? void 0 : data.description) || !(data === null || data === void 0 ? void 0 : data.title)) && (_jsx("p", { className: "truncate text-[13px] leading-[1.6] text-gray-400", children: (data === null || data === void 0 ? void 0 : data.description) || getHostLabel(url) }))] })), isError && !isLoading && (_jsx("p", { className: "truncate text-[13px] leading-[1.6] text-gray-400", children: getHostLabel(url) }))] })] }), _jsx("button", { type: "button", onClick: onRemove, "data-testid": "link-input-preview-remove", className: "flex h-4 w-4 shrink-0 items-center justify-center", "aria-label": t("link_preview.remove"), children: _jsx(AdaptiveIcon, { stroke: "x", font: "close-b", size: 16, className: "text-gray-900" }) })] }));
21
+ return (_jsxs("div", { "data-testid": "link-input-preview", "data-cp-name": "LinkInputPreview", className: "flex w-full shrink-0 items-center gap-2 overflow-hidden bg-blue-25 pr-2", children: [_jsxs("button", { type: "button", onClick: handleOpen, className: "flex min-w-0 flex-1 items-center gap-2 py-0 pl-0 text-left", children: [_jsx("div", { className: "flex h-[51px] w-[51px] shrink-0 items-center justify-center overflow-hidden bg-blue-25", children: isLoading ? (_jsx("div", { className: "h-5 w-5 animate-spin rounded-full border-2 border-[var(--dc-color-primary)] border-t-transparent" })) : (data === null || data === void 0 ? void 0 : data.image) ? (_jsx("img", { src: data.image, alt: "", className: "h-full w-full object-cover" })) : (_jsx(AdaptiveIcon, { stroke: "link", font: "link-alt-b", size: 24, className: "text-[var(--dc-color-primary)]" })) }), _jsx("div", { className: "min-w-0 flex-1", children: isLoading ? (_jsx("p", { className: "truncate text-[14px] font-medium leading-[1.6] text-gray-900", children: t("link_preview.loading") })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "truncate text-[14px] font-medium leading-[1.6] text-gray-900", children: (data === null || data === void 0 ? void 0 : data.title) || getHostLabel(url) }), ((data === null || data === void 0 ? void 0 : data.description) || !(data === null || data === void 0 ? void 0 : data.title)) && (_jsx("p", { className: "truncate text-[13px] leading-[1.6] text-gray-400", children: (data === null || data === void 0 ? void 0 : data.description) || getHostLabel(url) }))] })) })] }), _jsx("button", { type: "button", onClick: onRemove, "data-testid": "link-input-preview-remove", className: "flex h-4 w-4 shrink-0 items-center justify-center", "aria-label": t("link_preview.remove"), children: _jsx(AdaptiveIcon, { stroke: "x", font: "close-b", size: 16, className: "text-gray-900" }) })] }));
15
22
  };
16
23
  export default LinkInputPreview;
@@ -1 +1 @@
1
- {"version":3,"file":"MediaUploadButtons.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/MediaUploadButtons.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkB,UAAU,EAA0B,MAAM,MAAM,CAAC;AA2B1E,UAAU,uBAAuB;IAC/B,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAE9D,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,eAAe,EACf,aAAa,EACb,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,WAAkB,EAClB,cAAsB,GACvB,EAAE,uBAAuB,2CA6FzB"}
1
+ {"version":3,"file":"MediaUploadButtons.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/MediaUploadButtons.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkB,UAAU,EAA0B,MAAM,MAAM,CAAC;AAiC1E,UAAU,uBAAuB;IAC/B,eAAe,EAAE,UAAU,EAAE,CAAC;IAC9B,aAAa,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,KAAK,IAAI,CAAC;IAC7C,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,OAAO,CAAC;IACrB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC;IAE9D,WAAW,CAAC,EAAE,OAAO,CAAC;IAGtB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,CAAC,OAAO,UAAU,kBAAkB,CAAC,EACzC,eAAe,EACf,aAAa,EACb,QAAQ,EACR,WAAW,EACX,QAAQ,EACR,WAAkB,EAClB,cAAsB,GACvB,EAAE,uBAAuB,2CA0FzB"}
@@ -3,7 +3,7 @@ import { Button, Upload, message as antdMessage } from "antd";
3
3
  import { useTranslation } from "react-i18next";
4
4
  import { Icon } from "../../../components/icon";
5
5
  import { StrokeIcon } from "../../../components/icon/StrokeIcon";
6
- import { MAX_DOCUMENT_SIZE_MB, validateFile } from "../../../utils/fileValidation";
6
+ import { getAttachmentSizeError, getDocumentFormatError, getImageFormatError, MAX_DOCUMENT_SIZE_MB, validateFile, } from "../../../utils/fileValidation";
7
7
  import { DOCUMENT_TYPES } from "./editorConfig";
8
8
  import { mergePickedUploadFiles } from "./mergePickedUploadFiles";
9
9
  const AttachIcon = (_jsx("svg", { width: "22", height: "22", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M15.6 10.0001V11.2001C15.6 14.5137 12.9137 17.2001 9.59998 17.2001C6.28626 17.2001 3.59998 14.5137 3.59998 11.2001V6.79999C3.59998 4.59085 5.39084 2.79999 7.59998 2.79999C9.8091 2.79999 11.6 4.59085 11.6 6.79999V11.2C11.6 12.3045 10.7045 13.2 9.59998 13.2C8.49542 13.2 7.59998 12.3045 7.59998 11.2V7.99999", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
@@ -13,7 +13,7 @@ export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btn
13
13
  listUploadFiles.some((f) => DOCUMENT_TYPES.includes(f.type || ""));
14
14
  const beforeUploadImagesAndVideo = (file) => {
15
15
  if (!acceptVideo && file.type.startsWith("video/")) {
16
- antdMessage.error(t("invalid_image_format", { fileName: file.name }));
16
+ antdMessage.error(getImageFormatError(t, file));
17
17
  return Upload.LIST_IGNORE;
18
18
  }
19
19
  const validation = validateFile(file, t);
@@ -29,14 +29,11 @@ export default function MediaUploadButtons({ listUploadFiles, onFilesChange, btn
29
29
  return Upload.LIST_IGNORE;
30
30
  const isAllowed = DOCUMENT_TYPES.includes(file.type);
31
31
  if (!isAllowed) {
32
- antdMessage.error(t("invalid_document_format", { fileName: file.name }));
32
+ antdMessage.error(getDocumentFormatError(t, file));
33
33
  return Upload.LIST_IGNORE;
34
34
  }
35
35
  if (file.size / 1024 / 1024 > MAX_DOCUMENT_SIZE_MB) {
36
- antdMessage.error(t("file_size_exceeded", {
37
- fileName: file.name,
38
- maxSize: MAX_DOCUMENT_SIZE_MB,
39
- }));
36
+ antdMessage.error(getAttachmentSizeError(t, file, "document", MAX_DOCUMENT_SIZE_MB));
40
37
  return Upload.LIST_IGNORE;
41
38
  }
42
39
  return false;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/index.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAEL,mBAAmB,EAEnB,wBAAwB,EAEzB,MAAM,cAAc,CAAC;AAUtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAwB/C,UAAU,kBAAkB;IAC1B,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,qBAAqB,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACjE;AASD,eAAO,MAAM,oBAAoB,mDAc/B,CAAC;AAEH,eAAO,MAAM,uBAAuB,gCAAyC,CAAC;AAE9E,QAAA,MAAM,qBAAqB,GAAI,4CAG5B,kBAAkB,4CA+apB,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/index.tsx"],"names":[],"mappings":"AAkBA,OAAO,EAEL,mBAAmB,EAEnB,wBAAwB,EAEzB,MAAM,cAAc,CAAC;AAUtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAwB/C,UAAU,kBAAkB;IAC1B,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,qBAAqB,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACjE;AASD,eAAO,MAAM,oBAAoB,mDAc/B,CAAC;AAEH,eAAO,MAAM,uBAAuB,gCAAyC,CAAC;AAE9E,QAAA,MAAM,qBAAqB,GAAI,4CAG5B,kBAAkB,4CAydpB,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
@@ -25,7 +25,7 @@ import CannedResponse from "../../../components/canned-response";
25
25
  import CannedResponsePanel from "../../../components/canned-response/CannedResponsePanel";
26
26
  import CrmComposerCard from "./CrmComposerCard";
27
27
  import { CannedResponseTriggerPlugin } from "./CannedResponsePlugin";
28
- import { useBoolean } from "ahooks";
28
+ import { useBoolean, useLatest } from "ahooks";
29
29
  import useAuthStore from "../../../store/auth";
30
30
  import useConversationStore from "../../../store/conversation";
31
31
  import { useCurrentUserAccountType } from "../../../hooks/user/useCurrentUserAccountType";
@@ -142,6 +142,10 @@ const MessageFooterProvider = ({ currentSession, openCreateCannedModal, }) => {
142
142
  };
143
143
  }, [showLinkPreview, previewUrl, previewMetadata]);
144
144
  const [isSessionClosedModalOpen, { setTrue: openSessionClosedModal, setFalse: closeSessionClosedModal },] = useBoolean(false);
145
+ // Conversations whose message is still being built. Keyed per thread so a send in
146
+ // one thread neither blocks nor clears the composer of the thread opened next.
147
+ const buildingConversationsRef = useRef(new Set());
148
+ const currentConversationIdRef = useLatest(conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID);
145
149
  const pendingMessageRef = useRef(null);
146
150
  const { mutateAsync: createSession, isPending: isCreatingSession } = useCreateSession();
147
151
  const { mutateAsync: reopenSession, isPending: isReopeningSession } = useReopenSession();
@@ -163,6 +167,7 @@ const MessageFooterProvider = ({ currentSession, openCreateCannedModal, }) => {
163
167
  ? t("enter_internal_message")
164
168
  : t("enter_message");
165
169
  const onSendMessage = useCallback(({ plainText, richText, type, isInternal, mentions, }) => {
170
+ var _a;
166
171
  if ((currentSession === null || currentSession === void 0 ? void 0 : currentSession.isClosed) && !isInternal) {
167
172
  pendingMessageRef.current = {
168
173
  plainText,
@@ -172,34 +177,61 @@ const MessageFooterProvider = ({ currentSession, openCreateCannedModal, }) => {
172
177
  files: [...listUploadFiles],
173
178
  urlMetadata: resolveUrlMetadata(),
174
179
  mentions,
180
+ conversationId: conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID,
175
181
  };
176
182
  openSessionClosedModal();
177
183
  return false;
178
184
  }
179
- sendMergeMessage({
180
- plainText,
181
- richText,
182
- files: listUploadFiles,
183
- currentSession,
184
- isInternal,
185
- urlMetadata: resolveUrlMetadata(),
186
- mentions,
185
+ // A second Enter while the first message is still being built would send it twice.
186
+ const conversationId = (_a = conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID) !== null && _a !== void 0 ? _a : "";
187
+ if (buildingConversationsRef.current.has(conversationId))
188
+ return false;
189
+ buildingConversationsRef.current.add(conversationId);
190
+ const sentFileUids = new Set(listUploadFiles.map((file) => file.uid));
191
+ // The composer is cleared by the caller only when this resolves true, so a failed
192
+ // build (hung or rejected core call) keeps the text and files for another try.
193
+ return new Promise((resolve) => {
194
+ const settle = (shown) => {
195
+ buildingConversationsRef.current.delete(conversationId);
196
+ if (!shown)
197
+ toastMessage.error(t("message_send_failed_no_retry"));
198
+ if (shown) {
199
+ // The upload list is composer-wide: even after a thread switch the sent files
200
+ // must go, or they would ride along with the next thread's message.
201
+ setListUploadFiles((files) => files.filter((file) => !sentFileUids.has(file.uid)));
202
+ }
203
+ // After a thread switch the composer may still hold the sent text; the caller
204
+ // clears it only if it is unchanged, so a different draft is never touched.
205
+ resolve(shown);
206
+ };
207
+ void sendMergeMessage({
208
+ plainText,
209
+ richText,
210
+ files: listUploadFiles,
211
+ currentSession,
212
+ isInternal,
213
+ urlMetadata: resolveUrlMetadata(),
214
+ mentions,
215
+ onBuilt: settle,
216
+ });
187
217
  });
188
- setListUploadFiles([]);
189
- return true;
190
218
  }, [
219
+ t,
191
220
  sendMergeMessage,
192
221
  listUploadFiles,
193
222
  currentSession,
194
223
  openSessionClosedModal,
195
224
  resolveUrlMetadata,
225
+ conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID,
226
+ currentConversationIdRef,
196
227
  ]);
197
228
  const executePendingSend = useCallback((newSession) => {
198
229
  const pending = pendingMessageRef.current;
199
230
  if (!pending)
200
231
  return;
201
232
  const sessionToUse = newSession || currentSession;
202
- sendMergeMessage({
233
+ pendingMessageRef.current = null;
234
+ void sendMergeMessage({
203
235
  plainText: pending.plainText,
204
236
  richText: pending.richText,
205
237
  files: pending.files,
@@ -207,11 +239,20 @@ const MessageFooterProvider = ({ currentSession, openCreateCannedModal, }) => {
207
239
  isInternal: pending.isInternal,
208
240
  urlMetadata: pending.urlMetadata,
209
241
  mentions: pending.mentions,
242
+ onBuilt: (shown) => {
243
+ if (!shown) {
244
+ toastMessage.error(t("message_send_failed_no_retry"));
245
+ return;
246
+ }
247
+ const sentUids = new Set(pending.files.map((file) => file.uid));
248
+ setListUploadFiles((files) => files.filter((file) => !sentUids.has(file.uid)));
249
+ // Only the thread the message was written in has a text composer to clear.
250
+ if (currentConversationIdRef.current !== pending.conversationId)
251
+ return;
252
+ emit("CLEAR_COMPOSER");
253
+ },
210
254
  });
211
- setListUploadFiles([]);
212
- pendingMessageRef.current = null;
213
- emit("CLEAR_COMPOSER");
214
- }, [sendMergeMessage, currentSession]);
255
+ }, [t, sendMergeMessage, currentSession, currentConversationIdRef]);
215
256
  const handleSessionAction = useCallback(async (action) => {
216
257
  const conversationId = conversationData === null || conversationData === void 0 ? void 0 : conversationData.conversationID;
217
258
  if (!conversationId)
@@ -1 +1 @@
1
- {"version":3,"file":"useMentionOptions.d.ts","sourceRoot":"","sources":["../../../../../src/components/message/footer/mention/useMentionOptions.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAoB7C,eAAO,MAAM,iBAAiB,GAC5B,SAAS,MAAM,GAAG,SAAS,EAC3B,OAAO,MAAM,KACZ,aAAa,EA2Df,CAAC"}
1
+ {"version":3,"file":"useMentionOptions.d.ts","sourceRoot":"","sources":["../../../../../src/components/message/footer/mention/useMentionOptions.ts"],"names":[],"mappings":"AAKA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAoB7C,eAAO,MAAM,iBAAiB,GAC5B,SAAS,MAAM,GAAG,SAAS,EAC3B,OAAO,MAAM,KACZ,aAAa,EAwDf,CAAC"}
@@ -2,7 +2,6 @@ import { useEffect, useMemo } from "react";
2
2
  import { useTranslation } from "react-i18next";
3
3
  import { useGroupMembers } from "../../../../hooks/group/useGroupMembers";
4
4
  import { useSearchGroupMembers } from "../../../../hooks/group/useSearchGroupMembers";
5
- import useAuthStore from "../../../../store/auth";
6
5
  export const ALL_MEMBERS_USER_ID = "__all__";
7
6
  // Typing a name loads a big group's remaining members up to this many.
8
7
  const MENTION_MEMBER_LOAD_LIMIT = 10000;
@@ -27,7 +26,6 @@ export const useMentionOptions = (groupID, query) => {
27
26
  // Core keeps only part of a big group locally, so its search alone misses
28
27
  // members; typing a name also loads the rest, merged in as pages arrive.
29
28
  const { results: searchResults } = useSearchGroupMembers(groupID, hasMore ? query : "");
30
- const currentUserID = useAuthStore((state) => state.userID);
31
29
  const hasKeyword = query.trim().length > 0;
32
30
  useEffect(() => {
33
31
  if (!hasKeyword || !hasMore || isLoadingMore)
@@ -42,10 +40,8 @@ export const useMentionOptions = (groupID, query) => {
42
40
  const candidates = keyword
43
41
  ? [...members, ...searchResults.filter((member) => !loadedIDs.has(member.userID))]
44
42
  : members;
45
- // Self-tagging isn't a useful mention target — the sender already
46
- // knows who they are — so the current user is filtered out entirely.
43
+ // The sender stays in the list: the spec lists every group member.
47
44
  const memberOptions = candidates
48
- .filter((member) => member.userID !== currentUserID)
49
45
  .map((member) => ({
50
46
  userID: member.userID,
51
47
  displayName: member.nickname || member.userID,
@@ -69,5 +65,5 @@ export const useMentionOptions = (groupID, query) => {
69
65
  };
70
66
  const matchesAll = !keyword || allOption.displayName.toLowerCase().includes(keyword);
71
67
  return matchesAll ? [allOption, ...memberOptions] : memberOptions;
72
- }, [members, searchResults, query, t, currentUserID]);
68
+ }, [members, searchResults, query, t]);
73
69
  };
@@ -0,0 +1,7 @@
1
+ import { LexicalEditor } from "lexical";
2
+ export declare const resetEditorIfUnchanged: (editor: LexicalEditor, sent: {
3
+ plainText: string;
4
+ richText: string;
5
+ }) => void;
6
+ export declare const resetEditorAfterSend: (editor: LexicalEditor) => void;
7
+ //# sourceMappingURL=resetEditorAfterSend.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resetEditorAfterSend.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/resetEditorAfterSend.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAoE,MAAM,SAAS,CAAC;AAW1G,eAAO,MAAM,sBAAsB,GACjC,QAAQ,aAAa,EACrB,MAAM;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,SAU9C,CAAC;AAIF,eAAO,MAAM,oBAAoB,GAAI,QAAQ,aAAa,SAgDzD,CAAC"}