@droppii-org/chat-sdk 0.4.0 → 0.4.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 (115) hide show
  1. package/dist/assets/images/searchEmpty.png +0 -0
  2. package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
  3. package/dist/components/conversation-detail/ConversationDetailInput.js +142 -4
  4. package/dist/components/conversation-detail/LeaveGroupBlockedModal.d.ts +12 -0
  5. package/dist/components/conversation-detail/LeaveGroupBlockedModal.d.ts.map +1 -0
  6. package/dist/components/conversation-detail/LeaveGroupBlockedModal.js +18 -0
  7. package/dist/components/conversation-detail/LeaveGroupConfirmModal.d.ts +16 -0
  8. package/dist/components/conversation-detail/LeaveGroupConfirmModal.d.ts.map +1 -0
  9. package/dist/components/conversation-detail/LeaveGroupConfirmModal.js +21 -0
  10. package/dist/components/conversation-detail/ThreadProfilePanel.d.ts.map +1 -1
  11. package/dist/components/conversation-detail/ThreadProfilePanel.js +12 -2
  12. package/dist/components/conversation-detail/index.d.ts +4 -0
  13. package/dist/components/conversation-detail/index.d.ts.map +1 -1
  14. package/dist/components/conversation-detail/index.js +2 -0
  15. package/dist/components/conversation-detail/mention/BizMentionMenu.d.ts +15 -0
  16. package/dist/components/conversation-detail/mention/BizMentionMenu.d.ts.map +1 -0
  17. package/dist/components/conversation-detail/mention/BizMentionMenu.js +11 -0
  18. package/dist/components/conversation-detail/mention/buildMentionPayload.d.ts +4 -0
  19. package/dist/components/conversation-detail/mention/buildMentionPayload.d.ts.map +1 -0
  20. package/dist/components/conversation-detail/mention/buildMentionPayload.js +23 -0
  21. package/dist/components/conversation-detail/mention/getCaretCoordinates.d.ts +11 -0
  22. package/dist/components/conversation-detail/mention/getCaretCoordinates.d.ts.map +1 -0
  23. package/dist/components/conversation-detail/mention/getCaretCoordinates.js +36 -0
  24. package/dist/components/conversation-detail/mention/useMentionTokens.d.ts +21 -0
  25. package/dist/components/conversation-detail/mention/useMentionTokens.d.ts.map +1 -0
  26. package/dist/components/conversation-detail/mention/useMentionTokens.js +58 -0
  27. package/dist/components/conversation-detail/mention/useMentionTrigger.d.ts +7 -0
  28. package/dist/components/conversation-detail/mention/useMentionTrigger.d.ts.map +1 -0
  29. package/dist/components/conversation-detail/mention/useMentionTrigger.js +19 -0
  30. package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts +4 -1
  31. package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts.map +1 -1
  32. package/dist/components/conversation-inbox/ConversationInboxHeader.js +3 -2
  33. package/dist/components/conversation-inbox/contact-search/ContactSearchFilters.d.ts +8 -0
  34. package/dist/components/conversation-inbox/contact-search/ContactSearchFilters.d.ts.map +1 -0
  35. package/dist/components/conversation-inbox/contact-search/ContactSearchFilters.js +19 -0
  36. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts +7 -0
  37. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts.map +1 -0
  38. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.js +116 -0
  39. package/dist/components/conversation-inbox/contact-search/ContactSearchResultItem.d.ts +9 -0
  40. package/dist/components/conversation-inbox/contact-search/ContactSearchResultItem.d.ts.map +1 -0
  41. package/dist/components/conversation-inbox/contact-search/ContactSearchResultItem.js +34 -0
  42. package/dist/components/conversation-inbox/contact-search/ContactSearchSectionHeader.d.ts +8 -0
  43. package/dist/components/conversation-inbox/contact-search/ContactSearchSectionHeader.d.ts.map +1 -0
  44. package/dist/components/conversation-inbox/contact-search/ContactSearchSectionHeader.js +7 -0
  45. package/dist/components/conversation-inbox/contact-search/ContactSearchStateMessage.d.ts +8 -0
  46. package/dist/components/conversation-inbox/contact-search/ContactSearchStateMessage.d.ts.map +1 -0
  47. package/dist/components/conversation-inbox/contact-search/ContactSearchStateMessage.js +9 -0
  48. package/dist/components/conversation-inbox/contact-search/ContactSearchTabs.d.ts +10 -0
  49. package/dist/components/conversation-inbox/contact-search/ContactSearchTabs.d.ts.map +1 -0
  50. package/dist/components/conversation-inbox/contact-search/ContactSearchTabs.js +39 -0
  51. package/dist/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.d.ts +6 -0
  52. package/dist/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.d.ts.map +1 -0
  53. package/dist/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.js +9 -0
  54. package/dist/components/conversation-inbox/contact-search/constants.d.ts +18 -0
  55. package/dist/components/conversation-inbox/contact-search/constants.d.ts.map +1 -0
  56. package/dist/components/conversation-inbox/contact-search/constants.js +23 -0
  57. package/dist/components/conversation-inbox/contact-search/index.d.ts +10 -0
  58. package/dist/components/conversation-inbox/contact-search/index.d.ts.map +1 -0
  59. package/dist/components/conversation-inbox/contact-search/index.js +9 -0
  60. package/dist/components/conversation-inbox/contact-search/utils.d.ts +6 -0
  61. package/dist/components/conversation-inbox/contact-search/utils.d.ts.map +1 -0
  62. package/dist/components/conversation-inbox/contact-search/utils.js +44 -0
  63. package/dist/components/message/footer/mention/useMentionOptions.d.ts +1 -0
  64. package/dist/components/message/footer/mention/useMentionOptions.d.ts.map +1 -1
  65. package/dist/components/message/footer/mention/useMentionOptions.js +1 -1
  66. package/dist/components/search-conversation/SearchAll.d.ts.map +1 -1
  67. package/dist/components/search-conversation/SearchAll.js +9 -3
  68. package/dist/components/search-conversation/SearchConversationAsGroups.d.ts +6 -0
  69. package/dist/components/search-conversation/SearchConversationAsGroups.d.ts.map +1 -0
  70. package/dist/components/search-conversation/SearchConversationAsGroups.js +20 -0
  71. package/dist/components/search-conversation/index.d.ts +2 -1
  72. package/dist/components/search-conversation/index.d.ts.map +1 -1
  73. package/dist/components/search-conversation/index.js +7 -0
  74. package/dist/components/search-conversation/item/SearchItemAsGroup.d.ts +8 -0
  75. package/dist/components/search-conversation/item/SearchItemAsGroup.d.ts.map +1 -0
  76. package/dist/components/search-conversation/item/SearchItemAsGroup.js +34 -0
  77. package/dist/constants/images.d.ts +1 -0
  78. package/dist/constants/images.d.ts.map +1 -1
  79. package/dist/constants/images.js +2 -0
  80. package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
  81. package/dist/hooks/global/useGlobalEvent.js +11 -1
  82. package/dist/hooks/group/useConversationGroups.d.ts +5 -1
  83. package/dist/hooks/group/useConversationGroups.d.ts.map +1 -1
  84. package/dist/hooks/group/useConversationGroups.js +3 -3
  85. package/dist/hooks/group/useLeaveGroupAction.d.ts +24 -0
  86. package/dist/hooks/group/useLeaveGroupAction.d.ts.map +1 -0
  87. package/dist/hooks/group/useLeaveGroupAction.js +79 -0
  88. package/dist/hooks/group/useQuitGroup.d.ts +6 -0
  89. package/dist/hooks/group/useQuitGroup.d.ts.map +1 -0
  90. package/dist/hooks/group/useQuitGroup.js +62 -0
  91. package/dist/hooks/search/useSearchContacts.d.ts +18 -0
  92. package/dist/hooks/search/useSearchContacts.d.ts.map +1 -0
  93. package/dist/hooks/search/useSearchContacts.js +39 -0
  94. package/dist/hooks/search/useSearchGroups.d.ts +16 -0
  95. package/dist/hooks/search/useSearchGroups.d.ts.map +1 -0
  96. package/dist/hooks/search/useSearchGroups.js +65 -0
  97. package/dist/locales/vi/common.json +3 -0
  98. package/dist/locales/vi/conversation-inbox.json +36 -2
  99. package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
  100. package/dist/screens/conversation-inbox/index.js +14 -3
  101. package/dist/services/query.d.ts +2 -0
  102. package/dist/services/query.d.ts.map +1 -1
  103. package/dist/services/query.js +2 -0
  104. package/dist/services/routes.d.ts +2 -0
  105. package/dist/services/routes.d.ts.map +1 -1
  106. package/dist/services/routes.js +2 -0
  107. package/dist/store/conversation.d.ts.map +1 -1
  108. package/dist/store/conversation.js +3 -0
  109. package/dist/store/type.d.ts +2 -0
  110. package/dist/store/type.d.ts.map +1 -1
  111. package/dist/styles/global.css +1 -1
  112. package/dist/types/dto.d.ts +29 -1
  113. package/dist/types/dto.d.ts.map +1 -1
  114. package/dist/types/dto.js +10 -1
  115. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAM/D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,QAAA,MAAM,uBAAuB,GAAI,4EAM9B,4BAA4B,4CAya9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAiB/D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;CAC5B;AAED,QAAA,MAAM,uBAAuB,GAAI,4EAM9B,4BAA4B,4CAmlB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -17,10 +17,17 @@ import { FilePreviewList } from "../../components/message/footer/FilePreview";
