@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
@@ -0,0 +1,7 @@
1
+ interface ContactSearchPanelProps {
2
+ open: boolean;
3
+ onClose: () => void;
4
+ }
5
+ declare const ContactSearchPanel: ({ open, onClose }: ContactSearchPanelProps) => import("react/jsx-runtime").JSX.Element;
6
+ export default ContactSearchPanel;
7
+ //# sourceMappingURL=ContactSearchPanel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactSearchPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchPanel.tsx"],"names":[],"mappings":"AA2BA,UAAU,uBAAuB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,kBAAkB,GAAI,mBAAmB,uBAAuB,4CAoOrE,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -0,0 +1,116 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { useEffect, useState } from "react";
4
+ import { Drawer, Input, Spin } from "antd";
5
+ import { useDebounce } from "ahooks";
6
+ import InfiniteScroll from "react-infinite-scroll-component";
7
+ import { useTranslation } from "react-i18next";
8
+ import { Icon } from "../../../components/icon";
9
+ import { useChatContext } from "../../../context/ChatContext";
10
+ import { useSearchContacts } from "../../../hooks/search/useSearchContacts";
11
+ import ContactSearchFilters from "./ContactSearchFilters";
12
+ import ContactSearchResultItem from "./ContactSearchResultItem";
13
+ import ContactSearchSectionHeader from "./ContactSearchSectionHeader";
14
+ import ContactSearchStateMessage from "./ContactSearchStateMessage";
15
+ import ContactSearchTabs from "./ContactSearchTabs";
16
+ import ContactSearchViewMoreBar from "./ContactSearchViewMoreBar";
17
+ import { CONTACT_SEARCH_DEBOUNCE_MS, CONTACT_SEARCH_ICON_SIZE, CONTACT_SEARCH_PREVIEW_LIMIT, ContactSearchTab, } from "./constants";
18
+ import { buildContactSearchParams } from "./utils";
19
+ const SCROLL_TARGET_ID = "contact-search-scroll";
20
+ const ContactSearchPanel = ({ open, onClose }) => {
21
+ const { t } = useTranslation("conversation-inbox");
22
+ const { onNavigate } = useChatContext();
23
+ const [keyword, setKeyword] = useState("");
24
+ const [activeType, setActiveType] = useState();
25
+ const [activeTab, setActiveTab] = useState(ContactSearchTab.All);
26
+ const debouncedKeyword = useDebounce(keyword, {
27
+ wait: CONTACT_SEARCH_DEBOUNCE_MS,
28
+ });
29
+ const trimmedKeyword = debouncedKeyword.trim();
30
+ // The keyword and filter belong to one search session — reset both whenever
31
+ // the panel is reopened so a stale query never flashes.
32
+ useEffect(() => {
33
+ if (!open) {
34
+ setKeyword("");
35
+ setActiveType(undefined);
36
+ setActiveTab(ContactSearchTab.All);
37
+ }
38
+ }, [open]);
39
+ const { contacts, totalElements, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError, } = useSearchContacts({
40
+ searchTerm: trimmedKeyword,
41
+ type: activeType,
42
+ enabled: open,
43
+ });
44
+ const handleSelectContact = (contact) => {
45
+ const params = buildContactSearchParams(contact);
46
+ onClose();
47
+ onNavigate === null || onNavigate === void 0 ? void 0 : onNavigate(`${window.location.pathname}?${params.toString()}`);
48
+ };
49
+ // Filters belong to the `Liên hệ` tab only, so leaving it drops the active
50
+ // type — `Tất cả` always previews every contact type.
51
+ const handleChangeTab = (tab) => {
52
+ setActiveTab(tab);
53
+ if (tab !== ContactSearchTab.Contacts)
54
+ setActiveType(undefined);
55
+ };
56
+ const renderContactItems = (items) => items.map((contact) => (_jsx(ContactSearchResultItem, { contact: contact, searchTerm: trimmedKeyword, onSelect: handleSelectContact }, contact.id)));
57
+ // Every contact tab shares the same not-ready states before it can render
58
+ // results; returns null once there is a list to show.
59
+ const renderPlaceholderState = () => {
60
+ if (!trimmedKeyword) {
61
+ return (_jsx(ContactSearchStateMessage, { testId: "contact-search-initial", children: t("contact_search.initial") }));
62
+ }
63
+ if (isLoading) {
64
+ return (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Spin, {}) }));
65
+ }
66
+ if (isError) {
67
+ return (_jsx(ContactSearchStateMessage, { testId: "contact-search-failed", children: t("contact_search.failed") }));
68
+ }
69
+ if (contacts.length === 0) {
70
+ return (_jsxs(ContactSearchStateMessage, { testId: "contact-search-no-result", children: [t("contact_search.no_result_title"), _jsx("br", {}), t("contact_search.no_result_hint")] }));
71
+ }
72
+ return null;
73
+ };
74
+ // `Tất cả` only previews the contact section and hands off to `Liên hệ`;
75
+ // the message section belongs to the message-search ticket.
76
+ const renderAllTab = () => {
77
+ const placeholder = renderPlaceholderState();
78
+ if (placeholder)
79
+ return placeholder;
80
+ const hasMore = contacts.length > CONTACT_SEARCH_PREVIEW_LIMIT || !!hasNextPage;
81
+ const openContactsTab = () => handleChangeTab(ContactSearchTab.Contacts);
82
+ return (_jsxs("div", { className: "flex-1 overflow-y-auto", children: [_jsx(ContactSearchSectionHeader, { title: t("contact_search.tabs.contacts"), count: totalElements || undefined, onSelect: openContactsTab }), renderContactItems(contacts.slice(0, CONTACT_SEARCH_PREVIEW_LIMIT)), hasMore && _jsx(ContactSearchViewMoreBar, { onSelect: openContactsTab })] }));
83
+ };
84
+ const renderContactList = () => (_jsx("div", { id: SCROLL_TARGET_ID, className: "flex-1 overflow-y-auto", children: _jsx(InfiniteScroll, { dataLength: contacts.length, next: fetchNextPage, hasMore: !!hasNextPage, scrollableTarget: SCROLL_TARGET_ID, loader: isFetchingNextPage ? (_jsx("div", { className: "flex items-center justify-center py-3", children: _jsx(Spin, { size: "small" }) })) : null, children: renderContactItems(contacts) }) }));
85
+ // Filters stay mounted even with no results so the seller can switch type.
86
+ const renderContactsTab = () => {
87
+ var _a;
88
+ return (_jsxs(_Fragment, { children: [_jsx(ContactSearchFilters, { activeType: activeType, onChange: setActiveType }), (_a = renderPlaceholderState()) !== null && _a !== void 0 ? _a : renderContactList()] }));
89
+ };
90
+ const tabItems = [
91
+ {
92
+ key: ContactSearchTab.All,
93
+ label: t("contact_search.tabs.all"),
94
+ children: renderAllTab(),
95
+ },
96
+ {
97
+ key: ContactSearchTab.Contacts,
98
+ label: t("contact_search.tabs.contacts"),
99
+ children: renderContactsTab(),
100
+ },
101
+ {
102
+ key: ContactSearchTab.Messages,
103
+ label: t("contact_search.tabs.messages"),
104
+ children: (_jsx(ContactSearchStateMessage, { testId: "contact-search-messages", children: t("contact_search.messages_not_available") })),
105
+ },
106
+ ];
107
+ return (_jsx(Drawer, { open: open, onClose: onClose, placement: "right", closeIcon: false, width: "100%", "data-cp-name": "ContactSearchPanel", styles: { body: { padding: 0 } }, getContainer: false, destroyOnHidden: true, children: _jsxs("div", { className: "flex h-full flex-col bg-white", children: [_jsxs("div", { className: "flex items-center gap-2 px-3 py-2", children: [_jsx("button", { type: "button", "data-testid": "contact-search-back", "aria-label": t("contact_search.back"), className: "flex shrink-0 cursor-pointer items-center justify-center p-1 text-primary-400", onClick: onClose, children: _jsx(Icon, { icon: "arrow-left-o", size: 24 }) }), _jsx(Input, { allowClear: true, autoFocus: true, value: keyword, "data-testid": "contact-search-input", placeholder: t("search_placeholder"), prefix: _jsx("span", { className: "flex h-10 w-10 items-center justify-center", children: _jsx(Icon, { icon: "search-o", size: CONTACT_SEARCH_ICON_SIZE }) }),
108
+ // Padding lives on the prefix/input instead of the wrapper so the
109
+ // icon keeps its 40x40 box from the design.
110
+ className: "!h-10 !rounded-lg !border-none !bg-gray-25 !p-0 !shadow-none", classNames: {
111
+ prefix: "!m-0 text-gray-600",
112
+ input: "!bg-transparent !px-0 !text-[14px] !leading-[1.6] !tracking-[0.14px] !text-gray-900",
113
+ suffix: "!mr-4",
114
+ }, onChange: (event) => setKeyword(event.target.value) })] }), _jsx(ContactSearchTabs, { activeTab: activeTab, items: tabItems, onChange: handleChangeTab })] }) }));
115
+ };
116
+ export default ContactSearchPanel;
@@ -0,0 +1,9 @@
1
+ import { ContactSearchItem } from "../../../types/dto";
2
+ interface ContactSearchResultItemProps {
3
+ contact: ContactSearchItem;
4
+ searchTerm: string;
5
+ onSelect: (contact: ContactSearchItem) => void;
6
+ }
7
+ declare const ContactSearchResultItem: ({ contact, searchTerm, onSelect, }: ContactSearchResultItemProps) => import("react/jsx-runtime").JSX.Element;
8
+ export default ContactSearchResultItem;
9
+ //# sourceMappingURL=ContactSearchResultItem.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactSearchResultItem.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchResultItem.tsx"],"names":[],"mappings":"AAOA,OAAO,EAAE,iBAAiB,EAAqB,MAAM,aAAa,CAAC;AAGnE,UAAU,4BAA4B;IACpC,OAAO,EAAE,iBAAiB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,IAAI,CAAC;CAChD;AAED,QAAA,MAAM,uBAAuB,GAAI,oCAI9B,4BAA4B,4CAiE9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useTranslation } from "react-i18next";
4
+ import { Icon } from "../../../components/icon";
5
+ import ConversationPeerAvatar from "../../../components/conversation-inbox/ConversationPeerAvatar";
6
+ import { highlightSearch, sanitizeHtml } from "../../../utils/common";
7
+ import { resolvePeerBadgeLabel } from "../../../utils/conversation";
8
+ import { ContactSearchType } from "../../../types/dto";
9
+ import { formatLastOnlineTime, resolveContactPeerType } from "./utils";
10
+ const ContactSearchResultItem = ({ contact, searchTerm, onSelect, }) => {
11
+ const { t } = useTranslation("conversation-inbox");
12
+ const isGroup = contact.type === ContactSearchType.Group;
13
+ const peerType = resolveContactPeerType(contact.type);
14
+ // Supporting line differs per contact type (spec §Contact Result Content).
15
+ // PDP intentionally has none — its last online time was dropped from spec.
16
+ const buildSupportingText = () => {
17
+ var _a;
18
+ if (isGroup) {
19
+ return t("contact_search.member_count", {
20
+ count: (_a = contact.memberCount) !== null && _a !== void 0 ? _a : 0,
21
+ });
22
+ }
23
+ if (contact.type === ContactSearchType.PdpBot)
24
+ return "";
25
+ return formatLastOnlineTime(contact.lastOnlineTime);
26
+ };
27
+ const supportingText = buildSupportingText();
28
+ return (_jsxs("button", { type: "button", "data-cp-name": "ContactSearchResultItem", "data-testid": `contact-search-result-${contact.id}`, className: "flex w-full items-center gap-3 border-b border-gray-25 px-3 py-2.5 text-left hover:bg-gray-10", onClick: () => onSelect(contact), children: [_jsx(ConversationPeerAvatar, { peerType: peerType, displayName: contact.targetName, avatarUrl: contact.targetAvatar, badgeLabel: resolvePeerBadgeLabel(peerType, t) }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1", children: [isGroup && (_jsx(Icon, { icon: "user-three-b", size: 16, className: "shrink-0 text-gray-900", "aria-hidden": true })), _jsx("span", { className: "min-w-0 truncate text-[16px] font-medium leading-[1.6] text-gray-900",
29
+ // highlightSearch returns a <mark> around the matched part only.
30
+ dangerouslySetInnerHTML: {
31
+ __html: sanitizeHtml(highlightSearch(contact.targetName || "", searchTerm)),
32
+ } })] }), supportingText && (_jsx("span", { className: "truncate text-[14px] leading-[1.6] text-gray-500", children: supportingText }))] })] }));
33
+ };
34
+ export default ContactSearchResultItem;
@@ -0,0 +1,8 @@
1
+ interface ContactSearchSectionHeaderProps {
2
+ title: string;
3
+ count?: number;
4
+ onSelect: () => void;
5
+ }
6
+ declare const ContactSearchSectionHeader: ({ title, count, onSelect, }: ContactSearchSectionHeaderProps) => import("react/jsx-runtime").JSX.Element;
7
+ export default ContactSearchSectionHeader;
8
+ //# sourceMappingURL=ContactSearchSectionHeader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactSearchSectionHeader.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchSectionHeader.tsx"],"names":[],"mappings":"AAIA,UAAU,+BAA+B;IACvC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,QAAA,MAAM,0BAA0B,GAAI,6BAIjC,+BAA+B,4CA2BjC,CAAC;AAEF,eAAe,0BAA0B,CAAC"}
@@ -0,0 +1,7 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Icon } from "../../../components/icon";
4
+ const ContactSearchSectionHeader = ({ title, count, onSelect, }) => {
5
+ return (_jsxs("button", { type: "button", "data-cp-name": "ContactSearchSectionHeader", "data-testid": "contact-search-section-header", className: "flex w-full items-center gap-4 bg-white px-3 py-2 text-left", onClick: onSelect, children: [_jsxs("span", { className: "min-w-0 flex-1", children: [_jsx("span", { className: "text-[16px] font-bold leading-[1.6] tracking-[0.16px] text-gray-900", children: title }), count !== undefined && (_jsx("span", { className: "text-[14px] font-medium leading-[1.6] text-gray-400", children: ` (${count})` }))] }), _jsx(Icon, { icon: "angle-right-o", size: 20, className: "shrink-0 text-gray-400", "aria-hidden": true })] }));
6
+ };
7
+ export default ContactSearchSectionHeader;
@@ -0,0 +1,8 @@
1
+ import type { ReactNode } from "react";
2
+ interface ContactSearchStateMessageProps {
3
+ children: ReactNode;
4
+ testId?: string;
5
+ }
6
+ declare const ContactSearchStateMessage: ({ children, testId, }: ContactSearchStateMessageProps) => import("react/jsx-runtime").JSX.Element;
7
+ export default ContactSearchStateMessage;
8
+ //# sourceMappingURL=ContactSearchStateMessage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactSearchStateMessage.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchStateMessage.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAGvC,UAAU,8BAA8B;IACtC,QAAQ,EAAE,SAAS,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAID,QAAA,MAAM,yBAAyB,GAAI,uBAGhC,8BAA8B,4CAqBhC,CAAC;AAEF,eAAe,yBAAyB,CAAC"}
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { images } from "../../../constants/images";
4
+ // Shared body for the initial / no-result / failed states: the same 100px
5
+ // illustration above centered gray copy.
6
+ const ContactSearchStateMessage = ({ children, testId, }) => {
7
+ return (_jsxs("div", { "data-cp-name": "ContactSearchStateMessage", "data-testid": testId, className: "flex flex-1 flex-col items-center gap-3 bg-white px-8 py-[42px]", children: [_jsx("div", { className: "flex h-[100px] w-[100px] shrink-0 items-center justify-center", children: _jsx("img", { src: images.searchEmpty, alt: "", "aria-hidden": "true", className: "h-[87px] w-[101px] max-w-none" }) }), _jsx("p", { className: "w-full text-center text-[16px] leading-[1.6] tracking-[0.16px] text-gray-600", children: children })] }));
8
+ };
9
+ export default ContactSearchStateMessage;
@@ -0,0 +1,10 @@
1
+ import { type TabsProps } from "antd";
2
+ import { ContactSearchTab } from "./constants";
3
+ interface ContactSearchTabsProps {
4
+ activeTab: ContactSearchTab;
5
+ items: TabsProps["items"];
6
+ onChange: (tab: ContactSearchTab) => void;
7
+ }
8
+ declare const ContactSearchTabs: ({ activeTab, items, onChange, }: ContactSearchTabsProps) => import("react/jsx-runtime").JSX.Element;
9
+ export default ContactSearchTabs;
10
+ //# sourceMappingURL=ContactSearchTabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactSearchTabs.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchTabs.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAQ,KAAK,SAAS,EAAE,MAAM,MAAM,CAAC;AAC5C,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAiC/C,UAAU,sBAAsB;IAC9B,SAAS,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IAC1B,QAAQ,EAAE,CAAC,GAAG,EAAE,gBAAgB,KAAK,IAAI,CAAC;CAC3C;AAED,QAAA,MAAM,iBAAiB,GAAI,iCAIxB,sBAAsB,4CAgBxB,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
@@ -0,0 +1,39 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Tabs } from "antd";
4
+ // antd's line tabs are restyled to the design: no nav margin, adjacent tabs,
5
+ // 14px bold labels, and a 3px rounded ink bar in the brand colour.
6
+ const TAB_CLASS_NAMES = [
7
+ "flex flex-1 flex-col overflow-hidden",
8
+ "[&_.ant-tabs-nav]:mb-0 [&_.ant-tabs-nav]:h-[42px] [&_.ant-tabs-nav]:bg-white",
9
+ "[&_.ant-tabs-nav]:before:border-b [&_.ant-tabs-nav]:before:border-gray-25",
10
+ "[&_.ant-tabs-nav-wrap]:h-full [&_.ant-tabs-nav-list]:h-full",
11
+ // Design padding is `2px 12px 0 12px` — no bottom padding, the tab stretches
12
+ // to the bar height and the indicator sits flush at its bottom edge.
13
+ "[&_.ant-tabs-tab]:h-full [&_.ant-tabs-tab]:px-3 [&_.ant-tabs-tab]:pb-0 [&_.ant-tabs-tab]:pt-0.5",
14
+ "[&_.ant-tabs-tab+.ant-tabs-tab]:ml-0",
15
+ "[&_.ant-tabs-tab-btn]:text-[14px] [&_.ant-tabs-tab-btn]:font-bold",
16
+ "[&_.ant-tabs-tab-btn]:leading-[1.6] [&_.ant-tabs-tab-btn]:tracking-[0.14px]",
17
+ "[&_.ant-tabs-tab-btn]:!text-gray-400",
18
+ "[&_.ant-tabs-tab-active_.ant-tabs-tab-btn]:!text-primary-400",
19
+ "[&_.ant-tabs-ink-bar]:h-[3px] [&_.ant-tabs-ink-bar]:rounded",
20
+ "[&_.ant-tabs-ink-bar]:bg-primary-400",
21
+ "[&_.ant-tabs-content-holder]:flex [&_.ant-tabs-content-holder]:flex-1",
22
+ "[&_.ant-tabs-content-holder]:overflow-hidden",
23
+ "[&_.ant-tabs-content]:h-full",
24
+ // Only the active pane gets sizing — styling every `.ant-tabs-tabpane`
25
+ // overrides the height:0 antd uses to hide the inactive ones, which made
26
+ // two panes render side by side.
27
+ "[&_.ant-tabs-tabpane-active]:flex [&_.ant-tabs-tabpane-active]:h-full",
28
+ "[&_.ant-tabs-tabpane-active]:flex-col",
29
+ ].join(" ");
30
+ // The design draws the indicator under the label only, while antd spans the
31
+ // whole tab including its horizontal padding.
32
+ const TAB_HORIZONTAL_PADDING = 24;
33
+ const ContactSearchTabs = ({ activeTab, items, onChange, }) => {
34
+ return (_jsx(Tabs, { "data-cp-name": "ContactSearchTabs", activeKey: activeTab, items: items, className: TAB_CLASS_NAMES, tabBarGutter: 0, indicator: {
35
+ size: (origin) => origin - TAB_HORIZONTAL_PADDING,
36
+ align: "center",
37
+ }, destroyOnHidden: true, onChange: (key) => onChange(key) }));
38
+ };
39
+ export default ContactSearchTabs;
@@ -0,0 +1,6 @@
1
+ interface ContactSearchViewMoreBarProps {
2
+ onSelect: () => void;
3
+ }
4
+ declare const ContactSearchViewMoreBar: ({ onSelect, }: ContactSearchViewMoreBarProps) => import("react/jsx-runtime").JSX.Element;
5
+ export default ContactSearchViewMoreBar;
6
+ //# sourceMappingURL=ContactSearchViewMoreBar.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactSearchViewMoreBar.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchViewMoreBar.tsx"],"names":[],"mappings":"AAKA,UAAU,6BAA6B;IACrC,QAAQ,EAAE,MAAM,IAAI,CAAC;CACtB;AAED,QAAA,MAAM,wBAAwB,GAAI,eAE/B,6BAA6B,4CA4B/B,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
@@ -0,0 +1,9 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useTranslation } from "react-i18next";
4
+ import { Icon } from "../../../components/icon";
5
+ const ContactSearchViewMoreBar = ({ onSelect, }) => {
6
+ const { t } = useTranslation("conversation-inbox");
7
+ return (_jsx("div", { "data-cp-name": "ContactSearchViewMoreBar", className: "flex w-full flex-col items-center border-t border-gray-25 bg-white", children: _jsx("div", { className: "flex w-full items-center justify-center p-1", children: _jsxs("button", { type: "button", "data-testid": "contact-search-view-all", className: "flex h-8 items-center justify-center gap-2 px-3", onClick: onSelect, children: [_jsx("span", { className: "whitespace-nowrap text-center text-[13px] font-medium leading-[1.6] text-primary-400", children: t("contact_search.view_all") }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "shrink-0 text-primary-400", "aria-hidden": true })] }) }) }));
8
+ };
9
+ export default ContactSearchViewMoreBar;
@@ -0,0 +1,18 @@
1
+ import { ContactSearchType } from "../../../types/dto";
2
+ export declare const CONTACT_SEARCH_DEBOUNCE_MS = 300;
3
+ export declare const CONTACT_SEARCH_ICON_SIZE = 18;
4
+ export declare const CONTACT_SEARCH_PREVIEW_LIMIT = 5;
5
+ export declare enum ContactSearchTab {
6
+ All = "ALL",
7
+ Contacts = "CONTACTS",
8
+ Messages = "MESSAGES"
9
+ }
10
+ export declare const CONTACT_SEARCH_TABS: {
11
+ tab: ContactSearchTab;
12
+ labelKey: string;
13
+ }[];
14
+ export declare const CONTACT_SEARCH_FILTERS: {
15
+ type: ContactSearchType;
16
+ labelKey: string;
17
+ }[];
18
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/constants.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,0BAA0B,MAAM,CAAC;AAE9C,eAAO,MAAM,wBAAwB,KAAK,CAAC;AAI3C,eAAO,MAAM,4BAA4B,IAAI,CAAC;AAE9C,oBAAY,gBAAgB;IAC1B,GAAG,QAAQ;IACX,QAAQ,aAAa;IACrB,QAAQ,aAAa;CACtB;AAED,eAAO,MAAM,mBAAmB,EAAE;IAChC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;CAClB,EAIA,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE;IACnC,IAAI,EAAE,iBAAiB,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;CAClB,EAKA,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { ContactSearchType } from "../../../types/dto";
2
+ export const CONTACT_SEARCH_DEBOUNCE_MS = 300;
3
+ export const CONTACT_SEARCH_ICON_SIZE = 18;
4
+ // `Tất cả` shows a capped preview of the contact section before handing the
5
+ // seller over to the full `Liên hệ` tab.
6
+ export const CONTACT_SEARCH_PREVIEW_LIMIT = 5;
7
+ export var ContactSearchTab;
8
+ (function (ContactSearchTab) {
9
+ ContactSearchTab["All"] = "ALL";
10
+ ContactSearchTab["Contacts"] = "CONTACTS";
11
+ ContactSearchTab["Messages"] = "MESSAGES";
12
+ })(ContactSearchTab || (ContactSearchTab = {}));
13
+ export const CONTACT_SEARCH_TABS = [
14
+ { tab: ContactSearchTab.All, labelKey: "contact_search.tabs.all" },
15
+ { tab: ContactSearchTab.Contacts, labelKey: "contact_search.tabs.contacts" },
16
+ { tab: ContactSearchTab.Messages, labelKey: "contact_search.tabs.messages" },
17
+ ];
18
+ export const CONTACT_SEARCH_FILTERS = [
19
+ { type: ContactSearchType.Customer, labelKey: "contact_search.filters.customer" },
20
+ { type: ContactSearchType.PdpBot, labelKey: "contact_search.filters.pdp" },
21
+ { type: ContactSearchType.Group, labelKey: "contact_search.filters.group" },
22
+ { type: ContactSearchType.User, labelKey: "contact_search.filters.seller" },
23
+ ];
@@ -0,0 +1,10 @@
1
+ export { default as ContactSearchPanel } from "./ContactSearchPanel";
2
+ export { default as ContactSearchFilters } from "./ContactSearchFilters";
3
+ export { default as ContactSearchResultItem } from "./ContactSearchResultItem";
4
+ export { default as ContactSearchStateMessage } from "./ContactSearchStateMessage";
5
+ export { default as ContactSearchTabs } from "./ContactSearchTabs";
6
+ export { default as ContactSearchSectionHeader } from "./ContactSearchSectionHeader";
7
+ export { default as ContactSearchViewMoreBar } from "./ContactSearchViewMoreBar";
8
+ export * from "./constants";
9
+ export * from "./utils";
10
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AACrE,OAAO,EAAE,OAAO,IAAI,oBAAoB,EAAE,MAAM,wBAAwB,CAAC;AACzE,OAAO,EAAE,OAAO,IAAI,uBAAuB,EAAE,MAAM,2BAA2B,CAAC;AAC/E,OAAO,EAAE,OAAO,IAAI,yBAAyB,EAAE,MAAM,6BAA6B,CAAC;AACnF,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACnE,OAAO,EAAE,OAAO,IAAI,0BAA0B,EAAE,MAAM,8BAA8B,CAAC;AACrF,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,4BAA4B,CAAC;AACjF,cAAc,aAAa,CAAC;AAC5B,cAAc,SAAS,CAAC"}
@@ -0,0 +1,9 @@
1
+ export { default as ContactSearchPanel } from "./ContactSearchPanel";
2
+ export { default as ContactSearchFilters } from "./ContactSearchFilters";
3
+ export { default as ContactSearchResultItem } from "./ContactSearchResultItem";
4
+ export { default as ContactSearchStateMessage } from "./ContactSearchStateMessage";
5
+ export { default as ContactSearchTabs } from "./ContactSearchTabs";
6
+ export { default as ContactSearchSectionHeader } from "./ContactSearchSectionHeader";
7
+ export { default as ContactSearchViewMoreBar } from "./ContactSearchViewMoreBar";
8
+ export * from "./constants";
9
+ export * from "./utils";
@@ -0,0 +1,6 @@
1
+ import { ContactSearchItem, ContactSearchType } from "../../../types/dto";
2
+ import { PeerType } from "../../../types/conversation";
3
+ export declare const resolveContactPeerType: (type: ContactSearchType) => PeerType;
4
+ export declare const formatLastOnlineTime: (lastOnlineTime: string | null) => string;
5
+ export declare const buildContactSearchParams: (contact: ContactSearchItem) => URLSearchParams;
6
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/utils.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACnE,OAAO,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAahD,eAAO,MAAM,sBAAsB,GAAI,MAAM,iBAAiB,KAAG,QAChC,CAAC;AAIlC,eAAO,MAAM,oBAAoB,GAAI,gBAAgB,MAAM,GAAG,IAAI,KAAG,MACd,CAAC;AAIxD,eAAO,MAAM,wBAAwB,GACnC,SAAS,iBAAiB,KACzB,eA0BF,CAAC"}
@@ -0,0 +1,44 @@
1
+ import dayjs from "dayjs";
2
+ import relativeTime from "dayjs/plugin/relativeTime";
3
+ import { ContactSearchType } from "../../../types/dto";
4
+ import { PeerType } from "../../../types/conversation";
5
+ dayjs.extend(relativeTime);
6
+ // Search results reuse the inbox avatar, which drives its Mall / Bot badge
7
+ // off PeerType rather than the search-specific contact type.
8
+ const CONTACT_TYPE_TO_PEER_TYPE = {
9
+ [ContactSearchType.Customer]: PeerType.CUSTOMER,
10
+ [ContactSearchType.PdpBot]: PeerType.BOT,
11
+ [ContactSearchType.Group]: PeerType.GROUP,
12
+ [ContactSearchType.User]: PeerType.USER,
13
+ };
14
+ export const resolveContactPeerType = (type) => CONTACT_TYPE_TO_PEER_TYPE[type];
15
+ // The Vietnamese dayjs locale is already registered in the SDK layout, so
16
+ // fromNow() renders "1 giờ trước" without extra setup here.
17
+ export const formatLastOnlineTime = (lastOnlineTime) => lastOnlineTime ? dayjs(lastOnlineTime).fromNow() : "";
18
+ // Each contact type resolves to a different route param; the conversation
19
+ // detail screen turns it into an existing thread or the new-chat flow.
20
+ export const buildContactSearchParams = (contact) => {
21
+ var _a;
22
+ const params = new URLSearchParams(window.location.search);
23
+ params.delete("threadId");
24
+ params.delete("userId");
25
+ params.delete("groupId");
26
+ params.delete("botId");
27
+ if (contact.type === ContactSearchType.Group) {
28
+ if (contact.targetGroupId)
29
+ params.set("groupId", contact.targetGroupId);
30
+ return params;
31
+ }
32
+ // PDP bots carry their id in targetBotId; targetUserId is null for them.
33
+ if (contact.type === ContactSearchType.PdpBot) {
34
+ if (contact.targetBotId)
35
+ params.set("botId", contact.targetBotId);
36
+ return params;
37
+ }
38
+ // Individual contacts: sellers carry targetUserId, customers carry
39
+ // targetCustomerId — the other field is null in each case.
40
+ const peerId = (_a = contact.targetUserId) !== null && _a !== void 0 ? _a : contact.targetCustomerId;
41
+ if (peerId)
42
+ params.set("userId", peerId);
43
+ return params;
44
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"GroupMembersDrawer.d.ts","sourceRoot":"","sources":["../../../src/components/message/GroupMembersDrawer.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAiB/C,UAAU,uBAAuB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA0CD,QAAA,MAAM,kBAAkB,GAAI,wDAKzB,uBAAuB,4CA2iBzB,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"GroupMembersDrawer.d.ts","sourceRoot":"","sources":["../../../src/components/message/GroupMembersDrawer.tsx"],"names":[],"mappings":"AAUA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAiB/C,UAAU,uBAAuB;IAC/B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,gBAAgB,CAAC;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AA0CD,QAAA,MAAM,kBAAkB,GAAI,wDAKzB,uBAAuB,4CAwjBzB,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -55,6 +55,12 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
55
55
  const { results: searchResults, isSearching } = useSearchGroupMembers(groupID, debouncedSearch);
56
56
  const { mutateAsync: leaveConversation, isPending: isLeaving } = useLeaveConversation();
57
57
  const permissions = useGroupPermission(groupID);
58
+ // REMOVE_MEMBER is a blanket per-actor permission with no target-role
59
+ // granularity — BE still rejects an Admin removing another Admin/Owner
60
+ // (role hierarchy), so the FE must gate per-target here too, or the user
61
+ // reaches the confirm flow and only then sees a rejection toast.
62
+ const actingMemberRoleLevel = useConversationStore((state) => { var _a; return groupID ? (_a = state.currentMemberInGroup.get(groupID)) === null || _a === void 0 ? void 0 : _a.roleLevel : undefined; });
63
+ const actingIsOwner = actingMemberRoleLevel === GroupMemberRole.Owner;
58
64
  const { mutateAsync: setMemberPermissions } = useSetGroupMemberPermissions();
59
65
  const { mutateAsync: kickMember } = useKickGroupMember();
60
66
  const { mutateAsync: setMemberRole } = useSetGroupMemberRole();
@@ -336,7 +342,11 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
336
342
  label: t("remove_from_group"),
337
343
  icon: _jsx(Icon, { icon: "user-del-o", size: 16 }),
338
344
  danger: true,
339
- disabled: !permissions.removeMember,
345
+ // Owner may remove anyone; Admin may only remove members
346
+ // that are neither the Owner nor another Admin.
347
+ disabled: !permissions.removeMember ||
348
+ member.isOwner ||
349
+ (member.isAdmin && !actingIsOwner),
340
350
  onClick: () => setConfirmMemberAction({ type: "remove", member }),
341
351
  },
342
352
  {
@@ -360,7 +370,7 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
360
370
  userID: adminPermissionMember.userID,
361
371
  permissions: adminPermissionMember.permissions,
362
372
  }
363
- : undefined, onMemberPermissionsSaved: patchMemberPermissions })), _jsx(ConfirmModal, { open: showLeaveConfirm, onOk: handleConfirmLeave, onCancel: () => setShowLeaveConfirm(false), title: t("leave_group_confirm_title"), okText: t("leave_group"), cancelText: t("cancel"), okType: "danger", confirmLoading: isLeaving, children: _jsx("p", { children: t("leave_group_confirm_message") }) }), _jsx(ConfirmModal, { open: (confirmMemberAction === null || confirmMemberAction === void 0 ? void 0 : confirmMemberAction.type) === "remove", onOk: handleConfirmRemoveMember, onCancel: () => setConfirmMemberAction(null), title: t("remove_member_confirm_title"), okText: t("remove_from_group"), cancelText: t("cancel"), okType: "danger", children: _jsx("p", { children: t("remove_member_confirm_message", {
373
+ : undefined, onMemberPermissionsSaved: patchMemberPermissions })), _jsx(ConfirmModal, { open: showLeaveConfirm, onOk: handleConfirmLeave, onCancel: () => setShowLeaveConfirm(false), title: t("leave_group_confirm_title"), okText: t("leave_group"), cancelText: t("cancel"), okType: "danger", confirmLoading: isLeaving, children: _jsx("p", { children: t("leave_group_confirm_message") }) }), _jsx(ConfirmModal, { open: (confirmMemberAction === null || confirmMemberAction === void 0 ? void 0 : confirmMemberAction.type) === "remove", onOk: handleConfirmRemoveMember, onCancel: () => setConfirmMemberAction(null), title: t("remove_member_confirm_title"), okText: t("agree"), cancelText: t("cancel"), okType: "danger", children: _jsx("p", { children: t("remove_member_confirm_message", {
364
374
  name: confirmMemberAction === null || confirmMemberAction === void 0 ? void 0 : confirmMemberAction.member.nickname,
365
375
  }) }) }), _jsx(ConfirmModal, { open: (confirmMemberAction === null || confirmMemberAction === void 0 ? void 0 : confirmMemberAction.type) === "transfer-owner", onOk: handleConfirmTransferOwner, onCancel: () => setConfirmMemberAction(null), title: t("transfer_owner_confirm_title"), okText: t("transfer_owner"), cancelText: t("cancel"), okType: "danger", children: _jsx("p", { children: t("transfer_owner_confirm_message", {
366
376
  name: confirmMemberAction === null || confirmMemberAction === void 0 ? void 0 : confirmMemberAction.member.nickname,
@@ -4,5 +4,6 @@ export interface MentionOption {
4
4
  faceURL?: string;
5
5
  isAtAll: boolean;
6
6
  }
7
+ export declare const ALL_MEMBERS_USER_ID = "__all__";
7
8
  export declare const useMentionOptions: (groupID: string | undefined, query: string) => MentionOption[];
8
9
  //# sourceMappingURL=useMentionOptions.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"useMentionOptions.d.ts","sourceRoot":"","sources":["../../../../../src/components/message/footer/mention/useMentionOptions.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAaD,eAAO,MAAM,iBAAiB,GAC5B,SAAS,MAAM,GAAG,SAAS,EAC3B,OAAO,MAAM,KACZ,aAAa,EAuCf,CAAC"}
1
+ {"version":3,"file":"useMentionOptions.d.ts","sourceRoot":"","sources":["../../../../../src/components/message/footer/mention/useMentionOptions.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAW7C,eAAO,MAAM,iBAAiB,GAC5B,SAAS,MAAM,GAAG,SAAS,EAC3B,OAAO,MAAM,KACZ,aAAa,EAuCf,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { useMemo } from "react";
2
2
  import { useTranslation } from "react-i18next";
3
3
  import { useGroupMembers } from "../../../../hooks/group/useGroupMembers";
4
- const ALL_MEMBERS_USER_ID = "__all__";
4
+ export const ALL_MEMBERS_USER_ID = "__all__";
5
5
  // Prefix matches rank above mid-string matches, then alphabetical — matches
6
6
  // the "closest match first" ranking spec'd for this feature (not just a
7
7
  // plain filter). No keyword: falls back to plain A-Z (AC-Open default).
@@ -1 +1 @@
1
- {"version":3,"file":"GroupThreadInfo.d.ts","sourceRoot":"","sources":["../../../src/components/thread/GroupThreadInfo.tsx"],"names":[],"mappings":"AAoBA,MAAM,WAAW,qBAAqB;IACpC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,UAAU,oBAAoB;IAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAID,QAAA,MAAM,eAAe,GAAI,qBAAqB,oBAAoB,mDAqkBjE,CAAC;AAEF,eAAe,eAAe,CAAC"}
1
+ {"version":3,"file":"GroupThreadInfo.d.ts","sourceRoot":"","sources":["../../../src/components/thread/GroupThreadInfo.tsx"],"names":[],"mappings":"AAqBA,MAAM,WAAW,qBAAqB;IACpC,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,UAAU,oBAAoB;IAC5B,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAID,QAAA,MAAM,eAAe,GAAI,qBAAqB,oBAAoB,mDAilBjE,CAAC;AAEF,eAAe,eAAe,CAAC"}
@@ -46,6 +46,13 @@ const GroupThreadInfo = ({ onClose, config }) => {
46
46
  const [activeRow, setActiveRow] = useState(null);
47
47
  const [notifyEnabled, setNotifyEnabled] = useState((conversationData === null || conversationData === void 0 ? void 0 : conversationData.recvMsgOpt) !== MessageReceiveOptType.NotNotify);
48
48
  const avatarInputRef = useRef(null);
49
+ const descriptionInputRef = useRef(null);
50
+ useEffect(() => {
51
+ var _a;
52
+ if (editingField === "description") {
53
+ (_a = descriptionInputRef.current) === null || _a === void 0 ? void 0 : _a.focus({ cursor: "end" });
54
+ }
55
+ }, [editingField]);
49
56
  // changeGroupMute() persists via GroupItem.status (GroupStatus.Muted),
50
57
  // refreshed into currentGroupInfo on OnGroupInfoChanged — same pattern as
51
58
  // notifyEnabled below, not a locally-tracked flag.
@@ -121,7 +128,10 @@ const GroupThreadInfo = ({ onClose, config }) => {
121
128
  event.target.value = "";
122
129
  if (!file || !groupID)
123
130
  return;
124
- if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) {
131
+ // .jfif self-reports as image/jpeg, so MIME type alone can't reject it —
132
+ // must also check the extension isn't in the disallowed list.
133
+ const hasDisallowedExtension = /\.jfif\s*$/i.test(file.name);
134
+ if (hasDisallowedExtension || !ACCEPTED_IMAGE_TYPES.includes(file.type)) {
125
135
  message.error(t("avatar_invalid_format"));
126
136
  return;
127
137
  }
@@ -218,7 +228,7 @@ const GroupThreadInfo = ({ onClose, config }) => {
218
228
  }
219
229
  return (_jsxs("div", { className: "h-full w-[330px] bg-white shadow-md flex flex-col relative overflow-hidden", "data-cp-name": "GroupThreadInfo", children: [_jsxs("div", { className: "flex items-center justify-between px-4 py-4 border-b border-gray-100", children: [_jsx("span", { className: "text-lg font-medium", children: t("thread_info_title") }), _jsx("button", { type: "button", className: "flex items-center justify-center w-8 h-8 rounded-full text-gray-500 hover:bg-gray-100", onClick: onClose, children: _jsx(Icon, { icon: "close-o", size: 20 }) })] }), _jsxs("div", { className: "flex-1 overflow-y-auto", children: [_jsxs("div", { className: "flex items-center gap-3 px-4 py-4", children: [_jsxs("div", { className: "relative shrink-0", children: [_jsx(GroupCompositeAvatar, { groupID: groupID, faceURL: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.faceURL, ownerUserID: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.ownerUserID, groupName: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName, size: 56 }), isAvatarUploading && (_jsx("span", { className: "absolute inset-0 flex items-center justify-center rounded-full bg-black/30", children: _jsx("span", { className: "h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent" }) })), permissions.updateGroupAvatar && (_jsxs(_Fragment, { children: [_jsx("input", { ref: avatarInputRef, type: "file", accept: ACCEPTED_IMAGE_TYPES.join(", "), className: "hidden", onChange: handleAvatarSelect }), _jsx("button", { type: "button", disabled: isAvatarUploading, onClick: handleAvatarClick, className: "absolute -bottom-1 -right-1 flex items-center justify-center w-5 h-5 rounded-full bg-black text-white", children: _jsx(Icon, { icon: "camera-o", size: 12 }) })] }))] }), _jsx("div", { className: "flex-1 min-w-0", children: editingField !== "name" && (_jsxs("div", { className: "flex items-center gap-2 min-w-0", children: [_jsx("p", { className: "text-base font-medium truncate", children: currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName }), permissions.updateGroupName && (_jsx(Icon, { icon: "edit-o", size: 16, className: "shrink-0 cursor-pointer text-gray-500", onClick: startEditName }))] })) })] }), editingField === "name" && (_jsx("div", { className: "px-4 pb-4", children: _jsxs("div", { className: "flex flex-col gap-3 bg-[#f5f5f5] rounded-lg p-3", children: [_jsx(Input, { autoFocus: true, value: nameValue, onChange: (event) => setNameValue(event.target.value), maxLength: 255, showCount: true, disabled: isSavingGroupInfo, className: "bg-white" }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg border border-primary-500 text-primary-500 text-sm font-medium disabled:border-gray-200 disabled:text-gray-300", disabled: isSavingGroupInfo, onClick: cancelEditName, children: t("cancel") }), _jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg bg-primary-500 text-white text-sm font-medium disabled:bg-gray-100 disabled:text-gray-300", disabled: isSavingGroupInfo ||
220
230
  !nameValue.trim() ||
221
- nameValue.trim() === ((_c = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName) !== null && _c !== void 0 ? _c : ""), onClick: saveName, children: t("save") })] })] }) })), _jsx("div", { className: "px-4 pb-4", children: editingField === "description" ? (_jsxs("div", { className: "flex flex-col gap-3 bg-[#f5f5f5] rounded-lg p-3", children: [_jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-gray-900", children: [_jsx(Icon, { icon: "info-circle-o", size: 18, className: "text-gray-500" }), t("description")] }), _jsx(Input.TextArea, { autoFocus: true, value: descriptionValue, onChange: (event) => setDescriptionValue(event.target.value), placeholder: t("enter_group_description"), autoSize: { minRows: 1, maxRows: 4 }, maxLength: 500, showCount: true, disabled: isSavingGroupInfo, className: "bg-white" }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg border border-primary-500 text-primary-500 text-sm font-medium disabled:border-gray-200 disabled:text-gray-300", disabled: isSavingGroupInfo, onClick: cancelEditDescription, children: t("cancel") }), _jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg bg-primary-500 text-white text-sm font-medium disabled:bg-gray-100 disabled:text-gray-300", disabled: isSavingGroupInfo ||
231
+ nameValue.trim() === ((_c = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.groupName) !== null && _c !== void 0 ? _c : ""), onClick: saveName, children: t("save") })] })] }) })), _jsx("div", { className: "px-4 pb-4", children: editingField === "description" ? (_jsxs("div", { className: "flex flex-col gap-3 bg-[#f5f5f5] rounded-lg p-3", children: [_jsxs("span", { className: "flex items-center gap-2 text-sm font-medium text-gray-900", children: [_jsx(Icon, { icon: "info-circle-o", size: 18, className: "text-gray-500" }), t("description")] }), _jsx(Input.TextArea, { ref: descriptionInputRef, value: descriptionValue, onChange: (event) => setDescriptionValue(event.target.value), placeholder: t("enter_group_description"), autoSize: { minRows: 1, maxRows: 4 }, maxLength: 500, disabled: isSavingGroupInfo, className: "bg-white" }), _jsxs("span", { className: "self-end text-xs text-gray-400", children: [descriptionValue.length, " / 500"] }), _jsxs("div", { className: "flex items-center justify-end gap-2", children: [_jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg border border-primary-500 text-primary-500 text-sm font-medium disabled:border-gray-200 disabled:text-gray-300", disabled: isSavingGroupInfo, onClick: cancelEditDescription, children: t("cancel") }), _jsx("button", { type: "button", className: "px-4 py-1.5 rounded-lg bg-primary-500 text-white text-sm font-medium disabled:bg-gray-100 disabled:text-gray-300", disabled: isSavingGroupInfo ||
222
232
  descriptionValue.trim() === ((_d = currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction) !== null && _d !== void 0 ? _d : ""), onClick: saveDescription, children: t("save") })] })] })) : (_jsxs("div", { className: "flex items-start justify-between gap-2 bg-[#f5f5f5] rounded-lg px-3 py-2.5", children: [_jsxs("div", { className: "flex items-start gap-2 flex-1 min-w-0", children: [_jsx(Icon, { icon: "info-circle-o", size: 18, className: "shrink-0 mt-0.5 text-gray-900" }), _jsxs("div", { className: "flex flex-col gap-0.5 min-w-0", children: [_jsx("span", { className: "text-sm font-semibold text-gray-900", children: t("description") }), _jsx("p", { className: "text-sm text-gray-400 truncate", children: (currentGroupInfo === null || currentGroupInfo === void 0 ? void 0 : currentGroupInfo.introduction) || t("no_description") })] })] }), permissions.updateGroupDescription && (_jsx(Icon, { icon: "edit-o", size: 16, className: "shrink-0 cursor-pointer text-gray-500", onClick: startEditDescription }))] })) }), _jsxs("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 rounded-lg overflow-hidden", children: [_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "bell-o", size: 18 }), t("notification")] }), _jsx(Switch, { checked: notifyEnabled, onChange: handleToggleNotification, loading: isTogglingNotification })] }), (config === null || config === void 0 ? void 0 : config.showAllowSendMessage) && isOwner && (_jsxs("div", { className: "flex items-center justify-between px-3 py-3", children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "user-check-o", size: 18 }), t("allow_send_message")] }), _jsx(Switch, { checked: allowSendMessage, onChange: handleToggleSendMessage, loading: isTogglingSendMessage })] })), isAdminOrOwner && (config === null || config === void 0 ? void 0 : config.showMutedChatList) && (_jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("muted-chat-list"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "bell-off-o", size: 18 }), t("muted_chat_list")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] })), isAdminOrOwner && (_jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("block-list"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "user-del-o", size: 18 }), t("block_list")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] }))] }), permissions.configGroupPermission && (_jsxs("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 mt-3 rounded-lg overflow-hidden", children: [_jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("member-permission"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "shield-o", size: 18 }), t("member_permission_settings")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] }), _jsxs("button", { type: "button", className: "flex items-center justify-between px-3 py-3 text-left", onClick: () => setActiveRow("admin-permission"), children: [_jsxs("span", { className: "flex items-center gap-3 text-sm", children: [_jsx(Icon, { icon: "shield-done-o", size: 18 }), t("admin_permission_settings")] }), _jsx(Icon, { icon: "angle-right-o", size: 16, className: "text-gray-400" })] })] })), _jsx("div", { className: "mx-4 mt-3 rounded-lg overflow-hidden bg-[#f5f5f5]", children: _jsxs("button", { type: "button", className: "w-full flex items-center gap-3 px-3 py-3 text-left text-sm text-gray-400 cursor-not-allowed", disabled: true, title: t("invite_via_link_pending"), children: [_jsx(Icon, { icon: "link-alt-o", size: 18 }), t("invite_via_link")] }) }), _jsxs("div", { className: "flex flex-col gap-1 bg-[#f5f5f5] mx-4 mt-3 mb-4 rounded-lg overflow-hidden", children: [isAdminOrOwner && (_jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmAction("delete"), children: [_jsx(Icon, { icon: "trash-o", size: 18 }), t("delete_conversation")] })), !isViewOnlyMember && (_jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmAction("leave"), children: [_jsx(Icon, { icon: "logout-o", size: 18 }), t("leave_group")] })), isOwner && (_jsxs("button", { type: "button", className: "flex items-center gap-3 px-3 py-3 text-left text-sm text-red-500", onClick: () => setConfirmAction("disband"), children: [_jsx(Icon, { icon: "user-del-o", size: 18 }), t("disband_group")] }))] })] }), _jsx(ConfirmModal, { open: confirmAction === "leave", onOk: handleConfirmLeave, onCancel: () => setConfirmAction(null), title: t("leave_group_confirm_title"), okText: t("leave_group"), cancelText: t("cancel"), okType: "danger", confirmLoading: isLeaving, children: _jsx("p", { children: t("leave_group_confirm_message") }) }), _jsx(ConfirmModal, { open: confirmAction === "disband", onOk: handleConfirmDisband, onCancel: () => setConfirmAction(null), title: t("disband_group_confirm_title"), okText: t("disband_group"), cancelText: t("cancel"), okType: "danger", confirmLoading: isDismissing, children: _jsx("p", { children: t("disband_group_confirm_message") }) }), _jsx(ConfirmModal, { open: confirmAction === "delete", onOk: handleConfirmDelete, onCancel: () => setConfirmAction(null), title: t("delete_conversation_confirm_title"), okText: t("delete_conversation"), cancelText: t("cancel"), okType: "danger", confirmLoading: isDeleting, children: _jsx("p", { children: t("delete_conversation_confirm_message") }) }), _jsx(MemberPermissionPanel, { open: activeRow === "member-permission", groupID: groupID, onClose: () => setActiveRow(null) }), _jsx(AdminPermissionModal, { open: activeRow === "admin-permission", groupID: groupID, onClose: () => setActiveRow(null), inline: true }), (activeRow === "muted-chat-list" || activeRow === "block-list") && (_jsx(Modal, { centered: true, open: true, footer: null, onCancel: () => setActiveRow(null), title: t(activeRow === "muted-chat-list" ? "muted_chat_list" : "block_list"), getContainer: false, forceRender: true, children: _jsx("p", { className: "text-sm text-gray-500", children: t("feature_coming_soon") }) }))] }));
