import { Box, IconButton, InputAdornment, TextField, Typography, useMediaQuery, useTheme } from "@mui/material"; import { addOrUpdateMessages, clearMessageState, incrementMessageApiPage, setIsPreventAutoscroll, setTicketMessagesFetchState, useMessageStore, } from "@root/stores/messages"; import Message from "./Message"; import SendIcon from "@mui/icons-material/Send"; import AttachFileIcon from "@mui/icons-material/AttachFile"; import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react"; import { useParams } from "react-router-dom"; import { TicketMessage } from "@root/model/ticket"; import { sendTicketMessage } from "@root/api/tickets"; import { enqueueSnackbar } from "notistack"; import { useTicketStore } from "@root/stores/tickets"; import { getMessageFromFetchError, throttle, useEventListener, useSSESubscription, useTicketMessages, useToken, } from "@frontend/kitui"; import makeRequest from "@root/api/makeRequest"; import ChatImage from "./ChatImage"; import ChatDocument from "./ChatDocument"; import ChatVideo from "./ChatVideo"; import ChatMessage from "./ChatMessage"; import { ACCEPT_SEND_MEDIA_TYPES_MAP, MAX_FILE_SIZE, MAX_PHOTO_SIZE, MAX_VIDEO_SIZE } from "./fileUpload"; const tooLarge = "Файл слишком большой"; const checkAcceptableMediaType = (file: File) => { if (file === null) return ""; const segments = file?.name.split("."); const extension = segments[segments.length - 1]; const type = extension.toLowerCase(); switch (type) { case ACCEPT_SEND_MEDIA_TYPES_MAP.document.find((name) => name === type): if (file.size > MAX_FILE_SIZE) return tooLarge; return ""; case ACCEPT_SEND_MEDIA_TYPES_MAP.picture.find((name) => name === type): if (file.size > MAX_PHOTO_SIZE) return tooLarge; return ""; case ACCEPT_SEND_MEDIA_TYPES_MAP.video.find((name) => name === type): if (file.size > MAX_VIDEO_SIZE) return tooLarge; return ""; default: return "Не удалось отправить файл. Недопустимый тип"; } }; export default function Chat() { const token = useToken(); const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const tickets = useTicketStore((state) => state.tickets); const messages = useMessageStore((state) => state.messages); const [messageField, setMessageField] = useState(""); const ticketId = useParams().ticketId; const chatBoxRef = useRef(null); const messageApiPage = useMessageStore((state) => state.apiPage); const messagesPerPage = useMessageStore((state) => state.messagesPerPage); const isPreventAutoscroll = useMessageStore((state) => state.isPreventAutoscroll); const fetchState = useMessageStore((state) => state.ticketMessagesFetchState); const lastMessageId = useMessageStore((state) => state.lastMessageId); const fileInputRef = useRef(null); const [disableFileButton, setDisableFileButton] = useState(false); const ticket = tickets.find((ticket) => ticket.id === ticketId); useTicketMessages({ url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", ticketId, messagesPerPage, messageApiPage, onSuccess: (messages) => { if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1; addOrUpdateMessages(messages); }, onError: (error: Error) => { const message = getMessageFromFetchError(error); if (message) enqueueSnackbar(message); }, onFetchStateChange: setTicketMessagesFetchState, }); useSSESubscription({ enabled: Boolean(token) && Boolean(ticketId), url: process.env.REACT_APP_DOMAIN + `/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`, onNewData: addOrUpdateMessages, onDisconnect: () => { clearMessageState(); setIsPreventAutoscroll(false); }, marker: "ticket message", }); const throttledScrollHandler = useMemo( () => throttle(() => { const chatBox = chatBoxRef.current; if (!chatBox) return; const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20; setIsPreventAutoscroll(isPreventAutoscroll); if (fetchState !== "idle") return; if (chatBox.scrollTop < chatBox.clientHeight) { incrementMessageApiPage(); } }, 200), [fetchState] ); useEventListener("scroll", throttledScrollHandler, chatBoxRef); useEffect( function scrollOnNewMessage() { if (!chatBoxRef.current) return; if (!isPreventAutoscroll) { setTimeout(() => { scrollToBottom(); }, 50); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastMessageId] ); function scrollToBottom(behavior?: ScrollBehavior) { if (!chatBoxRef.current) return; const chatBox = chatBoxRef.current; chatBox.scroll({ left: 0, top: chatBox.scrollHeight, behavior, }); } function handleSendMessage() { if (!ticket || !messageField) return; sendTicketMessage({ files: [], lang: "ru", message: messageField, ticket: ticket.id, }); setMessageField(""); } const sendFile = async (file: File) => { if (file === undefined) return true; let data; const ticketId = ticket?.id; if (ticketId !== undefined) { try { const body = new FormData(); body.append(file.name, file); body.append("ticket", ticketId); await makeRequest({ url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles", body: body, method: "POST", }); } catch (error: any) { const errorMessage = getMessageFromFetchError(error); if (errorMessage) enqueueSnackbar(errorMessage); } return true; } }; const sendFileHC = async (file: File) => { const check = checkAcceptableMediaType(file); if (check.length > 0) { enqueueSnackbar(check); return; } setDisableFileButton(true); await sendFile(file); setDisableFileButton(false); }; function handleTextfieldKeyPress(e: KeyboardEvent) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } } return ( {ticket ? ticket.title : "Выберите тикет"} {ticket && messages.map((message) => { const isFileVideo = () => { if (message.files) { return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some((fileType) => message.files[0].toLowerCase().endsWith(fileType) ); } }; const isFileImage = () => { if (message.files) { return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some((fileType) => message.files[0].toLowerCase().endsWith(fileType) ); } }; const isFileDocument = () => { if (message.files) { return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some((fileType) => message.files[0].toLowerCase().endsWith(fileType) ); } }; if (message.files !== null && message.files.length > 0 && isFileImage()) { return ( ); } if (message.files !== null && message.files.length > 0 && isFileVideo()) { return ( ); } if (message.files !== null && message.files.length > 0 && isFileDocument()) { return ( ); } return ( ); })} {ticket && ( setMessageField(e.target.value)} onKeyPress={handleTextfieldKeyPress} id="message-input" placeholder="Написать сообщение" fullWidth multiline maxRows={8} InputProps={{ style: { backgroundColor: theme.palette.content.main, color: theme.palette.secondary.main, }, endAdornment: ( { if (!disableFileButton) fileInputRef.current?.click(); }} sx={{ height: "45px", width: "45px", p: 0, }} > { if (e.target.files?.[0]) sendFileHC(e.target.files?.[0]); }} style={{ display: "none" }} type="file" /> ), }} InputLabelProps={{ style: { color: theme.palette.secondary.main, }, }} /> )} ); }