@droppii-org/chat-sdk 0.7.10 → 0.7.12

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 (51) hide show
  1. package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
  2. package/dist/components/conversation-detail/ConversationDetailInput.js +19 -3
  3. package/dist/components/conversation-detail/ThreadProfilePanel.d.ts.map +1 -1
  4. package/dist/components/conversation-detail/ThreadProfilePanel.js +11 -2
  5. package/dist/components/conversation-detail/profile/GroupDescriptionCard.d.ts +7 -0
  6. package/dist/components/conversation-detail/profile/GroupDescriptionCard.d.ts.map +1 -0
  7. package/dist/components/conversation-detail/profile/GroupDescriptionCard.js +47 -0
  8. package/dist/components/conversation-detail/profile/GroupDescriptionPanel.d.ts +9 -0
  9. package/dist/components/conversation-detail/profile/GroupDescriptionPanel.d.ts.map +1 -0
  10. package/dist/components/conversation-detail/profile/GroupDescriptionPanel.js +11 -0
  11. package/dist/components/conversation-detail/profile/ThreadProfileActionRows.d.ts +3 -1
  12. package/dist/components/conversation-detail/profile/ThreadProfileActionRows.d.ts.map +1 -1
  13. package/dist/components/conversation-detail/profile/ThreadProfileActionRows.js +3 -2
  14. package/dist/components/conversation-detail/thread-search/ThreadSearchContent.d.ts +3 -1
  15. package/dist/components/conversation-detail/thread-search/ThreadSearchContent.d.ts.map +1 -1
  16. package/dist/components/conversation-detail/thread-search/ThreadSearchContent.js +2 -1
  17. package/dist/components/conversation-detail/thread-search/ThreadSearchPanel.d.ts +3 -1
  18. package/dist/components/conversation-detail/thread-search/ThreadSearchPanel.d.ts.map +1 -1
  19. package/dist/components/conversation-detail/thread-search/ThreadSearchPanel.js +2 -2
  20. package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts +8 -1
  21. package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts.map +1 -1
  22. package/dist/components/conversation-inbox/ConversationInboxHeader.js +20 -3
  23. package/dist/components/conversation-inbox/PublicGroupList.d.ts +3 -0
  24. package/dist/components/conversation-inbox/PublicGroupList.d.ts.map +1 -0
  25. package/dist/components/conversation-inbox/PublicGroupList.js +43 -0
  26. package/dist/components/conversation-inbox/contact-search/PublicGroupResultItem.d.ts +3 -1
  27. package/dist/components/conversation-inbox/contact-search/PublicGroupResultItem.d.ts.map +1 -1
  28. package/dist/components/conversation-inbox/contact-search/PublicGroupResultItem.js +12 -4
  29. package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
  30. package/dist/hooks/global/useGlobalEvent.js +5 -0
  31. package/dist/hooks/group/useGroupSendRestriction.d.ts +4 -0
  32. package/dist/hooks/group/useGroupSendRestriction.d.ts.map +1 -0
  33. package/dist/hooks/group/useGroupSendRestriction.js +31 -0
  34. package/dist/hooks/group/useJoinPublicGroup.d.ts +2 -0
  35. package/dist/hooks/group/useJoinPublicGroup.d.ts.map +1 -1
  36. package/dist/hooks/group/useJoinPublicGroup.js +44 -2
  37. package/dist/hooks/search/useSearchPublicGroups.d.ts +5 -1
  38. package/dist/hooks/search/useSearchPublicGroups.d.ts.map +1 -1
  39. package/dist/hooks/search/useSearchPublicGroups.js +5 -3
  40. package/dist/locales/vi/common.json +2 -1
  41. package/dist/locales/vi/conversation-inbox.json +8 -1
  42. package/dist/screens/conversation-detail/index.d.ts.map +1 -1
  43. package/dist/screens/conversation-detail/index.js +3 -5
  44. package/dist/screens/conversation-inbox/index.d.ts +2 -0
  45. package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
  46. package/dist/screens/conversation-inbox/index.js +9 -2
  47. package/dist/store/conversation.d.ts.map +1 -1
  48. package/dist/store/conversation.js +15 -11
  49. package/dist/styles/global.css +1 -1
  50. package/dist/utils/messageLog.js +1 -1
  51. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"useJoinPublicGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useJoinPublicGroup.ts"],"names":[],"mappings":"AAQA,UAAU,oBAAoB;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB;AAID,eAAO,MAAM,kBAAkB,wGAoB9B,CAAC"}