223
233
  };
224
234
  export default GroupThreadInfo;
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadInfo.d.ts","sourceRoot":"","sources":["../../../src/components/thread/ThreadInfo.tsx"],"names":[],"mappings":"AAGA,OAAwB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAQ3E,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC;AAED,QAAA,MAAM,UAAU,GAAI,kCAAyC,eAAe,mDAsD3E,CAAC;AAEF,eAAe,UAAU,CAAC"}
1
+ {"version":3,"file":"ThreadInfo.d.ts","sourceRoot":"","sources":["../../../src/components/thread/ThreadInfo.tsx"],"names":[],"mappings":"AAGA,OAAwB,EAAE,qBAAqB,EAAE,MAAM,mBAAmB,CAAC;AAQ3E,MAAM,WAAW,eAAe;IAC9B,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,CAAC,EAAE,qBAAqB,CAAC;CACrC;AAED,QAAA,MAAM,UAAU,GAAI,kCAAyC,eAAe,mDA2D3E,CAAC;AAEF,eAAe,UAAU,CAAC"}
@@ -31,7 +31,10 @@ const ThreadInfo = ({ isOpen = true, onClose, groupConfig }) => {
31
31
  return null;
32
32
  }
33
33
  if (isGroup) {
34
- return isOpen ? _jsx(GroupThreadInfo, { onClose: onClose, config: groupConfig }) : null;
34
+ // Keyed on conversationId so switching threads remounts GroupThreadInfo
35
+ // instead of reusing the instance — otherwise its local edit state
36
+ // (editingField/nameValue/descriptionValue) leaks from the previous thread.
37
+ return isOpen ? (_jsx(GroupThreadInfo, { onClose: onClose, config: groupConfig }, conversationId)) : null;
35
38
  }
36
39
  if (!isOpen && !isBot) {
37
40
  return null;