@droppii-org/chat-sdk 0.7.11 → 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-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 +6 -1
- package/dist/screens/conversation-inbox/index.d.ts +2 -0
- package/dist/screens/conversation-inbox/index.d.ts.map +1 -1
- package/dist/screens/conversation-inbox/index.js +9 -2
- package/dist/store/conversation.d.ts.map +1 -1
- package/dist/store/conversation.js +15 -11
- package/dist/utils/messageLog.js +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ConversationDetailInput.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ConversationDetailInput.tsx"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAoB/D,MAAM,WAAW,4BAA4B;IAC3C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,aAAa,CAAC,EAAE,MAAM,IAAI,CAAC;IAC3B,wEAAwE;IACxE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;;OAIG;IACH,mBAAmB,CAAC,EAAE,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACpE;AAED,QAAA,MAAM,uBAAuB,GAAI,8GAQ9B,4BAA4B,4CA4yB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
|
|
@@ -9,6 +9,7 @@ import { Icon } from "../../components/icon";
|
|
|
9
9
|
import { useSendMessage } from "../../hooks/message/useSendMessage";
|
|
10
10
|
import { useReportTypingState } from "../../hooks/message/useReportTypingState";
|
|
11
11
|
import { useJoinPublicGroup } from "../../hooks/group/useJoinPublicGroup";
|
|
12
|
+
import { useGroupSendRestriction } from "../../hooks/group/useGroupSendRestriction";
|
|
12
13
|
import { useAgentSupportThreadJoin } from "../../hooks/session/useAgentSupportThreadJoin";
|
|
13
14
|
import useAuthStore from "../../store/auth";
|
|
14
15
|
import useConversationStore from "../../store/conversation";
|
|
@@ -48,13 +49,14 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
|
|
|
48
49
|
const getSelfGroupPermissionByReq = useConversationStore((state) => state.getSelfGroupPermissionByReq);
|
|
49
50
|
const { mutate: joinPublicGroup, isPending: isJoiningPublicGroup } = useJoinPublicGroup();
|
|
50
51
|
const supportThreadJoin = useAgentSupportThreadJoin(conversationData, onJoinSupportThread);
|
|
52
|
+
const groupSendState = useGroupSendRestriction(conversationData);
|
|
51
53
|
const handleJoinPublicGroup = useCallback(() => {
|
|
52
54
|
const groupID = conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID;
|
|
53
55
|
if (!groupID)
|
|
54
56
|
return;
|
|
55
57
|
joinPublicGroup({ groupID }, {
|
|
56
58
|
onSuccess: async () => {
|
|
57
|
-
toastMessage.success(tCommon("
|
|
59
|
+
toastMessage.success(tCommon("public_group_joined"));
|
|
58
60
|
// updateCurrentConversation's revalidation only runs on a
|
|
59
61
|
// conversationID change — this is the same conversation the user
|
|
60
62
|
// was already viewing, so refresh membership/permissions directly
|
|
@@ -232,6 +234,17 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
|
|
|
232
234
|
useEffect(() => {
|
|
233
235
|
adjustTextareaHeight();
|
|
234
236
|
}, [value, adjustTextareaHeight]);
|
|
237
|
+
// The textarea mounts only once a spinner/notice branch gives way, after the
|
|
238
|
+
// draft was restored; size it again so a multi-line draft isn't one row tall.
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
requestAnimationFrame(adjustTextareaHeight);
|
|
241
|
+
}, [
|
|
242
|
+
groupSendState,
|
|
243
|
+
isViewOnlyMember,
|
|
244
|
+
supportThreadJoin.isCheckingMembership,
|
|
245
|
+
supportThreadJoin.needsJoin,
|
|
246
|
+
adjustTextareaHeight,
|
|
247
|
+
]);
|
|
235
248
|
useEffect(() => {
|
|
236
249
|
if (!isFocused)
|
|
237
250
|
return;
|
|
@@ -489,8 +502,8 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
|
|
|
489
502
|
event.preventDefault();
|
|
490
503
|
void handleSend();
|
|
491
504
|
};
|
|
492
|
-
// Don't flash a composer the agent may not be allowed to use.
|
|
493
|
-
if (supportThreadJoin.isCheckingMembership) {
|
|
505
|
+
// Don't flash a composer the agent or member may not be allowed to use.
|
|
506
|
+
if (supportThreadJoin.isCheckingMembership || groupSendState === "checking") {
|
|
494
507
|
return (_jsx("div", { "data-testid": "conversation-detail-input-checking", className: "flex w-full shrink-0 items-center justify-center bg-chat-bubble-neutral/[0.05] px-4 py-3", children: _jsx(Spin, { size: "small" }) }));
|
|
495
508
|
}
|
|
496
509
|
if (isViewOnlyMember || supportThreadJoin.needsJoin) {
|
|
@@ -500,6 +513,9 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
|
|
|
500
513
|
? supportThreadJoin.join
|
|
501
514
|
: handleJoinPublicGroup, children: tCommon("join_group") })] }));
|
|
502
515
|
}
|
|
516
|
+
if (groupSendState === "restricted") {
|
|
517
|
+
return (_jsx("div", { "data-testid": "conversation-detail-input-send-restricted", className: "flex w-full shrink-0 items-center justify-center bg-chat-bubble-neutral/[0.05] px-4 py-3", children: _jsx("span", { className: "text-sm text-gray-500", children: t("thread_detail.cannot_send_notice") }) }));
|
|
518
|
+
}
|
|
503
519
|
return (_jsxs("div", { ref: containerRef, "data-testid": "conversation-detail-input", "data-cp-name": "ConversationDetailInput", className: "flex w-full shrink-0 flex-col bg-chat-bubble-neutral/[0.05]", children: [_jsx("input", { ref: mediaInputRef, type: "file", accept: "image/jpeg,image/png,image/jpg,video/*", multiple: true, className: "hidden", onChange: handleMediaInputChange }), _jsx("input", { ref: documentInputRef, type: "file", accept: ".pdf,.doc,.docx", multiple: true, className: "hidden", onChange: handleDocumentInputChange }), _jsx(ReplyPreviewBar, {}), listUploadFiles.length > 0 && (_jsx(FilePreviewList, { files: listUploadFiles, onRemove: (file) => handleRemoveFile(file.uid), onVideoReady: (uid) => setPendingVideoUids((prev) => prev.filter((p) => p !== uid)) })), showLinkPreview && previewUrl && (_jsx(LinkInputPreview, { url: previewUrl, onRemove: () => setPreviewDismissed(true) })), _jsxs("div", { className: "flex items-end gap-3 px-3 py-2", children: [_jsx("div", { className: "flex shrink-0 items-center", children: _jsx("button", { type: "button", onClick: handleToggleMoreOption, "aria-label": t("thread_detail.more_option.toggle"), "aria-expanded": isMoreOptionOpen, "data-testid": "conversation-detail-more-option-toggle", className: "flex py-3 items-center justify-center rounded-full", children: isMoreOptionOpen ? (_jsx(Icon, { icon: "close-circle-b", size: 24, className: "text-[var(--dc-color-primary)]" })) : (_jsx(Icon, { icon: "plus-circle-o", size: 24, className: "text-gray-900" })) }) }), _jsx("div", { className: clsx("flex min-w-0 flex-1 overflow-hidden rounded-xl border bg-white transition-shadow", isFocused
|
|
504
520
|
? "border-[var(--dc-color-primary-focus)] shadow-[0px_0px_2px_var(--dc-color-primary-focus)]"
|
|
505
521
|
: "border-gray-50"), style: { minHeight: CONTAINER_MIN_HEIGHT }, children: _jsxs("div", { className: "flex min-w-0 flex-[1_0_0] items-center gap-2 self-stretch px-3 py-1", children: [onOpenBotMenu && (_jsx("button", { type: "button", onClick: onOpenBotMenu, "aria-label": t("thread_detail.bot.menu.open_menu"), "data-testid": "conversation-detail-open-bot-menu", className: "flex shrink-0 items-center justify-center", children: _jsx(Icon, { icon: "chat-suggest-o", size: 24, className: "text-gray-600" }) })), _jsx("textarea", { ref: textareaRef, value: value, rows: 1, onChange: (event) => {
|
|
@@ -1,10 +1,17 @@
|
|
|
1
|
+
export declare enum InboxTab {
|
|
2
|
+
Messages = "MESSAGES",
|
|
3
|
+
Community = "COMMUNITY"
|
|
4
|
+
}
|
|
1
5
|
export interface ConversationInboxHeaderProps {
|
|
2
6
|
onOpenSearch?: () => void;
|
|
3
7
|
/** Overrides the default "tabs.messages" i18n title (e.g. Mall buyer's fixed "Tin nhắn" label). */
|
|
4
8
|
title?: string;
|
|
5
9
|
/** Extra action rendered after the search icon — e.g. Mall seller's contact icon. */
|
|
6
10
|
rightSlot?: React.ReactNode;
|
|
11
|
+
/** With onChangeTab, shows the Tin nhắn | Cộng đồng tabs instead of the single title. */
|
|
12
|
+
activeTab?: InboxTab;
|
|
13
|
+
onChangeTab?: (tab: InboxTab) => void;
|
|
7
14
|
}
|
|
8
|
-
declare const ConversationInboxHeader: ({ onOpenSearch, title, rightSlot, }: ConversationInboxHeaderProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare const ConversationInboxHeader: ({ onOpenSearch, title, rightSlot, activeTab, onChangeTab, }: ConversationInboxHeaderProps) => import("react/jsx-runtime").JSX.Element;
|
|
9
16
|
export default ConversationInboxHeader;
|
|
10
17
|
//# sourceMappingURL=ConversationInboxHeader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ConversationInboxHeader.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-inbox/ConversationInboxHeader.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ConversationInboxHeader.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-inbox/ConversationInboxHeader.tsx"],"names":[],"mappings":"AAMA,oBAAY,QAAQ;IAClB,QAAQ,aAAa;IACrB,SAAS,cAAc;CACxB;AAOD,MAAM,WAAW,4BAA4B;IAC3C,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,mGAAmG;IACnG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,qFAAqF;IACrF,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC5B,yFAAyF;IACzF,SAAS,CAAC,EAAE,QAAQ,CAAC;IACrB,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,QAAQ,KAAK,IAAI,CAAC;CACvC;AA8CD,QAAA,MAAM,uBAAuB,GAAI,6DAM9B,4BAA4B,4CA0C9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import clsx from "clsx";
|
|
3
4
|
import { useTranslation } from "react-i18next";
|
|
4
5
|
import { Icon } from "../../components/icon";
|
|
5
|
-
|
|
6
|
+
export var InboxTab;
|
|
7
|
+
(function (InboxTab) {
|
|
8
|
+
InboxTab["Messages"] = "MESSAGES";
|
|
9
|
+
InboxTab["Community"] = "COMMUNITY";
|
|
10
|
+
})(InboxTab || (InboxTab = {}));
|
|
11
|
+
const INBOX_TABS = [
|
|
12
|
+
{ key: InboxTab.Messages, labelKey: "tabs.messages" },
|
|
13
|
+
{ key: InboxTab.Community, labelKey: "tabs.community" },
|
|
14
|
+
];
|
|
15
|
+
const HeaderTab = ({ label, active, testId, onClick }) => {
|
|
16
|
+
const content = (_jsxs(_Fragment, { children: [_jsx("span", { className: clsx("pb-1.5 text-center text-[16px] font-bold leading-[160%] tracking-[0.16px]", active ? "text-[var(--dc-color-primary)]" : "text-gray-400"), children: label }), _jsx("span", { className: clsx("h-[3px] w-full rounded-t", active ? "bg-[var(--dc-color-primary)]" : "bg-transparent") })] }));
|
|
17
|
+
const className = "flex h-full flex-col items-center justify-end px-3";
|
|
18
|
+
if (!onClick)
|
|
19
|
+
return _jsx("div", { className: className, children: content });
|
|
20
|
+
return (_jsx("button", { type: "button", role: "tab", "aria-selected": active, "data-testid": testId, className: clsx(className, "cursor-pointer"), onClick: onClick, children: content }));
|
|
21
|
+
};
|
|
22
|
+
const ConversationInboxHeader = ({ onOpenSearch, title, rightSlot, activeTab = InboxTab.Messages, onChangeTab, }) => {
|
|
6
23
|
const { t } = useTranslation("conversation-inbox");
|
|
7
|
-
return (_jsx("div", { "data-testid": "conversation-inbox-header", "data-cp-name": "ConversationInboxHeader", className: "flex w-full flex-col border-b border-gray-100 bg-white", children: _jsxs("div", { className: "flex h-12 items-end px-3", children: [
|
|
24
|
+
return (_jsx("div", { "data-testid": "conversation-inbox-header", "data-cp-name": "ConversationInboxHeader", className: "flex w-full flex-col border-b border-gray-100 bg-white", children: _jsxs("div", { className: "flex h-12 items-end px-3", children: [onChangeTab ? (_jsx("div", { role: "tablist", className: "flex h-full items-end", children: INBOX_TABS.map((tab) => (_jsx(HeaderTab, { label: t(tab.labelKey), active: activeTab === tab.key, testId: `conversation-inbox-tab-${tab.key.toLowerCase()}`, onClick: () => onChangeTab(tab.key) }, tab.key))) })) : (_jsx(HeaderTab, { label: title !== null && title !== void 0 ? title : t("tabs.messages"), active: true })), _jsxs("div", { className: "ml-auto mb-1.5 flex shrink-0 items-center gap-2", children: [onOpenSearch && (_jsx("button", { type: "button", "data-testid": "conversation-inbox-search", "aria-label": t("search_placeholder"), className: "flex shrink-0 cursor-pointer items-center justify-center p-1 text-[var(--dc-color-nav-icon)]", onClick: onOpenSearch, children: _jsx(Icon, { icon: "search-o", size: 24 }) })), rightSlot] })] }) }));
|
|
8
25
|
};
|
|
9
26
|
export default ConversationInboxHeader;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PublicGroupList.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-inbox/PublicGroupList.tsx"],"names":[],"mappings":"AAoBA,QAAA,MAAM,eAAe,+CAqFpB,CAAC;AAEF,eAAe,eAAe,CAAC"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Spin } from "antd";
|
|
4
|
+
import InfiniteScroll from "react-infinite-scroll-component";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
import { useChatContext } from "../../context/ChatContext";
|
|
7
|
+
import { useChatNavigate } from "../../hooks/conversation/useChatNavigate";
|
|
8
|
+
import { useSearchPublicGroups } from "../../hooks/search/useSearchPublicGroups";
|
|
9
|
+
import useConversationStore from "../../store/conversation";
|
|
10
|
+
import { ConnectStatus } from "../../types/chat";
|
|
11
|
+
import { ContactSearchStateMessage, PublicGroupResultItem, buildContactSearchParams, } from "./contact-search";
|
|
12
|
+
const SCROLL_TARGET_ID = "inbox-public-group-scroll";
|
|
13
|
+
// Inbox `Cộng đồng` tab: every public group the seller can see, joined or not,
|
|
14
|
+
// from the same searchPublicGroups call chat-mobile's CommunityTab uses.
|
|
15
|
+
const PublicGroupList = () => {
|
|
16
|
+
const { t } = useTranslation("conversation-inbox");
|
|
17
|
+
const onNavigate = useChatNavigate();
|
|
18
|
+
const setPendingResolveTarget = useConversationStore((state) => state.setPendingResolveTarget);
|
|
19
|
+
const { connectStatus } = useChatContext();
|
|
20
|
+
// The tab can be opened while the engine still boots; a call then fails at
|
|
21
|
+
// once and would leave the error state with nothing to retry it.
|
|
22
|
+
const { groups, fetchNextPage, hasNextPage, isFetchingNextPage, isPending, isError, } = useSearchPublicGroups({
|
|
23
|
+
searchTerm: "",
|
|
24
|
+
allowEmptyTerm: true,
|
|
25
|
+
enabled: connectStatus === ConnectStatus.Connected,
|
|
26
|
+
});
|
|
27
|
+
const handleOpenGroup = (groupID) => {
|
|
28
|
+
const target = { groupId: groupID };
|
|
29
|
+
setPendingResolveTarget(target);
|
|
30
|
+
onNavigate(`${window.location.pathname}?${buildContactSearchParams(target).toString()}`);
|
|
31
|
+
};
|
|
32
|
+
if (isPending) {
|
|
33
|
+
return (_jsx("div", { className: "flex flex-1 items-center justify-center py-12", children: _jsx(Spin, {}) }));
|
|
34
|
+
}
|
|
35
|
+
if (isError) {
|
|
36
|
+
return (_jsx(ContactSearchStateMessage, { testId: "inbox-public-group-failed", children: t("contact_search.failed") }));
|
|
37
|
+
}
|
|
38
|
+
if (groups.length === 0) {
|
|
39
|
+
return (_jsx(ContactSearchStateMessage, { testId: "inbox-public-group-empty", children: t("community.empty") }));
|
|
40
|
+
}
|
|
41
|
+
return (_jsx("div", { id: SCROLL_TARGET_ID, "data-testid": "inbox-public-group-list", "data-cp-name": "PublicGroupList", className: "flex-1 overflow-y-auto", children: _jsx(InfiniteScroll, { dataLength: groups.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: groups.map((group) => (_jsx(PublicGroupResultItem, { group: group, onOpen: handleOpenGroup, stayOnJoin: true }, group.groupID))) }) }));
|
|
42
|
+
};
|
|
43
|
+
export default PublicGroupList;
|
|
@@ -2,7 +2,9 @@ import type { SearchPublicGroupInfo } from "@droppii-org/wasm-client-sdk";
|
|
|
2
2
|
interface PublicGroupResultItemProps {
|
|
3
3
|
group: SearchPublicGroupInfo;
|
|
4
4
|
onOpen: (groupID: string) => void;
|
|
5
|
+
/** Inbox `Cộng đồng` tab: a join keeps the seller on the list and toasts instead of opening the thread. */
|
|
6
|
+
stayOnJoin?: boolean;
|
|
5
7
|
}
|
|
6
|
-
declare const PublicGroupResultItem: ({ group, onOpen, }: PublicGroupResultItemProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
declare const PublicGroupResultItem: ({ group, onOpen, stayOnJoin, }: PublicGroupResultItemProps) => import("react/jsx-runtime").JSX.Element;
|
|
7
9
|
export default PublicGroupResultItem;
|
|
8
10
|
//# sourceMappingURL=PublicGroupResultItem.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PublicGroupResultItem.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/PublicGroupResultItem.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAK1E,UAAU,0BAA0B;IAClC,KAAK,EAAE,qBAAqB,CAAC;IAC7B,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"PublicGroupResultItem.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-inbox/contact-search/PublicGroupResultItem.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAK1E,UAAU,0BAA0B;IAClC,KAAK,EAAE,qBAAqB,CAAC;IAC7B,MAAM,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,2GAA2G;IAC3G,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,QAAA,MAAM,qBAAqB,GAAI,gCAI5B,0BAA0B,4CAyF5B,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
|
|
@@ -1,22 +1,30 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import clsx from "clsx";
|
|
4
|
-
import { Spin } from "antd";
|
|
4
|
+
import { Spin, message as toastMessage } from "antd";
|
|
5
5
|
import { useTranslation } from "react-i18next";
|
|
6
6
|
import { useJoinPublicGroup } from "../../../hooks/group/useJoinPublicGroup";
|
|
7
7
|
import GroupAvatar from "../../../components/group/GroupAvatar";
|
|
8
8
|
import { CONTACT_SEARCH_AVATAR_SIZE } from "./constants";
|
|
9
|
-
const PublicGroupResultItem = ({ group, onOpen, }) => {
|
|
9
|
+
const PublicGroupResultItem = ({ group, onOpen, stayOnJoin = false, }) => {
|
|
10
10
|
var _a, _b;
|
|
11
11
|
const { t } = useTranslation("conversation-inbox");
|
|
12
|
+
const { t: tCommon } = useTranslation("common");
|
|
12
13
|
const { mutate: joinGroup, isPending } = useJoinPublicGroup();
|
|
13
|
-
// Joined groups open straight away;
|
|
14
|
+
// Joined groups open straight away; search joins first, then opens.
|
|
14
15
|
const handleAction = () => {
|
|
15
16
|
if (group.isJoined) {
|
|
16
17
|
onOpen(group.groupID);
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
|
-
|
|
20
|
+
if (!stayOnJoin) {
|
|
21
|
+
joinGroup({ groupID: group.groupID }, { onSuccess: () => onOpen(group.groupID) });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
joinGroup({ groupID: group.groupID }, {
|
|
25
|
+
onSuccess: () => toastMessage.success(tCommon("public_group_joined")),
|
|
26
|
+
onError: () => toastMessage.error(tCommon("join_group_failed")),
|
|
27
|
+
});
|
|
20
28
|
};
|
|
21
29
|
return (_jsxs("div", { "data-cp-name": "PublicGroupResultItem", "data-testid": `contact-search-public-group-${group.groupID}`, className: "flex items-center gap-2 border-b border-gray-25 bg-white p-3", children: [_jsxs("button", { type: "button", className: "flex min-w-0 flex-1 items-center gap-2 text-left", onClick: () => onOpen(group.groupID), children: [_jsx(GroupAvatar, { faceURL: group.faceURL, groupName: group.groupName, size: CONTACT_SEARCH_AVATAR_SIZE, className: "shrink-0" }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-1 pb-0.5", children: [_jsx("span", { className: "truncate text-[16px] font-medium leading-[1.6] text-black", children: group.groupName }), _jsxs("span", { className: "flex min-w-0 flex-col text-[14px] leading-[1.6] tracking-[0.14px] text-gray-600", children: [_jsx("span", { className: "truncate", children: t("contact_search.member_count", {
|
|
22
30
|
count: (_a = group.memberCount) !== null && _a !== void 0 ? _a : 0,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AAkDA,eAAO,MAAM,cAAc,YA4nB1B,CAAC"}
|
|
@@ -20,6 +20,7 @@ import { useQueryClient } from "@tanstack/react-query";
|
|
|
20
20
|
import { PeerType } from "../../types/conversation";
|
|
21
21
|
import { ignoreInternalLatestMessage } from "../../utils/common";
|
|
22
22
|
import { invalidateSessionQueries } from "../../utils/queryHelpers";
|
|
23
|
+
import { refreshPublicGroupLists } from "../../hooks/group/useJoinPublicGroup";
|
|
23
24
|
import emitter from "../../utils/events";
|
|
24
25
|
const notPushType = [MessageType.TypingMessage, MessageType.RevokeMessage];
|
|
25
26
|
export const useGlobalEvent = () => {
|
|
@@ -381,6 +382,8 @@ export const useGlobalEvent = () => {
|
|
|
381
382
|
conversationList: state.conversationList.filter((conversation) => conversation.groupID !== data.groupID),
|
|
382
383
|
}));
|
|
383
384
|
}
|
|
385
|
+
// Public-group cards turn back to Tham gia (or drop out when disbanded).
|
|
386
|
+
void refreshPublicGroupLists(queryClient, data.groupID);
|
|
384
387
|
};
|
|
385
388
|
// Thin wrappers matching the SDK's fixed callback signature, forwarding
|
|
386
389
|
// to leftGroupHandler with the right reason.
|
|
@@ -434,6 +437,8 @@ export const useGlobalEvent = () => {
|
|
|
434
437
|
void queryClient.invalidateQueries({
|
|
435
438
|
queryKey: [QUERY_KEYS.IS_JOINED_GROUP, data.groupID],
|
|
436
439
|
});
|
|
440
|
+
// A first-time join from another device or an invite flips the card to Truy cập.
|
|
441
|
+
void refreshPublicGroupLists(queryClient, data.groupID);
|
|
437
442
|
};
|
|
438
443
|
const setIMListener = () => {
|
|
439
444
|
//account
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ConversationItem } from "@droppii-org/wasm-client-sdk";
|
|
2
|
+
export type GroupSendState = "checking" | "restricted" | "allowed";
|
|
3
|
+
export declare const useGroupSendRestriction: (conversation?: ConversationItem | null) => GroupSendState;
|
|
4
|
+
//# sourceMappingURL=useGroupSendRestriction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useGroupSendRestriction.d.ts","sourceRoot":"","sources":["../../../src/hooks/group/useGroupSendRestriction.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,8BAA8B,CAAC;AAKtC,MAAM,MAAM,cAAc,GAAG,UAAU,GAAG,YAAY,GAAG,SAAS,CAAC;AAMnE,eAAO,MAAM,uBAAuB,GAClC,eAAe,gBAAgB,GAAG,IAAI,KACrC,cA4BF,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { GroupMemberRole, GroupStatus, } from "@droppii-org/wasm-client-sdk";
|
|
3
|
+
import useConversationStore from "../../store/conversation";
|
|
4
|
+
import { PeerType } from "../../types/conversation";
|
|
5
|
+
import { useGroupPermission } from "./useGroupPermission";
|
|
6
|
+
// A confirmed member is restricted with no SEND_MESSAGE permission, a muted
|
|
7
|
+
// group (owner/admin exempt) or an own mute. "checking" lasts until the store
|
|
8
|
+
// finishes this thread's membership check (it sets currentConversation last),
|
|
9
|
+
// so a restricted member never sees the composer flash first.
|
|
10
|
+
export const useGroupSendRestriction = (conversation) => {
|
|
11
|
+
var _a;
|
|
12
|
+
const isConfirmedGroup = (conversation === null || conversation === void 0 ? void 0 : conversation.peerType) === PeerType.GROUP;
|
|
13
|
+
const groupID = conversation === null || conversation === void 0 ? void 0 : conversation.groupID;
|
|
14
|
+
const isChecked = useConversationStore((state) => { var _a; return ((_a = state.currentConversation) === null || _a === void 0 ? void 0 : _a.conversationID) === (conversation === null || conversation === void 0 ? void 0 : conversation.conversationID); });
|
|
15
|
+
const groupInfo = useConversationStore((state) => groupID ? state.currentGroupInfo.get(groupID) : undefined);
|
|
16
|
+
const member = useConversationStore((state) => groupID ? state.currentMemberInGroup.get(groupID) : undefined);
|
|
17
|
+
const permissions = useGroupPermission(groupID);
|
|
18
|
+
if (!isConfirmedGroup)
|
|
19
|
+
return "allowed";
|
|
20
|
+
if (!isChecked)
|
|
21
|
+
return "checking";
|
|
22
|
+
// Non-members are handled by the view-only footer, not this notice.
|
|
23
|
+
if (!member)
|
|
24
|
+
return "allowed";
|
|
25
|
+
const isGroupMuted = (groupInfo === null || groupInfo === void 0 ? void 0 : groupInfo.status) === GroupStatus.Muted &&
|
|
26
|
+
member.roleLevel === GroupMemberRole.Normal;
|
|
27
|
+
const isMemberMuted = ((_a = member.muteEndTime) !== null && _a !== void 0 ? _a : 0) > Date.now();
|
|
28
|
+
return !permissions.sendMessage || isGroupMuted || isMemberMuted
|
|
29
|
+
? "restricted"
|
|
30
|
+
: "allowed";
|
|
31
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { type QueryClient } from "@tanstack/react-query";
|
|
1
2
|
interface JoinPublicGroupInput {
|
|
2
3
|
groupID: string;
|
|
3
4
|
}
|
|
5
|
+
export declare const refreshPublicGroupLists: (queryClient: QueryClient, groupID: string) => Promise<void>;
|
|
4
6
|
export declare const useJoinPublicGroup: () => import("@tanstack/react-query").UseMutationResult<unknown, Error, JoinPublicGroupInput, unknown>;
|
|
5
7
|
export {};
|
|
6
8
|
//# sourceMappingURL=useJoinPublicGroup.d.ts.map
|
|
@@ -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",
|
|
@@ -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
|
: {
|
package/dist/utils/messageLog.js
CHANGED
|
@@ -142,7 +142,7 @@ export const getGroupNotificationText = (contentType, detailJson, t, senderNickn
|
|
|
142
142
|
? String(t(key, { name: entrantName }))
|
|
143
143
|
: joinedViaLink
|
|
144
144
|
? `${entrantName} đã tham gia nhóm qua link`
|
|
145
|
-
: `${entrantName} đã
|
|
145
|
+
: `${entrantName} đã tham gia nhóm`;
|
|
146
146
|
}
|
|
147
147
|
case MessageType.GroupDismissed:
|
|
148
148
|
return t
|