@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":"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 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThreadProfilePanel.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ThreadProfilePanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;
|
|
1
|
+
{"version":3,"file":"ThreadProfilePanel.d.ts","sourceRoot":"","sources":["../../../src/components/conversation-detail/ThreadProfilePanel.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wCAAwC,CAAC;AAe7E,UAAU,uBAAuB;IAC/B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,gBAAgB,EAAE,CAAC,MAAM,EAAE,mBAAmB,KAAK,IAAI,CAAC;IACxD,iEAAiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,MAAM,IAAI,CAAC;IACtB,mFAAmF;IACnF,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAID,QAAA,MAAM,kBAAkB,GAAI,uFAQzB,uBAAuB,4CAwKzB,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
|
|
@@ -15,6 +15,7 @@ import LeaveGroupBlockedModal from "./LeaveGroupBlockedModal";
|
|
|
15
15
|
import LeaveGroupConfirmModal from "./LeaveGroupConfirmModal";
|
|
16
16
|
import ThreadProfileQuickActions from "./profile/ThreadProfileQuickActions";
|
|
17
17
|
import ThreadProfileActionRows from "./profile/ThreadProfileActionRows";
|
|
18
|
+
import GroupDescriptionCard from "./profile/GroupDescriptionCard";
|
|
18
19
|
const PROFILE_AVATAR_SIZE = 96;
|
|
19
20
|
const PERSON_PEER_TYPES = [PeerType.USER, PeerType.CUSTOMER];
|
|
20
21
|
const ThreadProfilePanel = ({ open, conversation, onClose, onSelectResource, showActions = false, onSearch, hideSearch = false, }) => {
|
|
@@ -23,6 +24,12 @@ const ThreadProfilePanel = ({ open, conversation, onClose, onSelectResource, sho
|
|
|
23
24
|
const { displayName, avatarUrl, peerType, badgeLabel } = useConversationPeerIdentity(conversation);
|
|
24
25
|
const isGroupThread = conversation.peerType === PeerType.GROUP;
|
|
25
26
|
const isViewOnly = useConversationStore((state) => isGroupThread && state.isViewOnlyMember);
|
|
27
|
+
const groupDescription = useConversationStore((state) => {
|
|
28
|
+
var _a, _b, _c;
|
|
29
|
+
return isGroupThread && conversation.groupID
|
|
30
|
+
? (_c = (_b = (_a = state.currentGroupInfo.get(conversation.groupID)) === null || _a === void 0 ? void 0 : _a.introduction) === null || _b === void 0 ? void 0 : _b.trim()) !== null && _c !== void 0 ? _c : ""
|
|
31
|
+
: "";
|
|
32
|
+
});
|
|
26
33
|
const showPersonRows = PERSON_PEER_TYPES.includes((_a = conversation.peerType) !== null && _a !== void 0 ? _a : "");
|
|
27
34
|
const { isTargetActive } = useConversationTarget(conversation.conversationID, open && showActions && showPersonRows);
|
|
28
35
|
const { canLeaveGroup, isPublicGroup, isLeaving, isConfirmOpen, isBlockedOpen, openLeaveGroup, confirmLeaveGroup, closeConfirm, closeBlocked, } = useLeaveGroupAction({
|
|
@@ -31,8 +38,10 @@ const ThreadProfilePanel = ({ open, conversation, onClose, onSelectResource, sho
|
|
|
31
38
|
isGroupThread,
|
|
32
39
|
onLeft: onClose,
|
|
33
40
|
});
|
|
34
|
-
return (_jsxs(Drawer, { open: open, onClose: onClose, placement: "right", closeIcon: false, width: "100%",
|
|
41
|
+
return (_jsxs(Drawer, { open: open, onClose: onClose, placement: "right", closeIcon: false, width: "100%",
|
|
42
|
+
// antd slides a drawer 180px aside when one opens inside it ("Mô tả nhóm").
|
|
43
|
+
push: false, "data-cp-name": "ThreadProfilePanel", styles: {
|
|
35
44
|
body: { padding: 0 },
|
|
36
|
-
}, getContainer: false, forceRender: true, destroyOnHidden: true, children: [_jsxs("div", { className: clsx("flex h-full flex-col bg-gray-25", showActions && "overflow-y-auto"), children: [_jsxs("div", { className: clsx("flex w-full items-center bg-white p-2", showActions && "sticky top-0 z-10 shrink-0"), children: [_jsx("button", { type: "button", "data-testid": "thread-profile-back", className: "flex shrink-0 cursor-pointer items-center justify-center p-2 text-[var(--dc-color-nav-icon)]", onClick: onClose, children: _jsx(Icon, { icon: "arrow-left-o", size: 24 }) }), _jsx("p", { className: "min-w-0 flex-1 truncate px-2 text-[18px] font-bold leading-[1.6] text-gray-900", children: t("thread_detail.options_title") })] }), _jsxs("div", { className: "flex flex-col items-center gap-2 bg-white px-0 py-2", children: [_jsxs("div", { className: "relative h-24 w-24 shrink-0", children: [_jsx(Avatar, { size: PROFILE_AVATAR_SIZE, src: avatarUrl || undefined, alt: displayName, className: "uppercase", children: (displayName === null || displayName === void 0 ? void 0 : displayName.charAt(0)) || "A" }), badgeLabel && (_jsx("span", { className: clsx("absolute bottom-1 right-0 flex h-[17px] items-center justify-center rounded-md border border-white px-1 text-[10px] font-semibold leading-3 text-white shadow-badge-inset", resolvePeerBadgeColor(peerType)), children: badgeLabel }))] }), _jsx("p", { className: "w-full
|
|
45
|
+
}, getContainer: false, forceRender: true, destroyOnHidden: true, children: [_jsxs("div", { className: clsx("flex h-full flex-col bg-gray-25", showActions && "overflow-y-auto"), children: [_jsxs("div", { className: clsx("flex w-full items-center bg-white p-2", showActions && "sticky top-0 z-10 shrink-0"), children: [_jsx("button", { type: "button", "data-testid": "thread-profile-back", className: "flex shrink-0 cursor-pointer items-center justify-center p-2 text-[var(--dc-color-nav-icon)]", onClick: onClose, children: _jsx(Icon, { icon: "arrow-left-o", size: 24 }) }), _jsx("p", { className: "min-w-0 flex-1 truncate px-2 text-[18px] font-bold leading-[1.6] text-gray-900", children: t("thread_detail.options_title") })] }), _jsxs("div", { className: "flex flex-col items-center gap-2 bg-white px-0 py-2", children: [_jsxs("div", { className: "relative h-24 w-24 shrink-0", children: [_jsx(Avatar, { size: PROFILE_AVATAR_SIZE, src: avatarUrl || undefined, alt: displayName, className: "uppercase", children: (displayName === null || displayName === void 0 ? void 0 : displayName.charAt(0)) || "A" }), badgeLabel && (_jsx("span", { className: clsx("absolute bottom-1 right-0 flex h-[17px] items-center justify-center rounded-md border border-white px-1 text-[10px] font-semibold leading-3 text-white shadow-badge-inset", resolvePeerBadgeColor(peerType)), children: badgeLabel }))] }), _jsx("p", { className: clsx("w-full text-center text-[18px] font-bold leading-[1.6] text-gray-900", showActions && isGroupThread ? "line-clamp-2 break-words px-3" : "truncate"), children: displayName }), showActions && (_jsx(ThreadProfileQuickActions, { conversation: conversation, isViewOnly: isViewOnly, onSearch: onSearch, hideSearch: hideSearch }, conversation.conversationID))] }), _jsx("div", { className: "flex flex-col px-3 py-1.5", children: _jsx(MediaEntryCard, { onSelect: onSelectResource }) }), showActions && groupDescription && (_jsx(GroupDescriptionCard, { groupName: displayName, description: groupDescription }, `description-${conversation.conversationID}`)), showActions && (_jsx(ThreadProfileActionRows, { conversation: conversation, displayName: displayName, showPersonRows: showPersonRows, isTargetActive: isTargetActive, isViewOnly: isViewOnly, isGroupThread: isGroupThread, canLeaveGroup: canLeaveGroup, onLeaveGroup: openLeaveGroup }, conversation.conversationID)), !showActions && canLeaveGroup && (_jsx("div", { className: "flex flex-col px-3 py-1.5", children: _jsx("div", { className: "overflow-hidden rounded-xl bg-white", children: _jsxs("button", { type: "button", "data-testid": "thread-profile-leave-group", className: "flex w-full cursor-pointer items-center gap-2 p-4 text-danger-400", onClick: openLeaveGroup, children: [_jsx(Icon, { icon: "logout-o", size: 24 }), _jsx("span", { className: "text-[16px] font-medium leading-[1.6]", children: t("thread_detail.leave_group.action") })] }) }) }))] }), _jsx(LeaveGroupConfirmModal, { open: isConfirmOpen, loading: isLeaving, isPublicGroup: isPublicGroup, onCancel: closeConfirm, onConfirm: confirmLeaveGroup }), _jsx(LeaveGroupBlockedModal, { open: isBlockedOpen, onClose: closeBlocked })] }));
|
|
37
46
|
};
|
|
38
47
|
export default ThreadProfilePanel;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
interface GroupDescriptionCardProps {
|
|
2
|
+
groupName: string;
|
|
3
|
+
description: string;
|
|
4
|
+
}
|
|
5
|
+
declare const GroupDescriptionCard: ({ groupName, description, }: GroupDescriptionCardProps) => import("react/jsx-runtime").JSX.Element;
|
|
6
|
+
export default GroupDescriptionCard;
|
|
7
|
+
//# sourceMappingURL=GroupDescriptionCard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GroupDescriptionCard.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/profile/GroupDescriptionCard.tsx"],"names":[],"mappings":"AA6BA,UAAU,yBAAyB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,QAAA,MAAM,oBAAoB,GAAI,6BAG3B,yBAAyB,4CAkE3B,CAAC;AAEF,eAAe,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import clsx from "clsx";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
import GroupDescriptionPanel from "./GroupDescriptionPanel";
|
|
7
|
+
const LINE_HEIGHT_PX = 25.6;
|
|
8
|
+
const MAX_LINES = 2;
|
|
9
|
+
const CLAMPED_HEIGHT_PX = MAX_LINES * LINE_HEIGHT_PX;
|
|
10
|
+
// Same clamp as CollapsibleText: fade the end of the last line under
|
|
11
|
+
// "…Xem thêm" without touching layout, so it never feeds the overflow check.
|
|
12
|
+
const LAST_LINE_FADE = "linear-gradient(#000, #000), linear-gradient(to right, #000 calc(100% - 7em), transparent calc(100% - 5.5em))";
|
|
13
|
+
const LAST_LINE_FADE_SIZE = `100% calc(100% - ${LINE_HEIGHT_PX}px), 100% ${LINE_HEIGHT_PX}px`;
|
|
14
|
+
const CLAMPED_STYLE = {
|
|
15
|
+
maxHeight: CLAMPED_HEIGHT_PX,
|
|
16
|
+
overflow: "clip",
|
|
17
|
+
maskImage: LAST_LINE_FADE,
|
|
18
|
+
maskSize: LAST_LINE_FADE_SIZE,
|
|
19
|
+
maskPosition: "top, bottom",
|
|
20
|
+
maskRepeat: "no-repeat",
|
|
21
|
+
WebkitMaskImage: LAST_LINE_FADE,
|
|
22
|
+
WebkitMaskSize: LAST_LINE_FADE_SIZE,
|
|
23
|
+
WebkitMaskPosition: "top, bottom",
|
|
24
|
+
WebkitMaskRepeat: "no-repeat",
|
|
25
|
+
};
|
|
26
|
+
const GroupDescriptionCard = ({ groupName, description, }) => {
|
|
27
|
+
const { t } = useTranslation("conversation-inbox");
|
|
28
|
+
const contentRef = useRef(null);
|
|
29
|
+
const [isOverflowing, setIsOverflowing] = useState(false);
|
|
30
|
+
const [isPanelOpen, setIsPanelOpen] = useState(false);
|
|
31
|
+
useLayoutEffect(() => {
|
|
32
|
+
const content = contentRef.current;
|
|
33
|
+
if (!content)
|
|
34
|
+
return;
|
|
35
|
+
// Half-line slack: subpixel line boxes can push an exact 2-line block past 51.2px.
|
|
36
|
+
const measure = () => setIsOverflowing(content.getBoundingClientRect().height >
|
|
37
|
+
CLAMPED_HEIGHT_PX + LINE_HEIGHT_PX / 2);
|
|
38
|
+
measure();
|
|
39
|
+
if (typeof ResizeObserver === "undefined")
|
|
40
|
+
return;
|
|
41
|
+
const observer = new ResizeObserver(measure);
|
|
42
|
+
observer.observe(content);
|
|
43
|
+
return () => observer.disconnect();
|
|
44
|
+
}, []);
|
|
45
|
+
return (_jsxs(_Fragment, { children: [_jsx("div", { className: "flex flex-col px-3 py-1.5", children: _jsxs("div", { "data-testid": "group-description-card", className: "relative rounded-xl bg-white p-3", children: [_jsx("div", { className: clsx(isOverflowing && "overflow-hidden"), style: isOverflowing ? CLAMPED_STYLE : undefined, children: _jsx("p", { ref: contentRef, "data-testid": "group-description-text", className: "whitespace-pre-line break-words text-[16px] font-medium leading-[1.6] text-gray-900", children: description }) }), isOverflowing && (_jsxs("button", { type: "button", "data-testid": "group-description-see-more", className: "absolute bottom-3 right-3 cursor-pointer whitespace-nowrap rounded text-[16px] font-medium leading-[1.6] text-gray-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-[var(--dc-color-link)]", onClick: () => setIsPanelOpen(true), children: [_jsx("span", { "aria-hidden": true, children: "\u2026" }), _jsx("span", { className: "text-[var(--dc-color-link)]", children: t("see_more", { ns: "common" }) })] }))] }) }), _jsx(GroupDescriptionPanel, { open: isPanelOpen, groupName: groupName, description: description, onClose: () => setIsPanelOpen(false) })] }));
|
|
46
|
+
};
|
|
47
|
+
export default GroupDescriptionCard;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
interface GroupDescriptionPanelProps {
|
|
2
|
+
open: boolean;
|
|
3
|
+
groupName: string;
|
|
4
|
+
description: string;
|
|
5
|
+
onClose: () => void;
|
|
6
|
+
}
|
|
7
|
+
declare const GroupDescriptionPanel: ({ open, groupName, description, onClose, }: GroupDescriptionPanelProps) => import("react/jsx-runtime").JSX.Element;
|
|
8
|
+
export default GroupDescriptionPanel;
|
|
9
|
+
//# sourceMappingURL=GroupDescriptionPanel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"GroupDescriptionPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/profile/GroupDescriptionPanel.tsx"],"names":[],"mappings":"AAMA,UAAU,0BAA0B;IAClC,IAAI,EAAE,OAAO,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED,QAAA,MAAM,qBAAqB,GAAI,4CAK5B,0BAA0B,4CAiD5B,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Drawer } from "antd";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { Icon } from "../../../components/icon";
|
|
6
|
+
const GroupDescriptionPanel = ({ open, groupName, description, onClose, }) => {
|
|
7
|
+
const { t } = useTranslation("conversation-inbox");
|
|
8
|
+
const title = t("thread_detail.profile.group_description_title");
|
|
9
|
+
return (_jsx(Drawer, { open: open, onClose: onClose, placement: "right", closeIcon: false, width: "100%", "aria-label": title, "data-cp-name": "GroupDescriptionPanel", styles: { body: { padding: 0 } }, getContainer: false, destroyOnHidden: true, children: _jsxs("div", { className: "flex h-full flex-col bg-white", children: [_jsxs("div", { className: "flex w-full shrink-0 items-center border-b border-gray-25 bg-white p-2", children: [_jsx("button", { type: "button", "data-testid": "group-description-close", "aria-label": t("thread_detail.profile.group_description_close"), className: "flex shrink-0 cursor-pointer items-center justify-center p-2 text-[var(--dc-color-nav-icon)]", onClick: onClose, children: _jsx(Icon, { icon: "close-o", size: 24 }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col px-2", children: [_jsx("p", { className: "truncate text-[18px] font-bold leading-[1.6] text-gray-900", children: title }), _jsx("p", { "data-testid": "group-description-group-name", className: "truncate text-[13px] font-medium leading-[1.6] text-gray-900", children: groupName })] })] }), _jsx("p", { "data-testid": "group-description-content", className: "min-h-0 flex-1 overflow-y-auto whitespace-pre-line break-words px-4 pb-4 pt-3 text-[16px] leading-[1.6] text-gray-900", children: description })] }) }));
|
|
10
|
+
};
|
|
11
|
+
export default GroupDescriptionPanel;
|
|
@@ -7,9 +7,11 @@ export interface ThreadProfileActionRowsProps {
|
|
|
7
7
|
isTargetActive: boolean;
|
|
8
8
|
/** A non-member viewing a public group: no pin, delete or leave. */
|
|
9
9
|
isViewOnly: boolean;
|
|
10
|
+
/** Group info drops delete history (spec: out of scope for groups). */
|
|
11
|
+
isGroupThread: boolean;
|
|
10
12
|
canLeaveGroup: boolean;
|
|
11
13
|
onLeaveGroup: () => void;
|
|
12
14
|
}
|
|
13
|
-
declare const ThreadProfileActionRows: ({ conversation, displayName, showPersonRows, isTargetActive, isViewOnly, canLeaveGroup, onLeaveGroup, }: ThreadProfileActionRowsProps) => import("react/jsx-runtime").JSX.Element;
|
|
15
|
+
declare const ThreadProfileActionRows: ({ conversation, displayName, showPersonRows, isTargetActive, isViewOnly, isGroupThread, canLeaveGroup, onLeaveGroup, }: ThreadProfileActionRowsProps) => import("react/jsx-runtime").JSX.Element;
|
|
14
16
|
export default ThreadProfileActionRows;
|
|
15
17
|
//# sourceMappingURL=ThreadProfileActionRows.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThreadProfileActionRows.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/profile/ThreadProfileActionRows.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAiDrE,MAAM,WAAW,4BAA4B;IAC3C,YAAY,EAAE,gBAAgB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,OAAO,CAAC;IACxB,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAC;IACpB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AAID,QAAA,MAAM,uBAAuB,GAAI,
|
|
1
|
+
{"version":3,"file":"ThreadProfileActionRows.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/profile/ThreadProfileActionRows.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAiDrE,MAAM,WAAW,4BAA4B;IAC3C,YAAY,EAAE,gBAAgB,CAAC;IAC/B,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,cAAc,EAAE,OAAO,CAAC;IACxB,cAAc,EAAE,OAAO,CAAC;IACxB,oEAAoE;IACpE,UAAU,EAAE,OAAO,CAAC;IACpB,uEAAuE;IACvE,aAAa,EAAE,OAAO,CAAC;IACvB,aAAa,EAAE,OAAO,CAAC;IACvB,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B;AAID,QAAA,MAAM,uBAAuB,GAAI,wHAS9B,4BAA4B,4CAsE9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
|
|
@@ -9,12 +9,13 @@ const ProfileActionRow = ({ icon, label, testId, danger, chevron, onClick, }) =>
|
|
|
9
9
|
const Card = ({ children }) => (_jsx("div", { className: "flex flex-col px-3 py-1.5", children: _jsx("div", { className: "overflow-hidden rounded-xl bg-white", children: children }) }));
|
|
10
10
|
// Orders, create group, add to group and delete history are UI-only until
|
|
11
11
|
// their own tickets land (spec: "UI ready requirement").
|
|
12
|
-
const ThreadProfileActionRows = ({ conversation, displayName, showPersonRows, isTargetActive, isViewOnly, canLeaveGroup, onLeaveGroup, }) => {
|
|
12
|
+
const ThreadProfileActionRows = ({ conversation, displayName, showPersonRows, isTargetActive, isViewOnly, isGroupThread, canLeaveGroup, onLeaveGroup, }) => {
|
|
13
13
|
const { t } = useTranslation("conversation-inbox");
|
|
14
14
|
const { togglePin, isLimitOpen, closeLimit } = usePinConversation();
|
|
15
15
|
const isPinned = !!conversation.isPinned;
|
|
16
16
|
const showGroupRows = showPersonRows && isTargetActive;
|
|
17
17
|
const canEditThread = !isViewOnly;
|
|
18
|
-
|
|
18
|
+
const showDeleteHistory = canEditThread && !isGroupThread;
|
|
19
|
+
return (_jsxs(_Fragment, { children: [(showPersonRows || canEditThread) && (_jsxs(Card, { children: [showPersonRows && (_jsx(ProfileActionRow, { icon: "receipt-o", label: t("thread_detail.profile.orders", { name: displayName }), testId: "thread-profile-orders", chevron: true })), showGroupRows && (_jsx(ProfileActionRow, { icon: "user-three-o", label: t("thread_detail.profile.create_group", { name: displayName }), testId: "thread-profile-create-group", chevron: true })), showGroupRows && (_jsx(ProfileActionRow, { icon: "user-add-o", label: t("thread_detail.profile.add_to_group"), testId: "thread-profile-add-to-group", chevron: true })), canEditThread && (_jsx(ProfileActionRow, { icon: isPinned ? "thumbtack-off-o" : "thumbtack-o", label: t(isPinned ? "thread_detail.profile.unpin" : "thread_detail.profile.pin"), testId: "thread-profile-pin", onClick: () => togglePin(conversation) }))] })), (canLeaveGroup || showDeleteHistory) && (_jsxs(Card, { children: [canLeaveGroup && (_jsx(ProfileActionRow, { icon: "logout-o", label: t("thread_detail.leave_group.action"), testId: "thread-profile-leave-group", danger: true, onClick: onLeaveGroup })), showDeleteHistory && (_jsx(ProfileActionRow, { icon: "trash-alt-o", label: t("thread_detail.profile.delete_history"), testId: "thread-profile-delete-history", danger: true }))] })), _jsx(PinThreadLimitModal, { open: isLimitOpen, onClose: closeLimit })] }));
|
|
19
20
|
};
|
|
20
21
|
export default ThreadProfileActionRows;
|
|
@@ -5,9 +5,11 @@ export interface ThreadSearchContentProps {
|
|
|
5
5
|
conversation: ConversationItem;
|
|
6
6
|
inputRef: RefObject<InputRef>;
|
|
7
7
|
onClose: () => void;
|
|
8
|
+
/** Runs when a hit is picked, before the panel closes. */
|
|
9
|
+
onJump?: () => void;
|
|
8
10
|
/** Host toast for a hit that was revoked meanwhile; the SDK toast shows otherwise. */
|
|
9
11
|
onSearchMessageRevoked?: () => void;
|
|
10
12
|
}
|
|
11
|
-
declare const ThreadSearchContent: ({ conversation, inputRef, onClose, onSearchMessageRevoked, }: ThreadSearchContentProps) => import("react/jsx-runtime").JSX.Element;
|
|
13
|
+
declare const ThreadSearchContent: ({ conversation, inputRef, onClose, onJump, onSearchMessageRevoked, }: ThreadSearchContentProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
14
|
export default ThreadSearchContent;
|
|
13
15
|
//# sourceMappingURL=ThreadSearchContent.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThreadSearchContent.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/thread-search/ThreadSearchContent.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAwC,KAAK,QAAQ,EAAE,MAAM,MAAM,CAAC;AAI3E,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,8BAA8B,CAAC;AAYtC,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,sFAAsF;IACtF,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAGD,QAAA,MAAM,mBAAmB,GAAI,
|
|
1
|
+
{"version":3,"file":"ThreadSearchContent.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/thread-search/ThreadSearchContent.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAY,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AACjD,OAAO,EAAwC,KAAK,QAAQ,EAAE,MAAM,MAAM,CAAC;AAI3E,OAAO,EAEL,KAAK,gBAAgB,EAEtB,MAAM,8BAA8B,CAAC;AAYtC,MAAM,WAAW,wBAAwB;IACvC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,QAAQ,EAAE,SAAS,CAAC,QAAQ,CAAC,CAAC;IAC9B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,sFAAsF;IACtF,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAGD,QAAA,MAAM,mBAAmB,GAAI,sEAM1B,wBAAwB,4CAqJ1B,CAAC;AAEF,eAAe,mBAAmB,CAAC"}
|
|
@@ -16,7 +16,7 @@ import ThreadSearchResultItem from "./ThreadSearchResultItem";
|
|
|
16
16
|
import { THREAD_SEARCH_DEBOUNCE_MS } from "./utils";
|
|
17
17
|
const SCROLL_TARGET_ID = "thread-search-results";
|
|
18
18
|
// Lives inside the Drawer, which destroys it on close: every open starts from an empty keyword.
|
|
19
|
-
const ThreadSearchContent = ({ conversation, inputRef, onClose, onSearchMessageRevoked, }) => {
|
|
19
|
+
const ThreadSearchContent = ({ conversation, inputRef, onClose, onJump, onSearchMessageRevoked, }) => {
|
|
20
20
|
const { t } = useTranslation("conversation-inbox");
|
|
21
21
|
const { t: tCommon } = useTranslation();
|
|
22
22
|
const selfUserID = useAuthStore((state) => state.userID);
|
|
@@ -40,6 +40,7 @@ const ThreadSearchContent = ({ conversation, inputRef, onClose, onSearchMessageR
|
|
|
40
40
|
store.setHeldHighlightClientMsgID(hit.clientMsgID);
|
|
41
41
|
store.setSearchClientMsgID(hit.clientMsgID);
|
|
42
42
|
emit("CHAT_LIST_SCROLL_TO_MESSAGE", hit.clientMsgID);
|
|
43
|
+
onJump === null || onJump === void 0 ? void 0 : onJump();
|
|
43
44
|
onClose();
|
|
44
45
|
};
|
|
45
46
|
const renderBody = () => {
|
|
@@ -3,9 +3,11 @@ interface ThreadSearchPanelProps {
|
|
|
3
3
|
open: boolean;
|
|
4
4
|
conversation: ConversationItem;
|
|
5
5
|
onClose: () => void;
|
|
6
|
+
/** Runs when a hit is picked, before the panel closes. */
|
|
7
|
+
onJump?: () => void;
|
|
6
8
|
/** Host toast for a hit that was revoked meanwhile; the SDK toast shows otherwise. */
|
|
7
9
|
onSearchMessageRevoked?: () => void;
|
|
8
10
|
}
|
|
9
|
-
declare const ThreadSearchPanel: ({ open, conversation, onClose, onSearchMessageRevoked, }: ThreadSearchPanelProps) => import("react/jsx-runtime").JSX.Element;
|
|
11
|
+
declare const ThreadSearchPanel: ({ open, conversation, onClose, onJump, onSearchMessageRevoked, }: ThreadSearchPanelProps) => import("react/jsx-runtime").JSX.Element;
|
|
10
12
|
export default ThreadSearchPanel;
|
|
11
13
|
//# sourceMappingURL=ThreadSearchPanel.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ThreadSearchPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/thread-search/ThreadSearchPanel.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,UAAU,sBAAsB;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,sFAAsF;IACtF,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAED,QAAA,MAAM,iBAAiB,GAAI,
|
|
1
|
+
{"version":3,"file":"ThreadSearchPanel.d.ts","sourceRoot":"","sources":["../../../../src/components/conversation-detail/thread-search/ThreadSearchPanel.tsx"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAGrE,UAAU,sBAAsB;IAC9B,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,gBAAgB,CAAC;IAC/B,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,0DAA0D;IAC1D,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,sFAAsF;IACtF,sBAAsB,CAAC,EAAE,MAAM,IAAI,CAAC;CACrC;AAED,QAAA,MAAM,iBAAiB,GAAI,kEAMxB,sBAAsB,4CA0BxB,CAAC;AAEF,eAAe,iBAAiB,CAAC"}
|
|
@@ -3,10 +3,10 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useRef } from "react";
|
|
4
4
|
import { Drawer } from "antd";
|
|
5
5
|
import ThreadSearchContent from "./ThreadSearchContent";
|
|
6
|
-
const ThreadSearchPanel = ({ open, conversation, onClose, onSearchMessageRevoked, }) => {
|
|
6
|
+
const ThreadSearchPanel = ({ open, conversation, onClose, onJump, onSearchMessageRevoked, }) => {
|
|
7
7
|
const inputRef = useRef(null);
|
|
8
8
|
return (_jsx(Drawer, { open: open, onClose: onClose, placement: "right", closeIcon: false, width: "100%", "data-cp-name": "ThreadSearchPanel", styles: { body: { padding: 0 } }, getContainer: false, destroyOnHidden: true,
|
|
9
9
|
// rc-drawer focuses its own panel after mount, which beats autoFocus.
|
|
10
|
-
afterOpenChange: (visible) => { var _a; return visible && ((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus()); }, children: _jsx(ThreadSearchContent, { conversation: conversation, inputRef: inputRef, onClose: onClose, onSearchMessageRevoked: onSearchMessageRevoked }) }));
|
|
10
|
+
afterOpenChange: (visible) => { var _a; return visible && ((_a = inputRef.current) === null || _a === void 0 ? void 0 : _a.focus()); }, children: _jsx(ThreadSearchContent, { conversation: conversation, inputRef: inputRef, onClose: onClose, onJump: onJump, onSearchMessageRevoked: onSearchMessageRevoked }) }));
|
|
11
11
|
};
|
|
12
12
|
export default ThreadSearchPanel;
|
|
@@ -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
|