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

559 lines
16 KiB
TypeScript
Raw Normal View History

2023-08-31 10:02:11 +00:00
import {
2024-01-23 14:18:22 +00:00
Box,
FormControl,
IconButton,
InputAdornment,
InputBase,
SxProps,
Theme,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
2023-08-31 10:02:11 +00:00
import {
2024-03-11 16:02:37 +00:00
TicketMessage,
makeRequest,
useTicketsFetcher,
2024-01-23 14:18:22 +00:00
useTicketMessages,
getMessageFromFetchError,
useSSESubscription,
useEventListener,
createTicket,
} from "@frontend/kitui";
2024-03-11 16:02:37 +00:00
import { enqueueSnackbar } from "notistack";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import ChatMessage from "../ChatMessage";
import SendIcon from "../icons/SendIcon";
2024-03-12 10:28:13 +00:00
import ArrowLeft from "@root/assets/Icons/arrowLeft";
2024-03-11 16:02:37 +00:00
import UserCircleIcon from "./UserCircleIcon";
import { throttle } from "@frontend/kitui";
2024-02-14 09:43:19 +00:00
import { sendTicketMessage, shownMessage } from "@root/api/ticket";
2024-01-23 14:18:22 +00:00
import { useSSETab } from "@root/utils/hooks/useSSETab";
2024-03-11 16:02:37 +00:00
import {
checkAcceptableMediaType,
MAX_FILE_SIZE,
ACCEPT_SEND_MEDIA_TYPES_MAP,
} from "@utils/checkAcceptableMediaType";
import {
useTicketStore,
setTicketData,
addOrUpdateUnauthMessages,
setUnauthTicketMessageFetchState,
setUnauthIsPreventAutoscroll,
incrementUnauthMessageApiPage,
setIsMessageSending,
} from "@root/stores/tickets";
import { useUserStore } from "@root/stores/user";
import AttachFileIcon from "@mui/icons-material/AttachFile";
import ChatDocument from "./ChatDocument";
import ChatImage from "./ChatImage";
import ChatVideo from "./ChatVideo";
2023-04-13 16:48:17 +00:00
2024-03-11 16:02:37 +00:00
type ModalWarningType =
| "errorType"
| "errorSize"
| "picture"
| "video"
| "audio"
| "document"
| null;
2023-04-13 16:48:17 +00:00
interface Props {
2024-02-12 16:39:30 +00:00
open: boolean;
2023-08-31 10:02:11 +00:00
sx?: SxProps<Theme>;
2024-03-12 10:28:13 +00:00
onclickArrow?: () => void;
2023-04-13 16:48:17 +00:00
}
2024-03-12 10:28:13 +00:00
export default function Chat({ open = false, onclickArrow, sx }: Props) {
2024-01-23 14:18:22 +00:00
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
2024-03-12 10:28:13 +00:00
const isMobile = useMediaQuery(theme.breakpoints.down(800));
2024-01-23 14:18:22 +00:00
const [messageField, setMessageField] = useState<string>("");
2024-03-11 16:02:37 +00:00
const [disableFileButton, setDisableFileButton] = useState(false);
const [modalWarningType, setModalWarningType] =
useState<ModalWarningType>(null);
2024-01-23 14:18:22 +00:00
const chatBoxRef = useRef<HTMLDivElement>(null);
2024-03-11 16:02:37 +00:00
const fileInputRef = useRef<HTMLInputElement>(null);
const user = useUserStore((state) => state.user?._id);
const ticket = useTicketStore(
(state) => state[user ? "authData" : "unauthData"]
);
const {
messages,
sessionData,
isMessageSending,
isPreventAutoscroll,
lastMessageId,
messagesPerPage,
unauthTicketMessageFetchState: fetchState,
apiPage: messageApiPage,
} = ticket;
2024-01-23 14:18:22 +00:00
const { isActiveSSETab, updateSSEValue } = useSSETab<TicketMessage[]>(
"ticket",
addOrUpdateUnauthMessages
);
2023-06-06 13:13:58 +00:00
2024-01-23 14:18:22 +00:00
useTicketMessages({
url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages",
isUnauth: true,
ticketId: sessionData?.ticketId,
messagesPerPage,
messageApiPage,
onSuccess: useCallback((messages) => {
2024-03-11 16:02:37 +00:00
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) {
2024-01-23 14:18:22 +00:00
chatBoxRef.current.scrollTop = 1;
2024-03-11 16:02:37 +00:00
}
2024-01-23 14:18:22 +00:00
addOrUpdateUnauthMessages(messages);
}, []),
onError: useCallback((error: Error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
}, []),
onFetchStateChange: setUnauthTicketMessageFetchState,
});
2023-06-06 13:13:58 +00:00
2024-01-23 14:18:22 +00:00
useSSESubscription<TicketMessage>({
enabled: isActiveSSETab && Boolean(sessionData),
url:
process.env.REACT_APP_DOMAIN +
`/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
onNewData: (ticketMessages) => {
updateSSEValue(ticketMessages);
2024-03-11 16:02:37 +00:00
2024-01-23 14:18:22 +00:00
addOrUpdateUnauthMessages(ticketMessages);
},
onDisconnect: useCallback(() => {
setUnauthIsPreventAutoscroll(false);
}, []),
marker: "ticket",
});
2023-05-03 16:24:15 +00:00
2024-03-11 16:02:37 +00:00
useTicketsFetcher({
url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets",
ticketsPerPage: 10,
ticketApiPage: 0,
onSuccess: (result) => {
if (result.data?.length) {
const currentTicket = result.data.find(
({ origin }) => !origin.includes("/support")
);
if (!currentTicket) {
return;
}
setTicketData({
ticketId: currentTicket.id,
sessionId: currentTicket.sess,
});
}
},
onError: (error: Error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
},
onFetchStateChange: () => {},
enabled: Boolean(user),
});
2024-01-23 14:18:22 +00:00
const throttledScrollHandler = useMemo(
() =>
throttle(() => {
const chatBox = chatBoxRef.current;
if (!chatBox) return;
2023-05-03 16:24:15 +00:00
2024-01-23 14:18:22 +00:00
const scrollBottom =
chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
setUnauthIsPreventAutoscroll(isPreventAutoscroll);
2023-05-03 16:24:15 +00:00
2024-01-23 14:18:22 +00:00
if (fetchState !== "idle") return;
2023-05-03 16:24:15 +00:00
2024-01-23 14:18:22 +00:00
if (chatBox.scrollTop < chatBox.clientHeight) {
incrementUnauthMessageApiPage();
}
}, 200),
[fetchState]
);
2023-05-03 16:24:15 +00:00
2024-01-23 14:18:22 +00:00
useEventListener("scroll", throttledScrollHandler, chatBoxRef);
2023-05-03 16:24:15 +00:00
2024-01-23 14:18:22 +00:00
useEffect(
function scrollOnNewMessage() {
if (!chatBoxRef.current) return;
2023-05-03 16:24:15 +00:00
2024-01-23 14:18:22 +00:00
if (!isPreventAutoscroll) {
setTimeout(() => {
scrollToBottom();
}, 50);
}
},
[lastMessageId]
);
2023-05-03 16:24:15 +00:00
2024-02-14 09:43:19 +00:00
useEffect(() => {
if (open) {
const newMessages = messages.filter(({ shown }) => shown.me !== 1);
newMessages.map(async ({ id }) => {
await shownMessage(id);
});
}
}, [open, messages]);
2024-01-23 14:18:22 +00:00
async function handleSendMessage() {
if (!messageField || isMessageSending) return;
2023-04-13 16:48:17 +00:00
2024-03-11 16:02:37 +00:00
if (!sessionData?.ticketId) {
2024-01-23 14:18:22 +00:00
setIsMessageSending(true);
createTicket({
url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
body: {
Title: "Unauth title",
Message: messageField,
},
2024-03-11 16:02:37 +00:00
useToken: Boolean(user),
2024-01-23 14:18:22 +00:00
})
.then((response) => {
2024-03-11 16:02:37 +00:00
setTicketData({
2024-01-23 14:18:22 +00:00
ticketId: response.Ticket,
sessionId: response.sess,
});
})
.catch((error) => {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
})
.finally(() => {
setMessageField("");
setIsMessageSending(false);
});
} else {
setIsMessageSending(true);
2023-04-13 16:48:17 +00:00
2024-01-23 14:18:22 +00:00
const [_, sendTicketMessageError] = await sendTicketMessage(
sessionData.ticketId,
messageField
);
2023-04-13 16:48:17 +00:00
2024-01-23 14:18:22 +00:00
if (sendTicketMessageError) {
enqueueSnackbar(sendTicketMessageError);
}
2023-05-03 16:24:15 +00:00
2024-01-23 14:18:22 +00:00
setMessageField("");
setIsMessageSending(false);
}
}
2023-05-03 16:24:15 +00:00
2024-01-23 14:18:22 +00:00
function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return;
2023-05-03 09:54:04 +00:00
2024-01-23 14:18:22 +00:00
const chatBox = chatBoxRef.current;
chatBox.scroll({
left: 0,
top: chatBox.scrollHeight,
behavior,
});
}
2023-08-31 10:02:11 +00:00
2024-01-23 14:18:22 +00:00
const handleTextfieldKeyPress: React.KeyboardEventHandler<
2023-08-31 10:02:11 +00:00
HTMLInputElement | HTMLTextAreaElement
> = (e) => {
2024-01-23 14:18:22 +00:00
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSendMessage();
}
};
2023-08-31 10:02:11 +00:00
2024-03-11 16:02:37 +00:00
const sendFile = async (file: File) => {
if (file === undefined) return true;
console.log("тут ошибка", modalWarningType);
let data;
if (!ticket.sessionData?.ticketId) {
try {
data = await createTicket({
url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
body: {
Title: "Unauth title",
Message: "",
},
useToken: Boolean(user),
});
setTicketData({
ticketId: data.Ticket,
sessionId: data.sess,
});
} catch (error: any) {
const errorMessage = getMessageFromFetchError(error);
if (errorMessage) enqueueSnackbar(errorMessage);
}
setIsMessageSending(false);
}
const ticketId = ticket.sessionData?.ticketId || data?.Ticket;
if (ticketId !== undefined) {
if (file.size > MAX_FILE_SIZE) return setModalWarningType("errorSize");
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) => {
console.log(file);
const check = checkAcceptableMediaType(file);
if (check.length > 0) {
enqueueSnackbar(check);
return;
}
setDisableFileButton(true);
await sendFile(file);
setDisableFileButton(false);
console.log(disableFileButton);
};
2024-01-23 14:18:22 +00:00
return (
2024-02-12 16:39:30 +00:00
<>
{open && (
2024-01-23 14:18:22 +00:00
<Box
sx={{
display: "flex",
flexDirection: "column",
2024-03-12 10:28:13 +00:00
height: isMobile
? "100%"
: "clamp(250px, calc(100vh - 90px), 600px)",
2024-02-12 16:39:30 +00:00
backgroundColor: "#944FEE",
borderRadius: "8px",
...sx,
2024-01-23 14:18:22 +00:00
}}
>
2024-02-12 16:39:30 +00:00
<Box
2024-01-23 14:18:22 +00:00
sx={{
2024-02-12 16:39:30 +00:00
display: "flex",
gap: "9px",
pl: "22px",
pt: "12px",
pb: "20px",
filter: "drop-shadow(0px 3px 12px rgba(37, 39, 52, 0.3))",
2024-01-23 14:18:22 +00:00
}}
>
2024-03-12 10:28:13 +00:00
{isMobile && (
<IconButton onClick={onclickArrow}>
<ArrowLeft color="white" />
</IconButton>
)}
2024-02-12 16:39:30 +00:00
<UserCircleIcon />
<Box
sx={{
mt: "5px",
display: "flex",
flexDirection: "column",
gap: "3px",
}}
>
<Typography>Мария</Typography>
<Typography
sx={{
fontSize: "16px",
lineHeight: "19px",
}}
>
онлайн-консультант
</Typography>
</Box>
</Box>
<Box
2024-01-23 14:18:22 +00:00
sx={{
2024-02-12 16:39:30 +00:00
flexGrow: 1,
backgroundColor: "white",
borderRadius: "8px",
display: "flex",
flexDirection: "column",
2024-01-23 14:18:22 +00:00
}}
2024-02-12 16:39:30 +00:00
>
<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,
}}
>
2024-03-11 16:02:37 +00:00
{ticket.sessionData?.ticketId &&
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.length > 0 && isFileImage()) {
return (
<ChatImage
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={
(ticket.sessionData?.sessionId || user) ===
message.user_id
}
/>
);
}
if (message.files.length > 0 && isFileVideo()) {
return (
<ChatVideo
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={
(ticket.sessionData?.sessionId || user) ===
message.user_id
}
/>
);
}
if (message.files.length > 0 && isFileDocument()) {
return (
<ChatDocument
unAuthenticated
key={message.id}
file={message.files[0]}
createdAt={message.created_at}
isSelf={
(ticket.sessionData?.sessionId || user) ===
message.user_id
}
/>
);
}
return (
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
createdAt={message.created_at}
isSelf={
(ticket.sessionData?.sessionId || user) ===
message.user_id
}
/>
);
})}
2024-02-12 16:39:30 +00:00
</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">
2024-03-11 16:02:37 +00:00
<IconButton
disabled={disableFileButton}
onClick={() => {
console.log(disableFileButton);
if (!disableFileButton) fileInputRef.current?.click();
}}
>
<AttachFileIcon />
</IconButton>
<input
ref={fileInputRef}
id="fileinput"
onChange={({ target }) => {
if (target.files?.[0]) {
sendFileHC(target.files?.[0]);
}
}}
style={{ display: "none" }}
type="file"
/>
2024-02-12 16:39:30 +00:00
<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>
)}
</>
2024-01-23 14:18:22 +00:00
);
2023-08-29 11:54:35 +00:00
}