@droppii-org/chat-sdk 0.7.5 → 0.7.7

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 (57) hide show
  1. package/dist/components/conversation/DeskBackgroundSyncStatus.d.ts +4 -0
  2. package/dist/components/conversation/DeskBackgroundSyncStatus.d.ts.map +1 -0
  3. package/dist/components/conversation/DeskBackgroundSyncStatus.js +17 -0
  4. package/dist/components/conversation/DeskConversationList.d.ts.map +1 -1
  5. package/dist/components/conversation/DeskConversationList.js +13 -3
  6. package/dist/components/conversation-detail/ConversationMessageList.d.ts.map +1 -1
  7. package/dist/components/conversation-detail/ConversationMessageList.js +3 -6
  8. package/dist/components/conversation-detail/PinnedMessagesPanel.d.ts.map +1 -1
  9. package/dist/components/conversation-detail/PinnedMessagesPanel.js +1 -1
  10. package/dist/components/conversation-detail/bot/BotMenuSheet.d.ts.map +1 -1
  11. package/dist/components/conversation-detail/bot/BotMenuSheet.js +1 -1
  12. package/dist/components/conversation-inbox/contact-search/ContactSearchCommunitySection.d.ts +10 -0
  13. package/dist/components/conversation-inbox/contact-search/ContactSearchCommunitySection.d.ts.map +1 -0
  14. package/dist/components/conversation-inbox/contact-search/ContactSearchCommunitySection.js +15 -0
  15. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.d.ts.map +1 -1
  16. package/dist/components/conversation-inbox/contact-search/ContactSearchPanel.js +39 -28
  17. package/dist/components/conversation-inbox/contact-search/constants.js +2 -2
  18. package/dist/components/message/MessageList.d.ts.map +1 -1
  19. package/dist/components/message/MessageList.js +8 -6
  20. package/dist/hooks/conversation/useResolveConversation.d.ts.map +1 -1
  21. package/dist/hooks/conversation/useResolveConversation.js +2 -5
  22. package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
  23. package/dist/hooks/global/useGlobalEvent.js +2 -0
  24. package/dist/hooks/init/useCanLoadChatData.d.ts +7 -0
  25. package/dist/hooks/init/useCanLoadChatData.d.ts.map +1 -0
  26. package/dist/hooks/init/useCanLoadChatData.js +15 -0
  27. package/dist/hooks/message/useCanPinMessage.d.ts +2 -0
  28. package/dist/hooks/message/useCanPinMessage.d.ts.map +1 -0
  29. package/dist/hooks/message/useCanPinMessage.js +29 -0
  30. package/dist/hooks/message/useMessage.d.ts.map +1 -1
  31. package/dist/hooks/message/useMessage.js +3 -5
  32. package/dist/hooks/message/useUnreadMessageIndicator.d.ts.map +1 -1
  33. package/dist/hooks/message/useUnreadMessageIndicator.js +3 -5
  34. package/dist/hooks/search/useSearchPublicGroups.d.ts +1 -0
  35. package/dist/hooks/search/useSearchPublicGroups.d.ts.map +1 -1
  36. package/dist/hooks/search/useSearchPublicGroups.js +6 -0
  37. package/dist/hooks/session/useAgentSupportThreadJoin.d.ts +1 -0
  38. package/dist/hooks/session/useAgentSupportThreadJoin.d.ts.map +1 -1
  39. package/dist/hooks/session/useAgentSupportThreadJoin.js +13 -2
  40. package/dist/hooks/session/useSessionLatestPreview.d.ts +4 -1
  41. package/dist/hooks/session/useSessionLatestPreview.d.ts.map +1 -1
  42. package/dist/hooks/session/useSessionLatestPreview.js +4 -1
  43. package/dist/locales/vi/boot.json +3 -2
  44. package/dist/locales/vi/common.json +1 -0
  45. package/dist/screens/conversation-detail/index.d.ts.map +1 -1
  46. package/dist/screens/conversation-detail/index.js +3 -1
  47. package/dist/screens/desk-message/DeskWorkspace.d.ts +3 -1
  48. package/dist/screens/desk-message/DeskWorkspace.d.ts.map +1 -1
  49. package/dist/screens/desk-message/DeskWorkspace.js +4 -4
  50. package/dist/screens/desk-message/index.d.ts +2 -0
  51. package/dist/screens/desk-message/index.d.ts.map +1 -1
  52. package/dist/screens/desk-message/index.js +26 -8
  53. package/dist/store/boot.d.ts +15 -0
  54. package/dist/store/boot.d.ts.map +1 -1
  55. package/dist/store/boot.js +20 -2
  56. package/dist/styles/global.css +1 -1
  57. package/package.json +1 -1
@@ -0,0 +1,4 @@
1
+ /** Under the list header while a fresh device's first sync keeps pulling the other conversations. */
2
+ declare const DeskBackgroundSyncStatus: () => import("react/jsx-runtime").JSX.Element | null;
3
+ export default DeskBackgroundSyncStatus;
4
+ //# sourceMappingURL=DeskBackgroundSyncStatus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DeskBackgroundSyncStatus.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskBackgroundSyncStatus.tsx"],"names":[],"mappings":"AAQA,qGAAqG;AACrG,QAAA,MAAM,wBAAwB,sDA+B7B,CAAC;AAEF,eAAe,wBAAwB,CAAC"}
@@ -0,0 +1,17 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useTranslation } from "react-i18next";
4
+ import BootProgressBar from "../../components/boot/BootProgressBar";
5
+ import useBootStore, { CONVERSATIONS_SYNCED_PROGRESS, selectIsSyncingInBackground, } from "../../store/boot";
6
+ /** Under the list header while a fresh device's first sync keeps pulling the other conversations. */
7
+ const DeskBackgroundSyncStatus = () => {
8
+ const { t } = useTranslation("boot");
9
+ const isSyncing = useBootStore(selectIsSyncingInBackground);
10
+ const progress = useBootStore((state) => state.syncProgress);
11
+ if (!isSyncing)
12
+ return null;
13
+ // Core reports exactly 10 when the conversations are stored; only later values say much.
14
+ const showPercent = progress > CONVERSATIONS_SYNCED_PROGRESS;
15
+ return (_jsxs("div", { "data-cp-name": "DeskBackgroundSyncStatus", className: "flex flex-col gap-1.5 border-b border-gray-25 px-4 py-2", children: [_jsxs("p", { role: "status", className: "m-0 flex items-center gap-2 text-xs text-gray-600", children: [_jsx("span", { "aria-hidden": "true", className: "h-3 w-3 shrink-0 rounded-full border-2 border-primary-25 border-t-primary-400 motion-safe:animate-spin" }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: t("desk.background_sync") }), showPercent && (_jsxs("span", { className: "font-semibold tabular-nums text-primary-400", children: [progress, "%"] }))] }), _jsx(BootProgressBar, { mode: showPercent ? "progress" : "indeterminate", value: progress, label: t("sync_progress_label") })] }));
16
+ };
17
+ export default DeskBackgroundSyncStatus;
@@ -1 +1 @@
1
- {"version":3,"file":"DeskConversationList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskConversationList.tsx"],"names":[],"mappings":"AA+BA,UAAU,yBAAyB;IACjC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;CACtD;AAED,QAAA,MAAM,oBAAoB,GAAI,mBAAmB,yBAAyB,4CAiRzE,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"DeskConversationList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation/DeskConversationList.tsx"],"names":[],"mappings":"AAiCA,UAAU,yBAAyB;IACjC,aAAa,CAAC,EAAE,CAAC,MAAM,EAAE,WAAW,GAAG,IAAI,KAAK,IAAI,CAAC;CACtD;AAED,QAAA,MAAM,oBAAoB,GAAI,mBAAmB,yBAAyB,4CA2RzE,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
@@ -20,10 +20,12 @@ import { DChatSDK } from "../../constants/sdk";
20
20
  import emitter from "../../utils/events";
