@droppii-org/chat-sdk 0.4.1 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/assets/images/searchEmpty.png +0 -0
  2. package/dist/components/conversation/DeskConversationList.d.ts.map +1 -1
  3. package/dist/components/conversation/DeskConversationList.js +22 -8
  4. package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
  5. package/dist/components/conversation-detail/ConversationDetailInput.js +142 -4
  6. package/dist/components/conversation-detail/mention/BizMentionMenu.d.ts +15 -0
  7. package/dist/components/conversation-detail/mention/BizMentionMenu.d.ts.map +1 -0
  8. package/dist/components/conversation-detail/mention/BizMentionMenu.js +11 -0
  9. package/dist/components/conversation-detail/mention/buildMentionPayload.d.ts +4 -0
  10. package/dist/components/conversation-detail/mention/buildMentionPayload.d.ts.map +1 -0
  11. package/dist/components/conversation-detail/mention/buildMentionPayload.js +23 -0
  12. package/dist/components/conversation-detail/mention/getCaretCoordinates.d.ts +11 -0
  13. package/dist/components/conversation-detail/mention/getCaretCoordinates.d.ts.map +1 -0
  14. package/dist/components/conversation-detail/mention/getCaretCoordinates.js +36 -0
  15. package/dist/components/conversation-detail/mention/useMentionTokens.d.ts +21 -0
  16. package/dist/components/conversation-detail/mention/useMentionTokens.d.ts.map +1 -0
  17. package/dist/components/conversation-detail/mention/useMentionTokens.js +58 -0
  18. package/dist/components/conversation-detail/mention/useMentionTrigger.d.ts +7 -0
  19. package/dist/components/conversation-detail/mention/useMentionTrigger.d.ts.map +1 -0
  20. package/dist/components/conversation-detail/mention/useMentionTrigger.js +19 -0
  21. package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts +4 -1
  22. package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts.map +1 -1
  23. package/dist/components/conversation-inbox/ConversationInboxHeader.js +3 -2
  24. package/dist/components/conversation-inbox/contact-search/ContactSearchFilters.d.ts +8 -0
  25. package/dist/components/conversation-inbox/contact-search/ContactSearchFilters.d.ts.map +1 -0
  26. package/dist/components/conversation-inbox/contact-search/ContactSearchFilters.js +19 -0
  27. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts +7 -0
  28. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts.map +1 -0
  29. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.js +116 -0
  30. package/dist/components/conversation-inbox/contact-search/ContactSearchResultItem.d.ts +9 -0
  31. package/dist/components/conversation-inbox/contact-search/ContactSearchResultItem.d.ts.map +1 -0
  32. package/dist/components/conversation-inbox/contact-search/ContactSearchResultItem.js +34 -0
  33. package/dist/components/conversation-inbox/contact-search/ContactSearchSectionHeader.d.ts +8 -0
  34. package/dist/components/conversation-inbox/contact-search/ContactSearchSectionHeader.d.ts.map +1 -0
  35. package/dist/components/conversation-inbox/contact-search/ContactSearchSectionHeader.js +7 -0
  36. package/dist/components/conversation-inbox/contact-search/ContactSearchStateMessage.d.ts +8 -0
  37. package/dist/components/conversation-inbox/contact-search/ContactSearchStateMessage.d.ts.map +1 -0
  38. package/dist/components/conversation-inbox/contact-search/ContactSearchStateMessage.js +9 -0
  39. package/dist/components/conversation-inbox/contact-search/ContactSearchTabs.d.ts +10 -0
  40. package/dist/components/conversation-inbox/contact-search/ContactSearchTabs.d.ts.map +1 -0
  41. package/dist/components/conversation-inbox/contact-search/ContactSearchTabs.js +39 -0
  42. package/dist/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.d.ts +6 -0
  43. package/dist/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.d.ts.map +1 -0
  44. package/dist/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.js +9 -0
  45. package/dist/components/conversation-inbox/contact-search/constants.d.ts +18 -0
  46. package/dist/components/conversation-inbox/contact-search/constants.d.ts.map +1 -0
  47. package/dist/components/conversation-inbox/contact-search/constants.js +23 -0
  48. package/dist/components/conversation-inbox/contact-search/index.d.ts +10 -0
  49. package/dist/components/conversation-inbox/contact-search/index.d.ts.map +1 -0
  50. package/dist/components/conversation-inbox/contact-search/index.js +9 -0
  51. package/dist/components/conversation-inbox/contact-search/utils.d.ts +6 -0
  52. package/dist/components/conversation-inbox/contact-search/utils.d.ts.map +1 -0
  53. package/dist/components/conversation-inbox/contact-search/utils.js +44 -0
  54. package/dist/components/message/GroupMembersDrawer.d.ts.map +1 -1
  55. package/dist/components/message/GroupMembersDrawer.js +12 -2
  56. package/dist/components/message/footer/mention/useMentionOptions.d.ts +1 -0
  57. package/dist/components/message/footer/mention/useMentionOptions.d.ts.map +1 -1
  58. package/dist/components/message/footer/mention/useMentionOptions.js +1 -1
  59. package/dist/components/thread/GroupThreadInfo.d.ts.map +1 -1
  60. package/dist/components/thread/GroupThreadInfo.js +12 -2
  61. package/dist/components/thread/ThreadInfo.d.ts.map +1 -1
  62. package/dist/components/thread/ThreadInfo.js +4 -1
  63. package/dist/constants/images.d.ts +1 -0
  64. package/dist/constants/images.d.ts.map +1 -1
  65. package/dist/constants/images.js +2 -0
  66. package/dist/hooks/search/useSearchContacts.d.ts +18 -0
  67. package/dist/hooks/search/useSearchContacts.d.ts.map +1 -0
  68. package/dist/hooks/search/useSearchContacts.js +39 -0
  69. package/dist/locales/vi/conversation-inbox.json +22 -1
  70. package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
  71. package/dist/screens/conversation-inbox/index.js +14 -3
  72. package/dist/services/query.d.ts +1 -0
  73. package/dist/services/query.d.ts.map +1 -1
  74. package/dist/services/query.js +1 -0
  75. package/dist/services/routes.d.ts +1 -0
  76. package/dist/services/routes.d.ts.map +1 -1
  77. package/dist/services/routes.js +1 -0
  78. package/dist/styles/global.css +1 -1
  79. package/dist/types/dto.d.ts +26 -0
  80. package/dist/types/dto.d.ts.map +1 -1
  81. package/dist/types/dto.js +10 -1
  82. package/package.json +2 -1
