front-hub/src/pages/Support/SupportChat.tsx

328 lines
9.3 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,
Button,
Fab,
FormControl,
IconButton,
InputAdornment,
InputBase,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import SendIcon from "@components/icons/SendIcon";
import { throttle, useToken } from "@frontend/kitui";
import { enqueueSnackbar } from "notistack";
import { useTicketStore } from "@root/stores/tickets";
import {
addOrUpdateMessages,
clearMessageState,
incrementMessageApiPage,
setIsPreventAutoscroll,
setTicketMessageFetchState,
useMessageStore,
} from "@root/stores/messages";
import { TicketMessage } from "@frontend/kitui";
import ChatMessage from "@root/components/ChatMessage";
import { cardShadow } from "@root/utils/theme";
import {
getMessageFromFetchError,
useEventListener,
useSSESubscription,
useTicketMessages,
} from "@frontend/kitui";
import { shownMessage, sendTicketMessage } from "@root/api/ticket";
import { withErrorBoundary } from "react-error-boundary";
import { handleComponentError } from "@root/utils/handleComponentError";
function SupportChat() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const isMobile = useMediaQuery(theme.breakpoints.up(460));
const [messageField, setMessageField] = useState<string>("");
const tickets = useTicketStore((state) => state.tickets);
const messages = useMessageStore((state) => state.messages);
const messageApiPage = useMessageStore((state) => state.apiPage);
const lastMessageId = useMessageStore((state) => state.lastMessageId);
const messagesPerPage = useMessageStore((state) => state.messagesPerPage);
const isPreventAutoscroll = useMessageStore(
(state) => state.isPreventAutoscroll
);
const token = useToken();
const ticketId = useParams().ticketId;
const ticket = tickets.find((ticket) => ticket.id === ticketId);
const chatBoxRef = useRef<HTMLDivElement>(null);
const fetchState = useMessageStore((state) => state.ticketMessageFetchState);
useTicketMessages({
url: "https://hub.pena.digital/heruvym/getMessages",
ticketId,
messagesPerPage,
messageApiPage,
onSuccess: (messages) => {
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1)
chatBoxRef.current.scrollTop = 1;
addOrUpdateMessages(messages);
},
onError: (error: Error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
},
onFetchStateChange: setTicketMessageFetchState,
});
useSSESubscription<TicketMessage>({
enabled: Boolean(token) && Boolean(ticketId),
url: `https://hub.pena.digital/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`,
onNewData: addOrUpdateMessages,
onDisconnect: useCallback(() => {
clearMessageState();
setIsPreventAutoscroll(false);
}, []),
marker: "ticket message",
});
const throttledScrollHandler = useMemo(
() =>
throttle(() => {
const chatBox = chatBoxRef.current;
if (!chatBox) return;
const scrollBottom =
chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20;
setIsPreventAutoscroll(isPreventAutoscroll);
if (fetchState !== "idle") return;
if (chatBox.scrollTop < chatBox.clientHeight) {
incrementMessageApiPage();
}
}, 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 (ticket) {
shownMessage(ticket.top_message.id);
}
}, [ticket]);
async function handleSendMessage() {
if (!ticket || !messageField) return;
const [, sendTicketMessageError] = await sendTicketMessage(
ticket.id,
messageField
);
if (sendTicketMessageError) {
return enqueueSnackbar(sendTicketMessageError);
}
setMessageField("");
}
function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return;
const chatBox = chatBoxRef.current;
chatBox.scroll({
left: 0,
top: chatBox.scrollHeight,
behavior,
});
}
const createdAt = ticket && new Date(ticket.created_at);
const createdAtString =
createdAt &&
createdAt.toLocaleDateString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
}) +
" " +
createdAt.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
});
return (
<Box
sx={{
backgroundColor: upMd ? "white" : undefined,
display: "flex",
flexGrow: 1,
maxHeight: upMd ? "443px" : undefined,
borderRadius: "12px",
p: upMd ? "20px" : undefined,
gap: "40px",
height: !upMd ? `calc(100% - ${isMobile ? 90 : 115}px)` : null,
boxShadow: upMd ? cardShadow : undefined,
}}
>
<Box
sx={{
display: "flex",
alignItems: "start",
flexDirection: "column",
flexGrow: 1,
}}
>
<Typography variant={upMd ? "h5" : "body2"} mb={"4px"}>
{ticket?.title}
</Typography>
<Typography
sx={{
fontWeight: 400,
fontSize: "14px",
lineHeight: "17px",
color: theme.palette.gray.main,
mb: upMd ? "9px" : "20px",
}}
>
Создан: {createdAtString}
</Typography>
<Box
sx={{
backgroundColor: "#ECECF3",
border: `1px solid ${theme.palette.gray.main}`,
borderRadius: "10px",
overflow: "hidden",
width: "100%",
minHeight: "345px",
display: "flex",
flexGrow: 1,
flexDirection: "column",
}}
>
<Box
sx={{
position: "relative",
width: "100%",
flexGrow: 1,
borderBottom: `1px solid ${theme.palette.gray.main}`,
height: "200px",
}}
>
{isPreventAutoscroll && (
<Fab
size="small"
onClick={() => scrollToBottom("smooth")}
sx={{
position: "absolute",
left: "10px",
bottom: "10px",
}}
>
<ArrowDownwardIcon />
</Fab>
)}
<Box
ref={chatBoxRef}
sx={{
display: "flex",
width: "100%",
flexDirection: "column",
gap: upMd ? "20px" : "16px",
px: upMd ? "20px" : "5px",
py: upMd ? "20px" : "13px",
overflowY: "auto",
height: "100%",
}}
>
{ticket &&
messages.map((message) => (
<ChatMessage
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={ticket.user === message.user_id}
/>
))}
</Box>
</Box>
<FormControl>
<InputBase
value={messageField}
fullWidth
placeholder="Текст обращения"
id="message"
multiline
sx={{
width: "100%",
p: 0,
}}
inputProps={{
sx: {
fontWeight: 400,
fontSize: "16px",
lineHeight: "19px",
pt: upMd ? "13px" : "28px",
pb: upMd ? "13px" : "24px",
px: "19px",
maxHeight: "calc(19px * 5)",
},
}}
onChange={(e) => setMessageField(e.target.value)}
endAdornment={
!upMd && (
<InputAdornment position="end">
<IconButton
onClick={handleSendMessage}
sx={{
height: "45px",
width: "45px",
mr: "13px",
p: 0,
}}
>
<SendIcon />
</IconButton>
</InputAdornment>
)
}
/>
</FormControl>
</Box>
</Box>
{upMd && (
<Box sx={{ alignSelf: "end" }}>
<Button
variant="pena-contained-dark"
onClick={handleSendMessage}
disabled={!messageField}
>
Отправить
</Button>
</Box>
)}
</Box>
);
}
export default withErrorBoundary(SupportChat, {
fallback: <Typography mt="8px" textAlign="center">Не удалось отобразить чат</Typography>,
onError: handleComponentError,
})