21
21
  import { useGetSessionSummary } from "../../hooks/session/useGetSessionSummary";
22
22
  import { getActiveFolder } from "../../store/sessionFolder";
23
+ import useBootStore from "../../store/boot";
23
24
  import { ALL_GROUPS_KEY } from "../../components/session/sessionFilters";
24
25
  import DeskListHeader from "./DeskListHeader";
25
26
  import DeskListEmptyState from "./DeskListEmptyState";
26
27
  import DeskListSkeleton from "./DeskListSkeleton";
28
+ import DeskBackgroundSyncStatus from "./DeskBackgroundSyncStatus";
27
29
  import { getListHeading } from "./listHeading";
28
30
  import ComposeNewMessageModal from "../../components/compose/ComposeNewMessageModal";
29
31
  import { useCloseOnSidePanelOpen } from "../../hooks/common/useCloseOnSidePanelOpen";
@@ -46,9 +48,9 @@ const DeskConversationList = ({ onOpenAppMenu }) => {
46
48
  options: { isEnabled: !isAllGroupsView },
47
49
  });
48
50
  const { groups: conversationGroups, fetchNextPage: fetchNextGroupsPage, hasNextPage: hasNextGroupsPage, isLoading: isConversationGroupsLoading, } = useConversationGroups({ enabled: isAllGroupsView });
49
- const latestPreviewMap = useSessionLatestPreview(rawSessions);
51
+ const { previews: latestPreviewMap, isLoading: isSessionPreviewLoading } = useSessionLatestPreview(rawSessions);
50
52
  const sessions = useSortedSessions(rawSessions, latestPreviewMap);
51
- const groupLatestPreviewMap = useSessionLatestPreview(conversationGroups);
53
+ const { previews: groupLatestPreviewMap, isLoading: isGroupPreviewLoading } = useSessionLatestPreview(conversationGroups);
52
54
  const { data: mySummary } = useGetSessionSummary();
53
55
  const folder = getActiveFolder(filterSummary, viewMode);
54
56
  const heading = getListHeading({ folder, mySummary, teams: teamSummaries });
