Merge branch 'support' into support-fixes

This commit is contained in:
Nastya 2024-02-13 21:32:53 +03:00
commit 4f3a15c735
2 changed files with 305 additions and 302 deletions

@ -1,319 +1,326 @@
import { import {
Box, Box,
FormControl, FormControl,
IconButton, IconButton,
InputAdornment, InputAdornment,
InputBase, InputBase,
SxProps, SxProps,
Theme, Theme,
Typography, Typography,
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material" } from "@mui/material";
import { TicketMessage } from "@frontend/kitui" import { TicketMessage } from "@frontend/kitui";
import { import {
addOrUpdateUnauthMessages, addOrUpdateUnauthMessages,
useUnauthTicketStore, useUnauthTicketStore,
incrementUnauthMessageApiPage, incrementUnauthMessageApiPage,
setUnauthIsPreventAutoscroll, setUnauthIsPreventAutoscroll,
setUnauthSessionData, setUnauthSessionData,
setIsMessageSending, setIsMessageSending,
setUnauthTicketMessageFetchState, setUnauthTicketMessageFetchState,
} from "@root/stores/unauthTicket" } from "@root/stores/unauthTicket";
import { enqueueSnackbar } from "notistack" import { enqueueSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ChatMessage from "../ChatMessage" import ChatMessage from "../ChatMessage";
import SendIcon from "../icons/SendIcon" import SendIcon from "../icons/SendIcon";
import UserCircleIcon from "./UserCircleIcon" import UserCircleIcon from "./UserCircleIcon";
import { throttle } from "@frontend/kitui" import { throttle } from "@frontend/kitui";
import { import {
useTicketMessages, useTicketMessages,
getMessageFromFetchError, getMessageFromFetchError,
useSSESubscription, useSSESubscription,
useEventListener, useEventListener,
createTicket, createTicket,
} from "@frontend/kitui" } from "@frontend/kitui";
import { sendTicketMessage } from "@root/api/ticket" import { sendTicketMessage } from "@root/api/ticket";
interface Props { interface Props {
open: boolean;
sx?: SxProps<Theme>; sx?: SxProps<Theme>;
} }
export default function Chat({ sx }: Props) { export default function Chat({ open = false, sx }: Props) {
const theme = useTheme() const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md")) const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [messageField, setMessageField] = useState<string>("") const [messageField, setMessageField] = useState<string>("");
const sessionData = useUnauthTicketStore((state) => state.sessionData) const sessionData = useUnauthTicketStore((state) => state.sessionData);
const messages = useUnauthTicketStore((state) => state.messages) const messages = useUnauthTicketStore((state) => state.messages);
const messageApiPage = useUnauthTicketStore((state) => state.apiPage) const messageApiPage = useUnauthTicketStore((state) => state.apiPage);
const messagesPerPage = useUnauthTicketStore( const messagesPerPage = useUnauthTicketStore(
(state) => state.messagesPerPage (state) => state.messagesPerPage
) );
const isMessageSending = useUnauthTicketStore( const isMessageSending = useUnauthTicketStore(
(state) => state.isMessageSending (state) => state.isMessageSending
) );
const isPreventAutoscroll = useUnauthTicketStore( const isPreventAutoscroll = useUnauthTicketStore(
(state) => state.isPreventAutoscroll (state) => state.isPreventAutoscroll
) );
const lastMessageId = useUnauthTicketStore((state) => state.lastMessageId) const lastMessageId = useUnauthTicketStore((state) => state.lastMessageId);
const fetchState = useUnauthTicketStore( const fetchState = useUnauthTicketStore(
(state) => state.unauthTicketMessageFetchState (state) => state.unauthTicketMessageFetchState
) );
const chatBoxRef = useRef<HTMLDivElement>(null) const chatBoxRef = useRef<HTMLDivElement>(null);
useTicketMessages({ useTicketMessages({
url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages",
isUnauth: true, isUnauth: true,
ticketId: sessionData?.ticketId, ticketId: sessionData?.ticketId,
messagesPerPage, messagesPerPage,
messageApiPage, messageApiPage,
onSuccess: useCallback((messages) => { onSuccess: useCallback((messages) => {
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1)
chatBoxRef.current.scrollTop = 1 chatBoxRef.current.scrollTop = 1;
addOrUpdateUnauthMessages(messages) addOrUpdateUnauthMessages(messages);
}, []), }, []),
onError: useCallback((error: Error) => { onError: useCallback((error: Error) => {
const message = getMessageFromFetchError(error) const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message) if (message) enqueueSnackbar(message);
}, []), }, []),
onFetchStateChange: setUnauthTicketMessageFetchState, onFetchStateChange: setUnauthTicketMessageFetchState,
}) });
useSSESubscription<TicketMessage>({ useSSESubscription<TicketMessage>({
enabled: Boolean(sessionData), enabled: Boolean(sessionData),
url: process.env.REACT_APP_DOMAIN + `/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`, url:
onNewData: addOrUpdateUnauthMessages, process.env.REACT_APP_DOMAIN +
onDisconnect: useCallback(() => { `/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
setUnauthIsPreventAutoscroll(false) onNewData: addOrUpdateUnauthMessages,
}, []), onDisconnect: useCallback(() => {
marker: "ticket", setUnauthIsPreventAutoscroll(false);
}) }, []),
marker: "ticket",
});
const throttledScrollHandler = useMemo( const throttledScrollHandler = useMemo(
() => () =>
throttle(() => { throttle(() => {
const chatBox = chatBoxRef.current const chatBox = chatBoxRef.current;
if (!chatBox) return if (!chatBox) return;
const scrollBottom = const scrollBottom =
chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
setUnauthIsPreventAutoscroll(isPreventAutoscroll) setUnauthIsPreventAutoscroll(isPreventAutoscroll);
if (fetchState !== "idle") return if (fetchState !== "idle") return;
if (chatBox.scrollTop < chatBox.clientHeight) { if (chatBox.scrollTop < chatBox.clientHeight) {
incrementUnauthMessageApiPage() incrementUnauthMessageApiPage();
} }
}, 200), }, 200),
[fetchState] [fetchState]
) );
useEventListener("scroll", throttledScrollHandler, chatBoxRef) useEventListener("scroll", throttledScrollHandler, chatBoxRef);
useEffect( useEffect(
function scrollOnNewMessage() { function scrollOnNewMessage() {
if (!chatBoxRef.current) return if (!chatBoxRef.current) return;
if (!isPreventAutoscroll) { if (!isPreventAutoscroll) {
setTimeout(() => { setTimeout(() => {
scrollToBottom() scrollToBottom();
}, 50) }, 50);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, },
[lastMessageId] [lastMessageId]
) );
async function handleSendMessage() { async function handleSendMessage() {
if (!messageField || isMessageSending) return if (!messageField || isMessageSending) return;
if (!sessionData) { if (!sessionData) {
setIsMessageSending(true) setIsMessageSending(true);
createTicket({ createTicket({
url: process.env.REACT_APP_DOMAIN + "/heruvym/create", url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
body: { body: {
Title: "Unauth title", Title: "Unauth title",
Message: messageField, Message: messageField,
}, },
useToken: false, useToken: false,
}) })
.then((response) => { .then((response) => {
setUnauthSessionData({ setUnauthSessionData({
ticketId: response.Ticket, ticketId: response.Ticket,
sessionId: response.sess, sessionId: response.sess,
}) });
}) })
.catch((error) => { .catch((error) => {
const errorMessage = getMessageFromFetchError(error) const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage) if (errorMessage) enqueueSnackbar(errorMessage);
}) })
.finally(() => { .finally(() => {
setMessageField("") setMessageField("");
setIsMessageSending(false) setIsMessageSending(false);
}) });
} else { } else {
setIsMessageSending(true) setIsMessageSending(true);
const [_, sendTicketMessageError] = await sendTicketMessage( const [_, sendTicketMessageError] = await sendTicketMessage(
sessionData.ticketId, sessionData.ticketId,
messageField messageField
) );
if (sendTicketMessageError) { if (sendTicketMessageError) {
enqueueSnackbar(sendTicketMessageError) enqueueSnackbar(sendTicketMessageError);
} }
setMessageField("") setMessageField("");
setIsMessageSending(false) 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()
}
} }
return ( function scrollToBottom(behavior?: ScrollBehavior) {
<Box if (!chatBoxRef.current) return;
sx={{
display: "flex", const chatBox = chatBoxRef.current;
flexDirection: "column", chatBox.scroll({
height: "clamp(250px, calc(100vh - 90px), 600px)", left: 0,
backgroundColor: "#944FEE", top: chatBox.scrollHeight,
borderRadius: "8px", behavior,
...sx, });
}} }
>
<Box const handleTextfieldKeyPress: React.KeyboardEventHandler<
sx={{ HTMLInputElement | HTMLTextAreaElement
display: "flex", > = (e) => {
gap: "9px", if (e.key === "Enter" && !e.shiftKey) {
pl: "22px", e.preventDefault();
pt: "12px", handleSendMessage();
pb: "20px", }
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))", };
}}
> return (
<UserCircleIcon /> <>
<Box {open && (
sx={{ <Box
mt: "5px", sx={{
display: "flex", display: "flex",
flexDirection: "column", flexDirection: "column",
gap: "3px", height: "clamp(250px, calc(100vh - 90px), 600px)",
}} backgroundColor: "#944FEE",
> borderRadius: "8px",
<Typography>Мария</Typography> ...sx,
<Typography }}
sx={{ >
fontSize: "16px", <Box
lineHeight: "19px", sx={{
}} display: "flex",
> gap: "9px",
онлайн-консультант pl: "22px",
</Typography> pt: "12px",
</Box> pb: "20px",
</Box> filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
<Box }}
sx={{ >
flexGrow: 1, <UserCircleIcon />
backgroundColor: "white", <Box
borderRadius: "8px", sx={{
display: "flex", mt: "5px",
flexDirection: "column", display: "flex",
}} flexDirection: "column",
> gap: "3px",
<Box }}
ref={chatBoxRef} >
sx={{ <Typography>Мария</Typography>
display: "flex", <Typography
width: "100%", sx={{
flexBasis: 0, fontSize: "16px",
flexDirection: "column", lineHeight: "19px",
gap: upMd ? "20px" : "16px", }}
px: upMd ? "20px" : "5px", >
py: upMd ? "20px" : "13px", онлайн-консультант
overflowY: "auto", </Typography>
flexGrow: 1, </Box>
}} </Box>
> <Box
{sessionData && sx={{
messages.map((message) => ( flexGrow: 1,
<ChatMessage backgroundColor: "white",
unAuthenticated borderRadius: "8px",
key={message.id} display: "flex",
text={message.message} flexDirection: "column",
createdAt={message.created_at} }}
isSelf={sessionData.sessionId === message.user_id} >
/> <Box
))} ref={chatBoxRef}
</Box> sx={{
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}> display: "flex",
<InputBase width: "100%",
value={messageField} flexBasis: 0,
fullWidth flexDirection: "column",
placeholder="Введите сообщение..." gap: upMd ? "20px" : "16px",
id="message" px: upMd ? "20px" : "5px",
multiline py: upMd ? "20px" : "13px",
onKeyDown={handleTextfieldKeyPress} overflowY: "auto",
sx={{ flexGrow: 1,
width: "100%", }}
p: 0, >
}} {sessionData &&
inputProps={{ messages.map((message) => (
sx: { <ChatMessage
fontWeight: 400, unAuthenticated
fontSize: "16px", key={message.id}
lineHeight: "19px", text={message.message}
pt: upMd ? "30px" : "28px", createdAt={message.created_at}
pb: upMd ? "30px" : "24px", isSelf={sessionData.sessionId === message.user_id}
px: "19px", />
maxHeight: "calc(19px * 5)", ))}
color: "black", </Box>
}, <FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
}} <InputBase
onChange={(e) => setMessageField(e.target.value)} value={messageField}
endAdornment={ fullWidth
<InputAdornment position="end"> placeholder="Введите сообщение..."
<IconButton id="message"
disabled={isMessageSending} multiline
onClick={handleSendMessage} onKeyDown={handleTextfieldKeyPress}
sx={{ sx={{
height: "53px", width: "100%",
width: "53px", p: 0,
mr: "13px", }}
p: 0, inputProps={{
opacity: isMessageSending ? 0.3 : 1, sx: {
}} fontWeight: 400,
> fontSize: "16px",
<SendIcon lineHeight: "19px",
style={{ pt: upMd ? "30px" : "28px",
width: "100%", pb: upMd ? "30px" : "24px",
height: "100%", px: "19px",
}} maxHeight: "calc(19px * 5)",
/> color: "black",
</IconButton> },
</InputAdornment> }}
} onChange={(e) => setMessageField(e.target.value)}
/> endAdornment={
</FormControl> <InputAdornment position="end">
</Box> <IconButton
</Box> disabled={isMessageSending}
) onClick={handleSendMessage}
sx={{
height: "53px",
width: "53px",
mr: "13px",
p: 0,
opacity: isMessageSending ? 0.3 : 1,
}}
>
<SendIcon
style={{
width: "100%",
height: "100%",
}}
/>
</IconButton>
</InputAdornment>
}
/>
</FormControl>
</Box>
</Box>
)}
</>
);
} }

@ -43,14 +43,10 @@ export default function FloatingSupportChat() {
zIndex: 10, zIndex: 10,
}} }}
> >
{isChatOpened && ( <Chat
<Chat open={isChatOpened}
sx={{ sx={{ alignSelf: "start", width: "clamp(200px, 100%, 400px)" }}
alignSelf: "start", />
width: "clamp(200px, 100%, 400px)",
}}
/>
)}
<Fab <Fab
disableRipple disableRipple
sx={{ sx={{