front-hub/src/components/FloatingSupportChat/Chat.tsx
2024-02-14 12:43:19 +03:00

337 lines
9.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
Box,
FormControl,
IconButton,
InputAdornment,
InputBase,
SxProps,
Theme,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { TicketMessage } from "@frontend/kitui";
import {
addOrUpdateUnauthMessages,
useUnauthTicketStore,
incrementUnauthMessageApiPage,
setUnauthIsPreventAutoscroll,
setUnauthSessionData,
setIsMessageSending,
setUnauthTicketMessageFetchState,
} from "@root/stores/unauthTicket";
import { enqueueSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ChatMessage from "../ChatMessage";
import SendIcon from "../icons/SendIcon";
import UserCircleIcon from "./UserCircleIcon";
import { throttle } from "@frontend/kitui";
import {
useTicketMessages,
getMessageFromFetchError,
useSSESubscription,
useEventListener,
createTicket,
} from "@frontend/kitui";
import { sendTicketMessage, shownMessage } from "@root/api/ticket";
interface Props {
open: boolean;
sx?: SxProps<Theme>;
}
export default function Chat({ open = false, sx }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [messageField, setMessageField] = useState<string>("");
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 isPreventAutoscroll = useUnauthTicketStore(
(state) => state.isPreventAutoscroll
);
const lastMessageId = useUnauthTicketStore((state) => state.lastMessageId);
const fetchState = useUnauthTicketStore(
(state) => state.unauthTicketMessageFetchState
);
const chatBoxRef = useRef<HTMLDivElement>(null);
useTicketMessages({
url: process.env.REACT_APP_DOMAIN + "/heruvym/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<TicketMessage>({
enabled: Boolean(sessionData),
url:
process.env.REACT_APP_DOMAIN +
`/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
onNewData: addOrUpdateUnauthMessages,
onDisconnect: useCallback(() => {
setUnauthIsPreventAutoscroll(false);
}, []),
marker: "ticket",
});
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) {
incrementUnauthMessageApiPage();
}
}, 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]
);
useEffect(() => {
if (open) {
const newMessages = messages.filter(({ shown }) => shown.me !== 1);
newMessages.map(async ({ id }) => {
await shownMessage(id);
});
}
}, [open, messages]);
async function handleSendMessage() {
if (!messageField || isMessageSending) return;
if (!sessionData) {
setIsMessageSending(true);
createTicket({
url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
body: {
Title: "Unauth title",
Message: messageField,
},
useToken: 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);
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();
}
};
return (
<>
{open && (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "clamp(250px, calc(100vh - 90px), 600px)",
backgroundColor: "#944FEE",
borderRadius: "8px",
...sx,
}}
>
<Box
sx={{
display: "flex",
gap: "9px",
pl: "22px",
pt: "12px",
pb: "20px",
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
}}
>
<UserCircleIcon />
<Box
sx={{
mt: "5px",
display: "flex",
flexDirection: "column",
gap: "3px",
}}
>
<Typography>Мария</Typography>
<Typography
sx={{
fontSize: "16px",
lineHeight: "19px",
}}
>
онлайн-консультант
</Typography>
</Box>
</Box>
<Box
sx={{
flexGrow: 1,
backgroundColor: "white",
borderRadius: "8px",
display: "flex",
flexDirection: "column",
}}
>
<Box
ref={chatBoxRef}
sx={{
display: "flex",
width: "100%",
flexBasis: 0,
flexDirection: "column",
gap: upMd ? "20px" : "16px",
px: upMd ? "20px" : "5px",
py: upMd ? "20px" : "13px",
overflowY: "auto",
flexGrow: 1,
}}
>
{sessionData &&
messages.map((message) => (
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={sessionData.sessionId === message.user_id}
/>
))}
</Box>
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
<InputBase
value={messageField}
fullWidth
placeholder="Введите сообщение..."
id="message"
multiline
onKeyDown={handleTextfieldKeyPress}
sx={{
width: "100%",
p: 0,
}}
inputProps={{
sx: {
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: upMd ? "30px" : "28px",
pb: upMd ? "30px" : "24px",
px: "19px",
maxHeight: "calc(19px * 5)",
color: "black",
},
}}
onChange={(e) => setMessageField(e.target.value)}
endAdornment={
<InputAdornment position="end">
<IconButton
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>
)}
</>
);
}