@@ -56,6 +58,14 @@ const DeskConversationList = ({ onOpenAppMenu }) => {
56
58
  const isListLoading = isAllGroupsView
57
59
  ? isConversationGroupsLoading
58
60
  : isSessionsLoading || isSyncing;
61
+ // The desk keeps its boot screen until the rows on screen have their conversation and preview.
62
+ const isFirstScreenLoaded = isAllGroupsView
63
+ ? !isConversationGroupsLoading && !isGroupPreviewLoading
64
+ : !isSessionsLoading && !isSyncing && !isSessionPreviewLoading;
65
+ useEffect(() => {
66
+ if (isFirstScreenLoaded)
67
+ useBootStore.getState().markFirstScreenReady();
68
+ }, [isFirstScreenLoaded]);
59
69
  const debouncedSearch = useDebounce(search, { wait: 500 });
60
70
  // Closing hands focus back to the field (drawer focus restore, Esc); that focus
61
71
  // must not reopen the search it just closed.
@@ -150,7 +160,7 @@ const DeskConversationList = ({ onOpenAppMenu }) => {
150
160
  if (!showSearch && value)
151
161
  setShowSearchTrue();
152
162
  setSearch(value);
153
- }, onSearchFocus: onSearchFocus, onCloseSearch: onCloseSearch, onCompose: () => setIsComposeOpen(true), onOpenAppMenu: onOpenAppMenu }), _jsxs("div", { className: "relative flex-1 min-h-0", children: [_jsx("div", { id: "scrollableConversationsDiv", "aria-busy": isListLoading, style: {
163
+ }, onSearchFocus: onSearchFocus, onCloseSearch: onCloseSearch, onCompose: () => setIsComposeOpen(true), onOpenAppMenu: onOpenAppMenu }), _jsx(DeskBackgroundSyncStatus, {}), _jsxs("div", { className: "relative flex-1 min-h-0", children: [_jsx("div", { id: "scrollableConversationsDiv", "aria-busy": isListLoading, style: {
154
164
  height: "100%",
155
165
  overflow: showSearch ? "hidden" : "auto",
156
166
  }, children: isAllGroupsView ? (isConversationGroupsLoading ? (_jsx(DeskListSkeleton, { rows: 8 })) : !(conversationGroups === null || conversationGroups === void 0 ? void 0 : conversationGroups.length) ? (_jsx(DeskListEmptyState, { filterKey: emptyFilterKey })) : (_jsx(InfiniteScroll, { dataLength: conversationGroups.length, next: fetchNextGroupsPage, hasMore: !!hasNextGroupsPage, loader: _jsx(DeskListSkeleton, { rows: 2 }), scrollableTarget: "scrollableConversationsDiv", children: _jsx("div", { className: "flex flex-col gap-0.5 px-1 pb-2", children: conversationGroups.map((group) => (_jsx(ConversationByGroupItem, { group: group, latestPreview: groupLatestPreviewMap.get(group.conversationId) }, group.groupId))) }) }))) : isSessionsLoading || (isSyncing && !(sessions === null || sessions === void 0 ? void 0 : sessions.length)) ? (
@@ -1 +1 @@
1
- {"version":3,"file":"ConversationMessageList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationMessageList.tsx"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAiCf,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAED,QAAA,MAAM,uBAAuB,GAAI,4DAI9B,4BAA4B,4CAsS9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"ConversationMessageList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationMessageList.tsx"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAiCf,MAAM,WAAW,4BAA4B;IAC3C,UAAU,CAAC,EAAE,SAAS,CAAC;IACvB;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAED,QAAA,MAAM,uBAAuB,GAAI,4DAI9B,4BAA4B,4CAkS9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -11,7 +11,7 @@ import { useMessageListScroll } from "../../hooks/message/useMessageListScroll";
11
11
  import { useUnreadMessageIndicator } from "../../hooks/message/useUnreadMessageIndicator";
12
12
  import { usePinMessage } from "../../hooks/message/usePinMessage";
13
13
  import { useRevokeMessage } from "../../hooks/message/useRevokeMessage";
14
- import { useGroupPermission } from "../../hooks/group/useGroupPermission";
14
+ import { useCanPinMessage } from "../../hooks/message/useCanPinMessage";
15
15
  import { useViewOnlyGroupRefresh } from "../../hooks/conversation/useViewOnlyGroupRefresh";
16
16
  import { useConsultantChatNavigation } from "../../hooks/conversation/useConsultantChatNavigation";
17
17
  import useConversationStore from "../../store/conversation";
@@ -27,7 +27,7 @@ import PinLimitModal from "./PinLimitModal";
27
27
  const ConversationMessageList = ({ bottomSlot, onCopyMessageLink, onSearchMessageRevoked, }) => {
28
28
  const { t } = useTranslation("conversation-inbox");
29
29
  const { t: tCommon } = useTranslation();
30
- const { user, allowPinMessageWithoutPermission } = useChatContext();
30
+ const { user } = useChatContext();
31
31
  const authUserID = useAuthStore((state) => state.userID);
32
32
  const conversation = useConversationStore((state) => state.conversationData);
33
33
  const selectedConversationId = useConversationStore((state) => state.selectedConversationId);
@@ -37,10 +37,7 @@ const ConversationMessageList = ({ bottomSlot, onCopyMessageLink, onSearchMessag
37
37
  const setSearchClientMsgID = useConversationStore((state) => state.setSearchClientMsgID);
38
38
  const conversationId = selectedConversationId || (conversation === null || conversation === void 0 ? void 0 : conversation.conversationID) || "";
39
39
  const isGroupChat = conversation ? resolveIsGroupChat(conversation) : false;
40
- const permissions = useGroupPermission(conversation === null || conversation === void 0 ? void 0 : conversation.groupID);
41
- // Some hosts run pin without their backend returning PIN_MESSAGE yet, so
42
- // the permission check would hide a feature that is live for them.
43
- const canPinMessage = !isGroupChat || permissions.pinMessage || allowPinMessageWithoutPermission;
40
+ const canPinMessage = useCanPinMessage();
44
41
  const viewerUserID = (user === null || user === void 0 ? void 0 : user.userID) || authUserID;
45
42
  const isAgentView = isSupportSessionAgentView(Object.assign(Object.assign({}, conversation), { conversationID: conversationId }), viewerUserID);
46
43
  // App history strips the assignment log and session dividers, which an
@@ -1 +1 @@
1
- {"version":3,"file":"PinnedMessagesPanel.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/PinnedMessagesPanel.tsx"],"names":[],"mappings":"AAYA,UAAU,wBAAwB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,QAAA,MAAM,mBAAmB,GAAI,8BAI1B,wBAAwB,4CAmH1B,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
1
+ {"version":3,"file":"PinnedMessagesPanel.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/PinnedMessagesPanel.tsx"],"names":[],"mappings":"AAYA,UAAU,wBAAwB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,QAAA,MAAM,mBAAmB,GAAI,8BAI1B,wBAAwB,4CAoH1B,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
@@ -57,6 +57,6 @@ const PinnedMessagesPanel = ({ open, onClose, canUnpin = true, }) => {
57
57
  borderTopRightRadius: 16,
58
58
  overflow: "hidden",
59
59
  },
60
- }, getContainer: false, forceRender: true, children: _jsxs("div", { className: "flex h-full flex-col bg-white", children: [_jsxs("div", { className: "flex items-center gap-4 border-b border-gray-25 px-3 py-4", children: [_jsx("span", { className: "flex-1 text-lg font-bold text-gray-900", children: t("pinned_panel_title") }), _jsx(Button, { type: "text", shape: "default", className: "h-6 w-6 p-0 text-gray-900", onClick: onClose, "aria-label": t("pinned_panel_title"), children: _jsx(Icon, { icon: "close-b", size: 24 }) })] }), _jsxs("div", { className: "flex flex-1 flex-col gap-3 overflow-y-auto py-3", children: [_jsx("p", { className: "px-3 text-sm leading-[160%] text-gray-600", children: t("pinned_panel_hint") }), pinnedMessageList.map((message) => (_jsx(PinnedMessageItem, { message: message, contextMenuOpen: openContextMenuId === message.clientMsgID, onContextMenuOpenChange: (isOpen) => setOpenContextMenuId(isOpen ? message.clientMsgID : null), onUnpin: handleUnpin, onJump: handleJump, canUnpin: canUnpin }, message.clientMsgID)))] }, openCount)] }) }));
60
+ }, getContainer: false, forceRender: true, children: _jsxs("div", { className: "flex h-full flex-col bg-white", children: [_jsxs("div", { className: "flex items-center gap-4 border-b border-gray-25 px-3 py-4", children: [_jsx("span", { className: "flex-1 text-lg font-bold text-gray-900", children: t("pinned_panel_title") }), _jsx(Button, { type: "text", shape: "default", className: "!h-6 !w-6 !p-0 text-gray-900", onClick: onClose, "aria-label": t("pinned_panel_title"), children: _jsx(Icon, { icon: "close-b", size: 24 }) })] }), _jsxs("div", { className: "flex flex-1 flex-col gap-3 overflow-y-auto py-3", children: [_jsx("p", { className: "px-3 text-sm leading-[160%] text-gray-600", children: t(canUnpin ? "pinned_panel_hint" : "pinned_panel_hint_view_only") }), pinnedMessageList.map((message) => (_jsx(PinnedMessageItem, { message: message, contextMenuOpen: openContextMenuId === message.clientMsgID, onContextMenuOpenChange: (isOpen) => setOpenContextMenuId(isOpen ? message.clientMsgID : null), onUnpin: handleUnpin, onJump: handleJump, canUnpin: canUnpin }, message.clientMsgID)))] }, openCount)] }) }));
61
61
  };
62
62
  export default PinnedMessagesPanel;
@@ -1 +1 @@
1
- {"version":3,"file":"BotMenuSheet.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/bot/BotMenuSheet.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAI5D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;CAC3C;AAED,QAAA,MAAM,YAAY,GAAI,+CAMnB,iBAAiB,4CAkEnB,CAAC;AAEF,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"BotMenuSheet.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/bot/BotMenuSheet.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAI5D,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,OAAO,CAAC;IACd,KAAK,EAAE,eAAe,EAAE,CAAC;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAC;CAC3C;AAED,QAAA,MAAM,YAAY,GAAI,+CAMnB,iBAAiB,4CAmEnB,CAAC;AAEF,eAAe,YAAY,CAAC"}
@@ -17,7 +17,7 @@ const BotMenuSheet = ({ open, items, disabled, onClose, onSelect, }) => {
17
17
  body: { padding: "16px 16px 32px" },
18
18
  mask: { backgroundColor: "rgba(0, 0, 0, 0.8)" },
19
19
  content: { background: "transparent", boxShadow: "none" },
20
- }, getContainer: false, forceRender: true, children: _jsxs("div", { className: "flex flex-col overflow-hidden rounded-2xl bg-white", children: [_jsxs("div", { className: "flex items-center gap-4 border-b border-gray-25 px-3 py-4", children: [_jsx("span", { className: "flex-1 text-[18px] font-bold leading-[1.6] text-gray-900", children: t("thread_detail.bot.menu.title") }), _jsx(Button, { type: "text", shape: "default", className: "h-6 w-6 p-0 text-gray-900", onClick: onClose, "aria-label": t("thread_detail.bot.menu.title"), children: _jsx(Icon, { icon: "close-b", size: 24 }) })] }), _jsx("div", { className: "flex flex-col", children: items.map((item, index) => (_jsxs("button", { type: "button", disabled: disabled, onClick: () => handleSelect(item), className: "flex items-center gap-4 border-b border-gray-25 px-3 py-3 text-left transition-colors last:border-b-0 disabled:cursor-not-allowed disabled:opacity-60", children: [_jsx("span", { className: "flex-1 text-[16px] font-medium leading-[1.6] text-gray-900", children: item.id === BOT_DEFAULT_BUTTON_ID
20
+ }, getContainer: false, forceRender: true, children: _jsxs("div", { className: "flex flex-col overflow-hidden rounded-2xl bg-white", children: [_jsxs("div", { className: "flex items-center gap-4 border-b border-gray-25 px-3 py-4", children: [_jsx("span", { className: "flex-1 text-[18px] font-bold leading-[1.6] text-gray-900", children: t("thread_detail.bot.menu.title") }), _jsx(Button, { type: "text", shape: "default", className: "!h-6 !w-6 !p-0 text-gray-900", onClick: onClose, "aria-label": t("thread_detail.bot.menu.title"), children: _jsx(Icon, { icon: "close-b", size: 24 }) })] }), _jsx("div", { className: "flex flex-col", children: items.map((item, index) => (_jsxs("button", { type: "button", disabled: disabled, onClick: () => handleSelect(item), className: "flex items-center gap-4 border-b border-gray-25 px-3 py-3 text-left transition-colors last:border-b-0 disabled:cursor-not-allowed disabled:opacity-60", children: [_jsx("span", { className: "flex-1 text-[16px] font-medium leading-[1.6] text-gray-900", children: item.id === BOT_DEFAULT_BUTTON_ID
21
21
  ? t("thread_detail.bot.restart")
22
22
  : item.label }), _jsx(Icon, { icon: "angle-right-o", size: CHEVRON_SIZE, className: "text-gray-900" })] }, `${item.id}-${index}`))) })] }) }));
23
23
  };
@@ -0,0 +1,10 @@
1
+ import type { SearchPublicGroupInfo } from "@droppii-org/wasm-client-sdk";
2
+ interface ContactSearchCommunitySectionProps {
3
+ groups: SearchPublicGroupInfo[];
4
+ total: number;
5
+ onOpenGroup: (groupID: string) => void;
6
+ onViewAll: () => void;
7
+ }
8
+ declare const ContactSearchCommunitySection: ({ groups, total, onOpenGroup, onViewAll, }: ContactSearchCommunitySectionProps) => import("react/jsx-runtime").JSX.Element;
9
+ export default ContactSearchCommunitySection;
10
+ //# sourceMappingURL=ContactSearchCommunitySection.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ContactSearchCommunitySection.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchCommunitySection.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAM1E,UAAU,kCAAkC;IAC1C,MAAM,EAAE,qBAAqB,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,SAAS,EAAE,MAAM,IAAI,CAAC;CACvB;AAGD,QAAA,MAAM,6BAA6B,GAAI,4CAKpC,kCAAkC,4CAwBpC,CAAC;AAEF,eAAe,6BAA6B,CAAC"}
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useTranslation } from "react-i18next";
4
+ import ContactSearchSectionHeader from "./ContactSearchSectionHeader";
5
+ import ContactSearchViewMoreBar from "./ContactSearchViewMoreBar";
6
+ import PublicGroupResultItem from "./PublicGroupResultItem";
7
+ import { CONTACT_SEARCH_PREVIEW_LIMIT } from "./constants";
8
+ // `Tất cả` preview of the `Cộng đồng` tab: same cards, capped like contacts.
9
+ const ContactSearchCommunitySection = ({ groups, total, onOpenGroup, onViewAll, }) => {
10
+ const { t } = useTranslation("conversation-inbox");
11
+ // Guards a short server total so a full page still gets its hand-off.
12
+ const matchCount = Math.max(total, groups.length);
13
+ return (_jsxs("section", { "data-cp-name": "ContactSearchCommunitySection", children: [_jsx(ContactSearchSectionHeader, { title: t("contact_search.tabs.community"), count: matchCount || undefined, onSelect: onViewAll }), groups.slice(0, CONTACT_SEARCH_PREVIEW_LIMIT).map((group) => (_jsx(PublicGroupResultItem, { group: group, onOpen: onOpenGroup }, group.groupID))), matchCount > CONTACT_SEARCH_PREVIEW_LIMIT && (_jsx(ContactSearchViewMoreBar, { onSelect: onViewAll }))] }));
14
+ };
15
+ export default ContactSearchCommunitySection;
@@ -1 +1 @@
1
- {"version":3,"file":"ContactSearchPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchPanel.tsx"],"names":[],"mappings":"AA+BA,UAAU,uBAAuB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,kBAAkB,GAAI,mBAAmB,uBAAuB,4CA2TrE,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
1
+ {"version":3,"file":"ContactSearchPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/ContactSearchPanel.tsx"],"names":[],"mappings":"AAgCA,UAAU,uBAAuB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,kBAAkB,GAAI,mBAAmB,uBAAuB,4CA2TrE,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
@@ -10,6 +10,7 @@ import { useChatNavigate } from "../../../hooks/conversation/useChatNavigate";
10
10
  import { useSearchContacts } from "../../../hooks/search/useSearchContacts";
11
11
  import { useSearchPublicGroups } from "../../../hooks/search/useSearchPublicGroups";
12
12
  import useConversationStore from "../../../store/conversation";
13
+ import ContactSearchCommunitySection from "./ContactSearchCommunitySection";
13
14
  import ContactSearchFilters from "./ContactSearchFilters";
14
15
  import ContactSearchResultItem from "./ContactSearchResultItem";
15
16
  import ContactSearchSectionHeader from "./ContactSearchSectionHeader";
@@ -46,9 +47,12 @@ const ContactSearchPanel = ({ open, onClose }) => {
46
47
  type: activeType,
47
48
  enabled: open,
48
49
  });
50
+ // `Tất cả` and `Cộng đồng` share one query, so switching tabs hits the cache.
49
51
  const publicGroups = useSearchPublicGroups({
50
52
  searchTerm: trimmedKeyword,
51
- enabled: open && activeTab === ContactSearchTab.Community,
53
+ enabled: open &&
54
+ (activeTab === ContactSearchTab.All ||
55
+ activeTab === ContactSearchTab.Community),
52
56
  });
53
57
  const handleOpenGroup = (groupID) => {
54
58
  const target = { groupId: groupID };
@@ -72,46 +76,53 @@ const ContactSearchPanel = ({ open, onClose }) => {
72
76
  setActiveType(undefined);
73
77
  };
74
78
  const renderContactItems = (items) => items.map((contact) => (_jsx(ContactSearchResultItem, { contact: contact, searchTerm: trimmedKeyword, onSelect: handleSelectContact }, contact.id)));
75
- // Every contact tab shares the same not-ready states before it can render
76
- // results; returns null once there is a list to show.
79
+ const renderInitialState = (testId = "contact-search-initial") => (_jsx(ContactSearchStateMessage, { testId: testId, children: t("contact_search.initial") }));
80
+ const renderLoadingState = () => (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Spin, {}) }));
81
+ const renderFailedState = (testId = "contact-search-failed") => (_jsx(ContactSearchStateMessage, { testId: testId, children: t("contact_search.failed") }));
82
+ const renderNoResultState = (testId = "contact-search-no-result") => (_jsxs(ContactSearchStateMessage, { testId: testId, children: [t("contact_search.no_result_title"), _jsx("br", {}), t("contact_search.no_result_hint")] }));
83
+ // The `Liên hệ` tab's not-ready states before it can render results;
84
+ // returns null once there is a list to show.
77
85
  const renderPlaceholderState = () => {
78
- if (!trimmedKeyword) {
79
- return (_jsx(ContactSearchStateMessage, { testId: "contact-search-initial", children: t("contact_search.initial") }));
80
- }
81
- if (isLoading) {
82
- return (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Spin, {}) }));
83
- }
84
- if (isError) {
85
- return (_jsx(ContactSearchStateMessage, { testId: "contact-search-failed", children: t("contact_search.failed") }));
86
- }
87
- if (contacts.length === 0) {
88
- return (_jsxs(ContactSearchStateMessage, { testId: "contact-search-no-result", children: [t("contact_search.no_result_title"), _jsx("br", {}), t("contact_search.no_result_hint")] }));
89
- }
86
+ if (!trimmedKeyword)
87
+ return renderInitialState();
88
+ if (isLoading)
89
+ return renderLoadingState();
90
+ if (isError)
91
+ return renderFailedState();
92
+ if (contacts.length === 0)
93
+ return renderNoResultState();
90
94
  return null;
91
95
  };
92
- // `Tất cả` only previews the contact section and hands off to `Liên hệ`;
93
- // the message section belongs to the message-search ticket.
96
+ // `Tất cả` previews contacts, then public groups; the message section
97
+ // belongs to the message-search ticket and goes between the two.
94
98
  const renderAllTab = () => {
95
- const placeholder = renderPlaceholderState();
96
- if (placeholder)
97
- return placeholder;
98
- const hasMore = contacts.length > CONTACT_SEARCH_PREVIEW_LIMIT || !!hasNextPage;
99
+ if (!trimmedKeyword)
100
+ return renderInitialState();
101
+ if (isLoading || publicGroups.isLoading)
102
+ return renderLoadingState();
103
+ if (isError && publicGroups.isError)
104
+ return renderFailedState();
105
+ // A source that failed drops its own section instead of the whole tab.
106
+ const showContacts = !isError && contacts.length > 0;
107
+ const showGroups = !publicGroups.isError && publicGroups.groups.length > 0;
108
+ if (!showContacts && !showGroups)
109
+ return renderNoResultState();
110
+ const hasMoreContacts = contacts.length > CONTACT_SEARCH_PREVIEW_LIMIT || !!hasNextPage;
99
111
  const openContactsTab = () => handleChangeTab(ContactSearchTab.Contacts);
100
- 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 })] }));
112
+ return (_jsxs("div", { className: "flex-1 overflow-y-auto", 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 }))] })), showGroups && (_jsx(ContactSearchCommunitySection, { groups: publicGroups.groups, total: publicGroups.total, onOpenGroup: handleOpenGroup, onViewAll: () => handleChangeTab(ContactSearchTab.Community) }))] }));
101
113
  };