@@ -1 +1 @@
1
- {"version":3,"file":"DeskConversationList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskConversationList.tsx"],"names":[],"mappings":"AA4BA,QAAA,MAAM,oBAAoB,+CA4SzB,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"DeskConversationList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskConversationList.tsx"],"names":[],"mappings":"AA4BA,QAAA,MAAM,oBAAoB,+CAqUzB,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -37,7 +37,10 @@ const DeskConversationList = () => {
37
37
  const viewMode = useSessionStore((state) => state.viewMode);
38
38
  const isAllGroupsView = viewMode === "allGroups";
39
39
  const { data: teamSummaries } = useGetTeamSessionSummary();
40
- const { dataFlatten: rawSessions, hasNextPage, fetchNextPage, refetch, } = useGetSession({ filter: filterSummary, options: { isEnabled: !isAllGroupsView } });
40
+ const { dataFlatten: rawSessions, hasNextPage, fetchNextPage, refetch, } = useGetSession({
41
+ filter: filterSummary,
42
+ options: { isEnabled: !isAllGroupsView },
43
+ });
41
44
  const { groups: conversationGroups, fetchNextPage: fetchNextGroupsPage, hasNextPage: hasNextGroupsPage, isLoading: isConversationGroupsLoading, } = useConversationGroups({ enabled: isAllGroupsView });
42
45
  const latestPreviewMap = useSessionLatestPreview(rawSessions);
43
46
  const sessions = useSortedSessions(rawSessions, latestPreviewMap);
@@ -66,6 +69,19 @@ const DeskConversationList = () => {
66
69
  }, 100);
