adminFront/src/pages/dashboard/Content/Support/Chat/Chat.tsx
2024-05-21 10:41:31 +03:00

365 lines
9.9 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, IconButton, InputAdornment, TextField, Typography, useMediaQuery, useTheme } from "@mui/material";
import {
addOrUpdateMessages,
clearMessageState,
incrementMessageApiPage,
setIsPreventAutoscroll,
setTicketMessagesFetchState,
useMessageStore,
} from "@root/stores/messages";
import Message from "./Message";
import SendIcon from "@mui/icons-material/Send";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import { KeyboardEvent, useEffect, useMemo, useRef, useState } from "react";
import { useParams } from "react-router-dom";
import { TicketMessage } from "@root/model/ticket";
import { sendTicketMessage } from "@root/api/tickets";
import { enqueueSnackbar } from "notistack";
import { useTicketStore } from "@root/stores/tickets";
import {
getMessageFromFetchError,
throttle,
useEventListener,
useSSESubscription,
useTicketMessages,
useToken,
} from "@frontend/kitui";
import makeRequest from "@root/api/makeRequest";
import ChatImage from "./ChatImage";
import ChatDocument from "./ChatDocument";
import ChatVideo from "./ChatVideo";
import ChatMessage from "./ChatMessage";
import { ACCEPT_SEND_MEDIA_TYPES_MAP, MAX_FILE_SIZE, MAX_PHOTO_SIZE, MAX_VIDEO_SIZE } from "./fileUpload";
const tooLarge = "Файл слишком большой";
const checkAcceptableMediaType = (file: File) => {
if (file === null) return "";
const segments = file?.name.split(".");
const extension = segments[segments.length - 1];
const type = extension.toLowerCase();
switch (type) {
case ACCEPT_SEND_MEDIA_TYPES_MAP.document.find((name) => name === type):
if (file.size > MAX_FILE_SIZE) return tooLarge;
return "";
case ACCEPT_SEND_MEDIA_TYPES_MAP.picture.find((name) => name === type):
if (file.size > MAX_PHOTO_SIZE) return tooLarge;
return "";
case ACCEPT_SEND_MEDIA_TYPES_MAP.video.find((name) => name === type):
if (file.size > MAX_VIDEO_SIZE) return tooLarge;
return "";
default:
return "Не удалось отправить файл. Недопустимый тип";
}
};
export default function Chat() {
const token = useToken();
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const tickets = useTicketStore((state) => state.tickets);
const messages = useMessageStore((state) => state.messages);
const [messageField, setMessageField] = useState<string>("");
const ticketId = useParams().ticketId;
const chatBoxRef = useRef<HTMLDivElement>(null);
const messageApiPage = useMessageStore((state) => state.apiPage);
const messagesPerPage = useMessageStore((state) => state.messagesPerPage);
const isPreventAutoscroll = useMessageStore((state) => state.isPreventAutoscroll);
const fetchState = useMessageStore((state) => state.ticketMessagesFetchState);
const lastMessageId = useMessageStore((state) => state.lastMessageId);
const fileInputRef = useRef<HTMLInputElement>(null);
const [disableFileButton, setDisableFileButton] = useState(false);
const ticket = tickets.find((ticket) => ticket.id === ticketId);
useTicketMessages({
url: process.env.REACT_APP_DOMAIN + "/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: setTicketMessagesFetchState,
});
useSSESubscription<TicketMessage>({
enabled: Boolean(token) && Boolean(ticketId),
url: process.env.REACT_APP_DOMAIN + `/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`,
onNewData: addOrUpdateMessages,
onDisconnect: () => {
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]
);
function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return;
const chatBox = chatBoxRef.current;
chatBox.scroll({
left: 0,
top: chatBox.scrollHeight,
behavior,
});
}
function handleSendMessage() {
if (!ticket || !messageField) return;
sendTicketMessage({
files: [],
lang: "ru",
message: messageField,
ticket: ticket.id,
});
setMessageField("");
}
const sendFile = async (file: File) => {
if (file === undefined) return true;
let data;
const ticketId = ticket?.id;
if (ticketId !== undefined) {
try {
const body = new FormData();
body.append(file.name, file);
body.append("ticket", ticketId);
await makeRequest({
url: process.env.REACT_APP_DOMAIN + "/heruvym/sendFiles",
body: body,
method: "POST",
});
} catch (error: any) {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
}
return true;
}
};
const sendFileHC = async (file: File) => {
const check = checkAcceptableMediaType(file);
if (check.length > 0) {
enqueueSnackbar(check);
return;
}
setDisableFileButton(true);
await sendFile(file);
setDisableFileButton(false);
};
function handleTextfieldKeyPress(e: KeyboardEvent) {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
}
return (
<Box
sx={{
border: "1px solid",
borderColor: theme.palette.grayDark.main,
height: "600px",
borderRadius: "3px",
p: "8px",
display: "flex",
flex: upMd ? "2 0 0" : undefined,
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
gap: "8px",
}}
>
<Typography>{ticket ? ticket.title : "Выберите тикет"}</Typography>
<Box
ref={chatBoxRef}
sx={{
width: "100%",
backgroundColor: "#46474a",
flexGrow: 1,
display: "flex",
flexDirection: "column",
gap: "12px",
p: "8px",
overflow: "auto",
colorScheme: "dark",
}}
>
{ticket &&
messages.map((message) => {
const isFileVideo = () => {
if (message.files) {
return ACCEPT_SEND_MEDIA_TYPES_MAP.video.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType)
);
}
};
const isFileImage = () => {
if (message.files) {
return ACCEPT_SEND_MEDIA_TYPES_MAP.picture.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType)
);
}
};
const isFileDocument = () => {
if (message.files) {
return ACCEPT_SEND_MEDIA_TYPES_MAP.document.some((fileType) =>
message.files[0].toLowerCase().endsWith(fileType)
);
}
};
if (message.files !== null && message.files.length > 0 && isFileImage()) {
return (
<ChatImage
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
}
if (message.files !== null && message.files.length > 0 && isFileVideo()) {
return (
<ChatVideo
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
}
if (message.files !== null && message.files.length > 0 && isFileDocument()) {
return (
<ChatDocument
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
}
return (
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={ticket.user !== message.user_id}
/>
);
})}
</Box>
{ticket && (
<TextField
value={messageField}
onChange={(e) => setMessageField(e.target.value)}
onKeyPress={handleTextfieldKeyPress}
id="message-input"
placeholder="Написать сообщение"
fullWidth
multiline
maxRows={8}
InputProps={{
style: {
backgroundColor: theme.palette.content.main,
color: theme.palette.secondary.main,
},
endAdornment: (
<InputAdornment position="end">
<IconButton
onClick={handleSendMessage}
sx={{
height: "45px",
width: "45px",
p: 0,
}}
>
<SendIcon sx={{ color: theme.palette.golden.main }} />
</IconButton>
<IconButton
onClick={() => {
if (!disableFileButton) fileInputRef.current?.click();
}}
sx={{
height: "45px",
width: "45px",
p: 0,
}}
>
<input
ref={fileInputRef}
id="fileinput"
onChange={(e) => {
if (e.target.files?.[0]) sendFileHC(e.target.files?.[0]);
}}
style={{ display: "none" }}
type="file"
/>
<AttachFileIcon sx={{ color: theme.palette.golden.main }} />
</IconButton>
</InputAdornment>
),
}}
InputLabelProps={{
style: {
color: theme.palette.secondary.main,
},
}}
/>
)}
</Box>
);
}