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

332 lines
9.4 KiB
TypeScript
Raw Normal View History

2023-08-10 06:53:12 +00:00
import {
Box,
Fab,
FormControl,
IconButton,
InputAdornment,
InputBase,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
2023-06-06 13:13:58 +00:00
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2022-12-24 10:41:12 +00:00
import { useParams } from "react-router-dom";
import CustomButton from "@components/CustomButton";
import SendIcon from "@components/icons/SendIcon";
2023-06-24 18:16:02 +00:00
import { makeRequest, throttle, useToken } from "@frontend/kitui";
2023-03-31 15:01:43 +00:00
import { enqueueSnackbar } from "notistack";
import { useTicketStore } from "@root/stores/tickets";
2023-08-10 06:53:12 +00:00
import {
addOrUpdateMessages,
clearMessageState,
incrementMessageApiPage,
setIsPreventAutoscroll,
useMessageStore,
} from "@root/stores/messages";
2023-06-06 13:13:58 +00:00
import { TicketMessage } from "@frontend/kitui";
2023-04-13 16:48:17 +00:00
import ChatMessage from "@root/components/ChatMessage";
2023-08-22 10:28:22 +00:00
import { cardShadow } from "@root/utils/theme";
2023-08-10 06:53:12 +00:00
import {
getMessageFromFetchError,
useEventListener,
useSSESubscription,
useTicketMessages,
} from "@frontend/kitui";
2023-03-30 12:33:28 +00:00
2022-12-15 14:09:07 +00:00
export default function SupportChat() {
2023-08-10 06:53:12 +00:00
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);
2023-06-06 13:13:58 +00:00
2023-08-10 06:53:12 +00:00
const fetchState = useTicketMessages({
url: "https://admin.pena.digital/heruvym/getMessages",
ticketId,
messagesPerPage,
messageApiPage,
onNewMessages: useCallback((messages) => {
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1)
chatBoxRef.current.scrollTop = 1;
addOrUpdateMessages(messages);
}, []),
onError: useCallback((error: Error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
}, []),
});
2023-03-31 15:01:43 +00:00
2023-08-10 06:53:12 +00:00
useSSESubscription<TicketMessage>({
enabled: Boolean(token) && Boolean(ticketId),
url: `https://admin.pena.digital/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`,
onNewData: addOrUpdateMessages,
onDisconnect: useCallback(() => {
clearMessageState();
setIsPreventAutoscroll(false);
}, []),
marker: "ticket message",
});
2022-12-24 10:41:12 +00:00
2023-08-10 06:53:12 +00:00
const throttledScrollHandler = useMemo(
() =>
throttle(() => {
2023-03-30 12:33:28 +00:00
const chatBox = chatBoxRef.current;
2023-06-06 13:13:58 +00:00
if (!chatBox) return;
2023-04-06 15:10:22 +00:00
2023-08-10 06:53:12 +00:00
const scrollBottom =
chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
2023-06-06 13:13:58 +00:00
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20;
setIsPreventAutoscroll(isPreventAutoscroll);
2023-04-06 15:10:22 +00:00
2023-06-06 13:13:58 +00:00
if (fetchState !== "idle") return;
2022-12-24 10:41:12 +00:00
2023-06-06 13:13:58 +00:00
if (chatBox.scrollTop < chatBox.clientHeight) {
2023-08-10 06:53:12 +00:00
incrementMessageApiPage();
2023-06-06 13:13:58 +00:00
}
2023-08-10 06:53:12 +00:00
}, 200),
[fetchState]
);
2022-12-24 10:41:12 +00:00
2023-08-10 06:53:12 +00:00
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
2022-12-24 10:41:12 +00:00
2023-08-10 06:53:12 +00:00
useEffect(
function scrollOnNewMessage() {
if (!chatBoxRef.current) return;
2022-12-24 10:41:12 +00:00
2023-08-10 06:53:12 +00:00
if (!isPreventAutoscroll) {
setTimeout(() => {
scrollToBottom();
}, 50);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
},
[lastMessageId]
);
2023-04-06 15:10:22 +00:00
2023-08-10 06:53:12 +00:00
useEffect(() => {
if (ticket)
makeRequest({
url: "https://admin.pena.digital/heruvym/shown",
method: "POST",
useToken: true,
2023-08-18 13:23:12 +00:00
body: { id: ticket.top_message.id },
2023-08-10 06:53:12 +00:00
});
}, [ticket]);
2022-12-24 10:41:12 +00:00
2023-08-10 06:53:12 +00:00
function handleSendMessage() {
if (!ticket || !messageField) return;
2023-03-31 15:01:43 +00:00
2023-08-10 06:53:12 +00:00
makeRequest({
url: "https://hub.pena.digital/heruvym/send",
method: "POST",
useToken: true,
body: {
ticket: ticket.id,
message: messageField,
lang: "ru",
files: [],
},
})
.then(() => {
setMessageField("");
})
.catch((error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
});
}
2023-03-31 15:01:43 +00:00
2023-08-10 06:53:12 +00:00
function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return;
const chatBox = chatBoxRef.current;
chatBox.scroll({
left: 0,
top: chatBox.scrollHeight,
behavior,
});
}
2022-12-15 14:09:07 +00:00
2023-08-10 06:53:12 +00:00
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, {
2023-04-06 08:47:28 +00:00
hour: "2-digit",
minute: "2-digit",
2023-08-10 06:53:12 +00:00
});
2023-04-06 08:47:28 +00:00
2023-08-10 06:53:12 +00:00
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",
2023-08-22 10:28:22 +00:00
color: theme.palette.gray.main,
2023-08-10 06:53:12 +00:00
mb: upMd ? "9px" : "20px",
}}
>
Создан: {createdAtString}
</Typography>
<Box
sx={{
backgroundColor: "#ECECF3",
2023-08-22 10:28:22 +00:00
border: `1px solid ${theme.palette.gray.main}`,
2023-08-10 06:53:12 +00:00
borderRadius: "10px",
overflow: "hidden",
width: "100%",
minHeight: "345px",
2023-04-06 08:47:28 +00:00
display: "flex",
flexGrow: 1,
2023-08-10 06:53:12 +00:00
flexDirection: "column",
}}
>
<Box
sx={{
position: "relative",
width: "100%",
flexGrow: 1,
2023-08-22 10:28:22 +00:00
borderBottom: `1px solid ${theme.palette.gray.main}`,
2023-08-10 06:53:12 +00:00
height: "200px",
}}
>
{isPreventAutoscroll && (
<Fab
size="small"
onClick={() => scrollToBottom("smooth")}
2022-12-15 14:09:07 +00:00
sx={{
2023-08-10 06:53:12 +00:00
position: "absolute",
left: "10px",
bottom: "10px",
2022-12-15 14:09:07 +00:00
}}
2023-08-10 06:53:12 +00:00
>
<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%",
}}
2022-12-15 14:09:07 +00:00
>
2023-08-10 06:53:12 +00:00
{ticket &&
messages.map((message) => (
<ChatMessage
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={ticket.user === message.user_id}
/>
))}
2022-12-15 14:09:07 +00:00
</Box>
2023-08-10 06:53:12 +00:00
</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,
}}
>
2023-08-10 06:53:12 +00:00
<SendIcon />
</IconButton>
</InputAdornment>
)
}
/>
</FormControl>
</Box>
</Box>
{upMd && (
<Box sx={{ alignSelf: "end" }}>
<CustomButton
onClick={handleSendMessage}
disabled={!messageField}
variant="contained"
sx={{
backgroundColor: theme.palette.purple.dark,
2023-08-10 06:53:12 +00:00
}}
>
Отправить
</CustomButton>
2022-12-15 14:09:07 +00:00
</Box>
2023-08-10 06:53:12 +00:00
)}
</Box>
);
}