@droppii-org/chat-sdk 0.6.49 → 0.6.50
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 +7 -1
- package/dist/components/conversation-detail/ConversationDetailInput.d.ts.map +1 -1
- package/dist/components/conversation-detail/ConversationDetailInput.js +14 -4
- package/dist/hooks/session/useAgentSupportThreadJoin.d.ts +13 -0
- package/dist/hooks/session/useAgentSupportThreadJoin.d.ts.map +1 -0
- package/dist/hooks/session/useAgentSupportThreadJoin.js +82 -0
- package/dist/hooks/session/useIsJoinedGroup.d.ts +1 -0
- package/dist/hooks/session/useIsJoinedGroup.d.ts.map +1 -1
- package/dist/hooks/session/useIsJoinedGroup.js +2 -1
- package/package.json +1 -1
|
@@ -7,7 +7,13 @@ export interface ConversationDetailInputProps {
|
|
|
7
7
|
onOpenBotMenu?: () => void;
|
|
8
8
|
/** Shows the sticker picker entry in the composer. Defaults to true. */
|
|
9
9
|
showSticker?: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Joins a support thread the viewer handles but isn't a member of (e.g. a
|
|
12
|
+
* PDP CS). When set, such a viewer gets a join prompt instead of the
|
|
13
|
+
* composer; the host owns the API, so other apps are unaffected.
|
|
14
|
+
*/
|
|
15
|
+
onJoinSupportThread?: (conversationId: string) => Promise<unknown>;
|
|
10
16
|
}
|
|
11
|
-
declare const ConversationDetailInput: ({ placeholder, onSendMessage, onInputChange, moreOptions, onOpenBotMenu, showSticker, }: ConversationDetailInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
declare const ConversationDetailInput: ({ placeholder, onSendMessage, onInputChange, moreOptions, onOpenBotMenu, showSticker, onJoinSupportThread, }: ConversationDetailInputProps) => import("react/jsx-runtime").JSX.Element;
|
|
12
18
|
export default ConversationDetailInput;
|
|
13
19
|
//# sourceMappingURL=ConversationDetailInput.d.ts.map
|
|
@@ -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":"AA4BA,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,4CAkxB9B,CAAC;AAEF,eAAe,uBAAuB,CAAC"}
|
|
@@ -4,11 +4,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
4
4
|
import { useTranslation } from "react-i18next";
|
|
5
5
|
import { useBoolean } from "ahooks";
|
|
6
6
|
import clsx from "clsx";
|
|
7
|
-
import { Button, message as toastMessage } from "antd";
|
|
7
|
+
import { Button, Spin, message as toastMessage } from "antd";
|
|
8
8
|
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 { useAgentSupportThreadJoin } from "../../hooks/session/useAgentSupportThreadJoin";
|
|
12
13
|
import useAuthStore from "../../store/auth";
|
|
13
14
|
import useConversationStore from "../../store/conversation";
|
|
14
15
|
import useMessageDraftStore from "../../store/messageDraft";
|
|
@@ -33,7 +34,7 @@ const MAX_LINES = 5;
|
|
|
33
34
|
const CONTAINER_MIN_HEIGHT = 48;
|
|
34
35
|
const FALLBACK_LINE_HEIGHT = 25.6;
|
|
35
36
|
const MAX_MENTIONED_USERS = 10;
|
|
36
|
-
const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, moreOptions, onOpenBotMenu, showSticker = true, }) => {
|
|
37
|
+
const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, moreOptions, onOpenBotMenu, showSticker = true, onJoinSupportThread, }) => {
|
|
37
38
|
var _a;
|
|
38
39
|
const { t } = useTranslation("conversation-inbox");
|
|
39
40
|
const { t: tCommon } = useTranslation("common");
|
|
@@ -46,6 +47,7 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
|
|
|
46
47
|
const getCurrentMemberInGroupByReq = useConversationStore((state) => state.getCurrentMemberInGroupByReq);
|
|
47
48
|
const getSelfGroupPermissionByReq = useConversationStore((state) => state.getSelfGroupPermissionByReq);
|
|
48
49
|
const { mutate: joinPublicGroup, isPending: isJoiningPublicGroup } = useJoinPublicGroup();
|
|
50
|
+
const supportThreadJoin = useAgentSupportThreadJoin(conversationData, onJoinSupportThread);
|
|
49
51
|
const handleJoinPublicGroup = useCallback(() => {
|
|
50
52
|
const groupID = conversationData === null || conversationData === void 0 ? void 0 : conversationData.groupID;
|
|
51
53
|
if (!groupID)
|
|
@@ -487,8 +489,16 @@ const ConversationDetailInput = ({ placeholder, onSendMessage, onInputChange, mo
|
|
|
487
489
|
event.preventDefault();
|
|
488
490
|
void handleSend();
|
|
489
491
|
};
|
|
490
|
-
|
|
491
|
-
|
|
492
|
+
// Don't flash a composer the agent may not be allowed to use.
|
|
493
|
+
if (supportThreadJoin.isCheckingMembership) {
|
|
494
|
+
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
|
+
}
|
|
496
|
+
if (isViewOnlyMember || supportThreadJoin.needsJoin) {
|
|
497
|
+
return (_jsxs("div", { "data-testid": "conversation-detail-input-view-only", className: "flex flex-col w-full shrink-0 items-center justify-center gap-3 bg-chat-bubble-neutral/[0.05] px-4 py-3 ", children: [_jsx("span", { className: "text-sm text-gray-500", children: t("thread_detail.view_only_notice") }), _jsx(Button, { type: "primary", size: "small", loading: supportThreadJoin.needsJoin
|
|
498
|
+
? supportThreadJoin.isJoining
|
|
499
|
+
: isJoiningPublicGroup, onClick: supportThreadJoin.needsJoin
|
|
500
|
+
? supportThreadJoin.join
|
|
501
|
+
: handleJoinPublicGroup, children: tCommon("join_group") })] }));
|
|
492
502
|
}
|
|
493
503
|
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
|
|
494
504
|
? "border-[var(--dc-color-primary-focus)] shadow-[0px_0px_2px_var(--dc-color-primary-focus)]"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type ConversationItem } from "@droppii-org/wasm-client-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* CRM's join gate for the thread detail: an agent viewing a support thread
|
|
4
|
+
* they aren't a member of can't send until they join. Opt-in — only a host
|
|
5
|
+
* that supplies `joinThread` (PDP Portal) gets it; owners and CRM never do.
|
|
6
|
+
*/
|
|
7
|
+
export declare const useAgentSupportThreadJoin: (conversation?: ConversationItem | null, joinThread?: (conversationId: string) => Promise<unknown>) => {
|
|
8
|
+
isCheckingMembership: boolean;
|
|
9
|
+
needsJoin: boolean;
|
|
10
|
+
join: () => Promise<void>;
|
|
11
|
+
isJoining: boolean;
|
|
12
|
+
};
|
|
13
|
+
//# sourceMappingURL=useAgentSupportThreadJoin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useAgentSupportThreadJoin.d.ts","sourceRoot":"","sources":["../../../src/hooks/session/useAgentSupportThreadJoin.ts"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,gBAAgB,EAGtB,MAAM,8BAA8B,CAAC;AAYtC;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,GACpC,eAAe,gBAAgB,GAAG,IAAI,EACtC,aAAa,CAAC,cAAc,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC;;;;;CAuE1D,CAAC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useCallback, useEffect, useState } from "react";
|
|
3
|
+
import { message } from "antd";
|
|
4
|
+
import { useTranslation } from "react-i18next";
|
|
5
|
+
import { CbEvents, } from "@droppii-org/wasm-client-sdk";
|
|
6
|
+
import { DChatSDK } from "../../constants/sdk";
|
|
7
|
+
import useAuthStore from "../../store/auth";
|
|
8
|
+
import { PeerType } from "../../types/conversation";
|
|
9
|
+
import { isSupportSessionAgentView } from "../../utils/conversation";
|
|
10
|
+
import { useIsJoinedGroup } from "./useIsJoinedGroup";
|
|
11
|
+
// The local OpenIM DB learns about a REST join seconds later, so a membership
|
|
12
|
+
// check right after still says "not a member". Joins made in this page
|
|
13
|
+
// session are trusted instead — kept outside React so a remount keeps them.
|
|
14
|
+
const joinedConversationIds = new Set();
|
|
15
|
+
/**
|
|
16
|
+
* CRM's join gate for the thread detail: an agent viewing a support thread
|
|
17
|
+
* they aren't a member of can't send until they join. Opt-in — only a host
|
|
18
|
+
* that supplies `joinThread` (PDP Portal) gets it; owners and CRM never do.
|
|
19
|
+
*/
|
|
20
|
+
export const useAgentSupportThreadJoin = (conversation, joinThread) => {
|
|
21
|
+
const { t } = useTranslation();
|
|
22
|
+
const userID = useAuthStore((state) => state.userID);
|
|
23
|
+
const isCrm = useAuthStore((state) => state.isCrm);
|
|
24
|
+
const isAgentSupportThread = !!joinThread &&
|
|
25
|
+
!isCrm &&
|
|
26
|
+
!!conversation &&
|
|
27
|
+
conversation.peerType !== PeerType.GROUP &&
|
|
28
|
+
isSupportSessionAgentView(conversation, userID);
|
|
29
|
+
const conversationId = conversation === null || conversation === void 0 ? void 0 : conversation.conversationID;
|
|
30
|
+
const groupId = isAgentSupportThread ? conversation === null || conversation === void 0 ? void 0 : conversation.groupID : undefined;
|
|
31
|
+
const { isJoined, isError, refetch } = useIsJoinedGroup(groupId);
|
|
32
|
+
const [joiningConversationId, setJoiningConversationId] = useState(null);
|
|
33
|
+
const [, setJoinCount] = useState(0);
|
|
34
|
+
// Membership can also come from elsewhere (auto-assign, another CS adding
|
|
35
|
+
// this agent) — the group reaching this device is the cue to re-check.
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!groupId || isJoined !== false)
|
|
38
|
+
return undefined;
|
|
39
|
+
const handleConversations = ({ data }) => {
|
|
40
|
+
if (data === null || data === void 0 ? void 0 : data.some((item) => item.groupID === groupId))
|
|
41
|
+
void refetch();
|
|
42
|
+
};
|
|
43
|
+
const handleJoinedGroup = ({ data }) => {
|
|
44
|
+
if ((data === null || data === void 0 ? void 0 : data.groupID) === groupId)
|
|
45
|
+
void refetch();
|
|
46
|
+
};
|
|
47
|
+
DChatSDK.on(CbEvents.OnNewConversation, handleConversations);
|
|
48
|
+
DChatSDK.on(CbEvents.OnJoinedGroupAdded, handleJoinedGroup);
|
|
49
|
+
return () => {
|
|
50
|
+
DChatSDK.off(CbEvents.OnNewConversation, handleConversations);
|
|
51
|
+
DChatSDK.off(CbEvents.OnJoinedGroupAdded, handleJoinedGroup);
|
|
52
|
+
};
|
|
53
|
+
}, [groupId, isJoined, refetch]);
|
|
54
|
+
const join = useCallback(async () => {
|
|
55
|
+
if (!conversationId || !joinThread)
|
|
56
|
+
return;
|
|
57
|
+
setJoiningConversationId(conversationId);
|
|
58
|
+
try {
|
|
59
|
+
await joinThread(conversationId);
|
|
60
|
+
}
|
|
61
|
+
catch (_a) {
|
|
62
|
+
message.error(t("join_group_failed"));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
setJoiningConversationId(null);
|
|
67
|
+
}
|
|
68
|
+
joinedConversationIds.add(conversationId);
|
|
69
|
+
setJoinCount((count) => count + 1);
|
|
70
|
+
message.success(t("join_group_success"));
|
|
71
|
+
}, [conversationId, joinThread, t]);
|
|
72
|
+
const isJoinedHere = !!conversationId && joinedConversationIds.has(conversationId);
|
|
73
|
+
const isGated = !!groupId && !isJoinedHere;
|
|
74
|
+
return {
|
|
75
|
+
isCheckingMembership: isGated && isJoined === null && !isError,
|
|
76
|
+
// A failed check falls back to the prompt; the join itself is the
|
|
77
|
+
// server's call, so this never strands the agent behind a spinner.
|
|
78
|
+
needsJoin: isGated && (isJoined === false || isError),
|
|
79
|
+
join,
|
|
80
|
+
isJoining: !!conversationId && joiningConversationId === conversationId,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export declare const useIsJoinedGroup: (groupId?: string) => {
|
|
2
2
|
isJoined: boolean | null;
|
|
3
|
+
isError: boolean;
|
|
3
4
|
refetch: (options?: import("@tanstack/react-query").RefetchOptions) => Promise<import("@tanstack/react-query").QueryObserverResult<boolean, Error>>;
|
|
4
5
|
};
|
|
5
6
|
//# sourceMappingURL=useIsJoinedGroup.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useIsJoinedGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/session/useIsJoinedGroup.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,GAAI,UAAU,MAAM
|
|
1
|
+
{"version":3,"file":"useIsJoinedGroup.d.ts","sourceRoot":"","sources":["../../../src/hooks/session/useIsJoinedGroup.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,gBAAgB,GAAI,UAAU,MAAM;;;;CAehD,CAAC"}
|
|
@@ -3,7 +3,7 @@ import { useQuery } from "@tanstack/react-query";
|
|
|
3
3
|
import { DChatSDK } from "../../constants/sdk";
|
|
4
4
|
import { QUERY_KEYS } from "../../services/query";
|
|
5
5
|
export const useIsJoinedGroup = (groupId) => {
|
|
6
|
-
const { data, isLoading, refetch } = useQuery({
|
|
6
|
+
const { data, isLoading, isError, refetch } = useQuery({
|
|
7
7
|
queryKey: [QUERY_KEYS.IS_JOINED_GROUP, groupId],
|
|
8
8
|
enabled: Boolean(groupId),
|
|
9
9
|
queryFn: async () => {
|
|
@@ -13,6 +13,7 @@ export const useIsJoinedGroup = (groupId) => {
|
|
|
13
13
|
});
|
|
14
14
|
return {
|
|
15
15
|
isJoined: isLoading ? null : (data !== null && data !== void 0 ? data : null),
|
|
16
|
+
isError,
|
|
16
17
|
refetch,
|
|
17
18
|
};
|
|
18
19
|
};
|