17
17
  import LinkInputPreview from "../../components/message/footer/LinkInputPreview";
18
18
  import { ReplyPreviewBar } from "../../components/message/reply/ReplyPreviewBar";
19
19
  import ConversationMoreOptionPanel from "./ConversationMoreOptionPanel";
20
+ import { useMentionTrigger } from "./mention/useMentionTrigger";
21
+ import { useMentionOptions } from "../../components/message/footer/mention/useMentionOptions";
22
+ import { useMentionTokens } from "./mention/useMentionTokens";
23
+ import BizMentionMenu from "./mention/BizMentionMenu";
24
+ import { getCaretCoordinates, disposeCaretCoordinates, } from "./mention/getCaretCoordinates";
25
+ import { buildMentionPayload } from "./mention/buildMentionPayload";
20
26
  const MAX_LINES = 5;
21
27
  const CONTAINER_MIN_HEIGHT = 48;
22
28
  const FALLBACK_LINE_HEIGHT = 25.6;
23
29
  const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, moreOptions, onOpenBotMenu, }) => {
30
+ var _a;
24
31
  const { t } = useTranslation("conversation-inbox");
25
32
  const { t: tCommon } = useTranslation("common");
26
33
  const conversationData = useConversationStore((state) => state.conversationData);
@@ -46,6 +53,20 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
46
53
  const [isSending, setIsSending] = useState(false);
47
54
  const [previewDismissed, setPreviewDismissed] = useState(false);
48
55
  const [isMoreOptionOpen, { setFalse: closeMoreOption, toggle: toggleMoreOption },] = useBoolean(false);
56
+ const [caretIndex, setCaretIndex] = useState(0);
57
+ const rawMentionTrigger = useMentionTrigger(value, caretIndex);
58
+ // Escape dismisses the menu for the CURRENT trigger span without erasing
59
+ // the typed "@query" text; a new "@" (different triggerStart) or the
60
+ // trigger closing naturally clears the dismissal automatically.
61
+ const [dismissedTriggerStart, setDismissedTriggerStart] = useState(null);
62
+ const mentionTrigger = (rawMentionTrigger === null || rawMentionTrigger === void 0 ? void 0 : rawMentionTrigger.triggerStart) === dismissedTriggerStart
63
+ ? null
64
+ : rawMentionTrigger;
65
+ const mentionOptions = useMentionOptions(conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID, (_a = mentionTrigger === null || mentionTrigger === void 0 ? void 0 : mentionTrigger.query) !== null && _a !== void 0 ? _a : "");
66
+ const isMentionMenuOpen = mentionTrigger !== null;
67
+ const [selectedMentionIndex, setSelectedMentionIndex] = useState(0);
68
+ const { tokens: mentionTokens, addToken, onValueChange, resetTokens } = useMentionTokens();
69
+ const [mentionMenuPosition, setMentionMenuPosition] = useState(null);
49
70
  const previewUrl = useMemo(() => {
50
71
  const urls = extractLinkUrls(value);
51
72
  return urls.length === 1 ? urls[0] : null;
@@ -89,7 +110,29 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
89
110
  closeMoreOption();
90
111
  setListUploadFiles([]);
91
112
  setIsVideoLoading(false);
92
- }, [conversationId, userID, closeMoreOption]);
113
+ resetTokens();
114
+ }, [conversationId, userID, closeMoreOption, resetTokens]);
115
+ useEffect(() => {
116
+ setSelectedMentionIndex(0);
117
+ }, [mentionTrigger === null || mentionTrigger === void 0 ? void 0 : mentionTrigger.triggerStart, mentionTrigger === null || mentionTrigger === void 0 ? void 0 : mentionTrigger.query]);
118
+ useEffect(() => {
119
+ const textarea = textareaRef.current;
120
+ if (!isMentionMenuOpen || !textarea) {
121
+ setMentionMenuPosition(null);
122
+ return;
123
+ }
124
+ const coords = getCaretCoordinates(textarea, caretIndex, {
125
+ viewportHeight: window.innerHeight,
126
+ });
127
+ setMentionMenuPosition({ top: coords.top, left: coords.left });
128
+ }, [isMentionMenuOpen, caretIndex, value]);
129
+ useEffect(() => {
130
+ const textarea = textareaRef.current;
131
+ return () => {
132
+ if (textarea)
133
+ disposeCaretCoordinates(textarea);
134
+ };
135
+ }, []);
93
136
  useEffect(() => {
94
137
  var _a;
95
138
  if (!conversationId || !userID) {
@@ -159,18 +202,33 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
159
202
  setListUploadFiles((prev) => prev.filter((f) => f.uid !== uid));
160
203
  setIsVideoLoading(false);
161
204
  }, []);
162
- const handleChange = (nextValue) => {
205
+ const handleChange = (nextValue, nextCaretIndex) => {
206
+ onValueChange(value, nextValue);
163
207
  setValue(nextValue);
208
+ // Update caretIndex directly from the change event rather than relying
209
+ // solely on the textarea's native `select` event — some environments
210
+ // (and possibly some assistive/automation input paths) don't fire
211
+ // `select` on every keystroke, which would silently desync caretIndex
212
+ // from the actual caret and break mention-trigger detection.
213
+ setCaretIndex(nextCaretIndex);
164
214
  if (conversationId && userID) {
165
215
  setDraft(userID, conversationId, nextValue);
166
216
  }
167
217
  onInputChange === null || onInputChange === void 0 ? void 0 : onInputChange(nextValue);
168
218
  };
219
+ // Covers caret-only movement (click, arrow keys) with no value change,
220
+ // which handleChange's onChange path doesn't fire for — the trigger can
221
+ // close/open purely from moving the caret.
222
+ const handleSelectionChange = (event) => {
223
+ var _a;
224
+ setCaretIndex((_a = event.currentTarget.selectionStart) !== null && _a !== void 0 ? _a : 0);
225
+ };
169
226
  const resetComposer = () => {
170
227
  setValue("");
171
228
  setListUploadFiles([]);
172
229
  setIsVideoLoading(false);
173
230
  setPreviewDismissed(false);
231
+ resetTokens();
174
232
  if (conversationId && userID) {
175
233
  clearDraft(userID, conversationId);
176
234
  }
@@ -189,6 +247,7 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
189
247
  image: previewMetadata.image,
190
248
  }
191
249
  : undefined;
250
+ const mentions = mentionTokens.length > 0 ? buildMentionPayload(mentionTokens) : undefined;
192
251
  resetComposer();
193
252
  setIsSending(true);
194
253
  try {
@@ -202,6 +261,7 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
202
261
  files: filesToSend,
203
262
  isInternal: false,
204
263
  urlMetadata,
264
+ mentions,
205
265
  });
206
266
  }
