@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.
- package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
- package/dist/components/conversation-detail/ConversationDetailInput.js +19 -3
- package/dist/components/conversation-detail/ThreadProfilePanel.d.ts.map +1 -1
- package/dist/components/conversation-detail/ThreadProfilePanel.js +11 -2
- package/dist/components/conversation-detail/profile/GroupDescriptionCard.d.ts +7 -0
- package/dist/components/conversation-detail/profile/GroupDescriptionCard.d.ts.map +1 -0
- package/dist/components/conversation-detail/profile/GroupDescriptionCard.js +47 -0
- package/dist/components/conversation-detail/profile/GroupDescriptionPanel.d.ts +9 -0
- package/dist/components/conversation-detail/profile/GroupDescriptionPanel.d.ts.map +1 -0
- package/dist/components/conversation-detail/profile/GroupDescriptionPanel.js +11 -0
- package/dist/components/conversation-detail/profile/ThreadProfileActionRows.d.ts +3 -1
- package/dist/components/conversation-detail/profile/ThreadProfileActionRows.d.ts.map +1 -1
- package/dist/components/conversation-detail/profile/ThreadProfileActionRows.js +3 -2
- package/dist/components/conversation-detail/thread-search/ThreadSearchContent.d.ts +3 -1
- package/dist/components/conversation-detail/thread-search/ThreadSearchContent.d.ts.map +1 -1
- package/dist/components/conversation-detail/thread-search/ThreadSearchContent.js +2 -1
- package/dist/components/conversation-detail/thread-search/ThreadSearchPanel.d.ts +3 -1
- package/dist/components/conversation-detail/thread-search/ThreadSearchPanel.d.ts.map +1 -1
- package/dist/components/conversation-detail/thread-search/ThreadSearchPanel.js +2 -2
- package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts +8 -1
- package/dist/components/conversation-inbox/ConversationInboxHeader.d.ts.map +1 -1
- package/dist/components/conversation-inbox/ConversationInboxHeader.js +20 -3
- package/dist/components/conversation-inbox/PublicGroupList.d.ts +3 -0
- package/dist/components/conversation-inbox/PublicGroupList.d.ts.map +1 -0
- package/dist/components/conversation-inbox/PublicGroupList.js +43 -0
- package/dist/components/conversation-inbox/contact-search/PublicGroupResultItem.d.ts +3 -1
- package/dist/components/conversation-inbox/contact-search/PublicGroupResultItem.d.ts.map +1 -1
- package/dist/components/conversation-inbox/contact-search/PublicGroupResultItem.js +12 -4
- package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
- package/dist/hooks/global/useGlobalEvent.js +5 -0
- package/dist/hooks/group/useGroupSendRestriction.d.ts +4 -0
- package/dist/hooks/group/useGroupSendRestriction.d.ts.map +1 -0
- package/dist/hooks/group/useGroupSendRestriction.js +31 -0
- package/dist/hooks/group/useJoinPublicGroup.d.ts +2 -0
- package/dist/hooks/group/useJoinPublicGroup.d.ts.map +1 -1
- package/dist/hooks/group/useJoinPublicGroup.js +44 -2
- package/dist/hooks/search/useSearchPublicGroups.d.ts +5 -1
- package/dist/hooks/search/useSearchPublicGroups.d.ts.map +1 -1
- package/dist/hooks/search/useSearchPublicGroups.js +5 -3
- package/dist/locales/vi/common.json +2 -1
- package/dist/locales/vi/conversation-inbox.json +8 -1
- package/dist/screens/conversation-detail/index.d.ts.map +1 -1
- package/dist/screens/conversation-detail/index.js +3 -5
- 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 +9 -2
- package/dist/store/conversation.d.ts.map +1 -1
- package/dist/store/conversation.js +15 -11
- package/dist/styles/global.css +1 -1
- package/dist/utils/messageLog.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useJoinPublicGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useJoinPublicGroup.ts"],"names":[],"mappings":"
|
|
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;
|
|
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}} đã
|
|
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,
|
|
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 ?
|
|
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":"
|
|
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;
|
|
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
|
-
//
|
|
244
|
-
//
|
|
245
|
-
//
|
|
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
|
|
271
|
-
// runs concurrently with getSelfGroupPermissionByReq,
|
|
272
|
-
//
|
|
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
|
|
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
|
: {
|