@droppii-org/chat-sdk 0.0.71 → 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":"useGlobalEvent.d.ts","sourceRoot":"","sources":["../../../src/hooks/global/useGlobalEvent.ts"],"names":[],"mappings":"AA8BA,eAAO,MAAM,cAAc,YAkQ1B,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,
@@ -75,6 +78,7 @@ export const useGlobalEvent = () => {
75
78
  var _a;
76
79
  if (data) {
77
80
  useAuthStore.getState().setChatToken((_a = data === null || data === void 0 ? void 0 : data.data) === null || _a === void 0 ? void 0 : _a.token);
81
+ getSelfUserInfo();
78
82
  }
79
83
  },
80
84
  });
@@ -84,7 +88,7 @@ export const useGlobalEvent = () => {
84
88
  getConversationListByReq(false);
85
89
  };
86
90
  const tryLogin = async () => {
87
- const { userID, chatToken, platformID, apiAddress, wsAddress } = useAuthStore.getState();
91
+ const { userID, chatToken, platformID, wsAddress } = useAuthStore.getState();
88
92
  try {
89
93
  await DChatSDK.login({
90
94
  userID,
@@ -210,13 +214,13 @@ export const useGlobalEvent = () => {
210
214
  };
211
215
  }, []);
212
216
  useEffect(() => {
213
- if (!!accessToken) {
217
+ if (!!accessToken && apiAddress) {
214
218
  userTokenHandler();
215
219
  }
216
- }, [accessToken]);
220
+ }, [accessToken, apiAddress]);
217
221
  useEffect(() => {
218
- if (!!chatToken) {
222
+ if (!!chatToken && apiAddress) {
219
223
  loginCheck();
220
224
  }
221
- }, [chatToken]);
225
+ }, [chatToken, apiAddress]);
222
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
  }));