207
267
  }
@@ -230,14 +290,89 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
230
290
  const insertNewline = (textarea) => {
231
291
  const { selectionStart, selectionEnd } = textarea;
232
292
  const nextValue = value.slice(0, selectionStart) + "\n" + value.slice(selectionEnd);
233
- handleChange(nextValue);
293
+ handleChange(nextValue, selectionStart + 1);
234
294
  requestAnimationFrame(() => {
235
295
  textarea.selectionStart = selectionStart + 1;
236
296
  textarea.selectionEnd = selectionStart + 1;
237
297
  adjustTextareaHeight();
238
298
  });
239
299
  };
300
+ const selectMentionOption = (option) => {
301
+ if (!mentionTrigger)
302
+ return;
303
+ const insertedText = "@" + option.displayName;
304
+ const nextValue = value.slice(0, mentionTrigger.triggerStart) +
305
+ insertedText +
306
+ " " +
307
+ value.slice(caretIndex);
308
+ const newToken = {
309
+ userID: option.userID,
310
+ displayName: option.displayName,
311
+ isAtAll: option.isAtAll,
312
+ start: mentionTrigger.triggerStart,
313
+ length: insertedText.length,
314
+ };
315
+ onValueChange(value, nextValue);
316
+ addToken(newToken);
317
+ setValue(nextValue);
318
+ if (conversationId && userID) {
319
+ setDraft(userID, conversationId, nextValue);
320
+ }
321
+ onInputChange === null || onInputChange === void 0 ? void 0 : onInputChange(nextValue);
322
+ const nextCaret = mentionTrigger.triggerStart + insertedText.length + 1;
323
+ // Update caretIndex synchronously (not deferred to the RAF below) so
324
+ // mentionTrigger is recomputed against the post-insertion caret
325
+ // immediately — otherwise a key pressed before the RAF fires (e.g. a
326
+ // fast Enter right after a click-to-select) would still see the old
327
+ // caretIndex against the new value, which can spuriously re-match the
328
+ // trigger regex and reopen the menu.
329
+ setCaretIndex(nextCaret);
330
+ // Selecting from the dropdown (mouse click) can move focus off the
331
+ // textarea onto the option button — return it and set the caret
332
+ // synchronously (not deferred to a RAF) so the composer is immediately
333
+ // ready for the next keystroke, with no window where a fast follow-up
334
+ // input could race a still-pending caret write.
335
+ const textarea = textareaRef.current;
336
+ if (textarea) {
337
+ textarea.focus();
338
+ textarea.selectionStart = nextCaret;
339
+ textarea.selectionEnd = nextCaret;
340
+ }
341
+ requestAnimationFrame(adjustTextareaHeight);
342
+ };
343
+ const closeMentionMenu = () => {
344
+ if (mentionTrigger)
345
+ setDismissedTriggerStart(mentionTrigger.triggerStart);
346
+ };
240
347
  const handleKeyDown = (event) => {
348
+ if (isMentionMenuOpen) {
349
+ if (event.key === "ArrowDown") {
350
+ event.preventDefault();
351
+ setSelectedMentionIndex((prev) => mentionOptions.length === 0
352
+ ? 0
353
+ : (prev + 1) % mentionOptions.length);
354
+ return;
355
+ }
356
+ if (event.key === "ArrowUp") {
357
+ event.preventDefault();
358
+ setSelectedMentionIndex((prev) => mentionOptions.length === 0
359
+ ? 0
360
+ : (prev - 1 + mentionOptions.length) % mentionOptions.length);
361
+ return;
362
+ }
363
+ if (event.key === "Enter" || event.key === "Tab") {
364
+ event.preventDefault();
365
+ const selected = mentionOptions[selectedMentionIndex];
366
+ if (selected)
367
+ selectMentionOption(selected);
368
+ return;
369
+ }
370
+ if (event.key === "Escape") {
371
+ event.preventDefault();
372
+ closeMentionMenu();
373
+ return;
374
+ }
375
+ }
241
376
  if (event.key !== "Enter")
242
377
  return;
243
378
  if (event.shiftKey) {
@@ -253,6 +388,9 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
253
388
  }
254
389
  return (_jsxs("div", { ref: containerRef, "data-testid": "conversation-detail-input", "data-cp-name": "ConversationDetailInput", className: "flex w-full shrink-0 flex-col bg-chat-bubble-neutral/[0.05]", children: [_jsx("input", { ref: mediaInputRef, type: "file", accept: "image/jpeg,image/png,image/jpg,video/*", multiple: true, className: "hidden", onChange: handleMediaInputChange }), _jsx("input", { ref: documentInputRef, type: "file", accept: ".pdf,.doc,.docx", multiple: true, className: "hidden", onChange: handleDocumentInputChange }), _jsx(ReplyPreviewBar, {}), listUploadFiles.length > 0 && (_jsx(FilePreviewList, { files: listUploadFiles, onRemove: (file) => handleRemoveFile(file.uid), onVideoReady: () => setIsVideoLoading(false) })), showLinkPreview && previewUrl && (_jsx(LinkInputPreview, { url: previewUrl, onRemove: () => setPreviewDismissed(true) })), _jsxs("div", { className: "flex items-end gap-3 px-3 py-2", children: [_jsx("div", { className: "flex shrink-0 items-center", children: _jsx("button", { type: "button", onClick: handleToggleMoreOption, "aria-label": t("thread_detail.more_option.toggle"), "aria-expanded": isMoreOptionOpen, "data-testid": "conversation-detail-more-option-toggle", className: "flex py-3 items-center justify-center rounded-full", children: isMoreOptionOpen ? (_jsx(Icon, { icon: "close-circle-b", size: 24, className: "text-primary-400" })) : (_jsx(Icon, { icon: "plus-circle-o", size: 24, className: "text-gray-900" })) }) }), _jsx("div", { className: clsx("flex min-w-0 flex-1 overflow-hidden rounded-xl border bg-white transition-shadow", isFocused
255
390
  ? "border-primary-200 shadow-primary-focus"
256
- : "border-gray-50"), style: { minHeight: CONTAINER_MIN_HEIGHT }, children: _jsxs("div", { className: "flex min-w-0 flex-[1_0_0] items-center gap-2 self-stretch px-3 py-1", children: [onOpenBotMenu && (_jsx("button", { type: "button", onClick: onOpenBotMenu, "aria-label": t("thread_detail.bot.menu.open_menu"), "data-testid": "conversation-detail-open-bot-menu", className: "flex shrink-0 items-center justify-center", children: _jsx(Icon, { icon: "chat-suggest-o", size: 24, className: "text-gray-600" }) })), _jsx("textarea", { ref: textareaRef, value: value, rows: 1, onChange: (event) => handleChange(event.target.value), onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : t("thread_detail.input_placeholder"), "data-testid": "conversation-detail-message-input", className: "conversation-message-input-scrollbar min-h-0 min-w-0 flex-1 resize-none appearance-none border-0 bg-transparent p-0 text-[16px] leading-[1.6] tracking-[0.16px] text-black outline-none ring-0 placeholder:text-gray-400 focus:border-0 focus:outline-none focus:ring-0" })] }) }), _jsx("div", { className: "flex shrink-0 items-center pb-1", children: _jsx("button", { type: "button", onClick: () => void handleSend(), disabled: !canSend, "data-testid": "conversation-detail-send", className: clsx("flex h-10 w-10 items-center justify-center rounded-full", canSend ? "bg-chat-send" : "bg-chat-send opacity-25"), children: _jsx(Icon, { icon: "send-b", size: 20, className: "text-white" }) }) })] }), isMoreOptionOpen && (_jsx(ConversationMoreOptionPanel, { disabled: !isConversationReady, options: moreOptions, onTriggerMediaInput: () => { var _a; return (_a = mediaInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, onTriggerDocumentInput: () => { var _a; return (_a = documentInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); } }))] }));
391
+ : "border-gray-50"), style: { minHeight: CONTAINER_MIN_HEIGHT }, children: _jsxs("div", { className: "flex min-w-0 flex-[1_0_0] items-center gap-2 self-stretch px-3 py-1", children: [onOpenBotMenu && (_jsx("button", { type: "button", onClick: onOpenBotMenu, "aria-label": t("thread_detail.bot.menu.open_menu"), "data-testid": "conversation-detail-open-bot-menu", className: "flex shrink-0 items-center justify-center", children: _jsx(Icon, { icon: "chat-suggest-o", size: 24, className: "text-gray-600" }) })), _jsx("textarea", { ref: textareaRef, value: value, rows: 1, onChange: (event) => {
392
+ var _a;
393
+ return handleChange(event.target.value, (_a = event.target.selectionStart) !== null && _a !== void 0 ? _a : event.target.value.length);
394
+ }, onSelect: handleSelectionChange, onFocus: handleFocus, onBlur: handleBlur, onKeyDown: handleKeyDown, placeholder: placeholder !== null && placeholder !== void 0 ? placeholder : t("thread_detail.input_placeholder"), "data-testid": "conversation-detail-message-input", className: "conversation-message-input-scrollbar min-h-0 min-w-0 flex-1 resize-none appearance-none border-0 bg-transparent p-0 text-[16px] leading-[1.6] tracking-[0.16px] text-black outline-none ring-0 placeholder:text-gray-400 focus:border-0 focus:outline-none focus:ring-0" }), isMentionMenuOpen && mentionMenuPosition && (_jsx(BizMentionMenu, { options: mentionOptions, selectedIndex: selectedMentionIndex, onSelect: selectMentionOption, onHoverIndex: setSelectedMentionIndex, position: mentionMenuPosition, visible: true }))] }) }), _jsx("div", { className: "flex shrink-0 items-center pb-1", children: _jsx("button", { type: "button", onClick: () => void handleSend(), disabled: !canSend, "data-testid": "conversation-detail-send", className: clsx("flex h-10 w-10 items-center justify-center rounded-full", canSend ? "bg-chat-send" : "bg-chat-send opacity-25"), children: _jsx(Icon, { icon: "send-b", size: 20, className: "text-white" }) }) })] }), isMoreOptionOpen && (_jsx(ConversationMoreOptionPanel, { disabled: !isConversationReady, options: moreOptions, onTriggerMediaInput: () => { var _a; return (_a = mediaInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); }, onTriggerDocumentInput: () => { var _a; return (_a = documentInputRef.current) === null || _a === void 0 ? void 0 : _a.click(); } }))] }));
257
395
  };
