@droppii-org/chat-sdk
Version:
Droppii React Chat SDK
158 lines (157 loc) • 10.2 kB
JavaScript
"use client";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
import { useCallback, useEffect, useRef, useState } from "react";
import { Button, Upload, message, } from "antd";
import clsx from "clsx";
import EmojiPicker from "./EmojiPicker";
import { Icon } from "../../icon";
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
import { $createParagraphNode, $getRoot, $getSelection, $isRangeSelection, } from "lexical";
import { $generateHtmlFromNodes } from "@lexical/html";
import { $isListNode, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, } from "@lexical/list";
import { $isQuoteNode } from "@lexical/rich-text";
import { useMessageFooterContext } from ".";
import { useTranslation } from "react-i18next";
import { validateFile, validateVideoLimit } from "../../../utils/fileValidation";
const documentTypes = [
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
];
const EmojiIcon = (_jsxs("svg", { width: "20", height: "20", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: [_jsx("path", { d: "M14 8.39997C14 8.8418 13.6418 9.19997 13.2 9.19997C12.7581 9.19997 12.4 8.8418 12.4 8.39997C12.4 7.95814 12.7581 7.59997 13.2 7.59997C13.6418 7.59997 14 7.95814 14 8.39997Z", fill: "white" }), _jsx("path", { d: "M7.59997 8.39997C7.59997 8.8418 7.2418 9.19997 6.79997 9.19997C6.35814 9.19997 5.99997 8.8418 5.99997 8.39997C5.99997 7.95814 6.35814 7.59997 6.79997 7.59997C7.2418 7.59997 7.59997 7.95814 7.59997 8.39997Z", fill: "white" }), _jsx("path", { d: "M7.59997 12.4C7.59997 12.4 8.49997 13.2 9.99997 13.2C11.5 13.2 12.4 12.4 12.4 12.4M14 8.39997C14 8.8418 13.6418 9.19997 13.2 9.19997C12.7581 9.19997 12.4 8.8418 12.4 8.39997C12.4 7.95814 12.7581 7.59997 13.2 7.59997C13.6418 7.59997 14 7.95814 14 8.39997ZM18 9.99997C18 14.4182 14.4182 18 9.99997 18C5.58169 18 1.99997 14.4182 1.99997 9.99997C1.99997 5.58169 5.58169 1.99997 9.99997 1.99997C14.4182 1.99997 18 5.58169 18 9.99997ZM7.59997 8.39997C7.59997 8.8418 7.2418 9.19997 6.79997 9.19997C6.35814 9.19997 5.99997 8.8418 5.99997 8.39997C5.99997 7.95814 6.35814 7.59997 6.79997 7.59997C7.2418 7.59997 7.59997 7.95814 7.59997 8.39997Z", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" })] }));
const AttachIcon = (_jsx("svg", { width: "22", height: "22", viewBox: "0 0 20 20", fill: "none", xmlns: "http://www.w3.org/2000/svg", children: _jsx("path", { d: "M15.6 10.0001V11.2001C15.6 14.5137 12.9137 17.2001 9.59998 17.2001C6.28626 17.2001 3.59998 14.5137 3.59998 11.2001V6.79999C3.59998 4.59085 5.39084 2.79999 7.59998 2.79999C9.8091 2.79999 11.6 4.59085 11.6 6.79999V11.2C11.6 12.3045 10.7045 13.2 9.59998 13.2C8.49542 13.2 7.59998 12.3045 7.59998 11.2V7.99999", stroke: "currentColor", strokeWidth: "1.5", strokeLinecap: "round", strokeLinejoin: "round" }) }));
const ActionBar = () => {
const [editor] = useLexicalComposerContext();
const { onSendMessage, setListUploadFiles } = useMessageFooterContext();
const containerRef = useRef(null);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const { listUploadFiles } = useMessageFooterContext();
const [isEmptyInput, setIsEmptyInput] = useState(true);
const { t } = useTranslation();
const canSend = !isEmptyInput || listUploadFiles.length > 0;
const handleSend = useCallback(() => {
let plainText = "";
let richText = "";
// lấy plain text & html
editor.getEditorState().read(() => {
plainText = $getRoot().getTextContent();
richText = $generateHtmlFromNodes(editor);
});
if (plainText.trim().length > 0 || listUploadFiles.length > 0) {
onSendMessage({
plainText,
richText,
type: listUploadFiles.length > 0 ? "file" : "text",
});
}
editor.update(() => {
const root = $getRoot();
root.clear();
const paragraph = $createParagraphNode();
root.append(paragraph);
paragraph.select();
const selection = $getSelection();
if ($isRangeSelection(selection)) {
const anchorNode = selection.anchor.getNode();
const topLevelNode = anchorNode.getTopLevelElementOrThrow();
const isListNode = $isListNode(topLevelNode);
const isQuoteNode = $isQuoteNode(topLevelNode);
const listType = isListNode ? topLevelNode.getListType() : undefined;
if (selection.hasFormat("bold")) {
selection.formatText("bold");
}
if (selection.hasFormat("italic")) {
selection.formatText("italic");
}
if (selection.hasFormat("strikethrough")) {
selection.formatText("strikethrough");
}
if (isQuoteNode) {
// Nếu đang là QuoteNode → chuyển về Paragraph (bình thường)
const paragraph = $createParagraphNode();
// copy con của quote sang paragraph
const children = topLevelNode.getChildren();
children.forEach((child) => paragraph.append(child));
topLevelNode.replace(paragraph);
}
if (isListNode && listType) {
editor.dispatchCommand(listType === "number"
? INSERT_ORDERED_LIST_COMMAND
: INSERT_UNORDERED_LIST_COMMAND, undefined);
}
}
});
}, [editor, onSendMessage]);
const handleEmojiSelect = useCallback((emoji) => {
editor.update(() => {
const selection = $getSelection();
if ($isRangeSelection(selection)) {
selection.insertText(emoji); // chèn emoji như text bình thường
}
});
setShowEmojiPicker(false);
}, [editor]);
const beforeUploadImagesAndVideo = (file, fileList) => {
// Validate file type and size
const validation = validateFile(file, t);
if (!validation.isValid) {
message.error(validation.error);
return Upload.LIST_IGNORE;
}
// Validate video limit
const videoValidation = validateVideoLimit([file], listUploadFiles, t);
if (!videoValidation.isValid) {
message.error(videoValidation.error);
return Upload.LIST_IGNORE;
}
// Check if multiple videos in current upload batch
const newVideos = fileList.filter((f) => { var _a; return (_a = f.type) === null || _a === void 0 ? void 0 : _a.startsWith("video/"); });
if (newVideos.length > 1) {
message.error(t("video_limit_exceeded", {
defaultValue: "Chỉ được phép tải lên 1 video duy nhất",
}));
return Upload.LIST_IGNORE;
}
return false;
};
const beforeUploadFile = (file) => {
const isAllowed = documentTypes.includes(file.type);
if (!isAllowed) {
message.error(`${file.name} không đúng định dạng (chỉ hỗ trợ PDF, DOC, DOCX)`);
return Upload.LIST_IGNORE;
}
return false;
};
const handleChange = (info) => {
var _a;
let newList = [...info.fileList];
const lastFile = info.file;
if (documentTypes.includes(lastFile.type || "")) {
newList = newList.filter((f) => !documentTypes.includes(f.type || ""));
const originFile = ((_a = lastFile.originFileObj) !== null && _a !== void 0 ? _a : lastFile);
newList.push(Object.assign(Object.assign({}, lastFile), { originFileObj: originFile }));
}
setListUploadFiles(newList);
};
useEffect(() => {
const handleClickOutside = (event) => {
if (containerRef.current &&
!containerRef.current.contains(event.target)) {
setShowEmojiPicker(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
useEffect(() => {
return editor.registerUpdateListener(({ editorState }) => {
editorState.read(() => {
const root = $getRoot();
const textContent = root.getTextContent().trim();
setIsEmptyInput(textContent.length <= 0);
});
});
}, [editor]);
return (_jsxs("div", { className: "flex items-center justify-between px-4", ref: containerRef, children: [_jsxs("div", { className: "flex items-center gap-3 relative", children: [_jsx(Button, { type: "text", shape: "default", className: clsx("text-gray-500 w-8 h-8 p-0", showEmojiPicker ? "bg-blue-100 text-blue-600" : "text-gray-500"), onClick: () => setShowEmojiPicker(!showEmojiPicker), children: EmojiIcon }), _jsx(Upload, { accept: "image/jpeg, image/png, image/jpg, video/*", beforeUpload: beforeUploadImagesAndVideo, multiple: true, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, children: _jsx(Button, { type: "text", shape: "default", className: "text-gray-500 w-8 h-8 p-0 text-gray-500", children: _jsx(Icon, { icon: "image-02-o", size: 22 }) }) }), _jsx(Upload, { accept: ".doc,.docx,.pdf", beforeUpload: beforeUploadFile, onChange: handleChange, showUploadList: false, fileList: listUploadFiles, children: _jsx(Button, { type: "text", shape: "default", className: "text-gray-500 w-8 h-8 p-0 text-gray-500", children: AttachIcon }) }), showEmojiPicker && (_jsx(EmojiPicker, { onEmojiSelect: handleEmojiSelect, onClose: () => setShowEmojiPicker(false) }))] }), _jsx(Button, { type: "text", shape: "default", className: "text-gray-500 w-8 h-8 p-0", onClick: handleSend, disabled: !canSend, children: _jsx(Icon, { icon: "send-b", size: 28, className: `${canSend ? "text-blue-500" : "text-gray-400"}` }) })] }));
};
export default ActionBar;