import { Box, IconButton, InputAdornment, TextField, Typography, useMediaQuery, useTheme } from "@mui/material"; import { addOrUpdateMessages, clearMessageState, incrementMessageApiPage, setIsPreventAutoscroll, setMessageFetchState, 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, useRef, useState } from "react"; import { useParams } from "react-router-dom"; import { GetMessagesRequest, TicketMessage } from "@root/model/ticket"; import { getTicketMessages, sendTicketMessage, subscribeToTicketMessages } from "@root/api/tickets"; import { enqueueSnackbar } from "notistack"; import { useTicketStore } from "@root/stores/tickets"; import { throttle } from "@root/utils/throttle"; import { authStore } from "@root/stores/auth"; export default function Chat() { const token = authStore(state => state.token); 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 messagesFetchStateRef = useRef(useMessageStore.getState().fetchState); const isPreventAutoscroll = useMessageStore(state => state.isPreventAutoscroll); const lastMessageId = useMessageStore(state => state.lastMessageId); const ticket = tickets.find(ticket => ticket.id === ticketId); useEffect(function fetchTicketMessages() { if (!ticketId) return; const getTicketsBody: GetMessagesRequest = { amt: messagesPerPage, page: messageApiPage, ticket: ticketId, }; const controller = new AbortController(); setMessageFetchState("fetching"); getTicketMessages({ body: getTicketsBody, signal: controller.signal, }).then(result => { console.log("GetMessagesResponse", result); if (result?.length > 0) { if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1; addOrUpdateMessages(result); setMessageFetchState("idle"); } else setMessageFetchState("all fetched"); }).catch(error => { console.log("Error fetching messages", error); enqueueSnackbar(error.message); }); return () => { controller.abort(); }; }, [messageApiPage, messagesPerPage, ticketId]); useEffect(function subscribeToMessages() { if (!ticketId || !token) return; const unsubscribe = subscribeToTicketMessages({ ticketId, accessToken: token, onMessage(event) { try { const newMessage = JSON.parse(event.data) as TicketMessage; console.log("SSE: parsed newMessage:", newMessage); addOrUpdateMessages([newMessage]); } catch (error) { console.log("SSE: couldn't parse:", event.data); console.log("Error parsing message SSE", error); } }, onError(event) { console.log("SSE Error:", event); }, }); return () => { unsubscribe(); clearMessageState(); setIsPreventAutoscroll(false); }; }, [ticketId, token]); useEffect(function attachScrollHandler() { if (!chatBoxRef.current) return; const chatBox = chatBoxRef.current; const scrollHandler = () => { const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; const isPreventAutoscroll = scrollBottom > chatBox.clientHeight; setIsPreventAutoscroll(isPreventAutoscroll); if (messagesFetchStateRef.current !== "idle") return; if (chatBox.scrollTop < chatBox.clientHeight) { incrementMessageApiPage(); } }; const throttledScrollHandler = throttle(scrollHandler, 200); chatBox.addEventListener("scroll", throttledScrollHandler); return () => { chatBox.removeEventListener("scroll", throttledScrollHandler); }; }, []); useEffect(function scrollOnNewMessage() { if (!chatBoxRef.current) return; if (!isPreventAutoscroll) { setTimeout(() => { scrollToBottom(); }, 50); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [lastMessageId]); useEffect(() => useMessageStore.subscribe(state => (messagesFetchStateRef.current = state.fetchState)), []); 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({ body: { 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, } }} /> } ); }