front-hub/src/components/FloatingSupportChat/Chat.tsx

282 lines
11 KiB
TypeScript
Raw Normal View History

2023-04-13 16:48:17 +00:00
import { Box, FormControl, IconButton, InputAdornment, InputBase, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
2023-05-03 10:35:20 +00:00
import { createTicket, getUnauthTicketMessages, sendTicketMessage, subscribeToUnauthTicketMessages } from "@root/api/tickets";
2023-04-13 16:48:17 +00:00
import { GetMessagesRequest, TicketMessage } from "@root/model/ticket";
2023-05-03 16:24:15 +00:00
import { addOrUpdateUnauthMessages, setUnauthTicketFetchState, useUnauthTicketStore, incrementUnauthMessageApiPage, setUnauthIsPreventAutoscroll, setUnauthSessionData } from "@root/stores/unauthTicket";
2023-04-13 16:48:17 +00:00
import { enqueueSnackbar } from "notistack";
import { useEffect, useRef, useState } from "react";
import ChatMessage from "../ChatMessage";
import SendIcon from "../icons/SendIcon";
import UserCircleIcon from "./UserCircleIcon";
2023-05-03 16:24:15 +00:00
import { throttle } from "@root/utils/decorators";
2023-04-13 16:48:17 +00:00
interface Props {
sx?: SxProps<Theme>;
}
export default function Chat({ sx }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [messageField, setMessageField] = useState<string>("");
2023-05-03 16:24:15 +00:00
const sessionData = useUnauthTicketStore(state => state.sessionData);
2023-04-13 16:48:17 +00:00
const messages = useUnauthTicketStore(state => state.messages);
const messageApiPage = useUnauthTicketStore(state => state.apiPage);
const messagesPerPage = useUnauthTicketStore(state => state.messagesPerPage);
2023-05-03 16:24:15 +00:00
const messagesFetchStateRef = useRef(useUnauthTicketStore.getState().fetchState);
const isPreventAutoscroll = useUnauthTicketStore(state => state.isPreventAutoscroll);
const lastMessageId = useUnauthTicketStore(state => state.lastMessageId);
2023-04-13 16:48:17 +00:00
const chatBoxRef = useRef<HTMLDivElement>();
useEffect(function fetchTicketMessages() {
2023-05-03 16:24:15 +00:00
if (!sessionData) return;
2023-04-13 16:48:17 +00:00
const getTicketsBody: GetMessagesRequest = {
amt: messagesPerPage,
page: messageApiPage,
2023-05-03 16:24:15 +00:00
ticket: sessionData.ticketId,
2023-04-13 16:48:17 +00:00
};
const controller = new AbortController();
setUnauthTicketFetchState("fetching");
getUnauthTicketMessages({
body: getTicketsBody,
signal: controller.signal,
}).then(result => {
console.log("GetMessagesResponse", result);
if (result?.length > 0) {
2023-05-12 19:10:43 +00:00
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) chatBoxRef.current.scrollTop = 1;
2023-04-13 16:48:17 +00:00
addOrUpdateUnauthMessages(result);
setUnauthTicketFetchState("idle");
} else setUnauthTicketFetchState("all fetched");
}).catch(error => {
console.log("Error fetching messages", error);
2023-05-03 16:24:15 +00:00
if (error.code !== "ERR_CANCELED") enqueueSnackbar(error.message);
2023-04-13 16:48:17 +00:00
});
return () => {
controller.abort();
};
2023-05-03 16:24:15 +00:00
}, [messageApiPage, messagesPerPage, sessionData]);
2023-04-13 16:48:17 +00:00
useEffect(function subscribeToMessages() {
2023-05-03 16:24:15 +00:00
if (!sessionData) return;
2023-04-13 16:48:17 +00:00
const unsubscribe = subscribeToUnauthTicketMessages({
2023-05-03 18:13:51 +00:00
sessionId: sessionData.sessionId,
ticketId: sessionData.ticketId,
2023-04-13 16:48:17 +00:00
onMessage(event) {
try {
const newMessage = JSON.parse(event.data) as TicketMessage;
2023-05-03 09:54:04 +00:00
if (!newMessage.id) throw new Error("Bad SSE response");
2023-05-03 10:35:20 +00:00
console.log("SSE: parsed newMessage:", newMessage);
2023-04-13 16:48:17 +00:00
addOrUpdateUnauthMessages([newMessage]);
} catch (error) {
2023-05-03 09:54:04 +00:00
console.log("SSE: couldn't parse:", event.data);
console.log("Error parsing SSE message", error);
2023-04-13 16:48:17 +00:00
}
},
onError(event) {
2023-05-03 09:54:04 +00:00
console.log("SSE Error:", event);
2023-04-13 16:48:17 +00:00
},
});
return () => {
unsubscribe();
2023-05-03 16:24:15 +00:00
setUnauthIsPreventAutoscroll(false);
2023-04-13 16:48:17 +00:00
};
2023-05-03 16:24:15 +00:00
}, [sessionData]);
2023-04-13 16:48:17 +00:00
2023-05-03 16:24:15 +00:00
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)), []);
2023-04-13 16:48:17 +00:00
async function handleSendMessage() {
if (!messageField) return;
2023-05-03 16:24:15 +00:00
if (!sessionData) {
2023-04-13 16:48:17 +00:00
const response = await createTicket({
Title: "Unauth title",
Message: messageField,
}, false);
2023-05-03 16:24:15 +00:00
setUnauthSessionData({
ticketId: response.Ticket,
sessionId: response.sess,
});
2023-04-13 16:48:17 +00:00
} else {
sendTicketMessage({
2023-05-03 16:24:15 +00:00
ticket: sessionData.ticketId,
2023-04-13 16:48:17 +00:00
message: messageField,
lang: "ru",
files: [],
2023-05-03 16:24:15 +00:00
}, true).catch(error => {
console.log("Coudn't send message", error);
enqueueSnackbar(error.message);
});
2023-04-13 16:48:17 +00:00
}
setMessageField("");
}
2023-05-03 16:24:15 +00:00
function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return;
const chatBox = chatBoxRef.current;
chatBox.scroll({
left: 0,
top: chatBox.scrollHeight,
behavior,
});
}
2023-05-03 09:54:04 +00:00
const handleTextfieldKeyPress: React.KeyboardEventHandler<HTMLInputElement | HTMLTextAreaElement> = (e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
2023-04-13 16:48:17 +00:00
return (
<Box sx={{
display: "flex",
flexDirection: "column",
2023-05-11 15:29:07 +00:00
height: "clamp(250px, 100dvh - 90px, 600px)",
2023-04-13 16:48:17 +00:00
backgroundColor: "#944FEE",
borderRadius: "8px",
...sx,
}}>
<Box sx={{
display: "flex",
gap: "9px",
pl: "22px",
pt: "12px",
2023-05-03 10:35:20 +00:00
pb: "20px",
2023-04-13 16:48:17 +00:00
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={{
2023-05-03 10:35:20 +00:00
flexGrow: 1,
2023-04-13 16:48:17 +00:00
backgroundColor: "white",
borderRadius: "8px",
display: "flex",
flexDirection: "column",
}}>
<Box
ref={chatBoxRef}
sx={{
display: "flex",
width: "100%",
2023-05-03 16:24:15 +00:00
flexBasis: 0,
2023-04-13 16:48:17 +00:00
flexDirection: "column",
gap: upMd ? "20px" : "16px",
px: upMd ? "20px" : "5px",
py: upMd ? "20px" : "13px",
overflowY: "auto",
flexGrow: 1,
}}
>
2023-05-03 16:24:15 +00:00
{sessionData && messages.map((message) => (
2023-04-13 16:48:17 +00:00
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
2023-05-09 16:28:52 +00:00
createdAt={message.created_at}
2023-05-03 16:24:15 +00:00
isSelf={sessionData.sessionId === message.user_id}
2023-04-13 16:48:17 +00:00
/>
))}
</Box>
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
<InputBase
value={messageField}
fullWidth
placeholder="Введите сообщение..."
id="message"
multiline
2023-05-03 09:54:04 +00:00
onKeyDown={handleTextfieldKeyPress}
2023-04-13 16:48:17 +00:00
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
onClick={handleSendMessage}
sx={{
height: "53px",
width: "53px",
mr: "13px",
p: 0,
}}
>
<SendIcon style={{
width: "100%",
height: "100%",
}} />
</IconButton>
</InputAdornment>
}
/>
</FormControl>
</Box>
</Box>
);
}