102
114
  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) }) }));
103
115
  const renderCommunityTab = () => {
104
116
  if (!trimmedKeyword) {
105
- return (_jsx(ContactSearchStateMessage, { testId: "contact-search-community-initial", children: t("contact_search.initial") }));
106
- }
107
- if (publicGroups.isLoading) {
108
- return (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Spin, {}) }));
117
+ return renderInitialState("contact-search-community-initial");
109
118
  }
119
+ if (publicGroups.isLoading)
120
+ return renderLoadingState();
110
121
  if (publicGroups.isError) {
111
- return (_jsx(ContactSearchStateMessage, { testId: "contact-search-community-failed", children: t("contact_search.failed") }));
122
+ return renderFailedState("contact-search-community-failed");
112
123
  }
113
124
  if (publicGroups.groups.length === 0) {
114
- return (_jsxs(ContactSearchStateMessage, { testId: "contact-search-community-no-result", children: [t("contact_search.no_result_title"), _jsx("br", {}), t("contact_search.no_result_hint")] }));
125
+ return renderNoResultState("contact-search-community-no-result");
115
126
  }
116
127
  return (_jsx("div", { id: COMMUNITY_SCROLL_TARGET_ID, className: "flex-1 overflow-y-auto", children: _jsx(InfiniteScroll, { dataLength: publicGroups.groups.length, next: publicGroups.fetchNextPage, hasMore: !!publicGroups.hasNextPage, scrollableTarget: COMMUNITY_SCROLL_TARGET_ID, loader: publicGroups.isFetchingNextPage ? (_jsx("div", { className: "flex items-center justify-center py-3", children: _jsx(Spin, { size: "small" }) })) : null, children: publicGroups.groups.map((group) => (_jsx(PublicGroupResultItem, { group: group, onOpen: handleOpenGroup }, group.groupID))) }) }));
