@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.
File without changes
@@ -1 +1 @@
1
- {"version":3,"file":"ActionBar.d.ts","sourceRoot":"","sources":["../../../../src/components/message/footer/ActionBar.tsx"],"names":[],"mappings":"AAkFA,QAAA,MAAM,SAAS,+CAwPd,CAAC;AAEF,eAAe,SAAS,CAAC"}
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
- const isImage = file.type === "image/jpeg" ||
93
- file.type === "image/png" ||
94
- file.type === "image/jpg";
95
- const isVideo = file.type.startsWith("video/");
96
- const maxSize = isVideo ? 200 : 5; // MB
97
- const isErrorSize = file.size / 1024 / 1024 > maxSize;
98
- // check format
99
- if ((!isImage && !isVideo) || isErrorSize) {
100
- if (!isImage && !isVideo) {
101
- message.error(`${file.name} không đúng định dạng JPG, JPEG, PNG hoặc VIDEO`);
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
- const currentVideos = listUploadFiles.filter((f) => { var _a; return (_a = f.type) === null || _a === void 0 ? void 0 : _a.startsWith("video/"); });
115
- if (newVideos.length > 1 || currentVideos.length + newVideos.length > 1) {
116
- message.error("Chỉ được phép tải lên 1 video duy nhất");
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,6 @@
1
+ interface PasteAndDropPluginProps {
2
+ onFilesAdded?: (files: File[]) => void;
3
+ }
4
+ declare const PasteAndDropPlugin: ({ onFilesAdded }: PasteAndDropPluginProps) => null;
5
+ export default PasteAndDropPlugin;
6
+ //# sourceMappingURL=PasteAndDropPlugin.d.ts.map
@@ -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;AAEvD,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,4CA4DpE,CAAC;AAEF,eAAe,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;AAcnE,eAAO,MAAM,iBAAiB,GAAI,wBAG/B;IACD,OAAO,EAAE,gBAAgB,CAAC;IAC1B,QAAQ,EAAE,OAAO,CAAC;CACnB,4CAwKA,CAAC;AAgCF,UAAU,mBAAmB;IAC3B,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,QAAA,MAAM,cAAc,GAAI,yBAAyB,mBAAmB,4CAgCnE,CAAC;AAEF,eAAe,cAAc,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 [note, setNote] = useState(session.note || "");
29
- const [issueDetail, setIssueDetail] = useState(session.issueDetail || "");
30
- const debouncedNote = useDebounce(note, { wait: 1000 });
31
- const debouncedIssueDetail = useDebounce(issueDetail, { wait: 1000 });
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
- setNote(session.note || "");
34
- setIssueDetail(session.issueDetail || "");
35
- }, [session.note, session.issueDetail]);
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 (debouncedNote !== (session.note || "")) {
38
- updateSession({ sessionId: session.id, note: debouncedNote }, {
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
- }, [debouncedNote, session.note, session.id, updateSession, queryClient]);
58
+ }, [
59
+ debouncedNoteInput,
60
+ session.note,
61
+ session.id,
62
+ updateSession,
63
+ queryClient,
64
+ ]);
47
65
  useEffect(() => {
48
- if (debouncedIssueDetail !== (session.issueDetail || "")) {
66
+ if (debouncedIssueDetailInput.sessionId === session.id &&
67
+ debouncedIssueDetailInput.value !== (session.issueDetail || "")) {
49
68
  updateSession({
50
69
  sessionId: session.id,
51
- issueDetail: debouncedIssueDetail,
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
- debouncedIssueDetail,
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: issueDetail, onChange: (e) => setIssueDetail(e.target.value), 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: note, onChange: (e) => setNote(e.target.value), 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" }))] }));
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 [isExpanded, { setTrue }] = useBoolean(false);
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: setTrue, 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" })] }));
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
- 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 }, session.id))) })) }))] }));
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;AAGjD,eAAO,MAAM,QAAQ,EAAE,UAAU,CAAC,OAAO,MAAM,CAI7C,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"}
@@ -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,YA4P1B,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
- const customData = JSON.parse(newServerMsg.customElem.data);
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, apiAddress, wsAddress } = useAuthStore.getState();
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
  };
@@ -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
+ });
@@ -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,wEAoCf,CAAC;AAEJ,eAAe,YAAY,CAAC"}
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"}
@@ -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
  }));