import { Box, FormControl, IconButton, InputAdornment, InputBase, SxProps, Theme, Typography, useMediaQuery, useTheme, } from "@mui/material"; import { getAuthToken, getMessageFromFetchError, throttle, TicketMessage, useSSESubscription, useTicketMessages, useTicketsFetcher, } from "@frontend/kitui"; import { enqueueSnackbar } from "notistack"; import { TouchEvent, useCallback, useEffect, useMemo, useRef, useState, WheelEvent, } from "react"; import ChatMessage from "../ChatMessage"; import SendIcon from "../icons/SendIcon"; import ArrowLeft from "@root/assets/Icons/arrowLeft"; import UserCircleIcon from "./UserCircleIcon"; import { sendTicketMessage, shownMessage, sendFile as sendFileRequest, } from "@root/api/ticket"; import { useSSETab } from "@root/utils/hooks/useSSETab"; import { ACCEPT_SEND_MEDIA_TYPES_MAP, checkAcceptableMediaType, MAX_FILE_SIZE, } from "@utils/checkAcceptableMediaType"; import { addOrUpdateUnauthMessages, clearTickets, incrementUnauthMessage, setIsMessageSending, setTicketData, setUnauthIsPreventAutoscroll, setUnauthTicketMessageFetchState, useTicketStore, } from "@root/stores/tickets"; import { useUserStore } from "@root/stores/user"; import AttachFileIcon from "@mui/icons-material/AttachFile"; import ChatDocument from "./ChatDocument"; import ChatImage from "./ChatImage"; import ChatVideo from "./ChatVideo"; import { createTicket } from "@api/ticket"; type ModalWarningType = | "errorType" | "errorSize" | "picture" | "video" | "audio" | "document" | null; interface Props { open: boolean; sx?: SxProps; onclickArrow?: () => void; } export default function Chat({ open = false, onclickArrow, sx }: Props) { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const isMobile = useMediaQuery(theme.breakpoints.down(800)); const [messageField, setMessageField] = useState(""); const [disableFileButton, setDisableFileButton] = useState(false); const [sseEnabled, setSseEnabled] = useState(true); const [modalWarningType, setModalWarningType] = useState(null); const chatBoxRef = useRef(null); const fileInputRef = useRef(null); const user = useUserStore((state) => state.user?._id); const ticket = useTicketStore( (state) => state[user ? "authData" : "unauthData"] ); const { messages, sessionData, isMessageSending, isPreventAutoscroll, lastMessageId, messagesPerPage, unauthTicketMessageFetchState: fetchState, apiPage: messageApiPage, } = ticket; const { isActiveSSETab, updateSSEValue } = useSSETab( "ticket", addOrUpdateUnauthMessages ); const getGreetingMessage: TicketMessage = useMemo(() => { const workingHoursMessage = "Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут"; const offHoursMessage = "Здравствуйте, к сожалению, сейчас операторы не работают. Задайте ваш вопрос, и вам ответят в 10:00 по московскому времени"; const date = new Date(); const currentHourUTC = date.getUTCHours(); const MscTime = 3; // Москва UTC+3; const moscowHour = (currentHourUTC + MscTime) % 24; const greetingMessage = moscowHour >= 3 && moscowHour < 10 ? offHoursMessage : workingHoursMessage; return { created_at: new Date().toISOString(), files: [], id: "111", message: greetingMessage, request_screenshot: "", session_id: "greetingMessage", shown: { me: 1 }, ticket_id: "111", user_id: "greetingMessage", }; }, [open]); useTicketMessages({ url: process.env.REACT_APP_DOMAIN + "/heruvym/v1.0.0/getMessages", isUnauth: true, ticketId: sessionData?.ticketId, messagesPerPage, messageApiPage, onSuccess: useCallback((messages) => { if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) { chatBoxRef.current.scrollTop = 1; } addOrUpdateUnauthMessages(messages); }, []), onError: useCallback((error: Error) => { const message = getMessageFromFetchError(error); if (message) enqueueSnackbar(message); }, []), onFetchStateChange: setUnauthTicketMessageFetchState, }); useSSESubscription({ enabled: sseEnabled && isActiveSSETab && Boolean(sessionData), url: process.env.REACT_APP_DOMAIN + `/heruvym/v1.0.0/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`, onNewData: (ticketMessages) => { console.log("ticketMessagesticketMessages") console.log(ticketMessages) const isTicketClosed = ticketMessages.some( (message) => message.session_id === "close" ); if (isTicketClosed) { clearTickets(); addOrUpdateUnauthMessages([getGreetingMessage]); if (!user) { localStorage.removeItem("unauth-ticket"); } return; } updateSSEValue(ticketMessages); addOrUpdateUnauthMessages(ticketMessages); }, onDisconnect: useCallback(() => { setUnauthIsPreventAutoscroll(false); setSseEnabled(false); }, []), marker: "ticket", }); useTicketsFetcher({ url: process.env.REACT_APP_DOMAIN + "/heruvym/v1.0.0/getTickets", ticketsPerPage: 10, ticketApiPage: 0, onSuccess: (result) => { if (result.data?.length) { const currentTicket = result.data.find( ({ origin, state }) => !origin.includes("/support") && state !== "close" ); if (!currentTicket) { return; } setTicketData({ ticketId: currentTicket.id, sessionId: currentTicket.sess, }); } }, onError: (error: Error) => { const message = getMessageFromFetchError(error); if (message) enqueueSnackbar(message); }, onFetchStateChange: () => {}, enabled: Boolean(user), }); const throttledScrollHandler = useMemo( () => throttle(() => { const chatBox = chatBoxRef.current; if (!chatBox) return; const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; const isPreventAutoscroll = scrollBottom > chatBox.clientHeight; setUnauthIsPreventAutoscroll(isPreventAutoscroll); if (fetchState !== "idle") return; if (chatBox.scrollTop < chatBox.clientHeight) { incrementUnauthMessage(); } }, 200), [fetchState] ); useEffect(() => { addOrUpdateUnauthMessages([getGreetingMessage]); scrollToBottom(); }, [open]); useEffect( function scrollOnNewMessage() { if (!chatBoxRef.current) return; if (!isPreventAutoscroll) { setTimeout(() => { scrollToBottom(); }, 50); } }, [lastMessageId] ); useEffect(() => { if (open) { const newMessages = messages.filter(({ shown }) => shown.me !== 1); newMessages.map(async ({ id }) => { await shownMessage(id); }); } }, [open, messages]); const loadNewMessages = ( event: WheelEvent | TouchEvent ) => { event.stopPropagation(); throttledScrollHandler(); }; async function handleSendMessage() { if (!messageField || isMessageSending) return; if (!sessionData?.ticketId) { setIsMessageSending(true); const [createTicketresult, createTicketerror] = await createTicket( "Unauth title", messageField, //При создании тикета, если нет у клиента токена, то хедер authorization вовсе не нужно слать Boolean(getAuthToken()) ); if (createTicketerror) { enqueueSnackbar(createTicketerror); } else if (createTicketresult) { setTicketData({ ticketId: createTicketresult.Ticket, sessionId: createTicketresult.sess, }); setSseEnabled(true); } setMessageField(""); setIsMessageSending(false); } else { setIsMessageSending(true); const [_, sendTicketMessageError] = await sendTicketMessage( sessionData.ticketId, messageField ); if (sendTicketMessageError) { enqueueSnackbar(sendTicketMessageError); } 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< HTMLInputElement | HTMLTextAreaElement > = (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSendMessage(); } }; const sendFile = async (file: File) => { if (file === undefined) return true; let data; if (!ticket.sessionData?.ticketId) { try { const [createTicketresult] = await createTicket("Unauth title", ""); if (createTicketresult) { data = createTicketresult; } if (data) { setTicketData({ ticketId: data.Ticket, sessionId: data.sess, }); } } catch (error: any) { const errorMessage = getMessageFromFetchError(error); if (errorMessage) enqueueSnackbar(errorMessage); } setIsMessageSending(false); } const ticketId = ticket.sessionData?.ticketId || data?.Ticket; if (ticketId !== undefined) { if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize"); const [, sendFileError] = await sendFileRequest(ticketId, file); if (sendFileError) { enqueueSnackbar(sendFileError); } 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); }; return ( <> {open && ( {isMobile && ( )} Данила онлайн-консультант время работы 10:00-3:00 по мск {ticket.sessionData?.ticketId && 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.length > 0 && isFileImage()) { return ( ); } if (message.files.length > 0 && isFileVideo()) { return ( ); } if (message.files.length > 0 && isFileDocument()) { return ( ); } return ( ); })} {!ticket.sessionData?.ticketId && ( )} setMessageField(e.target.value)} endAdornment={ { if (!disableFileButton) fileInputRef.current?.click(); }} > { if (target.files?.[0]) { sendFileHC(target.files?.[0]); } }} style={{ display: "none" }} type="file" /> } /> )} ); }