258
396
  export default ConversationDetailInput;
@@ -0,0 +1,12 @@
1
+ export interface LeaveGroupBlockedModalProps {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ }
5
+ /**
6
+ * Figma blocking modal shown when a group owner taps "Rời nhóm": centered
7
+ * title and body in one padded block (no divider between them), then a
8
+ * full-width solid primary action.
9
+ */
10
+ declare const LeaveGroupBlockedModal: ({ open, onClose, }: LeaveGroupBlockedModalProps) => import("react/jsx-runtime").JSX.Element;
11
+ export default LeaveGroupBlockedModal;
12
+ //# sourceMappingURL=LeaveGroupBlockedModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LeaveGroupBlockedModal.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/LeaveGroupBlockedModal.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;;;GAIG;AACH,QAAA,MAAM,sBAAsB,GAAI,oBAG7B,2BAA2B,4CA6C7B,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
@@ -0,0 +1,18 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Modal, Button } from "antd";
4
+ import { useTranslation } from "react-i18next";
5
+ const MODAL_WIDTH = 400;
6
+ /**
7
+ * Figma blocking modal shown when a group owner taps "Rời nhóm": centered
8
+ * title and body in one padded block (no divider between them), then a
9
+ * full-width solid primary action.
10
+ */
11
+ const LeaveGroupBlockedModal = ({ open, onClose, }) => {
12
+ const { t } = useTranslation("conversation-inbox");
13
+ return (_jsx(Modal, { "data-cp-name": "LeaveGroupBlockedModal", open: open, onCancel: onClose, footer: null, closable: false, centered: true, width: MODAL_WIDTH, styles: {
14
+ content: { padding: 0, overflow: "hidden", borderRadius: 16 },
15
+ body: { padding: 0 },
16
+ }, children: _jsxs("div", { role: "alertdialog", "aria-label": t("thread_detail.leave_group.blocked_title"), className: "flex flex-col bg-white", children: [_jsxs("div", { className: "flex flex-col gap-3 p-4 text-center text-gray-900", children: [_jsx("p", { className: "text-[18px] font-bold leading-[1.6]", children: t("thread_detail.leave_group.blocked_title") }), _jsx("p", { className: "text-[16px] leading-[1.6] tracking-[0.16px]", children: t("thread_detail.leave_group.blocked_desc") })] }), _jsx("div", { className: "border-t border-gray-25" }), _jsx("div", { className: "p-4", children: _jsx(Button, { "data-testid": "leave-group-blocked-confirm", type: "primary", className: "h-12 w-full !rounded-lg !border-primary-400 !bg-primary-400 !text-[16px] !font-medium !text-white hover:!border-primary-300 hover:!bg-primary-300", onClick: onClose, children: t("thread_detail.leave_group.blocked_confirm") }) })] }) }));
17
+ };
18
+ export default LeaveGroupBlockedModal;
@@ -0,0 +1,16 @@
1
+ export interface LeaveGroupConfirmModalProps {
2
+ open: boolean;
3
+ loading: boolean;
4
+ isPublicGroup: boolean;
5
+ onCancel: () => void;
6
+ onConfirm: () => void;
7
+ }
8
+ /**
9
+ * Figma "Rời nhóm" confirmation: centered title over a divider, left-aligned
10
+ * body, then two equal-width actions with a solid danger primary. Kept local
11
+ * to the feature instead of the shared ConfirmModal, whose antd footer puts
12
+ * auto-width buttons on the right.
13
+ */
14
+ declare const LeaveGroupConfirmModal: ({ open, loading, isPublicGroup, onCancel, onConfirm, }: LeaveGroupConfirmModalProps) => import("react/jsx-runtime").JSX.Element;
15
+ export default LeaveGroupConfirmModal;
16
+ //# sourceMappingURL=LeaveGroupConfirmModal.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LeaveGroupConfirmModal.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/LeaveGroupConfirmModal.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,2BAA2B;IAC1C,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,aAAa,EAAE,OAAO,CAAC;IACvB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAED;;;;;GAKG;AACH,QAAA,MAAM,sBAAsB,GAAI,wDAM7B,2BAA2B,4CAiE7B,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
@@ -0,0 +1,21 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Modal, Button } from "antd";
4
+ import { Trans, useTranslation } from "react-i18next";
5
+ const MODAL_WIDTH = 400;
6
+ /**
7
+ * Figma "Rời nhóm" confirmation: centered title over a divider, left-aligned
8
+ * body, then two equal-width actions with a solid danger primary. Kept local
9
+ * to the feature instead of the shared ConfirmModal, whose antd footer puts
10
+ * auto-width buttons on the right.
11
+ */
12
+ const LeaveGroupConfirmModal = ({ open, loading, isPublicGroup, onCancel, onConfirm, }) => {
13
+ const { t } = useTranslation("conversation-inbox");
14
+ return (_jsx(Modal, { "data-cp-name": "LeaveGroupConfirmModal", open: open, onCancel: onCancel, footer: null, closable: false, centered: true, width: MODAL_WIDTH, maskClosable: !loading, keyboard: !loading, styles: {
15
+ content: { padding: 0, overflow: "hidden", borderRadius: 16 },
16
+ body: { padding: 0 },
17
+ }, children: _jsxs("div", { role: "alertdialog", "aria-label": t("thread_detail.leave_group.confirm_title"), className: "flex flex-col bg-white", children: [_jsx("div", { className: "border-b border-gray-25 p-4", children: _jsx("p", { className: "text-center text-[18px] font-bold leading-[1.6] text-gray-900", children: t("thread_detail.leave_group.confirm_title") }) }), _jsx("p", { className: "p-4 text-[16px] leading-[1.6] tracking-[0.16px] text-gray-900", children: _jsx(Trans, { t: t, i18nKey: isPublicGroup
18
+ ? "thread_detail.leave_group.confirm_desc_public"
19
+ : "thread_detail.leave_group.confirm_desc_private", components: { bold: _jsx("strong", { className: "font-bold" }) } }) }), _jsx("div", { className: "border-t border-gray-25" }), _jsxs("div", { className: "flex gap-3 p-4", children: [_jsx(Button, { "data-testid": "leave-group-cancel", className: "h-12 flex-1 !rounded-lg !border-gray-25 !bg-gray-25 !text-[16px] !font-medium !text-gray-900", onClick: onCancel, disabled: loading, children: t("thread_detail.leave_group.confirm_secondary") }), _jsx(Button, { "data-testid": "leave-group-confirm", type: "primary", className: "h-12 flex-1 !rounded-lg !border-danger-400 !bg-danger-400 !text-[16px] !font-medium !text-white hover:!border-danger-300 hover:!bg-danger-300", onClick: onConfirm, loading: loading, children: t("thread_detail.leave_group.confirm_primary") })] })] }) }));
20
+ };
21
+ export default LeaveGroupConfirmModal;
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadProfilePanel.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ThreadProfilePanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAW7E,UAAU,uBAAuB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gBAAgB,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACzD;AAED,QAAA,MAAM,kBAAkB,GAAI,oDAKzB,uBAAuB,4CAmEzB,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"ThreadProfilePanel.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ThreadProfilePanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAc7E,UAAU,uBAAuB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gBAAgB,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;CACzD;AAED,QAAA,MAAM,kBAAkB,GAAI,oDAKzB,uBAAuB,4CA8GzB,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -5,8 +5,12 @@ import clsx from "clsx";
5
5
  import { useTranslation } from "react-i18next";