1
+ {"version":3,"file":"useJoinPublicGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useJoinPublicGroup.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,WAAW,EACjB,MAAM,uBAAuB,CAAC;AAU/B,UAAU,oBAAoB;IAC5B,OAAO,EAAE,MAAM,CAAC;CACjB;AAYD,eAAO,MAAM,uBAAuB,GAClC,aAAa,WAAW,EACxB,SAAS,MAAM,kBAQd,CAAC;AAoBJ,eAAO,MAAM,kBAAkB,wGA+C9B,CAAC"}
@@ -1,9 +1,34 @@
1
1
  "use client";
2
- import { useMutation, useQueryClient } from "@tanstack/react-query";
3
- import { GroupJoinSource } from "@droppii-org/wasm-client-sdk";
2
+ import { useMutation, useQueryClient, } from "@tanstack/react-query";
3
+ import { GroupJoinSource, } from "@droppii-org/wasm-client-sdk";
4
4
  import { DChatSDK } from "../../constants/sdk";
5
5
  import { QUERY_KEYS } from "../../services/query";
6
+ import useConversationStore from "../../store/conversation";
6
7
  import emitter from "../../utils/events";
8
+ const listShowsGroup = (query, groupID) => {
9
+ var _a, _b;
10
+ return !!((_b = (_a = query.state.data) === null || _a === void 0 ? void 0 : _a.pages) === null || _b === void 0 ? void 0 : _b.some((page) => { var _a; return (_a = page === null || page === void 0 ? void 0 : page.groups) === null || _a === void 0 ? void 0 : _a.some((group) => group.groupID === groupID); }));
11
+ };
12
+ // Refetches only the public-group lists that show this group, so membership
13
+ // events for unrelated threads don't reload every loaded page. A load already
14
+ // running (e.g. the next page) is left alone: cancelling it stalls paging.
15
+ export const refreshPublicGroupLists = (queryClient, groupID) => queryClient.invalidateQueries({
16
+ queryKey: [QUERY_KEYS.SEARCH_PUBLIC_GROUPS],
17
+ predicate: (query) => listShowsGroup(query, groupID),
18
+ }, { cancelRefetch: false });
19
+ const IN_FLIGHT_LIST_WAIT_MS = 5000;
20
+ // A list still loading was built from pre-join data and would undo the flip
21
+ // when it lands, so let it land first (bounded, in case the engine stalls).
22
+ const waitForPublicGroupLoads = (queryClient) => {
23
+ const loads = queryClient
24
+ .getQueryCache()
25
+ .findAll({ queryKey: [QUERY_KEYS.SEARCH_PUBLIC_GROUPS], fetchStatus: "fetching" })
26
+ .map((query) => { var _a; return (_a = query.promise) === null || _a === void 0 ? void 0 : _a.catch(() => undefined); });
27
+ return Promise.race([
28
+ Promise.all(loads),
29
+ new Promise((resolve) => setTimeout(resolve, IN_FLIGHT_LIST_WAIT_MS)),
30
+ ]);
31
+ };
7
32
  // Joining straight from search results, matching chat-mobile's GroupItem —
8
33
  // GroupJoinSource.Search is what BE uses to attribute the join.
9
34
  export const useJoinPublicGroup = () => {
@@ -20,9 +45,26 @@ export const useJoinPublicGroup = () => {
20
45
  },
21
46
  // Joining a group already cached locally fires no join event, so member-only UI reads this.
22
47
  onSuccess: async (_data, { groupID }) => {
48
+ // A kick earlier in the session leaves this marker, which would drop the
49
+ // member/permission refetch that follows the join.
50
+ useConversationStore.setState((state) => state.nonMemberGroupID === groupID ? { nonMemberGroupID: null } : state);
23
51
  const queryKey = [QUERY_KEYS.IS_JOINED_GROUP, groupID];
24
52
  await queryClient.cancelQueries({ queryKey });
25
53
  queryClient.setQueryData(queryKey, true);
54
+ // Every public-group list (inbox tab, search) flips this card to Truy cập and counts the new member.
55
+ await waitForPublicGroupLoads(queryClient);
56
+ queryClient.setQueriesData({ queryKey: [QUERY_KEYS.SEARCH_PUBLIC_GROUPS] }, (data) => data && Object.assign(Object.assign({}, data), { pages: data.pages.map((page) => {
57
+ var _a;
58
+ return (Object.assign(Object.assign({}, page), { groups: ((_a = page === null || page === void 0 ? void 0 : page.groups) !== null && _a !== void 0 ? _a : []).map((group) => {
59
+ var _a;
60
+ return group.groupID === groupID && !group.isJoined
61
+ ? Object.assign(Object.assign({}, group), { isJoined: true, memberCount: ((_a = group.memberCount) !== null && _a !== void 0 ? _a : 0) + 1 }) : group;
62
+ }) }));
63
+ }) }));
64
+ },
65
+ // A stale card can offer Tham gia for a group joined elsewhere; refetch so it settles.
66
+ onError: (_error, { groupID }) => {
67
+ void refreshPublicGroupLists(queryClient, groupID);
26
68
  },
27
69
  });
