@droppii-org/chat-sdk 0.7.12 → 0.7.14
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/components/conversation-detail/thread-search/ThreadSearchContent.d.ts.map +1 -1
- package/dist/components/conversation-detail/thread-search/ThreadSearchContent.js +4 -2
- package/dist/components/conversation-inbox/contact-search/ContactSearchMessageResultItem.d.ts +10 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchMessageResultItem.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchMessageResultItem.js +30 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchMessageSection.d.ts +12 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchMessageSection.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchMessageSection.js +14 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchMessagesTab.d.ts +11 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchMessagesTab.d.ts.map +1 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchMessagesTab.js +31 -0
- package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts +5 -1
- package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts.map +1 -1
- package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.js +54 -9
- package/dist/components/conversation-inbox/contact-search/constants.js +2 -2
- package/dist/components/message/GroupMembersDrawer.d.ts.map +1 -1
- package/dist/components/message/GroupMembersDrawer.js +35 -6
- package/dist/components/message/GroupMembersPanelCrm.d.ts +5 -1
- package/dist/components/message/GroupMembersPanelCrm.d.ts.map +1 -1
- package/dist/components/message/GroupMembersPanelCrm.js +7 -3
- package/dist/components/message/footer/mention/useMentionOptions.d.ts.map +1 -1
- package/dist/components/message/footer/mention/useMentionOptions.js +26 -5
- package/dist/hooks/group/useGroupMembers.d.ts +8 -1
- package/dist/hooks/group/useGroupMembers.d.ts.map +1 -1
- package/dist/hooks/group/useGroupMembers.js +83 -15
- package/dist/hooks/group/useSearchGroupMembers.d.ts.map +1 -1
- package/dist/hooks/group/useSearchGroupMembers.js +11 -2
- package/dist/hooks/search/useAppMessageSearch.d.ts +317 -0
- package/dist/hooks/search/useAppMessageSearch.d.ts.map +1 -0
- package/dist/hooks/search/useAppMessageSearch.js +53 -0
- package/dist/hooks/search/useSearchGlobalMessages.d.ts +306 -0
- package/dist/hooks/search/useSearchGlobalMessages.d.ts.map +1 -0
- package/dist/hooks/search/useSearchGlobalMessages.js +15 -0
- package/dist/hooks/search/useSearchThreadMessages.d.ts +115 -36
- package/dist/hooks/search/useSearchThreadMessages.d.ts.map +1 -1
- package/dist/hooks/search/useSearchThreadMessages.js +5 -52
- package/dist/locales/vi/conversation-inbox.json +0 -1
- package/dist/screens/conversation-inbox/index.d.ts +2 -0
- package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
- package/dist/screens/conversation-inbox/index.js +2 -2
- 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/styles/global.css +1 -1
- package/dist/types/dto.d.ts +15 -2
- package/dist/types/dto.d.ts.map +1 -1
- package/dist/utils/imCommon.d.ts +1 -0
- package/dist/utils/imCommon.d.ts.map +1 -1
- package/dist/utils/imCommon.js +16 -23
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThreadSearchContent.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/thread-search/ThreadSearchContent.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAwC,KAAK,QAAQ,EAAE,MAAM,MAAM,CAAC;AAI3E,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,8BAA8B,CAAC;AAYtC,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,sFAAsF;IACtF,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAGD,QAAA,MAAM,mBAAmB,GAAI,sEAM1B,wBAAwB,
|
|
1
|
+
{"version":3,"file":"ThreadSearchContent.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/thread-search/ThreadSearchContent.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAwC,KAAK,QAAQ,EAAE,MAAM,MAAM,CAAC;AAI3E,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,8BAA8B,CAAC;AAYtC,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,sFAAsF;IACtF,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAGD,QAAA,MAAM,mBAAmB,GAAI,sEAM1B,wBAAwB,4CA4I1B,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { Input, Spin, message as toastMessage } from "antd";
|
|
5
5
|
import { useDebounce } from "ahooks";
|
|
@@ -51,7 +51,9 @@ const ThreadSearchContent = ({ conversation, inputRef, onClose, onJump, onSearch
|
|
|
51
51
|
return (_jsx("div", { "data-testid": "thread-search-loading", className: "flex justify-center py-8", children: _jsx(Spin, {}) }));
|
|
52
52
|
}
|
|
53
53
|
if (results.length === 0) {
|
|
54
|
-
return (_jsx("div", { role: "status", className: "flex flex-1 flex-col", children: _jsx(ContactSearchStateMessage, { testId: "thread-search-empty", children: isError
|
|
54
|
+
return (_jsx("div", { role: "status", className: "flex flex-1 flex-col", children: _jsx(ContactSearchStateMessage, { testId: "thread-search-empty", children: isError
|
|
55
|
+
? t("thread_detail.search.failed")
|
|
56
|
+
: t("thread_detail.search.no_result_title") }) }));
|
|
55
57
|
}
|
|
56
58
|
return (_jsx("div", { id: SCROLL_TARGET_ID, className: "min-h-0 flex-1 overflow-y-auto", children: _jsx(InfiniteScroll, { dataLength: results.length, next: fetchNextPage,
|
|
57
59
|
// A failed page would otherwise leave the loader spinning for good.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { AppSearchMessageItem } from "../../../types/dto";
|
|
2
|
+
interface ContactSearchMessageResultItemProps {
|
|
3
|
+
item: AppSearchMessageItem;
|
|
4
|
+
selfUserID: string;
|
|
5
|
+
searchTerm: string;
|
|
6
|
+
onSelect: (item: AppSearchMessageItem) => void;
|
|
7
|
+
}
|
|
8
|
+
declare const ContactSearchMessageResultItem: ({ item, selfUserID, searchTerm, onSelect, }: ContactSearchMessageResultItemProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export default ContactSearchMessageResultItem;
|
|
10
|
+
//# sourceMappingURL=ContactSearchMessageResultItem.d.ts.map
|
package/dist/components/conversation-inbox/contact-search/ContactSearchMessageResultItem.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContactSearchMessageResultItem.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchMessageResultItem.tsx"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAKxD,UAAU,mCAAmC;IAC3C,IAAI,EAAE,oBAAoB,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAC;CAChD;AAID,QAAA,MAAM,8BAA8B,GAAI,6CAKrC,mCAAmC,4CA+ErC,CAAC;AAEF,eAAe,8BAA8B,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } 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 { getSearchHitText } from "../../../components/conversation-detail/thread-search/utils";
|
|
7
|
+
import { PeerType } from "../../../types/conversation";
|
|
8
|
+
import { formatTimestamp, splitHighlight } from "../../../utils/common";
|
|
9
|
+
import { resolvePeerBadgeLabel } from "../../../utils/conversation";
|
|
10
|
+
import { isGroupSession } from "../../../utils/imCommon";
|
|
11
|
+
// One message hit: the thread it belongs to on top, the matched text below,
|
|
12
|
+
// prefixed with its sender the way biz-mobile's global search does.
|
|
13
|
+
const ContactSearchMessageResultItem = ({ item, selfUserID, searchTerm, onSelect, }) => {
|
|
14
|
+
var _a;
|
|
15
|
+
const { t } = useTranslation("conversation-inbox");
|
|
16
|
+
const { chatLog, conversation } = item;
|
|
17
|
+
const peerType = (_a = conversation === null || conversation === void 0 ? void 0 : conversation.peerType) !== null && _a !== void 0 ? _a : (isGroupSession(chatLog.sessionType) ? PeerType.GROUP : PeerType.USER);
|
|
18
|
+
const isGroup = peerType === PeerType.GROUP;
|
|
19
|
+
const name = (conversation === null || conversation === void 0 ? void 0 : conversation.showName) || "";
|
|
20
|
+
// 1-1 and bot threads already name the other side in the title.
|
|
21
|
+
const sender = !!selfUserID && chatLog.sendID === selfUserID
|
|
22
|
+
? t("thread_detail.search.you")
|
|
23
|
+
: isGroup
|
|
24
|
+
? chatLog.senderNickname || ""
|
|
25
|
+
: "";
|
|
26
|
+
const text = getSearchHitText(chatLog);
|
|
27
|
+
const parts = splitHighlight(text, searchTerm.trim());
|
|
28
|
+
return (_jsxs("button", { type: "button", "data-cp-name": "ContactSearchMessageResultItem", "data-testid": "contact-search-message-result", 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(item), children: [_jsx(ConversationPeerAvatar, { peerType: peerType, displayName: name, avatarUrl: (conversation === null || conversation === void 0 ? void 0 : conversation.faceURL) || null, 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 flex-1 truncate text-[16px] font-medium leading-[1.6] text-black", children: name }), _jsx("span", { className: "shrink-0 pl-2 text-[12px] leading-[1.6] text-gray-400", children: formatTimestamp(chatLog.sendTime, { hasTime: true }) })] }), _jsxs("span", { "data-testid": "contact-search-message-preview", className: "flex min-w-0 text-[14px] leading-[1.6] tracking-[0.14px] text-gray-600", children: [sender && (_jsxs(_Fragment, { children: [_jsx("span", { className: "max-w-[50%] shrink-0 truncate", children: sender }), _jsx("span", { className: "shrink-0 whitespace-pre", children: ": " })] })), _jsx("span", { className: "min-w-0 truncate", children: parts ? (_jsxs(_Fragment, { children: [parts.before, _jsx("span", { className: "text-[var(--dc-color-primary)]", children: parts.match }), parts.after] })) : (text) })] })] })] }));
|
|
29
|
+
};
|
|
30
|
+
export default ContactSearchMessageResultItem;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AppSearchMessageItem } from "../../../types/dto";
|
|
2
|
+
interface ContactSearchMessageSectionProps {
|
|
3
|
+
items: AppSearchMessageItem[];
|
|
4
|
+
hasMore: boolean;
|
|
5
|
+
searchTerm: string;
|
|
6
|
+
selfUserID: string;
|
|
7
|
+
onSelect: (item: AppSearchMessageItem) => void;
|
|
8
|
+
onViewAll: () => void;
|
|
9
|
+
}
|
|
10
|
+
declare const ContactSearchMessageSection: ({ items, hasMore, searchTerm, selfUserID, onSelect, onViewAll, }: ContactSearchMessageSectionProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
export default ContactSearchMessageSection;
|
|
12
|
+
//# sourceMappingURL=ContactSearchMessageSection.d.ts.map
|
package/dist/components/conversation-inbox/contact-search/ContactSearchMessageSection.d.ts.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContactSearchMessageSection.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchMessageSection.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAMxD,UAAU,gCAAgC;IACxC,KAAK,EAAE,oBAAoB,EAAE,CAAC;IAC9B,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC/C,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAID,QAAA,MAAM,2BAA2B,GAAI,kEAOlC,gCAAgC,4CAqBlC,CAAC;AAEF,eAAe,2BAA2B,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useTranslation } from "react-i18next";
|
|
4
|
+
import ContactSearchMessageResultItem from "./ContactSearchMessageResultItem";
|
|
5
|
+
import ContactSearchSectionHeader from "./ContactSearchSectionHeader";
|
|
6
|
+
import ContactSearchViewMoreBar from "./ContactSearchViewMoreBar";
|
|
7
|
+
import { CONTACT_SEARCH_PREVIEW_LIMIT } from "./constants";
|
|
8
|
+
// `Tất cả` preview of the `Tin nhắn` tab. No count: the search API always
|
|
9
|
+
// reports a total of 0.
|
|
10
|
+
const ContactSearchMessageSection = ({ items, hasMore, searchTerm, selfUserID, onSelect, onViewAll, }) => {
|
|
11
|
+
const { t } = useTranslation("conversation-inbox");
|
|
12
|
+
return (_jsxs("section", { "data-cp-name": "ContactSearchMessageSection", children: [_jsx(ContactSearchSectionHeader, { title: t("contact_search.tabs.messages"), onSelect: onViewAll }), items.slice(0, CONTACT_SEARCH_PREVIEW_LIMIT).map((item) => (_jsx(ContactSearchMessageResultItem, { item: item, selfUserID: selfUserID, searchTerm: searchTerm, onSelect: onSelect }, item.chatLog.clientMsgID))), hasMore && _jsx(ContactSearchViewMoreBar, { onSelect: onViewAll })] }));
|
|
13
|
+
};
|
|
14
|
+
export default ContactSearchMessageSection;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { useSearchGlobalMessages } from "../../../hooks/search/useSearchGlobalMessages";
|
|
2
|
+
import type { AppSearchMessageItem } from "../../../types/dto";
|
|
3
|
+
interface ContactSearchMessagesTabProps {
|
|
4
|
+
searchTerm: string;
|
|
5
|
+
search: ReturnType<typeof useSearchGlobalMessages>;
|
|
6
|
+
selfUserID: string;
|
|
7
|
+
onSelect: (item: AppSearchMessageItem) => void;
|
|
8
|
+
}
|
|
9
|
+
declare const ContactSearchMessagesTab: ({ searchTerm, search, selfUserID, onSelect, }: ContactSearchMessagesTabProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export default ContactSearchMessagesTab;
|
|
11
|
+
//# sourceMappingURL=ContactSearchMessagesTab.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ContactSearchMessagesTab.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchMessagesTab.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,wCAAwC,CAAC;AACtF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAC;AAMxD,UAAU,6BAA6B;IACrC,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,UAAU,CAAC,OAAO,uBAAuB,CAAC,CAAC;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,CAAC,IAAI,EAAE,oBAAoB,KAAK,IAAI,CAAC;CAChD;AAID,QAAA,MAAM,wBAAwB,GAAI,+CAK/B,6BAA6B,4CA6D/B,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Spin } from "antd";
|
|
4
|
+
import InfiniteScroll from "react-infinite-scroll-component";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
import ContactSearchMessageResultItem from "./ContactSearchMessageResultItem";
|
|
7
|
+
import ContactSearchStateMessage from "./ContactSearchStateMessage";
|
|
8
|
+
const SCROLL_TARGET_ID = "contact-search-messages-scroll";
|
|
9
|
+
// Full `Tin nhắn` list. The query lives in the panel so `Tất cả` and this
|
|
10
|
+
// tab share one cache entry.
|
|
11
|
+
const ContactSearchMessagesTab = ({ searchTerm, search, selfUserID, onSelect, }) => {
|
|
12
|
+
const { t } = useTranslation("conversation-inbox");
|
|
13
|
+
const { results, isLoading, isError, hasNextPage, isFetchNextPageError } = search;
|
|
14
|
+
if (!searchTerm) {
|
|
15
|
+
return (_jsx(ContactSearchStateMessage, { testId: "contact-search-messages-initial", children: t("contact_search.initial") }));
|
|
16
|
+
}
|
|
17
|
+
if (isLoading) {
|
|
18
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Spin, {}) }));
|
|
19
|
+
}
|
|
20
|
+
if (isError && results.length === 0) {
|
|
21
|
+
return (_jsx(ContactSearchStateMessage, { testId: "contact-search-messages-failed", children: t("contact_search.failed") }));
|
|
22
|
+
}
|
|
23
|
+
// The message search spec dropped the helper line under the title.
|
|
24
|
+
if (results.length === 0) {
|
|
25
|
+
return (_jsx(ContactSearchStateMessage, { testId: "contact-search-messages-no-result", children: t("contact_search.no_result_title") }));
|
|
26
|
+
}
|
|
27
|
+
return (_jsx("div", { id: SCROLL_TARGET_ID, className: "flex-1 overflow-y-auto", children: _jsx(InfiniteScroll, { dataLength: results.length, next: search.fetchNextPage,
|
|
28
|
+
// A failed page would otherwise leave the loader spinning for good.
|
|
29
|
+
hasMore: !!hasNextPage && !isFetchNextPageError, scrollableTarget: SCROLL_TARGET_ID, loader: _jsx("div", { className: "flex items-center justify-center py-3", children: _jsx(Spin, { size: "small" }) }), children: results.map((item) => (_jsx(ContactSearchMessageResultItem, { item: item, selfUserID: selfUserID, searchTerm: searchTerm, onSelect: onSelect }, item.chatLog.clientMsgID))) }) }));
|
|
30
|
+
};
|
|
31
|
+
export default ContactSearchMessagesTab;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
interface ContactSearchPanelProps {
|
|
2
2
|
open: boolean;
|
|
3
3
|
onClose: () => void;
|
|
4
|
+
/** Searches messages in `Tất cả` and the `Tin nhắn` tab; off keeps the tab's placeholder. */
|
|
5
|
+
showMessageSearch?: boolean;
|
|
6
|
+
/** Host toast for a revoked message hit; the SDK toast shows otherwise. */
|
|
7
|
+
onSearchMessageRevoked?: () => void;
|
|
4
8
|
}
|
|
5
|
-
declare const ContactSearchPanel: ({ open, onClose }: ContactSearchPanelProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
declare const ContactSearchPanel: ({ open, onClose, showMessageSearch, onSearchMessageRevoked, }: ContactSearchPanelProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
10
|
export default ContactSearchPanel;
|
|
7
11
|
//# sourceMappingURL=ContactSearchPanel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ContactSearchPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchPanel.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ContactSearchPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchPanel.tsx"],"names":[],"mappings":"AAgDA,UAAU,uBAAuB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,2EAA2E;IAC3E,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAED,QAAA,MAAM,kBAAkB,GAAI,+DAKzB,uBAAuB,4CA6XzB,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { Drawer, Input, Spin } from "antd";
|
|
4
|
+
import { Drawer, Input, Spin, message as toastMessage, } from "antd";
|
|
5
5
|
import { useDebounce } from "ahooks";
|
|
6
6
|
import InfiniteScroll from "react-infinite-scroll-component";
|
|
7
7
|
import { useTranslation } from "react-i18next";
|
|
8
|
+
import { MessageType } from "@droppii-org/wasm-client-sdk";
|
|
8
9
|
import { Icon } from "../../../components/icon";
|
|
9
10
|
import { useChatNavigate } from "../../../hooks/conversation/useChatNavigate";
|
|
10
11
|
import { useSearchContacts } from "../../../hooks/search/useSearchContacts";
|
|
12
|
+
import { useSearchGlobalMessages } from "../../../hooks/search/useSearchGlobalMessages";
|
|
11
13
|
import { useSearchPublicGroups } from "../../../hooks/search/useSearchPublicGroups";
|
|
14
|
+
import useAuthStore from "../../../store/auth";
|
|
12
15
|
import useConversationStore from "../../../store/conversation";
|
|
16
|
+
import { getConversationIdFromMessage } from "../../../utils/conversation";
|
|
13
17
|
import ContactSearchCommunitySection from "./ContactSearchCommunitySection";
|
|
14
18
|
import ContactSearchFilters from "./ContactSearchFilters";
|
|
19
|
+
import ContactSearchMessageSection from "./ContactSearchMessageSection";
|
|
20
|
+
import ContactSearchMessagesTab from "./ContactSearchMessagesTab";
|
|
15
21
|
import ContactSearchResultItem from "./ContactSearchResultItem";
|
|
16
22
|
import ContactSearchSectionHeader from "./ContactSearchSectionHeader";
|
|
17
23
|
import ContactSearchStateMessage from "./ContactSearchStateMessage";
|
|
@@ -22,10 +28,13 @@ import { CONTACT_SEARCH_DEBOUNCE_MS, CONTACT_SEARCH_ICON_SIZE, CONTACT_SEARCH_PR
|
|
|
22
28
|
import { buildContactSearchParams, resolveContactSearchTarget } from "./utils";
|
|
23
29
|
const SCROLL_TARGET_ID = "contact-search-scroll";
|
|
24
30
|
const COMMUNITY_SCROLL_TARGET_ID = "contact-search-community-scroll";
|
|
25
|
-
const ContactSearchPanel = ({ open, onClose }) => {
|
|
31
|
+
const ContactSearchPanel = ({ open, onClose, showMessageSearch = false, onSearchMessageRevoked, }) => {
|
|
26
32
|
const { t } = useTranslation("conversation-inbox");
|
|
33
|
+
const { t: tCommon } = useTranslation();
|
|
27
34
|
const onNavigate = useChatNavigate();
|
|
35
|
+
const selfUserID = useAuthStore((state) => state.userID);
|
|
28
36
|
const setPendingResolveTarget = useConversationStore((state) => state.setPendingResolveTarget);
|
|
37
|
+
const setSelectedConversationId = useConversationStore((state) => state.setSelectedConversationId);
|
|
29
38
|
const [keyword, setKeyword] = useState("");
|
|
30
39
|
const [activeType, setActiveType] = useState();
|
|
31
40
|
const [activeTab, setActiveTab] = useState(ContactSearchTab.All);
|
|
@@ -54,6 +63,14 @@ const ContactSearchPanel = ({ open, onClose }) => {
|
|
|
54
63
|
(activeTab === ContactSearchTab.All ||
|
|
55
64
|
activeTab === ContactSearchTab.Community),
|
|
56
65
|
});
|
|
66
|
+
// Same sharing between `Tất cả` and `Tin nhắn`.
|
|
67
|
+
const messages = useSearchGlobalMessages({
|
|
68
|
+
searchTerm: trimmedKeyword,
|
|
69
|
+
enabled: open &&
|
|
70
|
+
showMessageSearch &&
|
|
71
|
+
(activeTab === ContactSearchTab.All ||
|
|
72
|
+
activeTab === ContactSearchTab.Messages),
|
|
73
|
+
});
|
|
57
74
|
const handleOpenGroup = (groupID) => {
|
|
58
75
|
const target = { groupId: groupID };
|
|
59
76
|
setPendingResolveTarget(target);
|
|
@@ -68,6 +85,26 @@ const ContactSearchPanel = ({ open, onClose }) => {
|
|
|
68
85
|
onClose();
|
|
69
86
|
onNavigate(`${window.location.pathname}?${params.toString()}`);
|
|
70
87
|
};
|
|
88
|
+
// A hit opens like a copied message link: the thread from the URL, then a
|
|
89
|
+
// jump to the message.
|
|
90
|
+
const handleSelectMessage = ({ chatLog, conversation }) => {
|
|
91
|
+
if (chatLog.isRevoked || chatLog.contentType === MessageType.RevokeMessage) {
|
|
92
|
+
if (onSearchMessageRevoked)
|
|
93
|
+
onSearchMessageRevoked();
|
|
94
|
+
else
|
|
95
|
+
toastMessage.info(tCommon("message_revoked_or_deleted"));
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const threadId = (conversation === null || conversation === void 0 ? void 0 : conversation.conversationID) || getConversationIdFromMessage(chatLog);
|
|
99
|
+
if (!threadId)
|
|
100
|
+
return;
|
|
101
|
+
const params = buildContactSearchParams({});
|
|
102
|
+
params.set("threadId", threadId);
|
|
103
|
+
params.set("messageId", chatLog.clientMsgID);
|
|
104
|
+
setSelectedConversationId(threadId);
|
|
105
|
+
onClose();
|
|
106
|
+
onNavigate(`${window.location.pathname}?${params.toString()}`);
|
|
107
|
+
};
|
|
71
108
|
// Filters belong to the `Liên hệ` tab only, so leaving it drops the active
|
|
72
109
|
// type — `Tất cả` always previews every contact type.
|
|
73
110
|
const handleChangeTab = (tab) => {
|
|
@@ -93,23 +130,31 @@ const ContactSearchPanel = ({ open, onClose }) => {
|
|
|
93
130
|
return renderNoResultState();
|
|
94
131
|
return null;
|
|
95
132
|
};
|
|
96
|
-
// `Tất cả` previews contacts, then public groups
|
|
97
|
-
// belongs to the message-search ticket and goes between the two.
|
|
133
|
+
// `Tất cả` previews contacts, then messages, then public groups.
|
|
98
134
|
const renderAllTab = () => {
|
|
99
135
|
if (!trimmedKeyword)
|
|
100
136
|
return renderInitialState();
|
|
101
|
-
if (isLoading || publicGroups.isLoading)
|
|
137
|
+
if (isLoading || publicGroups.isLoading || messages.isLoading) {
|
|
102
138
|
return renderLoadingState();
|
|
103
|
-
|
|
139
|
+
}
|
|
140
|
+
// A failed next page keeps the hits already loaded.
|
|
141
|
+
const messagesFailed = !showMessageSearch ||
|
|
142
|
+
(messages.isError && messages.results.length === 0);
|
|
143
|
+
if (isError && publicGroups.isError && messagesFailed) {
|
|
104
144
|
return renderFailedState();
|
|
145
|
+
}
|
|
105
146
|
// A source that failed drops its own section instead of the whole tab.
|
|
106
147
|
const showContacts = !isError && contacts.length > 0;
|
|
148
|
+
const showMessages = !messagesFailed && messages.results.length > 0;
|
|
107
149
|
const showGroups = !publicGroups.isError && publicGroups.groups.length > 0;
|
|
108
|
-
if (!showContacts && !showGroups)
|
|
150
|
+
if (!showContacts && !showMessages && !showGroups) {
|
|
109
151
|
return renderNoResultState();
|
|
152
|
+
}
|
|
110
153
|
const hasMoreContacts = contacts.length > CONTACT_SEARCH_PREVIEW_LIMIT || !!hasNextPage;
|
|
111
154
|
const openContactsTab = () => handleChangeTab(ContactSearchTab.Contacts);
|
|
112
|
-
|
|
155
|
+
// The design separates the sections with a 4px gray-5 band.
|
|
156
|
+
return (_jsxs("div", { className: "flex-1 overflow-y-auto [&>section+section]:border-t-4 [&>section+section]:border-gray-5", children: [showContacts && (_jsxs("section", { children: [_jsx(ContactSearchSectionHeader, { title: t("contact_search.tabs.contacts"), count: totalElements || undefined, onSelect: openContactsTab }), renderContactItems(contacts.slice(0, CONTACT_SEARCH_PREVIEW_LIMIT)), hasMoreContacts && (_jsx(ContactSearchViewMoreBar, { onSelect: openContactsTab }))] })), showMessages && (_jsx(ContactSearchMessageSection, { items: messages.results, hasMore: messages.results.length > CONTACT_SEARCH_PREVIEW_LIMIT ||
|
|
157
|
+
!!messages.hasNextPage, searchTerm: trimmedKeyword, selfUserID: selfUserID, onSelect: handleSelectMessage, onViewAll: () => handleChangeTab(ContactSearchTab.Messages) })), showGroups && (_jsx(ContactSearchCommunitySection, { groups: publicGroups.groups, total: publicGroups.total, onOpenGroup: handleOpenGroup, onViewAll: () => handleChangeTab(ContactSearchTab.Community) }))] }));
|
|
113
158
|
};
|
|
114
159
|
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) }) }));
|
|
115
160
|
const renderCommunityTab = () => {
|
|
@@ -145,7 +190,7 @@ const ContactSearchPanel = ({ open, onClose }) => {
|
|
|
145
190
|
{
|
|
146
191
|
key: ContactSearchTab.Messages,
|
|
147
192
|
label: t("contact_search.tabs.messages"),
|
|
148
|
-
children: (_jsx(ContactSearchStateMessage, { testId: "contact-search-messages", children: t("contact_search.messages_not_available") })),
|
|
193
|
+
children: showMessageSearch ? (_jsx(ContactSearchMessagesTab, { searchTerm: trimmedKeyword, search: messages, selfUserID: selfUserID, onSelect: handleSelectMessage })) : (_jsx(ContactSearchStateMessage, { testId: "contact-search-messages", children: t("contact_search.messages_not_available") })),
|
|
149
194
|
},
|
|
150
195
|
{
|
|
151
196
|
key: ContactSearchTab.Community,
|
|
@@ -2,8 +2,8 @@ import { ContactSearchType } from "../../../types/dto";
|
|
|
2
2
|
export const CONTACT_SEARCH_DEBOUNCE_MS = 300;
|
|
3
3
|
export const CONTACT_SEARCH_ICON_SIZE = 18;
|
|
4
4
|
export const CONTACT_SEARCH_AVATAR_SIZE = 48;
|
|
5
|
-
// `Tất cả` caps each section's preview (contacts, public groups)
|
|
6
|
-
// the seller over to the full
|
|
5
|
+
// `Tất cả` caps each section's preview (contacts, messages, public groups)
|
|
6
|
+
// before handing the seller over to the full tab.
|
|
7
7
|
export const CONTACT_SEARCH_PREVIEW_LIMIT = 5;
|
|
8
8
|
export var ContactSearchTab;
|
|
9
9
|
(function (ContactSearchTab) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GroupMembersDrawer.d.ts","sourceRoot":"","sources":["../../../src/components/message/GroupMembersDrawer.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GroupMembersDrawer.d.ts","sourceRoot":"","sources":["../../../src/components/message/GroupMembersDrawer.tsx"],"names":[],"mappings":"AAaA,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AA6B/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;AA6BD,QAAA,MAAM,kBAAkB,GAAI,wDAKzB,uBAAuB,4CA6rBzB,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
|
|
@@ -9,6 +9,7 @@ import { useTranslation } from "react-i18next";
|
|
|
9
9
|
import { useChatNavigate } from "../../hooks/conversation/useChatNavigate";
|
|
10
10
|
import useAuthStore from "../../store/auth";
|
|
11
11
|
import useConversationStore from "../../store/conversation";
|
|
12
|
+
import { PeerType } from "../../types/conversation";
|
|
12
13
|
import { useGroupMembers } from "../../hooks/group/useGroupMembers";
|
|
13
14
|
import { useSearchGroupMembers } from "../../hooks/group/useSearchGroupMembers";
|
|
14
15
|
import { useLeaveConversation } from "../../hooks/session/useLeaveConversation";
|
|
@@ -25,6 +26,9 @@ import useDeskPanelHostStore from "../../store/deskPanelHost";
|
|
|
25
26
|
import DeskSidePanel from "../../components/common/DeskSidePanel";
|
|
26
27
|
import GroupMembersPanelCrm from "./GroupMembersPanelCrm";
|
|
27
28
|
import { buildCrmMemberMenu } from "./groupMemberMenuCrm";
|
|
29
|
+
// A group's list loads page by page as the reader scrolls.
|
|
30
|
+
const MEMBER_PAGE_SIZE = 50;
|
|
31
|
+
const LOAD_MORE_THRESHOLD_PX = 80;
|
|
28
32
|
const getMemberDisplayName = (member) => {
|
|
29
33
|
return member.nickname || member.userID || "";
|
|
30
34
|
};
|
|
@@ -61,7 +65,14 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
|
|
|
61
65
|
// relation to who may manage "members", so those actions are hidden
|
|
62
66
|
// entirely rather than shown disabled.
|
|
63
67
|
const isRealGroup = isGroupConversation(conversationData !== null && conversationData !== void 0 ? conversationData : undefined);
|
|
64
|
-
|
|
68
|
+
// Same split as the header count: a group pages its list and takes its
|
|
69
|
+
// head-count from group info; other threads load and count the whole list.
|
|
70
|
+
const isPagedList = (conversationData === null || conversationData === void 0 ? void 0 : conversationData.peerType) === PeerType.GROUP;
|
|
71
|
+
const groupMemberCount = useConversationStore((state) => { var _a; return groupID ? (_a = state.currentGroupInfo.get(groupID)) === null || _a === void 0 ? void 0 : _a.memberCount : undefined; });
|
|
72
|
+
const { members, isLoading, isLoadingMore, hasMore, loadMore, error, refetch, patchMemberRole, patchMemberPermissions, } = useGroupMembers(groupID, {
|
|
73
|
+
pageSize: isPagedList ? MEMBER_PAGE_SIZE : undefined,
|
|
74
|
+
});
|
|
75
|
+
const totalCount = isPagedList ? groupMemberCount !== null && groupMemberCount !== void 0 ? groupMemberCount : 0 : members.length;
|
|
65
76
|
const { results: searchResults, isSearching } = useSearchGroupMembers(groupID, debouncedSearch);
|
|
66
77
|
const { mutateAsync: leaveConversation, isPending: isLeaving } = useLeaveConversation();
|
|
67
78
|
const permissions = useGroupPermission(groupID);
|
|
@@ -69,7 +80,8 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
|
|
|
69
80
|
// granularity — BE still rejects an Admin removing another Admin/Owner
|
|
70
81
|
// (role hierarchy), so the FE must gate per-target here too, or the user
|
|
71
82
|
// reaches the confirm flow and only then sees a rejection toast.
|
|
72
|
-
const
|
|
83
|
+
const actingMember = useConversationStore((state) => groupID ? state.currentMemberInGroup.get(groupID) : undefined);
|
|
84
|
+
const actingMemberRoleLevel = actingMember === null || actingMember === void 0 ? void 0 : actingMember.roleLevel;
|
|
73
85
|
const actingIsOwner = actingMemberRoleLevel === GroupMemberRole.Owner;
|
|
74
86
|
const { mutateAsync: setMemberPermissions } = useSetGroupMemberPermissions();
|
|
75
87
|
const { mutateAsync: kickMember } = useKickGroupMember();
|
|
@@ -85,7 +97,13 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
|
|
|
85
97
|
console.error("Error parsing conversation extra info:", error);
|
|
86
98
|
}
|
|
87
99
|
const isSearchActive = debouncedSearch.trim().length > 0;
|
|
88
|
-
|
|
100
|
+
// "Me" leads its role group even before paging reaches my row.
|
|
101
|
+
const listedMembers = useMemo(() => isPagedList &&
|
|
102
|
+
actingMember &&
|
|
103
|
+
!members.some((member) => member.userID === actingMember.userID)
|
|
104
|
+
? [actingMember, ...members]
|
|
105
|
+
: members, [isPagedList, actingMember, members]);
|
|
106
|
+
const sourceMembers = isSearchActive ? searchResults : listedMembers;
|
|
89
107
|
const sortedMembers = useMemo(() => {
|
|
90
108
|
return [...sourceMembers]
|
|
91
109
|
.map((member) => {
|
|
@@ -123,11 +141,22 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
|
|
|
123
141
|
if (memberA.isCustomer !== memberB.isCustomer) {
|
|
124
142
|
return memberA.isCustomer ? -1 : 1;
|
|
125
143
|
}
|
|
144
|
+
// Paged rows keep core's join order so a new page only appends below.
|
|
145
|
+
if (isPagedList && !isSearchActive)
|
|
146
|
+
return 0;
|
|
126
147
|
return memberA.nickname.localeCompare(memberB.nickname, "vi", {
|
|
127
148
|
sensitivity: "base",
|
|
128
149
|
});
|
|
129
150
|
});
|
|
130
|
-
}, [authUserID, sourceMembers, customerUserID, t]);
|
|
151
|
+
}, [authUserID, sourceMembers, customerUserID, t, isPagedList, isSearchActive]);
|
|
152
|
+
const handleListScroll = (event) => {
|
|
153
|
+
if (isSearchActive || !hasMore || isLoadingMore)
|
|
154
|
+
return;
|
|
155
|
+
const { scrollTop, scrollHeight, clientHeight } = event.currentTarget;
|
|
156
|
+
if (scrollHeight - scrollTop - clientHeight < LOAD_MORE_THRESHOLD_PX) {
|
|
157
|
+
void loadMore();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
131
160
|
const handleOpenDrawer = () => {
|
|
132
161
|
if (!groupID || disabled) {
|
|
133
162
|
return;
|
|
@@ -279,7 +308,7 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
|
|
|
279
308
|
useEffect(() => {
|
|
280
309
|
closePanel();
|
|
281
310
|
}, [groupID]);
|
|
282
|
-
const renderCrmPanel = (closeOnEscape) => (_jsx(GroupMembersPanelCrm, { totalCount:
|
|
311
|
+
const renderCrmPanel = (closeOnEscape) => (_jsx(GroupMembersPanelCrm, { totalCount: totalCount, members: sortedMembers.map((member) => isRealGroup ? member : Object.assign(Object.assign({}, member), { roleLabel: "" })), isRealGroup: isRealGroup, isLoading: isLoading || (isSearchActive && isSearching), isLoadingMore: isLoadingMore && !isSearchActive, onListScroll: handleListScroll, error: error, isSearchActive: isSearchActive, search: search, onSearchChange: setSearch, canAddMember: canAddMember, onAddMember: () => setShowAddMember(true), onClose: handleCloseDrawer, closeOnEscape: closeOnEscape, buildMenu: (member) => buildCrmMemberMenu({
|
|
283
312
|
member,
|
|
284
313
|
isRealGroup,
|
|
285
314
|
permissions,
|
|
@@ -409,7 +438,7 @@ const GroupMembersDrawer = ({ groupID, conversationId, currentSession, disabled
|
|
|
409
438
|
padding: 0,
|
|
410
439
|
height: "100%",
|
|
411
440
|
},
|
|
412
|
-
}, getContainer: false, destroyOnHidden: true, width: 360, children: _jsxs("div", { className: "flex flex-col h-full bg-white", 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("members") }), _jsx(Button, { type: "text", shape: "default", className: "text-gray-500 w-8 h-8 p-0", onClick: handleCloseDrawer, "aria-label": t("close"), children: _jsx(Icon, { icon: "close-b", size: 22 }) })] }), _jsxs("div", { className: "flex items-center gap-2 px-4 py-3", children: [_jsx(Input, { value: search, onChange: (event) => setSearch(event.target.value), placeholder: t("search_member_placeholder"), prefix: _jsx(Icon, { icon: "search-o", size: 16, className: "text-gray-400" }), className: "h-10" }), canAddMember && (_jsx(Button, { type: "primary", shape: "default", className: "w-10 h-10 p-0 shrink-0 flex items-center justify-center rounded-lg", onClick: () => setShowAddMember(true), children: _jsx(Icon, { icon: "user-add-b", size: 18 }) }))] }),
|
|
441
|
+
}, getContainer: false, destroyOnHidden: true, width: 360, children: _jsxs("div", { className: "flex flex-col h-full bg-white", 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("members") }), _jsx(Button, { type: "text", shape: "default", className: "text-gray-500 w-8 h-8 p-0", onClick: handleCloseDrawer, "aria-label": t("close"), children: _jsx(Icon, { icon: "close-b", size: 22 }) })] }), _jsxs("div", { className: "flex items-center gap-2 px-4 py-3", children: [_jsx(Input, { value: search, onChange: (event) => setSearch(event.target.value), placeholder: t("search_member_placeholder"), prefix: _jsx(Icon, { icon: "search-o", size: 16, className: "text-gray-400" }), className: "h-10" }), canAddMember && (_jsx(Button, { type: "primary", shape: "default", className: "w-10 h-10 p-0 shrink-0 flex items-center justify-center rounded-lg", onClick: () => setShowAddMember(true), children: _jsx(Icon, { icon: "user-add-b", size: 18 }) }))] }), _jsxs("div", { className: "flex-1 overflow-y-auto", onScroll: handleListScroll, children: [renderContent(), isLoadingMore && !isSearchActive && (_jsx("div", { className: "flex justify-center py-3", children: _jsx(Spin, { size: "small" }) }))] })] }) })), groupID && (_jsx(AddMemberModal, { open: showAddMember, groupID: groupID, onClose: () => setShowAddMember(false) })), groupID && (_jsx(AdminPermissionModal, { open: Boolean(adminPermissionMember), groupID: groupID, onClose: () => setAdminPermissionMember(null), member: adminPermissionMember
|
|
413
442
|
? {
|
|
414
443
|
userID: adminPermissionMember.userID,
|
|
415
444
|
nickname: adminPermissionMember.nickname,
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
import type { UIEvent } from "react";
|
|
1
2
|
import type { MenuProps } from "antd";
|
|
2
3
|
import type { MemberDisplayItem } from "./groupMemberTypes";
|
|
3
4
|
interface GroupMembersPanelCrmProps {
|
|
5
|
+
/** Whole-thread head-count; `members` may be only the pages loaded so far. */
|
|
4
6
|
totalCount: number;
|
|
5
7
|
members: MemberDisplayItem[];
|
|
6
8
|
/** Real groups split owners/admins from members; support threads show one list. */
|
|
7
9
|
isRealGroup: boolean;
|
|
8
10
|
isLoading: boolean;
|
|
11
|
+
isLoadingMore: boolean;
|
|
12
|
+
onListScroll: (event: UIEvent<HTMLDivElement>) => void;
|
|
9
13
|
error?: string;
|
|
10
14
|
isSearchActive: boolean;
|
|
11
15
|
search: string;
|
|
@@ -18,6 +22,6 @@ interface GroupMembersPanelCrmProps {
|
|
|
18
22
|
buildMenu: (member: MemberDisplayItem) => MenuProps["items"];
|
|
19
23
|
}
|
|
20
24
|
/** CRM members panel: search + add, owners/admins and members lists, per-member action menu. */
|
|
21
|
-
declare const GroupMembersPanelCrm: ({ totalCount, members, isRealGroup, isLoading, error, isSearchActive, search, onSearchChange, canAddMember, onAddMember, onClose, closeOnEscape, buildMenu, }: GroupMembersPanelCrmProps) => import("react/jsx-runtime").JSX.Element;
|
|
25
|
+
declare const GroupMembersPanelCrm: ({ totalCount, members, isRealGroup, isLoading, isLoadingMore, onListScroll, error, isSearchActive, search, onSearchChange, canAddMember, onAddMember, onClose, closeOnEscape, buildMenu, }: GroupMembersPanelCrmProps) => import("react/jsx-runtime").JSX.Element;
|
|
22
26
|
export default GroupMembersPanelCrm;
|
|
23
27
|
//# sourceMappingURL=GroupMembersPanelCrm.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"GroupMembersPanelCrm.d.ts","sourceRoot":"","sources":["../../../src/components/message/GroupMembersPanelCrm.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"GroupMembersPanelCrm.d.ts","sourceRoot":"","sources":["../../../src/components/message/GroupMembersPanelCrm.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAA4B,OAAO,EAAE,MAAM,OAAO,CAAC;AAE/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,MAAM,CAAC;AAMtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAE5D,UAAU,yBAAyB;IACjC,8EAA8E;IAC9E,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,mFAAmF;IACnF,WAAW,EAAE,OAAO,CAAC;IACrB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,CAAC,cAAc,CAAC,KAAK,IAAI,CAAC;IACvD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,OAAO,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,YAAY,EAAE,OAAO,CAAC;IACtB,WAAW,EAAE,MAAM,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,oEAAoE;IACpE,aAAa,EAAE,OAAO,CAAC;IACvB,SAAS,EAAE,CAAC,MAAM,EAAE,iBAAiB,KAAK,SAAS,CAAC,OAAO,CAAC,CAAC;CAC9D;AAQD,gGAAgG;AAChG,QAAA,MAAM,oBAAoB,GAAI,4LAgB3B,yBAAyB,4CAiI3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
|
|
@@ -10,7 +10,7 @@ import { FOCUS_RING_CLASS } from "../../utils/a11yClickable";
|
|
|
10
10
|
const CHIP = "inline-flex h-[22px] shrink-0 items-center gap-1 whitespace-nowrap rounded-full px-2 text-xs font-medium";
|
|
11
11
|
const SectionTitle = ({ children }) => (_jsx("h3", { className: "m-0 px-2 pb-1 pt-3 text-xs font-medium text-gray-600", children: children }));
|
|
12
12
|
/** CRM members panel: search + add, owners/admins and members lists, per-member action menu. */
|
|
13
|
-
const GroupMembersPanelCrm = ({ totalCount, members, isRealGroup, isLoading, error, isSearchActive, search, onSearchChange, canAddMember, onAddMember, onClose, closeOnEscape, buildMenu, }) => {
|
|
13
|
+
const GroupMembersPanelCrm = ({ totalCount, members, isRealGroup, isLoading, isLoadingMore, onListScroll, error, isSearchActive, search, onSearchChange, canAddMember, onAddMember, onClose, closeOnEscape, buildMenu, }) => {
|
|
14
14
|
const { t } = useTranslation();
|
|
15
15
|
const rootRef = useRef(null);
|
|
16
16
|
useEffect(() => {
|
|
@@ -26,6 +26,10 @@ const GroupMembersPanelCrm = ({ totalCount, members, isRealGroup, isLoading, err
|
|
|
26
26
|
};
|
|
27
27
|
const privileged = isRealGroup ? members.filter((m) => m.isOwner || m.isAdmin) : [];
|
|
28
28
|
const regular = isRealGroup ? members.filter((m) => !m.isOwner && !m.isAdmin) : members;
|
|
29
|
+
// Owners and admins come first in core's order, so the rest of the head-count is regular members.
|
|
30
|
+
const regularCount = isSearchActive
|
|
31
|
+
? regular.length
|
|
32
|
+
: Math.max(totalCount - privileged.length, regular.length);
|
|
29
33
|
const renderRow = (member) => {
|
|
30
34
|
const items = isRealGroup || member.isCurrentUser ? buildMenu(member) : undefined;
|
|
31
35
|
const roleChip = !isRealGroup && member.isOwner
|
|
@@ -48,8 +52,8 @@ const GroupMembersPanelCrm = ({ totalCount, members, isRealGroup, isLoading, err
|
|
|
48
52
|
if (!members.length) {
|
|
49
53
|
return _jsx(Empty, { description: isSearchActive ? t("no_member_found") : t("no_group_members") });
|
|
50
54
|
}
|
|
51
|
-
return (_jsxs(_Fragment, { children: [privileged.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { children: t("members_owner_admin_section") }), renderList(privileged)] })), regular.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { children: t("members_regular_section", { count:
|
|
55
|
+
return (_jsxs(_Fragment, { children: [privileged.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { children: t("members_owner_admin_section") }), renderList(privileged)] })), regular.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionTitle, { children: t("members_regular_section", { count: regularCount }) }), renderList(regular)] }))] }));
|
|
52
56
|
};
|
|
53
|
-
return (_jsxs("div", { ref: rootRef, tabIndex: -1, onKeyDown: handleKeyDown, "aria-label": t("members"), className: "flex h-full flex-col bg-white outline-none", "data-cp-name": "GroupMembersPanelCrm", children: [_jsx(GroupPanelHeader, { title: t("members_panel_title", { count: totalCount }), onClose: onClose }), _jsxs("div", { className: "flex gap-2 px-4 pb-1 pt-3", children: [_jsxs("label", { className: "flex h-9 min-w-0 flex-1 items-center gap-2 rounded-lg bg-gray-5 px-2.5 text-gray-600 focus-within:outline focus-within:outline-2 focus-within:outline-primary-200", children: [_jsx("span", { className: "sr-only", children: t("members_search_label") }), _jsx(Icon, { icon: "search-o", size: 16, className: "shrink-0" }), _jsx("input", { type: "search", value: search, onChange: (event) => onSearchChange(event.target.value), placeholder: t("members_search_label"), className: "min-w-0 flex-1 border-0 bg-transparent p-0 text-sm text-gray-900 outline-none placeholder:text-gray-600" })] }), canAddMember && (_jsx(Button, { type: "primary", className: "h-9 shrink-0 px-3", icon: _jsx(Icon, { icon: "user-add-o", size: 16 }), onClick: onAddMember, children: t("members_add") }))] }),
|
|
57
|
+
return (_jsxs("div", { ref: rootRef, tabIndex: -1, onKeyDown: handleKeyDown, "aria-label": t("members"), className: "flex h-full flex-col bg-white outline-none", "data-cp-name": "GroupMembersPanelCrm", children: [_jsx(GroupPanelHeader, { title: t("members_panel_title", { count: totalCount }), onClose: onClose }), _jsxs("div", { className: "flex gap-2 px-4 pb-1 pt-3", children: [_jsxs("label", { className: "flex h-9 min-w-0 flex-1 items-center gap-2 rounded-lg bg-gray-5 px-2.5 text-gray-600 focus-within:outline focus-within:outline-2 focus-within:outline-primary-200", children: [_jsx("span", { className: "sr-only", children: t("members_search_label") }), _jsx(Icon, { icon: "search-o", size: 16, className: "shrink-0" }), _jsx("input", { type: "search", value: search, onChange: (event) => onSearchChange(event.target.value), placeholder: t("members_search_label"), className: "min-w-0 flex-1 border-0 bg-transparent p-0 text-sm text-gray-900 outline-none placeholder:text-gray-600" })] }), canAddMember && (_jsx(Button, { type: "primary", className: "h-9 shrink-0 px-3", icon: _jsx(Icon, { icon: "user-add-o", size: 16 }), onClick: onAddMember, children: t("members_add") }))] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto px-2 pb-2", onScroll: onListScroll, children: [renderBody(), isLoadingMore && _jsx("div", { className: "flex justify-center py-3", children: _jsx(Spin, { size: "small" }) })] })] }));
|
|
54
58
|
};
|
|
55
59
|
export default GroupMembersPanelCrm;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useMentionOptions.d.ts","sourceRoot":"","sources":["../../../../../src/components/message/footer/mention/useMentionOptions.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useMentionOptions.d.ts","sourceRoot":"","sources":["../../../../../src/components/message/footer/mention/useMentionOptions.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,aAAa;IAC5B,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,eAAO,MAAM,mBAAmB,YAAY,CAAC;AAoB7C,eAAO,MAAM,iBAAiB,GAC5B,SAAS,MAAM,GAAG,SAAS,EAC3B,OAAO,MAAM,KACZ,aAAa,EA2Df,CAAC"}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { useMemo } from "react";
|
|
1
|
+
import { useEffect, useMemo } from "react";
|
|
2
2
|
import { useTranslation } from "react-i18next";
|
|
3
3
|
import { useGroupMembers } from "../../../../hooks/group/useGroupMembers";
|
|
4
|
+
import { useSearchGroupMembers } from "../../../../hooks/group/useSearchGroupMembers";
|
|
4
5
|
import useAuthStore from "../../../../store/auth";
|
|
5
6
|
export const ALL_MEMBERS_USER_ID = "__all__";
|
|
7
|
+
// Typing a name loads a big group's remaining members up to this many.
|
|
8
|
+
const MENTION_MEMBER_LOAD_LIMIT = 10000;
|
|
9
|
+
// The menu never renders more rows than one member page.
|
|
10
|
+
const MENTION_OPTION_LIMIT = 999;
|
|
6
11
|
// Prefix matches rank above mid-string matches, then alphabetical — matches
|
|
7
12
|
// the "closest match first" ranking spec'd for this feature (not just a
|
|
8
13
|
// plain filter). No keyword: falls back to plain A-Z (AC-Open default).
|
|
@@ -18,13 +23,28 @@ const matchRank = (displayName, keyword) => {
|
|
|
18
23
|
// left to handle here.
|
|
19
24
|
export const useMentionOptions = (groupID, query) => {
|
|
20
25
|
const { t } = useTranslation();
|
|
21
|
-
const { members } = useGroupMembers(groupID);
|
|
26
|
+
const { members, hasMore, isLoadingMore, loadMore } = useGroupMembers(groupID);
|
|
27
|
+
// Core keeps only part of a big group locally, so its search alone misses
|
|
28
|
+
// members; typing a name also loads the rest, merged in as pages arrive.
|
|
29
|
+
const { results: searchResults } = useSearchGroupMembers(groupID, hasMore ? query : "");
|
|
22
30
|
const currentUserID = useAuthStore((state) => state.userID);
|
|
31
|
+
const hasKeyword = query.trim().length > 0;
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (!hasKeyword || !hasMore || isLoadingMore)
|
|
34
|
+
return;
|
|
35
|
+
if (members.length >= MENTION_MEMBER_LOAD_LIMIT)
|
|
36
|
+
return;
|
|
37
|
+
void loadMore();
|
|
38
|
+
}, [hasKeyword, hasMore, isLoadingMore, members.length, loadMore]);
|
|
23
39
|
return useMemo(() => {
|
|
24
40
|
const keyword = query.trim().toLowerCase();
|
|
41
|
+
const loadedIDs = new Set(members.map((member) => member.userID));
|
|
42
|
+
const candidates = keyword
|
|
43
|
+
? [...members, ...searchResults.filter((member) => !loadedIDs.has(member.userID))]
|
|
44
|
+
: members;
|
|
25
45
|
// Self-tagging isn't a useful mention target — the sender already
|
|
26
46
|
// knows who they are — so the current user is filtered out entirely.
|
|
27
|
-
const memberOptions =
|
|
47
|
+
const memberOptions = candidates
|
|
28
48
|
.filter((member) => member.userID !== currentUserID)
|
|
29
49
|
.map((member) => ({
|
|
30
50
|
userID: member.userID,
|
|
@@ -40,7 +60,8 @@ export const useMentionOptions = (groupID, query) => {
|
|
|
40
60
|
return rankDiff !== 0
|
|
41
61
|
? rankDiff
|
|
42
62
|
: a.displayName.localeCompare(b.displayName);
|
|
43
|
-
})
|
|
63
|
+
})
|
|
64
|
+
.slice(0, MENTION_OPTION_LIMIT);
|
|
44
65
|
const allOption = {
|
|
45
66
|
userID: ALL_MEMBERS_USER_ID,
|
|
46
67
|
displayName: t("mention.all_members"),
|
|
@@ -48,5 +69,5 @@ export const useMentionOptions = (groupID, query) => {
|
|
|
48
69
|
};
|
|
49
70
|
const matchesAll = !keyword || allOption.displayName.toLowerCase().includes(keyword);
|
|
50
71
|
return matchesAll ? [allOption, ...memberOptions] : memberOptions;
|
|
51
|
-
}, [members, query, t, currentUserID]);
|
|
72
|
+
}, [members, searchResults, query, t, currentUserID]);
|
|
52
73
|
};
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { GroupMemberItem, GroupMemberRole } from "@droppii-org/wasm-client-sdk";
|
|
2
|
+
interface UseGroupMembersOptions {
|
|
3
|
+
/** Rows per getGroupMemberList call; callers that never loadMore get one page. */
|
|
4
|
+
pageSize?: number;
|
|
5
|
+
}
|
|
2
6
|
interface UseGroupMembersResult {
|
|
3
7
|
members: GroupMemberItem[];
|
|
4
8
|
isLoading: boolean;
|
|
9
|
+
isLoadingMore: boolean;
|
|
10
|
+
hasMore: boolean;
|
|
5
11
|
error?: string;
|
|
6
12
|
refetch: () => Promise<void>;
|
|
13
|
+
loadMore: () => Promise<void>;
|
|
7
14
|
patchMemberRole: (userID: string, roleLevel: GroupMemberRole) => void;
|
|
8
15
|
patchMemberPermissions: (userID: string, permissions: string[]) => void;
|
|
9
16
|
}
|
|
10
|
-
export declare const useGroupMembers: (groupID?: string) => UseGroupMembersResult;
|
|
17
|
+
export declare const useGroupMembers: (groupID?: string, { pageSize }?: UseGroupMembersOptions) => UseGroupMembersResult;
|
|
11
18
|
export {};
|
|
12
19
|
//# sourceMappingURL=useGroupMembers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGroupMembers.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useGroupMembers.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,eAAe,EAAE,eAAe,EAA4B,MAAM,8BAA8B,CAAC;
|
|
1
|
+
{"version":3,"file":"useGroupMembers.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useGroupMembers.ts"],"names":[],"mappings":"AACA,OAAO,EAAY,eAAe,EAAE,eAAe,EAA4B,MAAM,8BAA8B,CAAC;AAKpH,UAAU,sBAAsB;IAC9B,kFAAkF;IAClF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,UAAU,qBAAqB;IAC7B,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,EAAE,OAAO,CAAC;IACvB,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,eAAe,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,eAAe,KAAK,IAAI,CAAC;IACtE,sBAAsB,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACzE;AAOD,eAAO,MAAM,eAAe,GAC1B,UAAU,MAAM,EAChB,eAAkC,sBAA2B,KAC5D,qBAyKF,CAAC"}
|