117
128
  };
@@ -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ả` shows a capped preview of the contact section before handing the
6
- // seller over to the full `Liên hệ` tab.
5
+ // `Tất cả` caps each section's preview (contacts, public groups) before handing
6
+ // the seller over to the full `Liên hệ` / `Cộng đồng` 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":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageList.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAIxD,UAAU,gBAAgB;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qBAAqB,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAChE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAub3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"MessageList.d.ts","sourceRoot":"","sources":["../../../src/components/message/MessageList.tsx"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAIxD,UAAU,gBAAgB;IACxB,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,qBAAqB,CAAC,EAAE,CAAC,OAAO,CAAC,EAAE,mBAAmB,KAAK,IAAI,CAAC;IAChE,gBAAgB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC9B,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,uEAAuE;IACvE,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qDAAqD;IACrD,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,WAAW,GAAI,OAAO,gBAAgB,4CAyb3C,CAAC;AAEF,eAAe,WAAW,CAAC"}
@@ -74,12 +74,6 @@ const MessageList = (props) => {
74
74
  (!isConfirmedNonMember &&
75
75
  permissions.sendMessage &&
76
76
  !isRestrictedByGroupMute);
77
- // Some hosts run pin without their backend returning PIN_MESSAGE yet, so
78
- // the permission check would hide a feature that is live for them. Group
79
- // membership is still required either way.
80
- const canPinMessage = !isConfirmedGroup ||
81
- (!isConfirmedNonMember &&
82
- (permissions.pinMessage || allowPinMessageWithoutPermission));
83
77
  const setQuotedMessage = useConversationStore((state) => state.setQuotedMessage);
84
78
  const setSearchClientMsgID = useConversationStore((state) => state.setSearchClientMsgID);
85
79
  const [openMenuId, setOpenMenuId] = useState(null);
@@ -104,6 +98,14 @@ const MessageList = (props) => {
104
98
  groupId: rawGroupId,
105
99
  sessionId: latestConversationSession === null || latestConversationSession === void 0 ? void 0 : latestConversationSession.id,
106
100
  });
101
+ // The host flag covers backends without PIN_MESSAGE yet; group membership
102
+ // is still required either way.
103
+ const canPinInGroup = !isConfirmedNonMember &&
104
+ (permissions.pinMessage || !!allowPinMessageWithoutPermission);
105
+ // Only members manage a support thread's pins, so an agent joins first.
106
+ const canPinMessage = isConfirmedGroup
107
+ ? canPinInGroup
108
+ : !rawGroupId || isJoined === true;
107
109
  // A stale isViewOnlyMember=true only ever clears via the WASM join-group
108
110
  // path or a WS event — neither fires for a support thread joined via
109
111
  // useJoinSession's REST call. Sync it off once isJoined confirms true.
@@ -1 +1 @@
1
- {"version":3,"file":"useResolveConversation.d.ts","sourceRoot":"","sources":["../../../src/hooks/conversation/useResolveConversation.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,gBAAgB,EAIjB,MAAM,8BAA8B,CAAC;AAgBtC,MAAM,WAAW,4BAA4B;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsDD,eAAO,MAAM,wBAAwB,GACnC,OAAO,MAAM,KACZ,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAWjC,CAAC;AA+DF,eAAO,MAAM,sBAAsB,GAAI,wDAMpC,4BAA4B;;;CA8J9B,CAAC"}
1
+ {"version":3,"file":"useResolveConversation.d.ts","sourceRoot":"","sources":["../../../src/hooks/conversation/useResolveConversation.ts"],"names":[],"mappings":"AAGA,OAAO,EACL,gBAAgB,EAIjB,MAAM,8BAA8B,CAAC;AAetC,MAAM,WAAW,4BAA4B;IAC3C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAsDD,eAAO,MAAM,wBAAwB,GACnC,OAAO,MAAM,KACZ,OAAO,CAAC,gBAAgB,GAAG,IAAI,CAWjC,CAAC;AA+DF,eAAO,MAAM,sBAAsB,GAAI,wDAMpC,4BAA4B;;;CA2J9B,CAAC"}
@@ -4,8 +4,7 @@ import { GroupStatus, GroupVisibility, SessionType, } from "@droppii-org/wasm-cl
4
4
  import { DChatSDK } from "../../constants/sdk";
5
5
  import { apiInstance } from "../../services/api";
6
6
  import { ENDPOINTS } from "../../services/routes";
7
- import { ConnectStatus, SyncStatus } from "../../types/chat";
8
- import { useChatContext } from "../../context/ChatContext";
7
+ import { useCanLoadChatData } from "../../hooks/init/useCanLoadChatData";
9
8
  import useConversationStore from "../../store/conversation";
10
9
  import { getGroupIdFromConversationId, getPeerUserIdFromConversationId, resolveConversationBotId, } from "../../utils/conversation";
11
10
  import useAuthStore from "../../store/auth";
@@ -119,9 +118,7 @@ export const useResolveConversation = ({ conversationId, userId, groupId, botId,
119
118
  // conversationData looks truthy; only a rename to something else (a newer
120
119
  // resolve superseding this one) or an explicit reject below clears it.
121
120
  const pendingTargetRef = useRef(null);
122
- const { connectStatus, syncStatus } = useChatContext();
123
- const isSdkReady = connectStatus === ConnectStatus.Connected &&
124
- syncStatus !== SyncStatus.Loading;
121
+ const isSdkReady = useCanLoadChatData();
125
122
  const conversationData = useConversationStore((state) => state.conversationData);
126
123
  const removedFromGroupModalOpen = useConversationStore((state) => state.removedFromGroupModalOpen);
127
124
  const setConversationData = useConversationStore((state) => state.setConversationData);
@@ -1 +1 @@
1
- {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AAiDA,eAAO,MAAM,cAAc,YAmnB1B,CAAC"}
1
+ {"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AAiDA,eAAO,MAAM,cAAc,YAqnB1B,CAAC"}
@@ -234,7 +234,9 @@ export const useGlobalEvent = () => {
234
234
  }
235
235
  updateConnectStatus(ConnectStatus.Disconnected);
236
236
  };
237
+ // data is `reinstalled`: true when this device starts from an empty local DB.
237
238
  const syncStartHandler = ({ data }) => {
239
+ useBootStore.getState().markSyncStarted(data === true || data === "true");
238
240
  updateSyncStatus(SyncStatus.Loading);
239
241
  };
240
242
  const syncProgressHandler = ({ data }) => {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Whether threads can be opened and their history read: connected and not mid-sync. A fresh
3
+ * device's first sync counts as done once its conversations are stored, since history then
4
+ * loads on demand while the latest message of each conversation is still being pulled.
5
+ */
6
+ export declare const useCanLoadChatData: () => boolean;
7
+ //# sourceMappingURL=useCanLoadChatData.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCanLoadChatData.d.ts","sourceRoot":"","sources":["../../../src/hooks/init/useCanLoadChatData.ts"],"names":[],"mappings":"AAMA;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,eAO9B,CAAC"}
@@ -0,0 +1,15 @@
1
+ "use client";
2
+ import { useChatContext } from "../../context/ChatContext";
3
+ import useBootStore, { selectIsSyncingInBackground } from "../../store/boot";
4
+ import { ConnectStatus, SyncStatus } from "../../types/chat";
5
+ /**
6
+ * Whether threads can be opened and their history read: connected and not mid-sync. A fresh
7
+ * device's first sync counts as done once its conversations are stored, since history then
8
+ * loads on demand while the latest message of each conversation is still being pulled.
9
+ */
10
+ export const useCanLoadChatData = () => {
11
+ const { connectStatus, syncStatus } = useChatContext();
12
+ const isSyncingInBackground = useBootStore(selectIsSyncingInBackground);
13
+ return (connectStatus === ConnectStatus.Connected &&
14
+ (syncStatus !== SyncStatus.Loading || isSyncingInBackground));
15
+ };
@@ -0,0 +1,2 @@
1
+ export declare const useCanPinMessage: () => boolean;
2
+ //# sourceMappingURL=useCanPinMessage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCanPinMessage.d.ts","sourceRoot":"","sources":["../../../src/hooks/message/useCanPinMessage.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,gBAAgB,eA2B5B,CAAC"}
@@ -0,0 +1,29 @@
1
+ import { useChatContext } from "../../context/ChatContext";
2
+ import { useGroupPermission } from "../../hooks/group/useGroupPermission";
3
+ import { isSupportThreadJoinedHere } from "../../hooks/session/useAgentSupportThreadJoin";
4
+ import { useIsJoinedGroup } from "../../hooks/session/useIsJoinedGroup";
5
+ import useAuthStore from "../../store/auth";
6
+ import useConversationStore from "../../store/conversation";
7
+ import { isGroupChat, isSupportSessionAgentView } from "../../utils/conversation";
8
+ // Pin rule of DChatConversationDetail, shared by its toolbox and pinned panel.
9
+ // Desk's MessageList keeps its own rule, which also checks group membership.
10
+ export const useCanPinMessage = () => {
11
+ const { user, allowPinMessageWithoutPermission } = useChatContext();
12
+ const authUserID = useAuthStore((state) => state.userID);
13
+ const conversation = useConversationStore((state) => state.conversationData);
14
+ const permissions = useGroupPermission(conversation === null || conversation === void 0 ? void 0 : conversation.groupID);
15
+ const isGroup = conversation ? isGroupChat(conversation) : false;
16
+ // An agent on someone else's support thread manages pins only once joined.
17
+ const isAgentSupportThread = !!conversation &&
18
+ !isGroup &&
19
+ isSupportSessionAgentView(conversation, (user === null || user === void 0 ? void 0 : user.userID) || authUserID);
20
+ const { isJoined } = useIsJoinedGroup(isAgentSupportThread ? conversation === null || conversation === void 0 ? void 0 : conversation.groupID : undefined);
21
+ if (isAgentSupportThread) {
22
+ return (!conversation.groupID ||
23
+ isJoined === true ||
24
+ isSupportThreadJoinedHere(conversation.conversationID));
25
+ }
26
+ // Some hosts run pin without their backend returning PIN_MESSAGE yet, so
27
+ // the permission check would hide a feature that is live for them.
28
+ return (!isGroup || permissions.pinMessage || !!allowPinMessageWithoutPermission);
29
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"useMessage.d.ts","sourceRoot":"","sources":["../../../src/hooks/message/useMessage.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,WAAW,EACX,WAAW,EAEZ,MAAM,8BAA8B,CAAC;AAmBtC,eAAO,MAAM,kBAAkB,eAkB9B,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,WAAW,EACpB,OAAO,OAAO,EACd,eAAe;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5E,eAAe,MAAM,KACpB,OA4BF,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,iBAAiB,MAAM,EACvB,oBAAoB,MAAM,EAC1B,iBAAc,EACd,uBAAqB,EAGrB,yBAAyB,MAAM,IAAI;;;;;qBAgBd,WAAW,EAAE;;;;;;qBAAb,WAAW,EAAE;;;;;;;CAoZnC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,SACpB,CAAC;AAChC,eAAO,MAAM,gBAAgB,GAAI,SAAS,WAAW,SACpB,CAAC;AAClC,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,SACf,CAAC;AAEtC,eAAO,MAAM,kBAAkB,GAC7B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,EAC1B,YAAY,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,KAC9C,WAAW,GAAG,SAqBhB,CAAC;AAMF,eAAO,MAAM,oBAAoB,GAC/B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,KACzB,WAAW,GAAG,SAQhB,CAAC;AAmBF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,WAAW,EAAE,EACtB,SAAS,WAAW,EAAE,kBAgBvB,CAAC"}
1
+ {"version":3,"file":"useMessage.d.ts","sourceRoot":"","sources":["../../../src/hooks/message/useMessage.ts"],"names":[],"mappings":"AACA,OAAO,EAEL,WAAW,EACX,WAAW,EAEZ,MAAM,8BAA8B,CAAC;AAkBtC,eAAO,MAAM,kBAAkB,eAkB9B,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,SAAS,WAAW,EACpB,OAAO,OAAO,EACd,eAAe;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,EAC5E,eAAe,MAAM,KACpB,OA4BF,CAAC;AAEF,eAAO,MAAM,UAAU,GACrB,iBAAiB,MAAM,EACvB,oBAAoB,MAAM,EAC1B,iBAAc,EACd,uBAAqB,EAGrB,yBAAyB,MAAM,IAAI;;;;;qBAed,WAAW,EAAE;;;;;;qBAAb,WAAW,EAAE;;;;;;;CAoZnC,CAAC;AAEF,eAAO,MAAM,cAAc,GAAI,SAAS,WAAW,SACpB,CAAC;AAChC,eAAO,MAAM,gBAAgB,GAAI,SAAS,WAAW,SACpB,CAAC;AAClC,eAAO,MAAM,gBAAgB,GAAI,aAAa,MAAM,SACf,CAAC;AAEtC,eAAO,MAAM,kBAAkB,GAC7B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,EAC1B,YAAY,CAAC,SAAS,EAAE,WAAW,KAAK,OAAO,KAC9C,WAAW,GAAG,SAqBhB,CAAC;AAMF,eAAO,MAAM,oBAAoB,GAC/B,aAAa,WAAW,EAAE,EAC1B,SAAS,WAAW,EACpB,WAAW,MAAM,GAAG,MAAM,KACzB,WAAW,GAAG,SAQhB,CAAC;AAmBF,eAAO,MAAM,mBAAmB,GAC9B,SAAS,WAAW,EAAE,EACtB,SAAS,WAAW,EAAE,kBAgBvB,CAAC"}
@@ -7,9 +7,8 @@ import { useTranslation } from "react-i18next";
7
7
  import { useLatest, useRequest } from "ahooks";
8
8
  import emitter, { emit } from "../../utils/events";
9
9
  import isEmpty from "lodash/isEmpty";
10
- import { useChatContext } from "../../context/ChatContext";
11
10
  import useAuthStore from "../../store/auth";
12
- import { ConnectStatus, SyncStatus } from "../../types/chat";
11
+ import { useCanLoadChatData } from "../../hooks/init/useCanLoadChatData";
13
12
  import { GROUP_NOTIFICATION_MESSAGE_TYPES, isGroupNotificationMessageType, isSessionEventMessage, isSystemLogMessageType } from "../../utils/messageLog";
14
13
  import { isEventCustomMessage, isRenderableCustomMessage, } from "../../utils/customMessage";
15
14
  import { isGroupConversation } from "../../utils/imCommon";
@@ -60,12 +59,11 @@ export const useMessage = (conversationId, searchClientMsgID, enabled = true, us
60
59
  // Host-supplied, threaded down from the screen props the same way
61
60
  // onCopyMessageLink is. Omit it and the SDK raises its own antd toast.
62
61
  onSearchMessageRevoked) => {
63
- const { connectStatus, syncStatus } = useChatContext();
62
+ const canLoadChatData = useCanLoadChatData();
64
63
  const { t } = useTranslation();
65
64
  const userID = useAuthStore((state) => state.userID);
66
65
  const canLoad = enabled &&
67
- connectStatus === ConnectStatus.Connected &&
68
- syncStatus !== SyncStatus.Loading &&
66
+ canLoadChatData &&
69
67
  !!conversationId &&
70
68
  !!userID;
71
69
  const [loadState, setLoadState] = useState({
@@ -1 +1 @@
1
- {"version":3,"file":"useUnreadMessageIndicator.d.ts","sourceRoot":"","sources":["../../../src/hooks/message/useUnreadMessageIndicator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAUhE,eAAO,MAAM,8BAA8B,OAAO,CAAC;AAoEnD,UAAU,+BAA+B;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,WAAW,EAAE,CAAC;CAC5B;AAED;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,qDAIvC,+BAA+B;;;;;;CA8FjC,CAAC"}
1
+ {"version":3,"file":"useUnreadMessageIndicator.d.ts","sourceRoot":"","sources":["../../../src/hooks/message/useUnreadMessageIndicator.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAShE,eAAO,MAAM,8BAA8B,OAAO,CAAC;AAoEnD,UAAU,+BAA+B;IACvC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,0EAA0E;IAC1E,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,WAAW,EAAE,WAAW,EAAE,CAAC;CAC5B;AAED;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GAAI,qDAIvC,+BAA+B;;;;;;CA6FjC,CAAC"}
@@ -2,10 +2,9 @@
2
2
  import { useCallback, useEffect, useRef, useState } from "react";
3
3
  import { useLatest } from "ahooks";
4
4
  import { DChatSDK } from "../../constants/sdk";
5
- import { useChatContext } from "../../context/ChatContext";
6
5
  import useAuthStore from "../../store/auth";
7
6
  import useConversationStore from "../../store/conversation";
8
- import { ConnectStatus, SyncStatus } from "../../types/chat";
7
+ import { useCanLoadChatData } from "../../hooks/init/useCanLoadChatData";
9
8
  // Mark-as-read waits for this lookup, so a call that never settles must not
10
9
  // leave the thread unread.
11
10
  export const FIRST_UNREAD_LOOKUP_TIMEOUT_MS = 5000;
@@ -61,13 +60,12 @@ const isOlderThan = (message, boundary) => message.seq && boundary.seq
61
60
  */
62
61
  export const useUnreadMessageIndicator = ({ conversationId = "", searchClientMsgID, messageList, }) => {
63
62
  var _a;
64
- const { connectStatus, syncStatus } = useChatContext();
63
+ const canLoadChatData = useCanLoadChatData();
65
64
  const userID = useAuthStore((state) => state.userID);
66
65
  const storedConversationId = useConversationStore((state) => { var _a; return (_a = state.conversationData) === null || _a === void 0 ? void 0 : _a.conversationID; });
67
66
  // Waiting for the store to hold this conversation also waits for its open
68
67
  // target, which some hosts set a moment after the selected id.
69
- const canLookup = connectStatus === ConnectStatus.Connected &&
70
- syncStatus !== SyncStatus.Loading &&
68
+ const canLookup = canLoadChatData &&
71
69
  !!conversationId &&
72
70
  !!userID &&
73
71
  storedConversationId === conversationId;
@@ -6,6 +6,7 @@ interface UseSearchPublicGroupsParams {
6
6
  }
7
7
  export declare const useSearchPublicGroups: ({ searchTerm, enabled, pageSize, }: UseSearchPublicGroupsParams) => {
8
8
  groups: SearchPublicGroupInfo[];
9
+ total: number;
9
10
  fetchNextPage: (options?: import("@tanstack/react-query").FetchNextPageOptions) => Promise<import("@tanstack/react-query").InfiniteQueryObserverResult<import("@tanstack/react-query").InfiniteData<import("@droppii-org/wasm-client-sdk").SearchPublicGroupsResult, unknown>, Error>>;
10
11
  hasNextPage: boolean;
11
12
  isFetchingNextPage: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"useSearchPublicGroups.d.ts","sourceRoot":"","sources":["../../../src/hooks/search/useSearchPublicGroups.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,8BAA8B,CAAC;AAKtC,UAAU,2BAA2B;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAKD,eAAO,MAAM,qBAAqB,GAAI,oCAInC,2BAA2B;;;;;;;CAqD7B,CAAC"}
1
+ {"version":3,"file":"useSearchPublicGroups.d.ts","sourceRoot":"","sources":["../../../src/hooks/search/useSearchPublicGroups.ts"],"names":[],"mappings":"AAIA,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,8BAA8B,CAAC;AAKtC,UAAU,2BAA2B;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAKD,eAAO,MAAM,qBAAqB,GAAI,oCAInC,2BAA2B;;;;;;;;CA0D7B,CAAC"}
@@ -9,6 +9,7 @@ import { PAGE_SIZE } from "../../constants";
9
9
  // same call chat-mobile uses for its `Cộng đồng` tab, so both platforms return
10
10
  // the identical public-group set (joined and not yet joined).
11
11
  export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = PAGE_SIZE.medium, }) => {
12
+ var _a, _b, _c;
12
13
  const trimmedSearchTerm = searchTerm.trim();
13
14
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } = useInfiniteQuery({
14
15
  initialPageParam: 0,
@@ -29,6 +30,9 @@ export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = P
29
30
  return fetched < ((_a = lastPage === null || lastPage === void 0 ? void 0 : lastPage.total) !== null && _a !== void 0 ? _a : 0) ? fetched : undefined;
30
31
  },
31
32
  enabled: enabled && trimmedSearchTerm !== "",
33
+ // `Tất cả` waits for this query, so a failing engine must not hold it
34
+ // through the default three retries.
35
+ retry: 1,
32
36
  });
33
37
  // A group can repeat across offsets while the backing list shifts.
34
38
  const groups = useMemo(() => {
@@ -46,6 +50,8 @@ export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = P
46
50
  }, [data]);
47
51
  return {
48
52
  groups,
53
+ // Every page carries the same server-side total for the keyword.
54
+ total: (_c = (_b = (_a = data === null || data === void 0 ? void 0 : data.pages) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.total) !== null && _c !== void 0 ? _c : 0,
49
55
  fetchNextPage,
50
56
  hasNextPage,
51
57
  isFetchingNextPage,
@@ -1,4 +1,5 @@
1
1
  import { type ConversationItem } from "@droppii-org/wasm-client-sdk";
2
+ export declare const isSupportThreadJoinedHere: (conversationId?: string) => boolean;
2
3
  /**
3
4
  * CRM's join gate for the thread detail: an agent viewing a support thread
4
5
  * they aren't a member of can't send until they join. Opt-in — only a host