import { Box, FormControl, IconButton, InputAdornment, InputBase, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material"; import { createTicket, getUnauthTicketMessages, sendTicketMessage, subscribeToUnauthTicketMessages } from "@root/api/tickets"; import { GetMessagesRequest, TicketMessage } from "@root/model/ticket"; import { addOrUpdateUnauthMessages, setUnauthTicketFetchState, useUnauthTicketStore, incrementUnauthMessageApiPage, setUnauthIsPreventAutoscroll, setUnauthSessionData, setIsMessageSending } from "@root/stores/unauthTicket"; import { enqueueSnackbar } from "notistack"; import { useEffect, useRef, useState } from "react"; import ChatMessage from "../ChatMessage"; import SendIcon from "../icons/SendIcon"; import UserCircleIcon from "./UserCircleIcon"; import { throttle } from "@root/utils/decorators"; import { getMessageFromFetchError } from "@root/utils/backendMessageHandler"; interface Props { sx?: SxProps; } export default function Chat({ sx }: Props) { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const [messageField, setMessageField] = useState(""); const sessionData = useUnauthTicketStore(state => state.sessionData); const messages = useUnauthTicketStore(state => state.messages); const messageApiPage = useUnauthTicketStore(state => state.apiPage); const messagesPerPage = useUnauthTicketStore(state => state.messagesPerPage); const isMessageSending = useUnauthTicketStore(state => state.isMessageSending); const messagesFetchStateRef = useRef(useUnauthTicketStore.getState().fetchState); const isPreventAutoscroll = useUnauthTicketStore(state => state.isPreventAutoscroll); const lastMessageId = useUnauthTicketStore(state => state.lastMessageId); const chatBoxRef = useRef(); useEffect(function fetchTicketMessages() { if (!sessionData) return; const getTicketsBody: GetMessagesRequest = { amt: messagesPerPage, page: messageApiPage, ticket: sessionData.ticketId, }; const controller = new AbortController(); setUnauthTicketFetchState("fetching"); getUnauthTicketMessages({ 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; addOrUpdateUnauthMessages(result); setUnauthTicketFetchState("idle"); } else setUnauthTicketFetchState("all fetched"); }).catch(error => { console.log("Error fetching messages", error); if (error.code !== "ERR_CANCELED") enqueueSnackbar(error.message); }); return () => { controller.abort(); }; }, [messageApiPage, messagesPerPage, sessionData]); useEffect(function subscribeToMessages() { if (!sessionData) return; const unsubscribe = subscribeToUnauthTicketMessages({ sessionId: sessionData.sessionId, ticketId: sessionData.ticketId, onMessage(event) { try { const newMessage = JSON.parse(event.data) as TicketMessage; if (!newMessage.id) throw new Error("Bad SSE response"); console.log("SSE: parsed newMessage:", newMessage); addOrUpdateUnauthMessages([newMessage]); } catch (error) { console.log("SSE: couldn't parse:", event.data); console.log("Error parsing SSE message", error); } }, onError(event) { console.log("SSE Error:", event); }, }); return () => { unsubscribe(); setUnauthIsPreventAutoscroll(false); }; }, [sessionData]); 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; setUnauthIsPreventAutoscroll(isPreventAutoscroll); if (messagesFetchStateRef.current !== "idle") return; if (chatBox.scrollTop < chatBox.clientHeight) { incrementUnauthMessageApiPage(); } }; 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(() => useUnauthTicketStore.subscribe(state => (messagesFetchStateRef.current = state.fetchState)), []); async function handleSendMessage() { if (!messageField || isMessageSending) return; if (!sessionData) { setIsMessageSending(true); createTicket({ Title: "Unauth title", Message: messageField, }, false).then(response => { setUnauthSessionData({ ticketId: response.Ticket, sessionId: response.sess, }); }).catch(error => { const errorMessage = getMessageFromFetchError(error); if (errorMessage) enqueueSnackbar(errorMessage); }).finally(() => { setMessageField(""); setIsMessageSending(false); }); } else { setIsMessageSending(true); sendTicketMessage({ ticket: sessionData.ticketId, message: messageField, lang: "ru", files: [], }, true).catch(error => { const errorMessage = getMessageFromFetchError(error); if (errorMessage) enqueueSnackbar(errorMessage); }).finally(() => { setMessageField(""); setIsMessageSending(false); }); } } function scrollToBottom(behavior?: ScrollBehavior) { if (!chatBoxRef.current) return; const chatBox = chatBoxRef.current; chatBox.scroll({ left: 0, top: chatBox.scrollHeight, behavior, }); } const handleTextfieldKeyPress: React.KeyboardEventHandler = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; return ( Мария онлайн-консультант {sessionData && messages.map((message) => ( ))} setMessageField(e.target.value)} endAdornment={ } /> ); }