6
6
  import { Icon } from "../../components/icon";
7
7
  import MediaEntryCard from "../../components/media-resources/MediaEntryCard";
8
+ import { PeerType } from "../../types/conversation";
8
9
  import { resolveConversationDisplayName, resolvePeerBadgeColor, resolvePeerBadgeLabel, } from "../../utils/conversation";
9
10
  import { useConversationAvatarUrl } from "../../hooks/conversation/useConversationAvatarUrl";
11
+ import { useLeaveGroupAction } from "../../hooks/group/useLeaveGroupAction";
12
+ import LeaveGroupBlockedModal from "./LeaveGroupBlockedModal";
13
+ import LeaveGroupConfirmModal from "./LeaveGroupConfirmModal";
10
14
  const PROFILE_AVATAR_SIZE = 96;
11
15
  const ThreadProfilePanel = ({ open, conversation, onClose, onSelectResource, }) => {
12
16
  const { t } = useTranslation("conversation-inbox");
@@ -14,8 +18,14 @@ const ThreadProfilePanel = ({ open, conversation, onClose, onSelectResource, })
14
18
  const avatarUrl = useConversationAvatarUrl(conversation);
15
19
  const peerType = conversation.peerType;
16
20
  const badgeLabel = resolvePeerBadgeLabel(peerType, t);
17
- return (_jsx(Drawer, { open: open, onClose: onClose, placement: "right", closeIcon: false, width: "100%", "data-cp-name": "ThreadProfilePanel", styles: {
21
+ const { canLeaveGroup, isPublicGroup, isLeaving, isConfirmOpen, isBlockedOpen, openLeaveGroup, confirmLeaveGroup, closeConfirm, closeBlocked, } = useLeaveGroupAction({
22
+ groupID: conversation.groupID,
23
+ conversationID: conversation.conversationID,
24
+ isGroupThread: peerType === PeerType.GROUP,
25
+ onLeft: onClose,
26
+ });
27
+ return (_jsxs(Drawer, { open: open, onClose: onClose, placement: "right", closeIcon: false, width: "100%", "data-cp-name": "ThreadProfilePanel", styles: {
18
28
  body: { padding: 0 },
19
- }, getContainer: false, forceRender: true, destroyOnHidden: true, children: _jsxs("div", { className: "flex h-full flex-col bg-gray-25", children: [_jsxs("div", { className: "flex w-full items-center bg-white p-2", children: [_jsx("button", { type: "button", "data-testid": "thread-profile-back", className: "flex shrink-0 cursor-pointer items-center justify-center p-2 text-primary-400", onClick: onClose, children: _jsx(Icon, { icon: "arrow-left-o", size: 24 }) }), _jsx("p", { className: "min-w-0 flex-1 truncate px-2 text-[18px] font-bold leading-[1.6] text-gray-900", children: t("thread_detail.options_title") })] }), _jsxs("div", { className: "flex flex-col items-center gap-2 bg-white px-0 py-2", children: [_jsxs("div", { className: "relative h-24 w-24 shrink-0", children: [_jsx(Avatar, { size: PROFILE_AVATAR_SIZE, src: avatarUrl || undefined, alt: displayName, className: "uppercase", children: (displayName === null || displayName === void 0 ? void 0 : displayName.charAt(0)) || "A" }), badgeLabel && (_jsx("span", { className: clsx("absolute bottom-1 right-0 flex h-[17px] items-center justify-center rounded-md border border-white px-1 text-[10px] font-semibold leading-3 text-white shadow-badge-inset", resolvePeerBadgeColor(peerType)), children: badgeLabel }))] }), _jsx("p", { className: "w-full truncate text-center text-[18px] font-bold leading-[1.6] text-gray-900", children: displayName })] }), _jsx("div", { className: "flex flex-col px-3 py-1.5", children: _jsx(MediaEntryCard, { onSelect: onSelectResource }) })] }) }));
29
+ }, getContainer: false, forceRender: true, destroyOnHidden: true, children: [_jsxs("div", { className: "flex h-full flex-col bg-gray-25", children: [_jsxs("div", { className: "flex w-full items-center bg-white p-2", children: [_jsx("button", { type: "button", "data-testid": "thread-profile-back", className: "flex shrink-0 cursor-pointer items-center justify-center p-2 text-primary-400", onClick: onClose, children: _jsx(Icon, { icon: "arrow-left-o", size: 24 }) }), _jsx("p", { className: "min-w-0 flex-1 truncate px-2 text-[18px] font-bold leading-[1.6] text-gray-900", children: t("thread_detail.options_title") })] }), _jsxs("div", { className: "flex flex-col items-center gap-2 bg-white px-0 py-2", children: [_jsxs("div", { className: "relative h-24 w-24 shrink-0", children: [_jsx(Avatar, { size: PROFILE_AVATAR_SIZE, src: avatarUrl || undefined, alt: displayName, className: "uppercase", children: (displayName === null || displayName === void 0 ? void 0 : displayName.charAt(0)) || "A" }), badgeLabel && (_jsx("span", { className: clsx("absolute bottom-1 right-0 flex h-[17px] items-center justify-center rounded-md border border-white px-1 text-[10px] font-semibold leading-3 text-white shadow-badge-inset", resolvePeerBadgeColor(peerType)), children: badgeLabel }))] }), _jsx("p", { className: "w-full truncate text-center text-[18px] font-bold leading-[1.6] text-gray-900", children: displayName })] }), _jsx("div", { className: "flex flex-col px-3 py-1.5", children: _jsx(MediaEntryCard, { onSelect: onSelectResource }) }), canLeaveGroup && (_jsx("div", { className: "flex flex-col px-3 py-1.5", children: _jsx("div", { className: "overflow-hidden rounded-xl bg-white", children: _jsxs("button", { type: "button", "data-testid": "thread-profile-leave-group", className: "flex w-full cursor-pointer items-center gap-2 p-4 text-danger-400", onClick: openLeaveGroup, children: [_jsx(Icon, { icon: "logout-o", size: 24 }), _jsx("span", { className: "text-[16px] font-medium leading-[1.6]", children: t("thread_detail.leave_group.action") })] }) }) }))] }), _jsx(LeaveGroupConfirmModal, { open: isConfirmOpen, loading: isLeaving, isPublicGroup: isPublicGroup, onCancel: closeConfirm, onConfirm: confirmLeaveGroup }), _jsx(LeaveGroupBlockedModal, { open: isBlockedOpen, onClose: closeBlocked })] }));
20
30
  };
