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"; 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 ticket = tickets.find(ticket => ticket.id === ticketId); useTicketMessages({ url: "https://admin.pena.digital/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: `https://admin.pena.digital/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(""); } function handleAddAttachment() { } function handleTextfieldKeyPress(e: KeyboardEvent) { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } } return ( {ticket ? ticket.title : "Выберите тикет"} {ticket && messages.map(message => )} {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: ( ) }} InputLabelProps={{ style: { color: theme.palette.secondary.main, } }} /> } ); }