28
70
  };
@@ -3,14 +3,18 @@ interface UseSearchPublicGroupsParams {
3
3
  searchTerm: string;
4
4
  enabled?: boolean;
5
5
  pageSize?: number;
6
+ /** The inbox `Cộng đồng` tab lists every public group, so it queries with an empty keyword. */
7
+ allowEmptyTerm?: boolean;
6
8
  }
7
- export declare const useSearchPublicGroups: ({ searchTerm, enabled, pageSize, }: UseSearchPublicGroupsParams) => {
9
+ export declare const useSearchPublicGroups: ({ searchTerm, enabled, pageSize, allowEmptyTerm, }: UseSearchPublicGroupsParams) => {
8
10
  groups: SearchPublicGroupInfo[];
9
11
  total: number;
10
12
  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>>;
11
13
  hasNextPage: boolean;
12
14
  isFetchingNextPage: boolean;
13
15
  isLoading: boolean;
16
+ /** True until the first page arrives, including while the query waits to be enabled. */
17
+ isPending: boolean;
14
18
  isError: boolean;
15
19
  };
16
20
  export {};
@@ -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;;;;;;;;CA0D7B,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;IAClB,+FAA+F;IAC/F,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAKD,eAAO,MAAM,qBAAqB,GAAI,oDAKnC,2BAA2B;;;;;;;IAgE1B,wFAAwF;;;CAI3F,CAAC"}
@@ -8,10 +8,10 @@ import { PAGE_SIZE } from "../../constants";
8
8
  // Community results come from the IM SDK rather than the contacts REST API —
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
- export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = PAGE_SIZE.medium, }) => {
11
+ export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = PAGE_SIZE.medium, allowEmptyTerm = false, }) => {
12
12
  var _a, _b, _c;
13
13
  const trimmedSearchTerm = searchTerm.trim();
14
- const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isError } = useInfiniteQuery({
14
+ const { data, fetchNextPage, hasNextPage, isFetchingNextPage, isLoading, isPending, isError, } = useInfiniteQuery({
15
15
  initialPageParam: 0,
16
16
  queryKey: [QUERY_KEYS.SEARCH_PUBLIC_GROUPS, trimmedSearchTerm, pageSize],
17
17
  queryFn: async ({ pageParam = 0 }) => {
@@ -29,7 +29,7 @@ export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = P
29
29
  const fetched = allPages.reduce((total, page) => { var _a, _b; return total + ((_b = (_a = page === null || page === void 0 ? void 0 : page.groups) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0); }, 0);
30
30
  return fetched < ((_a = lastPage === null || lastPage === void 0 ? void 0 : lastPage.total) !== null && _a !== void 0 ? _a : 0) ? fetched : undefined;
31
31
  },
32
- enabled: enabled && trimmedSearchTerm !== "",
32
+ enabled: enabled && (allowEmptyTerm || trimmedSearchTerm !== ""),
33
33
  // `Tất cả` waits for this query, so a failing engine must not hold it
34
34
  // through the default three retries.
35
35
  retry: 1,
@@ -56,6 +56,8 @@ export const useSearchPublicGroups = ({ searchTerm, enabled = true, pageSize = P
56
56
  hasNextPage,
57
57
  isFetchingNextPage,
58
58
  isLoading,
59
+ /** True until the first page arrives, including while the query waits to be enabled. */
60
+ isPending,
59
61
  isError,
60
62
  };
61
63
  };
@@ -137,7 +137,7 @@
137
137
  "group_notification_owner_transferred": "{{name}} đã chuyển quyền trưởng nhóm cho {{newOwner}}",
138
138
  "group_notification_member_kicked": "{{name}} đã xóa {{members}} khỏi nhóm",
139
139
  "group_notification_member_invited": "{{name}} đã thêm {{members}} vào nhóm",
140
- "group_notification_member_enter": "{{name}} đã vào nhóm",
140
+ "group_notification_member_enter": "{{name}} đã tham gia nhóm",
141
141
  "group_notification_member_enter_via_link": "{{name}} đã tham gia nhóm qua link",
142
142
  "group_notification_dismissed": "{{name}} đã giải tán nhóm",
143
143
  "group_notification_member_muted": "{{name}} đã tắt quyền gửi tin nhắn của {{target}}",
@@ -220,6 +220,7 @@
220
220
  "join_group": "Tham gia",
221
221
  "join_group_success": "Tham gia nhóm thành công",
222
222
  "join_group_failed": "Tham gia nhóm thất bại",
223
+ "public_group_joined": "Đã tham gia nhóm",
223
224
  "join_group_confirm_title": "Tham gia nhóm",
224
225
  "join_group_confirm_cta": "Tham gia nhóm",
225
226
  "join_link_unavailable_title": "Liên kết không còn khả dụng",
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "tabs": {
3
- "messages": "Tin nhắn"
3
+ "messages": "Tin nhắn",
4
+ "community": "Cộng đồng"
5
+ },
6
+ "community": {
7
+ "empty": "Không có nhóm cộng đồng nào"
4
8
  },
5
9
  "search_placeholder": "Tìm kiếm",
6
10
  "compose": {
@@ -97,6 +101,7 @@
97
101
  "options_title": "Tuỳ chọn",
98
102
  "input_placeholder": "Nhập tin nhắn...",
99
103
  "view_only_notice": "Bạn cần tham gia nhóm để gửi tin nhắn",
104
+ "cannot_send_notice": "Bạn không thể gửi tin nhắn vào nhóm này",
100
105
  "resolve_failed": "Không thể mở cuộc trò chuyện",
101
106
  "search": {
102
107
  "open": "Tìm tin nhắn",
@@ -117,6 +122,8 @@
117
122
  "pin": "Ghim trò chuyện",
118
123
  "unpin": "Bỏ ghim trò chuyện",
119
124
  "delete_history": "Xoá lịch sử trò chuyện",
125
+ "group_description_title": "Mô tả nhóm",
126
+ "group_description_close": "Đóng mô tả nhóm",
120
127
  "pin_success": "Đã ghim đoạn chat thành công",
121
128
  "unpin_success": "Đã bỏ ghim đoạn chat thành công",
122
129
  "pin_failed": "Có lỗi xảy ra vui lòng thử lại",
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation-detail/index.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AAarF,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,6CAA6C,CAAC;AASrD,MAAM,WAAW,4BACf,SAAQ,4BAA4B,EAClC,4BAA4B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,oHAAoH;IACpH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,6GAA6G;IAC7G,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,QAAA,MAAM,uBAAuB,GAAI,oLAgB9B,4BAA4B,mDAwT9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation-detail/index.tsx"],"names":[],"mappings":"AAuBA,OAAO,KAAK,EAAE,4BAA4B,EAAE,MAAM,kCAAkC,CAAC;AAarF,OAAO,EAEL,KAAK,4BAA4B,EAClC,MAAM,6CAA6C,CAAC;AASrD,MAAM,WAAW,4BACf,SAAQ,4BAA4B,EAClC,4BAA4B;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,IAAI,CAAC;IAC1E,kGAAkG;IAClG,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;IACpC,oHAAoH;IACpH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,6GAA6G;IAC7G,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED,QAAA,MAAM,uBAAuB,GAAI,oLAgB9B,4BAA4B,mDAsT9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
@@ -72,10 +72,6 @@ const DChatConversationDetail = (_a) => {
72
72
  const [isProfileOpen, { setTrue: openProfile, setFalse: closeProfile }] = useBoolean(false);
73
73
  const [resourceTab, setResourceTab] = useState(null);
74
74
  const [isSearchOpen, { setTrue: openSearch, setFalse: closeSearch }] = useBoolean(false);
75
- const openSearchFromProfile = () => {
76
- closeProfile();
77
- openSearch();
78
- };
79
75
  // The search API only serves members: hide it while the check says no or is pending.
80
76
  // A failed check keeps it (the API still gates); a 1-1 exposes no groupID and is the viewer's own.
81
77
  const { isJoined, isError: isJoinCheckFailed, refetch: refetchIsJoined, } = useIsJoinedGroup(showThreadSearch ? conversation === null || conversation === void 0 ? void 0 : conversation.groupID : undefined);
@@ -167,6 +163,8 @@ const DChatConversationDetail = (_a) => {
167
163
  !botFlow.flowIsEnd &&
168
164
  botFlow.shouldShowButtons &&
169
165
  botFlow.currentButtonsBlock ? (_jsx(BotButtonMessage, { buttons: botFlow.currentButtonsBlock.data.buttons, disabled: botFlow.isProcessing, onSelect: botFlow.selectButton, onRestart: botFlow.restart })) : null;
170
- return (_jsxs("div", { "data-testid": "conversation-detail-root", className: clsx("relative flex h-full w-full flex-col overflow-hidden bg-white", className), children: [_jsx(ConversationDetailHeader, { conversation: conversation, onBack: handleBack, onProfileClick: openProfile, onProfileIconClick: showProfileActions ? openProfile : undefined, onSearchClick: canSearch ? openSearch : undefined }), _jsx(PinnedMessageBar, { canUnpin: canPinMessage }), _jsxs("div", { [COMPOSER_DISMISS_ZONE_ATTR]: "", className: "relative flex min-h-0 flex-1 flex-col", children: [children !== null && children !== void 0 ? children : (_jsx(ConversationMessageList, { bottomSlot: botButtonsSlot, onCopyMessageLink: onCopyMessageLink, onSearchMessageRevoked: onSearchMessageRevoked })), _jsx(TypingIndicatorBar, { conversationId: conversation.conversationID })] }), footer, isBotThread && (_jsxs(_Fragment, { children: [_jsx(BotMenuSheet, { open: isMenuOpen, items: botFlow.menuItems, disabled: botFlow.isProcessing, onClose: closeMenu, onSelect: botFlow.selectMenuOption }), _jsx(CsSessionConfirmModal, { open: botFlow.isCsConfirmOpen, loading: botFlow.isProcessing, onCancel: botFlow.cancelMenuOption, onConfirm: botFlow.confirmEndCsSession })] })), _jsx(ThreadProfilePanel, { open: isProfileOpen, conversation: conversation, onClose: closeProfile, onSelectResource: setResourceTab, showActions: showProfileActions, onSearch: canSearch ? openSearchFromProfile : undefined, hideSearch: showThreadSearch && !canSearch }), showThreadSearch && (_jsx(ThreadSearchPanel, { open: isSearchOpen && canSearch, conversation: conversation, onClose: closeSearch, onSearchMessageRevoked: onSearchMessageRevoked }, conversation.conversationID)), _jsx(MediaResourcePanel, { open: resourceTab !== null, initialTab: resourceTab !== null && resourceTab !== void 0 ? resourceTab : MediaResourceTabKey.Image, onClose: () => setResourceTab(null) })] }));
166
+ return (_jsxs("div", { "data-testid": "conversation-detail-root", className: clsx("relative flex h-full w-full flex-col overflow-hidden bg-white", className), children: [_jsx(ConversationDetailHeader, { conversation: conversation, onBack: handleBack, onProfileClick: openProfile, onProfileIconClick: showProfileActions ? openProfile : undefined, onSearchClick: canSearch ? openSearch : undefined }), _jsx(PinnedMessageBar, { canUnpin: canPinMessage }), _jsxs("div", { [COMPOSER_DISMISS_ZONE_ATTR]: "", className: "relative flex min-h-0 flex-1 flex-col", children: [children !== null && children !== void 0 ? children : (_jsx(ConversationMessageList, { bottomSlot: botButtonsSlot, onCopyMessageLink: onCopyMessageLink, onSearchMessageRevoked: onSearchMessageRevoked })), _jsx(TypingIndicatorBar, { conversationId: conversation.conversationID })] }), footer, isBotThread && (_jsxs(_Fragment, { children: [_jsx(BotMenuSheet, { open: isMenuOpen, items: botFlow.menuItems, disabled: botFlow.isProcessing, onClose: closeMenu, onSelect: botFlow.selectMenuOption }), _jsx(CsSessionConfirmModal, { open: botFlow.isCsConfirmOpen, loading: botFlow.isProcessing, onCancel: botFlow.cancelMenuOption, onConfirm: botFlow.confirmEndCsSession })] })), _jsx(ThreadProfilePanel, { open: isProfileOpen, conversation: conversation, onClose: closeProfile, onSelectResource: setResourceTab, showActions: showProfileActions, onSearch: canSearch ? openSearch : undefined, hideSearch: showThreadSearch && !canSearch }), showThreadSearch && (_jsx(ThreadSearchPanel, { open: isSearchOpen && canSearch, conversation: conversation, onClose: closeSearch,
167
+ // Search opened from the profile stacks on it: back returns there, a jump closes both.
168
+ onJump: closeProfile, onSearchMessageRevoked: onSearchMessageRevoked }, conversation.conversationID)), _jsx(MediaResourcePanel, { open: resourceTab !== null, initialTab: resourceTab !== null && resourceTab !== void 0 ? resourceTab : MediaResourceTabKey.Image, onClose: () => setResourceTab(null) })] }));
171
169
  };
172
170
  export default DChatConversationDetail;
@@ -26,6 +26,8 @@ export interface DChatConversationInboxProps {
26
26
  showPinThread?: boolean;
27
27
  /** Forwarded to the detail screen — see DChatConversationDetailProps.showThreadSearch. */
28
28
  showThreadSearch?: boolean;
29
+ /** BIZ: Tin nhắn | Cộng đồng tabs in the header, Cộng đồng listing public groups. Defaults to false. */
30
+ showCommunityTab?: boolean;
29
31
  }
30
32
  declare const DChatConversationInbox: (props: DChatConversationInboxProps) => import("react/jsx-runtime").JSX.Element;
31
33
  export default DChatConversationInbox;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation-inbox/index.tsx"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAM/D,MAAM,WAAW,2BAA2B;IAC1C,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,6FAA6F;IAC7F,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,yEAAyE;IACzE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC;;;;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;IACpC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mHAAmH;IACnH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0FAA0F;IAC1F,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AA4OD,QAAA,MAAM,sBAAsB,GAAI,OAAO,2BAA2B,4CAQjE,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/screens/conversation-inbox/index.tsx"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AAC/E,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAM/D,MAAM,WAAW,2BAA2B;IAC1C,cAAc,CAAC,EAAE,CAAC,YAAY,EAAE,oBAAoB,KAAK,IAAI,CAAC;IAC9D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,6FAA6F;IAC7F,YAAY,CAAC,EAAE,YAAY,CAAC;IAC5B,yEAAyE;IACzE,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAClC;;;;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;IACpC,+EAA+E;IAC/E,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,4FAA4F;IAC5F,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,mHAAmH;IACnH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,0FAA0F;IAC1F,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,wGAAwG;IACxG,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B;AAqPD,QAAA,MAAM,sBAAsB,GAAI,OAAO,2BAA2B,4CAQjE,CAAC;AAEF,eAAe,sBAAsB,CAAC"}
@@ -1,5 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useState } from "react";
3
4
  import { useSearchParams } from "next/navigation";
4
5
  import { useTranslation } from "react-i18next";
5
6
  import { Empty } from "antd";
@@ -11,6 +12,8 @@ import { useChatContext } from "../../context/ChatContext";
11
12
  import { useConversationList } from "../../hooks/conversation/useConversationList";
12
13
  import { useActiveConversation } from "../../hooks/conversation/useActiveConversation";
13
14
  import { ConversationInboxHeader } from "../../components/conversation-inbox";
15
+ import { InboxTab } from "../../components/conversation-inbox/ConversationInboxHeader";
16
+ import PublicGroupList from "../../components/conversation-inbox/PublicGroupList";
14
17
  import ConversationInboxList from "../../components/conversation-inbox/ConversationInboxList";
15
18
  import { ContactSearchPanel } from "../../components/conversation-inbox/contact-search";
16
19
  import DChatConversationDetail from "../../screens/conversation-detail";
@@ -77,11 +80,15 @@ const useInboxConversationList = (chatCategory) => {
77
80
  const isApplicationTypeValid = Object.values(DChatApplicationType).includes(applicationType);
78
81
  return Object.assign(Object.assign({}, useConversationList({ enabled: isApplicationTypeValid, chatCategory })), { isApplicationTypeValid });
79
82
  };
80
- const DChatConversationInboxBody = ({ onSelectThread, className, moreOptions, showSearch = true, headerTitle, headerRightSlot, onCopyMessageLink, onSearchMessageRevoked, showSticker, showProfileActions, showPinThread, showThreadSearch, target, conversationList, }) => {
83
+ const DChatConversationInboxBody = ({ onSelectThread, className, moreOptions, showSearch = true, headerTitle, headerRightSlot, onCopyMessageLink, onSearchMessageRevoked, showSticker, showProfileActions, showPinThread, showThreadSearch, showCommunityTab = false, target, conversationList, }) => {
81
84
  const { t } = useTranslation("conversation-inbox");
82
85
  const selectedConversationId = useConversationStore((state) => state.selectedConversationId);
83
86
  const removedFromGroupModalOpen = useConversationStore((state) => state.removedFromGroupModalOpen);
84
87
  const [isSearchOpen, { setTrue: openSearch, setFalse: closeSearch }] = useBoolean(false);
88
+ // Kept in the body, not the list: the body stays mounted while a thread is
89
+ // open, so Back lands on the same tab.
90
+ const [activeTab, setActiveTab] = useState(InboxTab.Messages);
91
+ const isCommunityTab = showCommunityTab && activeTab === InboxTab.Community;
85
92
  const { threadId, userId, groupId, botId, messageId } = target;
86
93
  // Any of the four targets opens the detail screen — useResolveConversation
87
94
  // turns userId/groupId/botId into a thread (creating the bot one when
@@ -105,7 +112,7 @@ const DChatConversationInboxBody = ({ onSelectThread, className, moreOptions, sh
105
112
  // `relative` + `overflow-hidden` keep ContactSearchPanel's Drawer
106
113
  // (getContainer={false}) inside this column instead of escaping to
107
114
  // the viewport — same containment the conversation detail root uses.
108
- "relative flex h-full w-full flex-col overflow-hidden bg-white", className), children: [_jsx(ConversationInboxHeader, { onOpenSearch: showSearch ? openSearch : undefined, title: headerTitle, rightSlot: headerRightSlot }), !conversationList.isApplicationTypeValid ? (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Empty, { image: _jsx("div", { className: "flex justify-center", children: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-[var(--dc-color-primary-light)]" }) }), description: t("empty.no_conversations") }) })) : (_jsx(ConversationInboxList, { items: conversationList.items, isLoading: conversationList.isLoading, isFetchingNextPage: conversationList.isFetchingNextPage, hasNextPage: conversationList.hasNextPage, onLoadMore: conversationList.fetchNextPage, onSelectThread: onSelectThread, showPinThread: showPinThread })), _jsx(ContactSearchPanel, { open: isSearchOpen, onClose: closeSearch })] }));
115
+ "relative flex h-full w-full flex-col overflow-hidden bg-white", className), children: [_jsx(ConversationInboxHeader, { onOpenSearch: showSearch ? openSearch : undefined, title: headerTitle, rightSlot: headerRightSlot, activeTab: activeTab, onChangeTab: showCommunityTab ? setActiveTab : undefined }), isCommunityTab ? (_jsx(PublicGroupList, {})) : !conversationList.isApplicationTypeValid ? (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Empty, { image: _jsx("div", { className: "flex justify-center", children: _jsx(Icon, { icon: "chat-square-b", size: 80, className: "text-[var(--dc-color-primary-light)]" }) }), description: t("empty.no_conversations") }) })) : (_jsx(ConversationInboxList, { items: conversationList.items, isLoading: conversationList.isLoading, isFetchingNextPage: conversationList.isFetchingNextPage, hasNextPage: conversationList.hasNextPage, onLoadMore: conversationList.fetchNextPage, onSelectThread: onSelectThread, showPinThread: showPinThread })), _jsx(ContactSearchPanel, { open: isSearchOpen, onClose: closeSearch })] }));
109
116
  };
110
117
  // ChatProvider's useUrlState prop picks which target-resolution variant
111
118
  // mounts — kept as two full components (not one with a conditional hook)
@@ -1 +1 @@
1
- {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/store/conversation.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAY3C,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,SAElD,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,oFAOpB,CAAC;AAUX,eAAO,MAAM,cAAc,GAAI,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,SAEhE,CAAC;AAEF,QAAA,MAAM,oBAAoB,gFA4hBvB,CAAC;AAEJ,eAAe,oBAAoB,CAAC"}
1
+ {"version":3,"file":"conversation.d.ts","sourceRoot":"","sources":["../../src/store/conversation.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,iBAAiB,EAAE,MAAM,QAAQ,CAAC;AAiB3C,eAAO,MAAM,kBAAkB,GAAI,SAAS,OAAO,SAElD,CAAC;AAEF,oFAAoF;AACpF,eAAO,MAAM,iBAAiB,oFAOpB,CAAC;AAUX,eAAO,MAAM,cAAc,GAAI,IAAI,CAAC,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC,GAAG,IAAI,SAEhE,CAAC;AAEF,QAAA,MAAM,oBAAoB,gFA0hBvB,CAAC;AAEJ,eAAe,oBAAoB,CAAC"}
@@ -7,6 +7,10 @@ import { PeerType } from "../types/conversation";
7
7
  import useUserStore from "./user";
8
8
  import useAuthStore from "./auth";
9
9
  const CONVERSATION_SPLIT_COUNT = 500;
10
+ // Bumped on every updateCurrentConversation call and on clear: a group check
11
+ // that resolves after the user moved on (even back to the thread already set)
12
+ // must not apply its verdict or overwrite currentConversation.
13
+ let conversationSwitchSeq = 0;
10
14
  // Module-level (not store state): ChatProvider syncs this from its useUrlState
11
15
  // prop so clearSelectedConversation below can skip touching window.history in
12
16
  // bubble mode — this store has no React context access of its own.
@@ -118,6 +122,7 @@ const useConversationStore = create((set, get) => ({
118
122
  },
119
123
  updateCurrentConversation: async (conversation, options) => {
120
124
  var _a;
125
+ const switchSeq = ++conversationSwitchSeq;
121
126
  if (!conversation) {
122
127
  set({
123
128
  currentConversation: undefined,
@@ -168,6 +173,8 @@ const useConversationStore = create((set, get) => ({
168
173
  // fail closed until an unrelated WS event happens to refresh it.
169
174
  get().getCurrentMemberInGroupByReq(groupID),
170
175
  ]);
176
+ if (switchSeq !== conversationSwitchSeq)
177
+ return;
171
178
  }
172
179
  }
173
180
  // Derives view-only/block state from fetch/cache above for fresh
@@ -240,12 +247,9 @@ const useConversationStore = create((set, get) => ({
240
247
  conversationList: state.conversationList.map((conversation) => conversation.groupID === groupID
241
248
  ? Object.assign(Object.assign({}, conversation), { showName: groupInfo.groupName, faceURL: groupInfo.faceURL }) : conversation),
242
249
  }));
243
- // Kick-guard only — a response for a group the user has since navigated
244
- // away from is still allowed to populate the cache (that's what makes
245
- // reopening it fast); only a confirmed non-member response is dropped.
246
- if (get().nonMemberGroupID === groupID) {
247
- return;
248
- }
250
+ // No kick-guard here: group info grants no membership, and a kicked
251
+ // viewer needs its visibility to reopen a public group read-only instead
252
+ // of being blocked. Member/permission fetches keep the guard.
249
253
  set((state) => ({
250
254
  currentGroupInfo: new Map(state.currentGroupInfo).set(groupID, Object.assign({}, groupInfo)),
251
255
  }));
@@ -267,9 +271,9 @@ const useConversationStore = create((set, get) => ({
267
271
  console.error("Error fetching group members:", error);
268
272
  return;
269
273
  }
270
- // Kick-guard only (same rationale as getCurrentGroupInfoByReq above) —
271
- // runs concurrently with getSelfGroupPermissionByReq, so this can still
272
- // race a kick event; nonMemberGroupID is what actually protects here.
274
+ // Kick-guard: a response still in flight when a kick lands must not
275
+ // restore the membership; runs concurrently with getSelfGroupPermissionByReq,
276
+ // so nonMemberGroupID is what actually protects here.
273
277
  if (get().nonMemberGroupID === groupID || !memberInfo) {
274
278
  return;
275
279
  }
@@ -306,6 +310,7 @@ const useConversationStore = create((set, get) => ({
306
310
  });
307
311
  },
308
312
  clearSelectedConversation: (options) => {
313
+ conversationSwitchSeq += 1;
309
314
  // DeskConversationList's threadId-driven effect re-selects the same
310
315
  // conversation on its next run otherwise (conversationList changes
311
316
  // trigger it) — clobbering this reset right back.
@@ -483,8 +488,7 @@ const useConversationStore = create((set, get) => ({
483
488
  if (isSameInfo && isSameHeader)
484
489
  return state;
485
490
  return Object.assign({
486
- // Set directly: getCurrentGroupInfoByReq's kick-guard skips groups the
487
- // viewer has left, which is exactly who reaches this view.
491
+ // Set from this poll's response; no second getSpecifiedGroupsInfo call.
488
492
  currentGroupInfo: new Map(state.currentGroupInfo).set(groupID, Object.assign({}, latest)) }, (isSameHeader
489
493
  ? {}
490
494
  : {