@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.
- package/dist/assets/images/searchEmpty.png +0 -0
- package/dist/components/conversation/DeskConversationList.d.ts.map +1 -1
- package/dist/components/conversation/DeskConversationList.js +22 -8
- package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
- package/dist/components/conversation-detail/ConversationDetailInput.js +142 -4
- package/dist/components/conversation-detail/mention/BizMentionMenu.d.ts +15 -0
- package/dist/components/conversation-detail/mention/BizMentionMenu.d.ts.map +1 -0
- package/dist/components/conversation-detail/mention/BizMentionMenu.js +11 -0
- package/dist/components/conversation-detail/mention/buildMentionPayload.d.ts +4 -0
- package/dist/components/conversation-detail/mention/buildMentionPayload.d.ts.map +1 -0
- package/dist/components/conversation-detail/mention/buildMentionPayload.js +23 -0
- package/dist/components/conversation-detail/mention/getCaretCoordinates.d.ts +11 -0
- package/dist/components/conversation-detail/mention/getCaretCoordinates.d.ts.map +1 -0
- package/dist/components/conversation-detail/mention/getCaretCoordinates.js +36 -0
- package/dist/components/conversation-detail/mention/useMentionTokens.d.ts +21 -0
- package/dist/components/conversation-detail/mention/useMentionTokens.d.ts.map +1 -0
- package/dist/components/conversation-detail/mention/useMentionTokens.js +58 -0
- package/dist/components/conversation-detail/mention/useMentionTrigger.d.ts +7 -0
- package/dist/components/conversation-detail/mention/useMentionTrigger.d.ts.map +1 -0
- package/dist/components/conversation-detail/mention/useMentionTrigger.js +19 -0
- package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts +4 -1
- package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts.map +1 -1
- package/dist/components/conversation-inbox/ConversationInboxHeader.js +3 -2
- package/dist/components/conversation-inbox/contact-search/ContactSearchFilters.d.ts +8 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchFilters.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchFilters.js +19 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts +7 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.js +116 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchResultItem.d.ts +9 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchResultItem.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchResultItem.js +34 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchSectionHeader.d.ts +8 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchSectionHeader.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchSectionHeader.js +7 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchStateMessage.d.ts +8 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchStateMessage.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchStateMessage.js +9 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchTabs.d.ts +10 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchTabs.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchTabs.js +39 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.d.ts +6 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.js +9 -0
- package/dist/components/conversation-inbox/contact-search/constants.d.ts +18 -0
- package/dist/components/conversation-inbox/contact-search/constants.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/constants.js +23 -0
- package/dist/components/conversation-inbox/contact-search/index.d.ts +10 -0
- package/dist/components/conversation-inbox/contact-search/index.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/index.js +9 -0
- package/dist/components/conversation-inbox/contact-search/utils.d.ts +6 -0
- package/dist/components/conversation-inbox/contact-search/utils.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/utils.js +44 -0
- package/dist/components/message/GroupMembersDrawer.d.ts.map +1 -1
- package/dist/components/message/GroupMembersDrawer.js +12 -2
- package/dist/components/message/footer/mention/useMentionOptions.d.ts +1 -0
- package/dist/components/message/footer/mention/useMentionOptions.d.ts.map +1 -1
- package/dist/components/message/footer/mention/useMentionOptions.js +1 -1
- package/dist/components/thread/GroupThreadInfo.d.ts.map +1 -1
- package/dist/components/thread/GroupThreadInfo.js +12 -2
- package/dist/components/thread/ThreadInfo.d.ts.map +1 -1
- package/dist/components/thread/ThreadInfo.js +4 -1
- package/dist/constants/images.d.ts +1 -0
- package/dist/constants/images.d.ts.map +1 -1
- package/dist/constants/images.js +2 -0
- package/dist/hooks/search/useSearchContacts.d.ts +18 -0
- package/dist/hooks/search/useSearchContacts.d.ts.map +1 -0
- package/dist/hooks/search/useSearchContacts.js +39 -0
- package/dist/locales/vi/conversation-inbox.json +22 -1
- package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
- package/dist/screens/conversation-inbox/index.js +14 -3
- package/dist/services/query.d.ts +1 -0
- package/dist/services/query.d.ts.map +1 -1
- package/dist/services/query.js +1 -0
- package/dist/services/routes.d.ts +1 -0
- package/dist/services/routes.d.ts.map +1 -1
- package/dist/services/routes.js +1 -0
- package/dist/styles/global.css +1 -1
- package/dist/types/dto.d.ts +26 -0
- package/dist/types/dto.d.ts.map +1 -1
- package/dist/types/dto.js +10 -1
- package/package.json +2 -1
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"DeskConversationList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskConversationList.tsx"],"names":[],"mappings":"AA4BA,QAAA,MAAM,oBAAoB,+
|
|
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({
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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;
|
|
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
|
-
|
|
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) =>
|
|
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 @@
|
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
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:
|
|
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;
|