21
31
  export default ThreadProfilePanel;
@@ -5,6 +5,10 @@ export type { ConversationDetailInputProps } from "./ConversationDetailInput";
5
5
  export { default as ConversationMessageList } from "./ConversationMessageList";
6
6
  export { default as PinnedMessageBar } from "./PinnedMessageBar";
7
7
  export { default as PinLimitModal } from "./PinLimitModal";
8
+ export { default as LeaveGroupConfirmModal } from "./LeaveGroupConfirmModal";
9
+ export type { LeaveGroupConfirmModalProps } from "./LeaveGroupConfirmModal";
10
+ export { default as LeaveGroupBlockedModal } from "./LeaveGroupBlockedModal";
11
+ export type { LeaveGroupBlockedModalProps } from "./LeaveGroupBlockedModal";
8
12
  export { default as ConversationMoreOptionPanel } from "./ConversationMoreOptionPanel";
9
13
  export type { ConversationMoreOptionPanelProps } from "./ConversationMoreOptionPanel";
10
14
  export { default as ConversationMoreOptionItem } from "./ConversationMoreOptionItem";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACjF,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAEhF,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAC/E,YAAY,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAE9E,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAE/E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE3D,OAAO,EAAE,OAAO,IAAI,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AACvF,YAAY,EAAE,gCAAgC,EAAE,MAAM,+BAA+B,CAAC;AACtF,OAAO,EAAE,OAAO,IAAI,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AACrF,YAAY,EAAE,+BAA+B,EAAE,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACjF,YAAY,EAAE,6BAA6B,EAAE,MAAM,4BAA4B,CAAC;AAEhF,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAC/E,YAAY,EAAE,4BAA4B,EAAE,MAAM,2BAA2B,CAAC;AAE9E,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAE/E,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AACjE,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAE3D,OAAO,EAAE,OAAO,IAAI,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAC7E,YAAY,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAC5E,OAAO,EAAE,OAAO,IAAI,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAC7E,YAAY,EAAE,2BAA2B,EAAE,MAAM,0BAA0B,CAAC;AAE5E,OAAO,EAAE,OAAO,IAAI,2BAA2B,EAAE,MAAM,+BAA+B,CAAC;AACvF,YAAY,EAAE,gCAAgC,EAAE,MAAM,+BAA+B,CAAC;AACtF,OAAO,EAAE,OAAO,IAAI,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AACrF,YAAY,EAAE,+BAA+B,EAAE,MAAM,8BAA8B,CAAC"}
