@droppii-org/chat-sdk 0.0.70 → 0.1.0
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/assets/sdk/sql-wasm.wasm +0 -0
- package/dist/components/message/footer/ActionBar.d.ts.map +1 -1
- package/dist/components/message/footer/ActionBar.js +18 -22
- package/dist/components/message/footer/PasteAndDropPlugin.d.ts +6 -0
- package/dist/components/message/footer/PasteAndDropPlugin.d.ts.map +1 -0
- package/dist/components/message/footer/PasteAndDropPlugin.js +115 -0
- package/dist/components/message/footer/index.d.ts.map +1 -1
- package/dist/components/message/footer/index.js +2 -1
- package/dist/components/thread/SessionSection.d.ts.map +1 -1
- package/dist/components/thread/SessionSection.js +52 -18
- package/dist/constants/sdk.d.ts.map +1 -1
- package/dist/constants/sdk.js +1 -2
- package/dist/hooks/global/useGlobalEvent.d.ts.map +1 -1
- package/dist/hooks/global/useGlobalEvent.js +16 -6
- package/dist/services/api.js +49 -0
- package/dist/store/auth.d.ts.map +1 -1
- package/dist/store/auth.js +5 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/chat.d.ts +12 -1
- package/dist/types/chat.d.ts.map +1 -1
- package/dist/utils/fileValidation.d.ts +26 -0
- package/dist/utils/fileValidation.d.ts.map +1 -0
- package/dist/utils/fileValidation.js +84 -0
- package/package.json +10 -11
|
File without changes
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ActionBar.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/ActionBar.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"ActionBar.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/ActionBar.tsx"],"names":[],"mappings":"AAoFA,QAAA,MAAM,SAAS,+CA0Od,CAAC;AAEF,eAAe,SAAS,CAAC"}
|
|
@@ -11,6 +11,8 @@ import { $generateHtmlFromNodes } from "@lexical/html";
|
|
|
11
11
|
import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, } from "@lexical/list";
|
|
12
12
|
import { $isQuoteNode } from "@lexical/rich-text";
|
|
13
13
|
import { useMessageFooterContext } from ".";
|
|
14
|
+
import { useTranslation } from "react-i18next";
|
|
15
|
+
import { validateFile, validateVideoLimit } from "../../../utils/fileValidation";
|
|
14
16
|
const documentTypes = [
|
|
15
17
|
"application/pdf",
|
|
16
18
|
"application/msword",
|
|
@@ -25,6 +27,7 @@ const ActionBar = () => {
|
|
|
25
27
|
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
26
28
|
const { listUploadFiles } = useMessageFooterContext();
|
|
27
29
|
const [isEmptyInput, setIsEmptyInput] = useState(true);
|
|
30
|
+
const { t } = useTranslation();
|
|
28
31
|
const canSend = !isEmptyInput || listUploadFiles.length > 0;
|
|
29
32
|
const handleSend = useCallback(() => {
|
|
30
33
|
let plainText = "";
|
|
@@ -89,31 +92,24 @@ const ActionBar = () => {
|
|
|
89
92
|
setShowEmojiPicker(false);
|
|
90
93
|
}, [editor]);
|
|
91
94
|
const beforeUploadImagesAndVideo = (file, fileList) => {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
//
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
103
|
-
if (isErrorSize) {
|
|
104
|
-
if (isImage) {
|
|
105
|
-
message.error(`${file.name} có kích thước tập tin vượt quá ${maxSize}MB`);
|
|
106
|
-
}
|
|
107
|
-
else {
|
|
108
|
-
message.error(`Tệp không được vượt quá ${maxSize}MB`);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
95
|
+
// Validate file type and size
|
|
96
|
+
const validation = validateFile(file, t);
|
|
97
|
+
if (!validation.isValid) {
|
|
98
|
+
message.error(validation.error);
|
|
99
|
+
return Upload.LIST_IGNORE;
|
|
100
|
+
}
|
|
101
|
+
// Validate video limit
|
|
102
|
+
const videoValidation = validateVideoLimit([file], listUploadFiles, t);
|
|
103
|
+
if (!videoValidation.isValid) {
|
|
104
|
+
message.error(videoValidation.error);
|
|
111
105
|
return Upload.LIST_IGNORE;
|
|
112
106
|
}
|
|
107
|
+
// Check if multiple videos in current upload batch
|
|
113
108
|
const newVideos = fileList.filter((f) => { var _a; return (_a = f.type) === null || _a === void 0 ? void 0 : _a.startsWith("video/"); });
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
109
|
+
if (newVideos.length > 1) {
|
|
110
|
+
message.error(t("video_limit_exceeded", {
|
|
111
|
+
defaultValue: "Chỉ được phép tải lên 1 video duy nhất",
|
|
112
|
+
}));
|
|
117
113
|
return Upload.LIST_IGNORE;
|
|
118
114
|
}
|
|
119
115
|
return false;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PasteAndDropPlugin.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/PasteAndDropPlugin.tsx"],"names":[],"mappings":"AASA,UAAU,uBAAuB;IAC/B,YAAY,CAAC,EAAE,CAAC,KAAK,EAAE,IAAI,EAAE,KAAK,IAAI,CAAC;CACxC;AAED,QAAA,MAAM,kBAAkB,GAAI,kBAAkB,uBAAuB,SAqIpE,CAAC;AAEF,eAAe,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
3
|
+
import { useEffect } from "react";
|
|
4
|
+
import { COMMAND_PRIORITY_HIGH, PASTE_COMMAND, DROP_COMMAND } from "lexical";
|
|
5
|
+
import { useMessageFooterContext } from ".";
|
|
6
|
+
import { useTranslation } from "react-i18next";
|
|
7
|
+
import { processAndValidateFiles, ACCEPTED_IMAGE_TYPES } from "../../../utils/fileValidation";
|
|
8
|
+
const PasteAndDropPlugin = ({ onFilesAdded }) => {
|
|
9
|
+
const [editor] = useLexicalComposerContext();
|
|
10
|
+
const { setListUploadFiles } = useMessageFooterContext();
|
|
11
|
+
const { t } = useTranslation();
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
// Handle paste event
|
|
14
|
+
const unregisterPaste = editor.registerCommand(PASTE_COMMAND, (event) => {
|
|
15
|
+
try {
|
|
16
|
+
const clipboardData = event.clipboardData;
|
|
17
|
+
if (!clipboardData)
|
|
18
|
+
return false;
|
|
19
|
+
const items = Array.from(clipboardData.items);
|
|
20
|
+
const imageItems = items.filter((item) => item.type.startsWith("image/"));
|
|
21
|
+
if (imageItems.length > 0) {
|
|
22
|
+
event.preventDefault();
|
|
23
|
+
const files = [];
|
|
24
|
+
imageItems.forEach((item) => {
|
|
25
|
+
const file = item.getAsFile();
|
|
26
|
+
if (file) {
|
|
27
|
+
files.push(file);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
if (files.length > 0) {
|
|
31
|
+
setListUploadFiles((prev) => {
|
|
32
|
+
const validFiles = processAndValidateFiles(files, {
|
|
33
|
+
t,
|
|
34
|
+
currentUploadedFiles: prev,
|
|
35
|
+
});
|
|
36
|
+
if (validFiles.length > 0) {
|
|
37
|
+
onFilesAdded === null || onFilesAdded === void 0 ? void 0 : onFilesAdded(files);
|
|
38
|
+
return [...prev, ...validFiles];
|
|
39
|
+
}
|
|
40
|
+
return prev;
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
console.error("Error handling paste:", error);
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}, COMMAND_PRIORITY_HIGH);
|
|
52
|
+
// Handle drop event
|
|
53
|
+
const unregisterDrop = editor.registerCommand(DROP_COMMAND, (event) => {
|
|
54
|
+
try {
|
|
55
|
+
const dataTransfer = event.dataTransfer;
|
|
56
|
+
if (!dataTransfer)
|
|
57
|
+
return false;
|
|
58
|
+
const files = Array.from(dataTransfer.files);
|
|
59
|
+
const imageOrVideoFiles = files.filter((file) => ACCEPTED_IMAGE_TYPES.includes(file.type) ||
|
|
60
|
+
file.type.startsWith("video/"));
|
|
61
|
+
if (imageOrVideoFiles.length > 0) {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
setListUploadFiles((prev) => {
|
|
64
|
+
const validFiles = processAndValidateFiles(imageOrVideoFiles, {
|
|
65
|
+
t,
|
|
66
|
+
currentUploadedFiles: prev,
|
|
67
|
+
});
|
|
68
|
+
if (validFiles.length > 0) {
|
|
69
|
+
onFilesAdded === null || onFilesAdded === void 0 ? void 0 : onFilesAdded(imageOrVideoFiles);
|
|
70
|
+
return [...prev, ...validFiles];
|
|
71
|
+
}
|
|
72
|
+
return prev;
|
|
73
|
+
});
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error("Error handling drop:", error);
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
82
|
+
}, COMMAND_PRIORITY_HIGH);
|
|
83
|
+
return () => {
|
|
84
|
+
unregisterPaste();
|
|
85
|
+
unregisterDrop();
|
|
86
|
+
};
|
|
87
|
+
}, [editor, setListUploadFiles, t, onFilesAdded]);
|
|
88
|
+
// Add visual feedback for drag over
|
|
89
|
+
useEffect(() => {
|
|
90
|
+
const editorElement = editor.getRootElement();
|
|
91
|
+
if (!editorElement)
|
|
92
|
+
return;
|
|
93
|
+
const handleDragOverVisual = (e) => {
|
|
94
|
+
e.preventDefault();
|
|
95
|
+
editorElement.classList.add("border-blue-500", "bg-blue-50");
|
|
96
|
+
};
|
|
97
|
+
const handleDragLeaveVisual = (e) => {
|
|
98
|
+
e.preventDefault();
|
|
99
|
+
editorElement.classList.remove("border-blue-500", "bg-blue-50");
|
|
100
|
+
};
|
|
101
|
+
const handleDropVisual = () => {
|
|
102
|
+
editorElement.classList.remove("border-blue-500", "bg-blue-50");
|
|
103
|
+
};
|
|
104
|
+
editorElement.addEventListener("dragover", handleDragOverVisual);
|
|
105
|
+
editorElement.addEventListener("dragleave", handleDragLeaveVisual);
|
|
106
|
+
editorElement.addEventListener("drop", handleDropVisual);
|
|
107
|
+
return () => {
|
|
108
|
+
editorElement.removeEventListener("dragover", handleDragOverVisual);
|
|
109
|
+
editorElement.removeEventListener("dragleave", handleDragLeaveVisual);
|
|
110
|
+
editorElement.removeEventListener("drop", handleDropVisual);
|
|
111
|
+
};
|
|
112
|
+
}, [editor]);
|
|
113
|
+
return null;
|
|
114
|
+
};
|
|
115
|
+
export default PasteAndDropPlugin;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/index.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAK/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/index.tsx"],"names":[],"mappings":"AAcA,OAAO,EAAE,wBAAwB,EAAE,MAAM,qBAAqB,CAAC;AAK/D,OAAO,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAGvD,UAAU,kBAAkB;IAC1B,cAAc,CAAC,EAAE,gBAAgB,CAAC;CACnC;AA+BD,eAAO,MAAM,oBAAoB,mDAI/B,CAAC;AAEH,eAAO,MAAM,uBAAuB,gCAAyC,CAAC;AAE9E,QAAA,MAAM,qBAAqB,GAAI,oBAAoB,kBAAkB,4CA6DpE,CAAC;AAEF,eAAe,qBAAqB,CAAC"}
|
|
@@ -16,6 +16,7 @@ import ActionBar from "./ActionBar";
|
|
|
16
16
|
import { useSendMessage } from "../../../hooks/message/useSendMessage";
|
|
17
17
|
import FilePreview from "./FilePreview";
|
|
18
18
|
import { useTranslation } from "react-i18next";
|
|
19
|
+
import PasteAndDropPlugin from "./PasteAndDropPlugin";
|
|
19
20
|
const theme = {
|
|
20
21
|
text: {
|
|
21
22
|
bold: "font-bold",
|
|
@@ -66,6 +67,6 @@ const MessageFooterProvider = ({ currentSession }) => {
|
|
|
66
67
|
}
|
|
67
68
|
setListUploadFiles([]);
|
|
68
69
|
}, [sendMergeMessage, sendTextMessage, listUploadFiles, currentSession]);
|
|
69
|
-
return (_jsx(MessageFooterContext.Provider, { value: { onSendMessage, listUploadFiles, setListUploadFiles }, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsxs("div", { className: "border-t pb-2 flex flex-col gap-1 bg-white", children: [listUploadFiles.length > 0 && _jsx(FilePreview, {}), _jsx(ToolbarPlugin, {}), _jsx("div", { className: "relative px-4", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "border border-indigo-500 rounded-md bg-blue-100 min-h-[64px] max-h-[140px] overflow-y-auto px-3 py-2 text-sm" }), ErrorBoundary: LexicalErrorBoundary, "aria-placeholder": t("enter_message"), placeholder: _jsx("div", { className: "absolute top-2 left-7 pointer-events-none", children: _jsx("p", { className: "text-gray-500 text-sm", children: t("enter_message") }) }) }) }), _jsx(ActionBar, {})] }), _jsx(LinkPlugin, {}), _jsx(ListPlugin, {}), _jsx(EnterHandler, {})] }) }));
|
|
70
|
+
return (_jsx(MessageFooterContext.Provider, { value: { onSendMessage, listUploadFiles, setListUploadFiles }, children: _jsxs(LexicalComposer, { initialConfig: initialConfig, children: [_jsxs("div", { className: "border-t pb-2 flex flex-col gap-1 bg-white", children: [listUploadFiles.length > 0 && _jsx(FilePreview, {}), _jsx(ToolbarPlugin, {}), _jsx("div", { className: "relative px-4", children: _jsx(RichTextPlugin, { contentEditable: _jsx(ContentEditable, { className: "border border-indigo-500 rounded-md bg-blue-100 min-h-[64px] max-h-[140px] overflow-y-auto px-3 py-2 text-sm" }), ErrorBoundary: LexicalErrorBoundary, "aria-placeholder": t("enter_message"), placeholder: _jsx("div", { className: "absolute top-2 left-7 pointer-events-none", children: _jsx("p", { className: "text-gray-500 text-sm", children: t("enter_message") }) }) }) }), _jsx(ActionBar, {})] }), _jsx(LinkPlugin, {}), _jsx(ListPlugin, {}), _jsx(EnterHandler, {}), _jsx(PasteAndDropPlugin, {})] }) }));
|
|
70
71
|
};
|
|
71
72
|
export default MessageFooterProvider;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionSection.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SessionSection.tsx"],"names":[],"mappings":"AASA,OAAO,EAAkB,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"SessionSection.d.ts","sourceRoot":"","sources":["../../../src/components/thread/SessionSection.tsx"],"names":[],"mappings":"AASA,OAAO,EAAkB,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAmBnE,eAAO,MAAM,iBAAiB,GAAI,wBAG/B;IACD,OAAO,EAAE,gBAAgB,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB,4CAsMA,CAAC;AA8CF,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,QAAA,MAAM,cAAc,GAAI,yBAAyB,mBAAmB,4CAkDnE,CAAC;AAEF,eAAe,cAAc,CAAC"}
|
|
@@ -25,17 +25,29 @@ export const SessionDetailCard = ({ session, isActive, }) => {
|
|
|
25
25
|
const { data: availableLabels, isLoading: isLoadingLabels } = useGetLabelSession();
|
|
26
26
|
const { mutate: updateSession } = useUpdateSessionInfo();
|
|
27
27
|
const { mutate: closeSession, isPending: isClosing } = useCloseSession();
|
|
28
|
-
const [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
const [noteInput, setNoteInput] = useState({
|
|
29
|
+
value: session.note || "",
|
|
30
|
+
sessionId: session.id,
|
|
31
|
+
});
|
|
32
|
+
const [issueDetailInput, setIssueDetailInput] = useState({
|
|
33
|
+
value: session.issueDetail || "",
|
|
34
|
+
sessionId: session.id,
|
|
35
|
+
});
|
|
36
|
+
const debouncedNoteInput = useDebounce(noteInput, { wait: 500 });
|
|
37
|
+
const debouncedIssueDetailInput = useDebounce(issueDetailInput, {
|
|
38
|
+
wait: 500,
|
|
39
|
+
});
|
|
32
40
|
useEffect(() => {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
setNoteInput({ value: session.note || "", sessionId: session.id });
|
|
42
|
+
setIssueDetailInput({
|
|
43
|
+
value: session.issueDetail || "",
|
|
44
|
+
sessionId: session.id,
|
|
45
|
+
});
|
|
46
|
+
}, [session.id]);
|
|
36
47
|
useEffect(() => {
|
|
37
|
-
if (
|
|
38
|
-
|
|
48
|
+
if (debouncedNoteInput.sessionId === session.id &&
|
|
49
|
+
debouncedNoteInput.value !== (session.note || "")) {
|
|
50
|
+
updateSession({ sessionId: session.id, note: debouncedNoteInput.value }, {
|
|
39
51
|
onSuccess: () => {
|
|
40
52
|
queryClient.invalidateQueries({
|
|
41
53
|
queryKey: [QUERY_KEYS.GET_LIST_SESSION_BY_CONVERSATION],
|
|
@@ -43,12 +55,19 @@ export const SessionDetailCard = ({ session, isActive, }) => {
|
|
|
43
55
|
},
|
|
44
56
|
});
|
|
45
57
|
}
|
|
46
|
-
}, [
|
|
58
|
+
}, [
|
|
59
|
+
debouncedNoteInput,
|
|
60
|
+
session.note,
|
|
61
|
+
session.id,
|
|
62
|
+
updateSession,
|
|
63
|
+
queryClient,
|
|
64
|
+
]);
|
|
47
65
|
useEffect(() => {
|
|
48
|
-
if (
|
|
66
|
+
if (debouncedIssueDetailInput.sessionId === session.id &&
|
|
67
|
+
debouncedIssueDetailInput.value !== (session.issueDetail || "")) {
|
|
49
68
|
updateSession({
|
|
50
69
|
sessionId: session.id,
|
|
51
|
-
issueDetail:
|
|
70
|
+
issueDetail: debouncedIssueDetailInput.value,
|
|
52
71
|
}, {
|
|
53
72
|
onSuccess: () => {
|
|
54
73
|
queryClient.invalidateQueries({
|
|
@@ -58,7 +77,7 @@ export const SessionDetailCard = ({ session, isActive, }) => {
|
|
|
58
77
|
});
|
|
59
78
|
}
|
|
60
79
|
}, [
|
|
61
|
-
|
|
80
|
+
debouncedIssueDetailInput,
|
|
62
81
|
session.issueDetail,
|
|
63
82
|
session.id,
|
|
64
83
|
updateSession,
|
|
@@ -90,18 +109,33 @@ export const SessionDetailCard = ({ session, isActive, }) => {
|
|
|
90
109
|
value: label.id,
|
|
91
110
|
label: label.name,
|
|
92
111
|
}));
|
|
93
|
-
return (_jsxs("div", { className: `p-3 border rounded-lg shadow-md ${isActive ? "border-blue-500" : "border-gray-300"}`, children: [_jsxs("div", { className: "flex justify-between items-start mb-3", children: [_jsx("div", { className: "flex items-center gap-2", children: _jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Icon, { icon: "chat-square-b", size: 16, className: "text-gray-500" }), _jsxs("p", { className: "font-semibold text-sm", children: ["Session ", session.order] })] }), _jsx("p", { className: "text-xs text-gray-400", children: formatSessionDate(session.closedDate || session.createdDate) })] }) }), isActive ? (_jsx(Tag, { color: "processing", children: "\u0110ang ho\u1EA1t \u0111\u1ED9ng" })) : (_jsx(Avatar, { src: (_a = session.supporter) === null || _a === void 0 ? void 0 : _a.avatar, size: 24, children: ((_d = (_c = (_b = session.supporter) === null || _b === void 0 ? void 0 : _b.username) === null || _c === void 0 ? void 0 : _c.charAt) === null || _d === void 0 ? void 0 : _d.call(_c, 0)) || "A" }))] }), _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("div", { className: "flex items-center gap-2 text-gray-500", children: [_jsx(Icon, { icon: "tag-o", size: 16, className: "mt-1" }), _jsx(Select, { mode: "multiple", loading: isLoadingLabels, className: "w-full", placeholder: "Th\u00EAm th\u1EBB...", options: labelOptions, value: (_e = session.labels) === null || _e === void 0 ? void 0 : _e.map((label) => label.id), onChange: handleUpdateLabels, removeIcon: true })] }), _jsxs("div", { className: "flex items-start gap-2 text-gray-500", children: [_jsx(Icon, { icon: "info-circle-o", size: 16, className: "mt-1" }), _jsx(TextArea, { value:
|
|
112
|
+
return (_jsxs("div", { className: `p-3 border rounded-lg shadow-md ${isActive ? "border-blue-500" : "border-gray-300"}`, children: [_jsxs("div", { className: "flex justify-between items-start mb-3", children: [_jsx("div", { className: "flex items-center gap-2", children: _jsxs("div", { className: "flex flex-col", children: [_jsxs("div", { className: "flex items-center gap-1", children: [_jsx(Icon, { icon: "chat-square-b", size: 16, className: "text-gray-500" }), _jsxs("p", { className: "font-semibold text-sm", children: ["Session ", session.order] })] }), _jsx("p", { className: "text-xs text-gray-400", children: formatSessionDate(session.closedDate || session.createdDate) })] }) }), isActive ? (_jsx(Tag, { color: "processing", children: "\u0110ang ho\u1EA1t \u0111\u1ED9ng" })) : (_jsx(Avatar, { src: (_a = session.supporter) === null || _a === void 0 ? void 0 : _a.avatar, size: 24, children: ((_d = (_c = (_b = session.supporter) === null || _b === void 0 ? void 0 : _b.username) === null || _c === void 0 ? void 0 : _c.charAt) === null || _d === void 0 ? void 0 : _d.call(_c, 0)) || "A" }))] }), _jsxs("div", { className: "flex flex-col gap-3 text-sm", children: [_jsxs("div", { className: "flex items-center gap-2 text-gray-500", children: [_jsx(Icon, { icon: "tag-o", size: 16, className: "mt-1" }), _jsx(Select, { mode: "multiple", loading: isLoadingLabels, className: "w-full", placeholder: "Th\u00EAm th\u1EBB...", options: labelOptions, value: (_e = session.labels) === null || _e === void 0 ? void 0 : _e.map((label) => label.id), onChange: handleUpdateLabels, removeIcon: true })] }), _jsxs("div", { className: "flex items-start gap-2 text-gray-500", children: [_jsx(Icon, { icon: "info-circle-o", size: 16, className: "mt-1" }), _jsx(TextArea, { value: issueDetailInput.value, onChange: (e) => setIssueDetailInput({
|
|
113
|
+
value: e.target.value,
|
|
114
|
+
sessionId: session.id,
|
|
115
|
+
}), placeholder: "V\u1EA5n \u0111\u1EC1 c\u1EE5 th\u1EC3...", className: "border-none !shadow-none py-0 px-2", autoSize: { minRows: 1, maxRows: 3 } })] }), _jsxs("div", { className: "flex items-start gap-2 text-gray-500", children: [_jsx(Icon, { icon: "paper-o", size: 16, className: "mt-1" }), _jsx(TextArea, { value: noteInput.value, onChange: (e) => setNoteInput({ value: e.target.value, sessionId: session.id }), placeholder: "Ghi ch\u00FA...", className: "border-none !shadow-none py-0 px-2", autoSize: { minRows: 1, maxRows: 3 } })] })] }), isActive && (_jsx(Button, { type: "primary", block: true, className: "mt-4", onClick: handleCloseSession, loading: isClosing, children: "\u0110\u00F3ng" }))] }));
|
|
94
116
|
};
|
|
95
|
-
const ClosedSessionItem = ({ session }) => {
|
|
117
|
+
const ClosedSessionItem = ({ session, expandedSessionId, onToggleExpand, }) => {
|
|
96
118
|
var _a, _b, _c, _d;
|
|
97
|
-
const
|
|
119
|
+
const isExpanded = session.id === expandedSessionId;
|
|
120
|
+
const handleClick = () => {
|
|
121
|
+
onToggleExpand(session.id);
|
|
122
|
+
};
|
|
98
123
|
if (isExpanded) {
|
|
99
124
|
return (_jsx("div", { children: _jsx(SessionDetailCard, { session: session, isActive: false }) }));
|
|
100
125
|
}
|
|
101
|
-
return (_jsxs("div", { className: "flex justify-between items-center p-2 rounded-md hover:bg-gray-100 cursor-pointer", onClick:
|
|
126
|
+
return (_jsxs("div", { className: "flex justify-between items-center p-2 rounded-md hover:bg-gray-100 cursor-pointer", onClick: handleClick, children: [_jsxs("div", { className: "flex items-center gap-2 text-sm", children: [_jsx(Icon, { icon: "chat-square-b", size: 16, className: "text-gray-500" }), _jsxs("span", { className: "font-semibold", children: ["Session ", session.order] }), _jsx("span", { className: "text-gray-400 text-xs", children: formatSessionDate(session.closedDate || session.createdDate) })] }), _jsx(Avatar, { src: (_a = session.supporter) === null || _a === void 0 ? void 0 : _a.avatar, size: 24, children: ((_d = (_c = (_b = session.supporter) === null || _b === void 0 ? void 0 : _b.username) === null || _c === void 0 ? void 0 : _c.charAt) === null || _d === void 0 ? void 0 : _d.call(_c, 0)) || "A" })] }));
|
|
102
127
|
};
|
|
103
128
|
const SessionSection = ({ sessions, isLoading }) => {
|
|
104
129
|
const [isOpen, { toggle }] = useBoolean(true);
|
|
105
|
-
|
|
130
|
+
const [expandedSessionId, setExpandedSessionId] = useState(null);
|
|
131
|
+
const handleToggleExpand = (sessionId) => {
|
|
132
|
+
setExpandedSessionId((prevId) => (prevId === sessionId ? null : sessionId));
|
|
133
|
+
};
|
|
134
|
+
useEffect(() => {
|
|
135
|
+
if (!isOpen) {
|
|
136
|
+
setExpandedSessionId(null);
|
|
137
|
+
}
|
|
138
|
+
}, [isOpen]);
|
|
139
|
+
return (_jsxs("div", { className: "flex flex-col border-b", children: [_jsxs("div", { role: "button", onClick: toggle, className: "flex items-center justify-between px-4 py-2 rounded-md hover:bg-gray-100 sticky top-0 bg-white z-10", children: [_jsx("h3", { className: "font-bold text-gray-500 text-xs tracking-wider", children: "SESSIONS" }), _jsx(Icon, { icon: isOpen ? "angle-up-o" : "angle-down-o", size: 18 })] }), isOpen && (_jsx("div", { className: "px-4 pt-2 pb-4", children: isLoading ? (_jsx("div", { className: "text-center mt-4", children: _jsx(Spin, {}) })) : (_jsx("div", { className: "flex flex-col gap-1", children: sessions.map((session) => (_jsx(ClosedSessionItem, { session: session, expandedSessionId: expandedSessionId, onToggleExpand: handleToggleExpand }, session.id))) })) }))] }));
|
|
106
140
|
};
|
|
107
141
|
export default SessionSection;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/constants/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"sdk.d.ts","sourceRoot":"","sources":["../../src/constants/sdk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AAEjD,eAAO,MAAM,QAAQ,EAAE,UAAU,CAAC,OAAO,MAAM,CAI7C,CAAC"}
|
package/dist/constants/sdk.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { getSDK } from "@openim/wasm-client-sdk";
|
|
2
|
-
import { coreWasmPath } from "./index";
|
|
3
2
|
export const DChatSDK = getSDK({
|
|
4
|
-
coreWasmPath,
|
|
3
|
+
coreWasmPath: typeof window !== "undefined" ? (window.localStorage.getItem("coreWasmPath") || '') : undefined,
|
|
5
4
|
sqlWasmPath: "/sql-wasm.wasm",
|
|
6
5
|
debug: process.env.NODE_ENV === "development",
|
|
7
6
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,cAAc,
|
|
1
|
+
{"version":3,"file":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,cAAc,YAsQ1B,CAAC"}
|
|
@@ -20,6 +20,9 @@ export const useGlobalEvent = () => {
|
|
|
20
20
|
const { mutate: refetchChatToken } = useRefetchChatToken();
|
|
21
21
|
const accessToken = useAuthStore((state) => state.accessToken);
|
|
22
22
|
const chatToken = useAuthStore((state) => state.chatToken);
|
|
23
|
+
const apiAddress = useAuthStore((state) => {
|
|
24
|
+
return state.apiAddress;
|
|
25
|
+
});
|
|
23
26
|
const revokedMessageHandler = ({ data }) => {
|
|
24
27
|
updateOneMessage({
|
|
25
28
|
clientMsgID: data.clientMsgID,
|
|
@@ -45,8 +48,14 @@ export const useGlobalEvent = () => {
|
|
|
45
48
|
}
|
|
46
49
|
};
|
|
47
50
|
const handleNewMessage = (newServerMsg) => {
|
|
51
|
+
let customData = null;
|
|
48
52
|
if (newServerMsg.contentType === MessageType.CustomMessage) {
|
|
49
|
-
|
|
53
|
+
try {
|
|
54
|
+
customData = JSON.parse(newServerMsg.customElem.data);
|
|
55
|
+
}
|
|
56
|
+
catch (e) {
|
|
57
|
+
console.error("Failed to parse custom message data", e);
|
|
58
|
+
}
|
|
50
59
|
if (customData &&
|
|
51
60
|
CustomType.CallingInvite <= customData.customType &&
|
|
52
61
|
customData.customType <= CustomType.CallingHungup) {
|
|
@@ -69,6 +78,7 @@ export const useGlobalEvent = () => {
|
|
|
69
78
|
var _a;
|
|
70
79
|
if (data) {
|
|
71
80
|
useAuthStore.getState().setChatToken((_a = data === null || data === void 0 ? void 0 : data.data) === null || _a === void 0 ? void 0 : _a.token);
|
|
81
|
+
getSelfUserInfo();
|
|
72
82
|
}
|
|
73
83
|
},
|
|
74
84
|
});
|
|
@@ -78,7 +88,7 @@ export const useGlobalEvent = () => {
|
|
|
78
88
|
getConversationListByReq(false);
|
|
79
89
|
};
|
|
80
90
|
const tryLogin = async () => {
|
|
81
|
-
const { userID, chatToken, platformID,
|
|
91
|
+
const { userID, chatToken, platformID, wsAddress } = useAuthStore.getState();
|
|
82
92
|
try {
|
|
83
93
|
await DChatSDK.login({
|
|
84
94
|
userID,
|
|
@@ -204,13 +214,13 @@ export const useGlobalEvent = () => {
|
|
|
204
214
|
};
|
|
205
215
|
}, []);
|
|
206
216
|
useEffect(() => {
|
|
207
|
-
if (!!accessToken) {
|
|
217
|
+
if (!!accessToken && apiAddress) {
|
|
208
218
|
userTokenHandler();
|
|
209
219
|
}
|
|
210
|
-
}, [accessToken]);
|
|
220
|
+
}, [accessToken, apiAddress]);
|
|
211
221
|
useEffect(() => {
|
|
212
|
-
if (!!chatToken) {
|
|
222
|
+
if (!!chatToken && apiAddress) {
|
|
213
223
|
loginCheck();
|
|
214
224
|
}
|
|
215
|
-
}, [chatToken]);
|
|
225
|
+
}, [chatToken, apiAddress]);
|
|
216
226
|
};
|
package/dist/services/api.js
CHANGED
|
@@ -8,9 +8,58 @@ export const apiInstance = axios.create({
|
|
|
8
8
|
},
|
|
9
9
|
timeout: TIMEOUT,
|
|
10
10
|
});
|
|
11
|
+
// Module-level refresh promise cache to prevent concurrent refresh calls
|
|
12
|
+
let sdkRefreshPromise = null;
|
|
11
13
|
apiInstance.interceptors.request.use((config) => {
|
|
12
14
|
return Object.assign(Object.assign({}, config), { baseURL: useAuthStore.getState().apiAddress, headers: Object.assign(Object.assign({}, config.headers), { Authorization: `Bearer ${useAuthStore.getState().accessToken}`, token: useAuthStore.getState().chatToken }) });
|
|
13
15
|
}, (error) => {
|
|
14
16
|
// Handle errors globally
|
|
15
17
|
return Promise.reject(error);
|
|
16
18
|
});
|
|
19
|
+
// Response interceptor for 401 handling - delegates to consumer app
|
|
20
|
+
apiInstance.interceptors.response.use((response) => response, async (error) => {
|
|
21
|
+
var _a;
|
|
22
|
+
const originalRequest = error.config;
|
|
23
|
+
// Prevent infinite loops with _retry flag
|
|
24
|
+
if (originalRequest._retry) {
|
|
25
|
+
return Promise.reject(error);
|
|
26
|
+
}
|
|
27
|
+
// Handle 401 responses
|
|
28
|
+
if (((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) === 401) {
|
|
29
|
+
const { onTokenRefresh, onAuthError } = useAuthStore.getState();
|
|
30
|
+
// No callback provided, reject immediately
|
|
31
|
+
if (!onTokenRefresh) {
|
|
32
|
+
const authError = new Error("Token expired, no refresh callback provided");
|
|
33
|
+
onAuthError === null || onAuthError === void 0 ? void 0 : onAuthError(authError);
|
|
34
|
+
return Promise.reject(error);
|
|
35
|
+
}
|
|
36
|
+
originalRequest._retry = true;
|
|
37
|
+
try {
|
|
38
|
+
// Cache refresh promise to ensure single refresh call for concurrent requests
|
|
39
|
+
if (!sdkRefreshPromise) {
|
|
40
|
+
sdkRefreshPromise = onTokenRefresh()
|
|
41
|
+
.then((newToken) => {
|
|
42
|
+
// Update SDK's access token
|
|
43
|
+
useAuthStore.getState().setAccessToken(newToken);
|
|
44
|
+
return newToken;
|
|
45
|
+
})
|
|
46
|
+
.catch((err) => {
|
|
47
|
+
// Notify consumer app of auth error
|
|
48
|
+
onAuthError === null || onAuthError === void 0 ? void 0 : onAuthError(err);
|
|
49
|
+
throw err;
|
|
50
|
+
})
|
|
51
|
+
.finally(() => {
|
|
52
|
+
sdkRefreshPromise = null;
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const newToken = await sdkRefreshPromise;
|
|
56
|
+
// Retry original request with new token
|
|
57
|
+
originalRequest.headers.Authorization = `Bearer ${newToken}`;
|
|
58
|
+
return apiInstance(originalRequest);
|
|
59
|
+
}
|
|
60
|
+
catch (refreshError) {
|
|
61
|
+
return Promise.reject(error);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return Promise.reject(error);
|
|
65
|
+
});
|
package/dist/store/auth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/store/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAKnC,QAAA,MAAM,YAAY,
|
|
1
|
+
{"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../../src/store/auth.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,QAAQ,CAAC;AAKnC,QAAA,MAAM,YAAY,wEA0Cf,CAAC;AAEJ,eAAe,YAAY,CAAC"}
|
package/dist/store/auth.js
CHANGED
|
@@ -12,9 +12,11 @@ const useAuthStore = create((set, get) => ({
|
|
|
12
12
|
applicationType: DChatApplicationType.OBEFE,
|
|
13
13
|
isCx: false,
|
|
14
14
|
isCrm: false,
|
|
15
|
+
onTokenRefresh: undefined,
|
|
16
|
+
onAuthError: undefined,
|
|
15
17
|
setAccessToken: (token) => set({ accessToken: token }),
|
|
16
18
|
setChatToken: (token) => set({ chatToken: token }),
|
|
17
|
-
initAuthStore: ({ accessToken, chatToken, apiAddress, wsAddress, platformID, userID, applicationType, isCrm, }) => {
|
|
19
|
+
initAuthStore: ({ accessToken, chatToken, apiAddress, wsAddress, platformID, userID, applicationType, isCrm, onTokenRefresh, onAuthError, }) => {
|
|
18
20
|
var _a;
|
|
19
21
|
const jwtParser = !!accessToken ? jwtDecode(accessToken) : null;
|
|
20
22
|
const isCx = !!isCrm && !!((_a = jwtParser === null || jwtParser === void 0 ? void 0 : jwtParser.role) === null || _a === void 0 ? void 0 : _a.includes("CRM_LIVE_CHAT"));
|
|
@@ -28,6 +30,8 @@ const useAuthStore = create((set, get) => ({
|
|
|
28
30
|
applicationType,
|
|
29
31
|
isCx,
|
|
30
32
|
isCrm: !!isCrm,
|
|
33
|
+
onTokenRefresh,
|
|
34
|
+
onAuthError,
|
|
31
35
|
});
|
|
32
36
|
},
|
|
33
37
|
}));
|