67
70
  }
68
71
  }, [showSearch]);
72
+ // Switching filter/team while search is open left the search bar and
73
+ // result drawer open over the newly-filtered list — close and clear it
74
+ // so the user starts fresh in the new filter context. Skips the initial
75
+ // mount so it never fights a future restored/URL-driven search state.
76
+ const isFilterEffectMountRef = useRef(true);
77
+ useEffect(() => {
78
+ if (isFilterEffectMountRef.current) {
79
+ isFilterEffectMountRef.current = false;
80
+ return;
81
+ }
82
+ onCloseSearch();
83
+ // eslint-disable-next-line react-hooks/exhaustive-deps
84
+ }, [filterSummary, isAllGroupsView]);
69
85
  useEffect(() => {
70
86
  const threadId = searchParams.get("threadId");
71
87
  if (!threadId) {
@@ -82,8 +98,7 @@ const DeskConversationList = () => {
82
98
  // threadId can go stale between the fetch starting and resolving
83
99
  // (e.g. the user was removed from that group mid-flight) — only
84
100
  // apply the result if the URL still points at this conversation.
85
- if (res.data.length > 0 &&
86
- searchParams.get("threadId") === threadId) {
101
+ if (res.data.length > 0 && searchParams.get("threadId") === threadId) {
87
102
  setConversationData(res.data[0]);
88
103
  updateConversationList(res.data, "filter");
89
104
  }
@@ -107,11 +122,10 @@ const DeskConversationList = () => {
107
122
  setShowSearchTrue();
108
123
  }
109
124
  setSearch(e.target.value);
110
- }, className: "rounded-lg text-sm flex-1 h-[36px]", size: "large", allowClear: true, onClick: setShowSearchTrue, value: search, autoFocus: false, onClear: onCloseSearch }), showSearch ? (_jsx(Button, { onClick: onCloseSearch, variant: "outlined", className: "p-0 w-[36px] h-[36px] text-gray-500", children: _jsx(Icon, { icon: "close-b", size: 22 }) })) : (_jsx(Button, { "data-testid": "conversation-list-compose-icon", onClick: () => setIsComposeOpen(true), variant: "outlined", className: "p-0 w-[36px] h-[36px] shrink-0 text-gray-500", children: _jsx(Icon, { icon: "edit-b", size: 20 }) }))] }), _jsxs("div", { className: "flex items-center justify-between mt-2", children: [_jsx("span", { className: "text-sm font-semibold text-blue-500 truncate", children: pathName }), isAllGroupsView ? (_jsxs("span", { className: "flex items-center gap-1.5 shrink-0", children: [_jsx(Icon, { icon: "user-three-o", size: 16, className: "text-gray-500" }), _jsx("span", { className: "text-sm font-semibold text-gray-800", children: t("all_groups") })] })) : (filterConfig && (_jsxs("span", { className: "flex items-center gap-1.5 shrink-0", children: [_jsx(Icon, { icon: filterConfig.iconName, size: 16, className: "text-gray-500" }), _jsx("span", { className: "text-sm font-semibold text-gray-800", children: t(filterConfig.labelKey) })] })))] })] }), _jsxs("div", { id: "scrollableConversationsDiv", style: {
111
- height: "100%",
112
- overflow: showSearch ? "hidden" : "auto",
113
- position: "relative",
114
- }, children: [isAllGroupsView ? (isConversationGroupsLoading ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Spin, {}) })) : !(conversationGroups === null || conversationGroups === void 0 ? void 0 : conversationGroups.length) ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Empty, { image: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-gray-300" }), description: t("no_conversation") }) })) : (_jsx(InfiniteScroll, { dataLength: conversationGroups.length, next: fetchNextGroupsPage, hasMore: !!hasNextGroupsPage, loader: _jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) }), scrollableTarget: "scrollableConversationsDiv", children: conversationGroups.map((group) => (_jsx(ConversationByGroupItem, { group: group, latestPreview: groupLatestPreviewMap.get(group.conversationId) }, group.groupId))) }))) : (_jsx(InfiniteScroll, { dataLength: (sessions === null || sessions === void 0 ? void 0 : sessions.length) || 0, next: fetchNextPage, hasMore: hasNextPage, loader: _jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) }), scrollableTarget: "scrollableConversationsDiv", children: (sessions === null || sessions === void 0 ? void 0 : sessions.length) === 0 ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Empty, { image: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-gray-300" }), description: t("no_conversation") }) })) : ((_b = sessions === null || sessions === void 0 ? void 0 : sessions.map) === null || _b === void 0 ? void 0 : _b.call(sessions, (session) => (_jsx(ConversationBySessionItem, { sessionItem: session, latestPreview: latestPreviewMap.get(session.conversationId) }, session.conversationId)))) })), _jsx(Drawer, { open: showSearch, mask: false, closeIcon: false, styles: {
125
+ }, className: "rounded-lg text-sm flex-1 h-[36px]", size: "large", allowClear: true, onClick: setShowSearchTrue, value: search, autoFocus: false, onClear: onCloseSearch }), showSearch ? (_jsx(Button, { onClick: onCloseSearch, variant: "outlined", className: "p-0 w-[36px] h-[36px] text-gray-500", children: _jsx(Icon, { icon: "close-b", size: 22 }) })) : (_jsx(Button, { "data-testid": "conversation-list-compose-icon", onClick: () => setIsComposeOpen(true), variant: "outlined", className: "p-0 w-[36px] h-[36px] shrink-0 text-gray-500", children: _jsx(Icon, { icon: "edit-b", size: 20 }) }))] }), _jsxs("div", { className: "flex items-center justify-between mt-2", children: [_jsx("span", { className: "text-sm font-semibold text-blue-500 truncate", children: pathName }), isAllGroupsView ? (_jsxs("span", { className: "flex items-center gap-1.5 shrink-0", children: [_jsx(Icon, { icon: "user-three-o", size: 16, className: "text-gray-500" }), _jsx("span", { className: "text-sm font-semibold text-gray-800", children: t("all_groups") })] })) : (filterConfig && (_jsxs("span", { className: "flex items-center gap-1.5 shrink-0", children: [_jsx(Icon, { icon: filterConfig.iconName, size: 16, className: "text-gray-500" }), _jsx("span", { className: "text-sm font-semibold text-gray-800", children: t(filterConfig.labelKey) })] })))] })] }), _jsxs("div", { className: "relative flex-1 min-h-0", children: [_jsx("div", { id: "scrollableConversationsDiv", style: {
126
+ height: "100%",
127
+ overflow: showSearch ? "hidden" : "auto",
128
+ }, children: isAllGroupsView ? (isConversationGroupsLoading ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Spin, {}) })) : !(conversationGroups === null || conversationGroups === void 0 ? void 0 : conversationGroups.length) ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Empty, { image: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-gray-300" }), description: t("no_conversation") }) })) : (_jsx(InfiniteScroll, { dataLength: conversationGroups.length, next: fetchNextGroupsPage, hasMore: !!hasNextGroupsPage, loader: _jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) }), scrollableTarget: "scrollableConversationsDiv", children: conversationGroups.map((group) => (_jsx(ConversationByGroupItem, { group: group, latestPreview: groupLatestPreviewMap.get(group.conversationId) }, group.groupId))) }))) : (_jsx(InfiniteScroll, { dataLength: (sessions === null || sessions === void 0 ? void 0 : sessions.length) || 0, next: fetchNextPage, hasMore: hasNextPage, loader: _jsx("div", { className: "flex items-center justify-center py-2", children: _jsx(Spin, {}) }), scrollableTarget: "scrollableConversationsDiv", children: (sessions === null || sessions === void 0 ? void 0 : sessions.length) === 0 ? (_jsx("div", { className: "flex items-center justify-center py-12", children: _jsx(Empty, { image: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-gray-300" }), description: t("no_conversation") }) })) : ((_b = sessions === null || sessions === void 0 ? void 0 : sessions.map) === null || _b === void 0 ? void 0 : _b.call(sessions, (session) => (_jsx(ConversationBySessionItem, { sessionItem: session, latestPreview: latestPreviewMap.get(session.conversationId) }, session.conversationId)))) })) }), _jsx(Drawer, { open: showSearch, mask: false, closeIcon: false, styles: {
115
129
  body: {
116
130
  padding: 0,
117
131
  },
@@ -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,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"}
@@ -0,0 +1,19 @@
1
+ import { useMemo } from "react";
2
+ // Mirrors @lexical/react/LexicalTypeaheadMenuPlugin's useBasicTypeaheadTriggerMatch
3
+ // regex for trigger="@", minLength=0 (the exact config MentionMenu.tsx uses),
4
+ // so BIZ's plain-textarea trigger behavior matches CRM's Lexical composer.
5
+ // The {0,75} + trailing $ means a query over 75 chars doesn't get
6
+ // truncated — it simply stops triggering, same as the Lexical original.
7
+ const TRIGGER_REGEX = /(^|\s|\()([@]((?:[^@.,+*?$@|#{}()^\-[\]\\/!%'"~=<>_:;\s]){0,75}))$/;
8
+ export const matchMentionTrigger = (text) => {
9
+ const match = TRIGGER_REGEX.exec(text);
10
+ if (!match)
11
+ return null;
12
+ const boundary = match[1];
13
+ const query = match[3];
14
+ return {
15
+ triggerStart: match.index + boundary.length,
16
+ query,
17
+ };
18
+ };
19
+ export const useMentionTrigger = (value, caretIndex) => useMemo(() => matchMentionTrigger(value.slice(0, caretIndex)), [value, caretIndex]);
@@ -1,3 +1,6 @@
1
- declare const ConversationInboxHeader: () => import("react/jsx-runtime").JSX.Element;
1
+ export interface ConversationInboxHeaderProps {
2
+ onOpenSearch?: () => void;
3
+ }
4
+ declare const ConversationInboxHeader: ({ onOpenSearch, }: ConversationInboxHeaderProps) => import("react/jsx-runtime").JSX.Element;
2
5
  export default ConversationInboxHeader;
3
6
  //# sourceMappingURL=ConversationInboxHeader.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationInboxHeader.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-inbox/ConversationInboxHeader.tsx"],"names":[],"mappings":"AAIA,QAAA,MAAM,uBAAuB,+CAmB5B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"ConversationInboxHeader.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-inbox/ConversationInboxHeader.tsx"],"names":[],"mappings":"AAKA,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;CAC3B;AAED,QAAA,MAAM,uBAAuB,GAAI,mBAE9B,4BAA4B,4CA8B9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -1,8 +1,9 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useTranslation } from "react-i18next";
4
- const ConversationInboxHeader = () => {
4
+ import { Icon } from "../../components/icon";
5
+ const ConversationInboxHeader = ({ onOpenSearch, }) => {
5
6
  const { t } = useTranslation("conversation-inbox");
6
- return (_jsx("div", { "data-testid": "conversation-inbox-header", "data-cp-name": "ConversationInboxHeader", className: "flex w-full flex-col border-b border-gray-100 bg-white", children: _jsx("div", { className: "flex h-12 items-end px-3", children: _jsxs("div", { className: "flex h-full flex-col items-center justify-end px-3", children: [_jsx("span", { className: "pb-1.5 text-center text-[16px] font-bold leading-[160%] tracking-[0.16px] text-primary-400", children: t("tabs.messages") }), _jsx("span", { className: "h-[3px] w-full rounded-t bg-primary-400" })] }) }) }));
7
+ return (_jsx("div", { "data-testid": "conversation-inbox-header", "data-cp-name": "ConversationInboxHeader", className: "flex w-full flex-col border-b border-gray-100 bg-white", children: _jsxs("div", { className: "flex h-12 items-end px-3", children: [_jsxs("div", { className: "flex h-full flex-col items-center justify-end px-3", children: [_jsx("span", { className: "pb-1.5 text-center text-[16px] font-bold leading-[160%] tracking-[0.16px] text-primary-400", children: t("tabs.messages") }), _jsx("span", { className: "h-[3px] w-full rounded-t bg-primary-400" })] }), onOpenSearch && (_jsx("button", { type: "button", "data-testid": "conversation-inbox-search", "aria-label": t("search_placeholder"), className: "ml-auto mb-1.5 flex shrink-0 cursor-pointer items-center justify-center p-1 text-primary-400", onClick: onOpenSearch, children: _jsx(Icon, { icon: "search-o", size: 24 }) }))] }) }));
7
8
  };
8
9
  export default ConversationInboxHeader;
@@ -0,0 +1,8 @@
1
+ import { ContactSearchType } from "../../../types/dto";
2
+ interface ContactSearchFiltersProps {
3
+ activeType?: ContactSearchType;
4
+ onChange: (type?: ContactSearchType) => void;
5
+ }
6
+ declare const ContactSearchFilters: ({ activeType, onChange, }: ContactSearchFiltersProps) => import("react/jsx-runtime").JSX.Element;
7
+ export default ContactSearchFilters;
8
+ //# sourceMappingURL=ContactSearchFilters.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactSearchFilters.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchFilters.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAGhD,UAAU,yBAAyB;IACjC,UAAU,CAAC,EAAE,iBAAiB,CAAC;IAC/B,QAAQ,EAAE,CAAC,IAAI,CAAC,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAC9C;AAED,QAAA,MAAM,oBAAoB,GAAI,2BAG3B,yBAAyB,4CA0C3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import clsx from "clsx";
4
+ import { useTranslation } from "react-i18next";
5
+ import { CONTACT_SEARCH_FILTERS } from "./constants";
6
+ const ContactSearchFilters = ({ activeType, onChange, }) => {
7
+ const { t } = useTranslation("conversation-inbox");
8
+ // Selecting the active chip again clears the filter and returns all types.
9
+ const handleSelect = (type) => {
10
+ onChange(activeType === type ? undefined : type);
11
+ };
12
+ return (_jsx("div", { "data-cp-name": "ContactSearchFilters", "data-testid": "contact-search-filters", className: "w-full border-b border-gray-25 bg-white py-3", children: _jsx("div", { className: "flex items-start gap-2 overflow-x-auto px-3", children: CONTACT_SEARCH_FILTERS.map(({ type, labelKey }) => {
13
+ const isActive = activeType === type;
14
+ return (_jsx("button", { type: "button", "aria-pressed": isActive, "data-testid": `contact-search-filter-${type}`, className: clsx("flex h-7 shrink-0 cursor-pointer items-center justify-center whitespace-nowrap rounded-lg border px-3", "text-center text-[12px] font-medium leading-[1.6] tracking-[0.12px]", isActive
15
+ ? "border-primary-600 bg-primary-25 text-primary-600"
16
+ : "border-gray-200 text-gray-900"), onClick: () => handleSelect(type), children: t(labelKey) }, type));
17
+ }) }) }));
18
+ };
19
+ export default ContactSearchFilters;