@@ -3,5 +3,7 @@ export { default as ConversationDetailInput } from "./ConversationDetailInput";
3
3
  export { default as ConversationMessageList } from "./ConversationMessageList";
4
4
  export { default as PinnedMessageBar } from "./PinnedMessageBar";
5
5
  export { default as PinLimitModal } from "./PinLimitModal";
6
+ export { default as LeaveGroupConfirmModal } from "./LeaveGroupConfirmModal";
7
+ export { default as LeaveGroupBlockedModal } from "./LeaveGroupBlockedModal";
6
8
  export { default as ConversationMoreOptionPanel } from "./ConversationMoreOptionPanel";
7
9
  export { default as ConversationMoreOptionItem } from "./ConversationMoreOptionItem";
@@ -0,0 +1,15 @@
1
+ import type { MentionOption } from "../../../components/message/footer/mention/useMentionOptions";
2
+ export interface BizMentionMenuProps {
3
+ options: MentionOption[];
4
+ selectedIndex: number;
5
+ onSelect: (option: MentionOption) => void;
6
+ onHoverIndex: (index: number) => void;
7
+ position: {
8
+ top: number;
9
+ left: number;
10
+ };
11
+ visible: boolean;
12
+ }
13
+ declare const BizMentionMenu: ({ options, selectedIndex, onSelect, onHoverIndex, position, visible, }: BizMentionMenuProps) => import("react/jsx-runtime").JSX.Element | null;
14
+ export default BizMentionMenu;
15
+ //# sourceMappingURL=BizMentionMenu.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BizMentionMenu.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/mention/BizMentionMenu.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uDAAuD,CAAC;AAE3F,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,CAAC,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,YAAY,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACtC,QAAQ,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;IACxC,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,QAAA,MAAM,cAAc,GAAI,wEAOrB,mBAAmB,mDAgDrB,CAAC;AAEF,eAAe,cAAc,CAAC"}
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useTranslation } from "react-i18next";
4
+ import { Avatar } from "antd";
5
+ const BizMentionMenu = ({ options, selectedIndex, onSelect, onHoverIndex, position, visible, }) => {
6
+ const { t } = useTranslation();
7
+ if (!visible)
8
+ return null;
9
+ return (_jsxs("div", { "data-cp-name": "BizMentionMenu", "data-testid": "biz-mention-menu", className: "fixed z-50 max-h-[220px] w-[260px] overflow-y-auto rounded-md border border-gray-200 bg-white py-1 shadow-lg", style: { top: position.top, left: position.left }, children: [_jsx("div", { className: "px-3 py-1 text-xs font-medium text-gray-400", children: t("mention.menu_title") }), options.length === 0 ? (_jsx("div", { className: "px-3 py-2 text-sm text-gray-500", children: t("mention.no_result") })) : (options.map((option, index) => (_jsxs("button", { type: "button", "data-selected": index === selectedIndex, className: `flex w-full items-center gap-2 px-3 py-2 text-left text-sm ${index === selectedIndex ? "bg-blue-50" : "hover:bg-gray-50"}`, onMouseEnter: () => onHoverIndex(index), onClick: () => onSelect(option), children: [option.isAtAll ? (_jsx("span", { className: "flex h-6 w-6 shrink-0 items-center justify-center rounded-full bg-gray-100 text-gray-500", children: "@" })) : (_jsx(Avatar, { size: 24, src: option.faceURL, children: option.displayName.charAt(0) })), _jsx("span", { className: "min-w-0 flex-1 truncate", children: option.displayName })] }, option.userID))))] }));
10
+ };
11
+ export default BizMentionMenu;
@@ -0,0 +1,4 @@
1
+ import type { MentionSendPayload } from "../../../types/chat";
2
+ import type { MentionToken } from "./useMentionTokens";
3
+ export declare const buildMentionPayload: (tokens: MentionToken[]) => MentionSendPayload;
4
+ //# sourceMappingURL=buildMentionPayload.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildMentionPayload.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/mention/buildMentionPayload.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQvD,eAAO,MAAM,mBAAmB,GAC9B,QAAQ,YAAY,EAAE,KACrB,kBAkBF,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { ALL_MEMBERS_USER_ID } from "../../../components/message/footer/mention/useMentionOptions";
2
+ // The "__all__" sentinel is not a real user — useSendMessage already signals
3
+ // tag-all via the isAtAll boolean (prepending its own AT_ALL_TAG sentinel),
4
+ // so it must never leak into atUserIDList/atUsersInfo. It DOES still need a
5
+ // mentionOffsets entry (userID: ALL_MEMBERS_USER_ID) so the rendered "Tất
6
+ // cả" text gets highlighted by splitByOffsets — mirrors extractMentions.ts
7
+ // (the CRM/Lexical composer's equivalent) exactly.
8
+ export const buildMentionPayload = (tokens) => {
9
+ const memberTokens = tokens.filter((token) => token.userID !== ALL_MEMBERS_USER_ID);
10
+ return {
11
+ atUserIDList: memberTokens.map((token) => token.userID),
12
+ atUsersInfo: memberTokens.map((token) => ({
13
+ atUserID: token.userID,
14
+ groupNickname: token.displayName,
15
+ })),
16
+ isAtAll: tokens.some((token) => token.userID === ALL_MEMBERS_USER_ID),
17
+ mentionOffsets: tokens.map((token) => ({
18
+ userID: token.userID,
19
+ offset: token.start,
20
+ length: token.length,
21
+ })),
22
+ };
23
+ };
@@ -0,0 +1,11 @@
1
+ export interface CaretPixelCoordinates {
2
+ top: number;
3
+ left: number;
4
+ direction: "up" | "down";
5
+ }
6
+ export interface GetCaretCoordinatesOptions {
7
+ viewportHeight: number;
8
+ }
9
+ export declare const getCaretCoordinates: (textarea: HTMLTextAreaElement, position: number, { viewportHeight }: GetCaretCoordinatesOptions) => CaretPixelCoordinates;
10
+ export declare const disposeCaretCoordinates: (textarea: HTMLTextAreaElement) => void;
11
+ //# sourceMappingURL=getCaretCoordinates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"getCaretCoordinates.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/mention/getCaretCoordinates.ts"],"names":[],"mappings":"AAkBA,MAAM,WAAW,qBAAqB;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,IAAI,GAAG,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,0BAA0B;IACzC,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,eAAO,MAAM,mBAAmB,GAC9B,UAAU,mBAAmB,EAC7B,UAAU,MAAM,EAChB,oBAAoB,0BAA0B,KAC7C,qBAsBF,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,UAAU,mBAAmB,KAAG,IAEvE,CAAC"}
@@ -0,0 +1,36 @@
1
+ import CaretCoordinates from "textarea-caret-position";
2
+ // The real package's own README describes a stateless function API that
3
+ // its shipped index.js does not match — it's actually a stateful class
4
+ // that inserts a permanent mirror-div + resize listener per instance.
5
+ // Cache one instance per textarea so we don't leak a DOM node on every
6
+ // keystroke; callers must invoke disposeCaretCoordinates on unmount.
7
+ const instances = new WeakMap();
8
+ const FALLBACK_LINE_HEIGHT = 25.6;
9
+ const getLineHeight = (textarea) => {
10
+ const computed = parseFloat(window.getComputedStyle(textarea).lineHeight);
11
+ return Number.isFinite(computed) && computed > 0
12
+ ? computed
13
+ : FALLBACK_LINE_HEIGHT;
14
+ };
15
+ export const getCaretCoordinates = (textarea, position, { viewportHeight }) => {
16
+ let instance = instances.get(textarea);
17
+ if (!instance) {
18
+ instance = new CaretCoordinates(textarea);
19
+ instances.set(textarea, instance);
20
+ }
21
+ const caret = instance.get(position, position);
22
+ const rect = textarea.getBoundingClientRect();
23
+ const lineHeight = getLineHeight(textarea);
24
+ const left = rect.left + caret.left - textarea.scrollLeft;
25
+ const caretTop = rect.top + caret.top - textarea.scrollTop;
26
+ const belowTop = caretTop + lineHeight;
27
+ const opensDown = belowTop + lineHeight <= viewportHeight;
28
+ return {
29
+ left,
30
+ top: opensDown ? belowTop : caretTop,
31
+ direction: opensDown ? "down" : "up",
32
+ };
33
+ };
34
+ export const disposeCaretCoordinates = (textarea) => {
35
+ instances.delete(textarea);
36
+ };
@@ -0,0 +1,21 @@
1
+ export interface MentionToken {
2
+ userID: string;
3
+ displayName: string;
4
+ isAtAll: boolean;
5
+ start: number;
6
+ length: number;
7
+ }
8
+ export interface EditRange {
9
+ editStart: number;
10
+ deletedLength: number;
11
+ insertedLength: number;
12
+ }
13
+ export declare const computeEditRange: (oldValue: string, newValue: string) => EditRange;
14
+ export interface UseMentionTokensResult {
15
+ tokens: MentionToken[];
16
+ addToken: (token: MentionToken) => void;
17
+ onValueChange: (oldValue: string, newValue: string) => void;
18
+ resetTokens: () => void;
19
+ }
20
+ export declare const useMentionTokens: () => UseMentionTokensResult;
21
+ //# sourceMappingURL=useMentionTokens.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMentionTokens.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/mention/useMentionTokens.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,SAAS;IACxB,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,cAAc,EAAE,MAAM,CAAC;CACxB;AAMD,eAAO,MAAM,gBAAgB,GAC3B,UAAU,MAAM,EAChB,UAAU,MAAM,KACf,SA4BF,CAAC;AA6BF,MAAM,WAAW,sBAAsB;IACrC,MAAM,EAAE,YAAY,EAAE,CAAC;IACvB,QAAQ,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC,aAAa,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IAC5D,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB;AAED,eAAO,MAAM,gBAAgB,QAAO,sBAqBnC,CAAC"}
@@ -0,0 +1,58 @@
1
+ import { useCallback, useState } from "react";
2
+ // Textarea edits are always a single contiguous range replacement (one
3
+ // keystroke, one backspace, or one paste/selection-replace) — so the exact
4
+ // edit range is derivable from the common prefix/suffix between old and new
5
+ // value, no full diff algorithm needed.
6
+ export const computeEditRange = (oldValue, newValue) => {
7
+ let prefixLength = 0;
8
+ const maxPrefix = Math.min(oldValue.length, newValue.length);
9
+ while (prefixLength < maxPrefix &&
10
+ oldValue[prefixLength] === newValue[prefixLength]) {
11
+ prefixLength += 1;
12
+ }
13
+ let suffixLength = 0;
14
+ const maxSuffix = Math.min(oldValue.length - prefixLength, newValue.length - prefixLength);
15
+ while (suffixLength < maxSuffix &&
16
+ oldValue[oldValue.length - 1 - suffixLength] ===
17
+ newValue[newValue.length - 1 - suffixLength]) {
18
+ suffixLength += 1;
19
+ }
20
+ return {
21
+ editStart: prefixLength,
22
+ deletedLength: oldValue.length - prefixLength - suffixLength,
23
+ insertedLength: newValue.length - prefixLength - suffixLength,
24
+ };
25
+ };
26
+ const isTokenIntact = (token, value) => value.slice(token.start, token.start + token.length) ===
27
+ "@" + token.displayName;
28
+ const applyEdit = (token, edit, newValue) => {
29
+ const editEnd = edit.editStart + edit.deletedLength;
30
+ const delta = edit.insertedLength - edit.deletedLength;
31
+ if (edit.editStart >= token.start + token.length) {
32
+ // Edit strictly after the token — unaffected.
33
+ return token;
34
+ }
35
+ if (editEnd <= token.start) {
36
+ // Edit strictly before the token — shift start by the net delta.
37
+ const shifted = Object.assign(Object.assign({}, token), { start: token.start + delta });
38
+ return isTokenIntact(shifted, newValue) ? shifted : null;
39
+ }
40
+ // Edit overlaps the token's range in some way — treat as broken.
41
+ return null;
42
+ };
43
+ export const useMentionTokens = () => {
44
+ const [tokens, setTokens] = useState([]);
45
+ const addToken = useCallback((token) => {
46
+ setTokens((prev) => [...prev, token]);
47
+ }, []);
48
+ const onValueChange = useCallback((oldValue, newValue) => {
49
+ const edit = computeEditRange(oldValue, newValue);
50
+ setTokens((prev) => prev
51
+ .map((token) => applyEdit(token, edit, newValue))
52
+ .filter((token) => token !== null));
53
+ }, []);
54
+ const resetTokens = useCallback(() => {
55
+ setTokens([]);
56
+ }, []);
57
+ return { tokens, addToken, onValueChange, resetTokens };
58
+ };
@@ -0,0 +1,7 @@
1
+ export interface MentionTriggerMatch {
2
+ triggerStart: number;
3
+ query: string;
4
+ }
5
+ export declare const matchMentionTrigger: (text: string) => MentionTriggerMatch | null;
6
+ export declare const useMentionTrigger: (value: string, caretIndex: number) => MentionTriggerMatch | null;
7
+ //# sourceMappingURL=useMentionTrigger.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useMentionTrigger.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/mention/useMentionTrigger.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,mBAAmB;IAClC,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,eAAO,MAAM,mBAAmB,GAC9B,MAAM,MAAM,KACX,mBAAmB,GAAG,IAWxB,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,OAAO,MAAM,EACb,YAAY,MAAM,KACjB,mBAAmB,GAAG,IAItB,CAAC"}