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

630 lines
18 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 {
createTicket,
2024-01-23 14:18:22 +00:00
getMessageFromFetchError,
throttle,
TicketMessage,
2024-01-23 14:18:22 +00:00
useSSESubscription,
useTicketMessages,
useTicketsFetcher,
2024-01-23 14:18:22 +00:00
} from "@frontend/kitui";
import { enqueueSnackbar } from "notistack";
import {
TouchEvent,
useCallback,
useEffect,
useMemo,
useRef,
useState,
2024-05-27 15:43:38 +00:00
WheelEvent,
} from "react";
2024-03-11 16:02:37 +00:00
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";
2024-05-27 15:43:38 +00:00
import {
sendTicketMessage,
shownMessage,
sendFile as sendFileRequest,
} from "@root/api/ticket";
import { useSSETab } from "@root/utils/hooks/useSSETab";
2024-03-11 16:02:37 +00:00
import {
ACCEPT_SEND_MEDIA_TYPES_MAP,
2024-03-11 16:02:37 +00:00
checkAcceptableMediaType,
MAX_FILE_SIZE,
} from "@utils/checkAcceptableMediaType";
import {
addOrUpdateUnauthMessages,
clearTickets,
2024-03-13 14:36:47 +00:00
incrementUnauthMessage,
2024-03-11 16:02:37 +00:00
setIsMessageSending,
setTicketData,
setUnauthIsPreventAutoscroll,
setUnauthTicketMessageFetchState,
useTicketStore,
2024-03-11 16:02:37 +00:00
} from "@root/stores/tickets";
import { useUserStore } from "@root/stores/user";
2024-03-11 16:02:37 +00:00
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 [sseEnabled, setSseEnabled] = useState(true);
2024-03-11 16:02:37 +00:00
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
const getGreetingMessage: TicketMessage = useMemo(() => {
const workingHoursMessage =
"Здравствуйте, задайте ваш вопрос и наш оператор вам ответит в течение 10 минут";
const offHoursMessage =
"Здравствуйте, к сожалению, сейчас операторы не работают. Задайте ваш вопрос, и вам ответят в 10:00 по московскому времени";
const date = new Date();
const currentHourUTC = date.getUTCHours();
const MscTime = 3; // Москва UTC+3;
const moscowHour = (currentHourUTC + MscTime) % 24;
const greetingMessage =
moscowHour >= 3 && moscowHour < 10
? offHoursMessage
: workingHoursMessage;
2024-05-27 15:43:38 +00:00
return {
created_at: new Date().toISOString(),
files: [],
id: "111",
message: greetingMessage,
request_screenshot: "",
session_id: "greetingMessage",
shown: { me: 1 },
ticket_id: "111",
user_id: "greetingMessage",
2024-05-27 15:43:38 +00:00
};
}, [open]);
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,
});
useSSESubscription<TicketMessage>({
enabled: sseEnabled && isActiveSSETab && Boolean(sessionData),
2024-01-23 14:18:22 +00:00
url:
process.env.REACT_APP_DOMAIN +
`/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
onNewData: (ticketMessages) => {
const isTicketClosed = ticketMessages.some(
(message) => message.session_id === "close"
);
if (isTicketClosed) {
clearTickets();
addOrUpdateUnauthMessages([getGreetingMessage]);
if (!user) {
localStorage.removeItem("unauth-ticket");
}
return;
}
2024-01-23 14:18:22 +00:00
updateSSEValue(ticketMessages);
addOrUpdateUnauthMessages(ticketMessages);
},
onDisconnect: useCallback(() => {
setUnauthIsPreventAutoscroll(false);
setSseEnabled(false);
2024-01-23 14:18:22 +00:00
}, []),
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(
2024-05-27 15:43:38 +00:00
({ origin, state }) =>
!origin.includes("/support") && state !== "close"
2024-03-11 16:02:37 +00:00
);
if (!currentTicket) {
return;
}
setTicketData({
ticketId: currentTicket.id,
sessionId: currentTicket.sess,
});
}
},
onError: (error: Error) => {
const message = getMessageFromFetchError(error);
if (message) enqueueSnackbar(message);
},
2024-05-27 15:43:38 +00:00
onFetchStateChange: () => {},
2024-03-11 16:02:37 +00:00
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
if (chatBox.scrollTop < chatBox.clientHeight) {
2024-03-13 14:36:47 +00:00
incrementUnauthMessage();
2024-01-23 14:18:22 +00:00
}
}, 200),
[fetchState]
);
2023-05-03 16:24:15 +00:00
useEffect(() => {
addOrUpdateUnauthMessages([getGreetingMessage]);
scrollToBottom();
}, [open]);
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-03-13 14:36:47 +00:00
const loadNewMessages = (
event: WheelEvent<HTMLDivElement> | TouchEvent<HTMLDivElement>
) => {
event.stopPropagation();
throttledScrollHandler();
};
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,
});
setSseEnabled(true);
2024-01-23 14:18:22 +00:00
})
.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;
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");
2024-05-27 15:43:38 +00:00
const [, sendFileError] = await sendFileRequest(
ticketId,
file
);
if(sendFileError) {
enqueueSnackbar(sendFileError)
}
2024-03-11 16:02:37 +00:00
}
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);
};
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>
2024-02-12 16:39:30 +00:00
<Typography
sx={{
fontSize: "16px",
lineHeight: "19px",
}}
>
онлайн-консультант
</Typography>
<Typography
sx={{
fontSize: "16px",
lineHeight: "19px",
}}
>
время работы 10:00-3:00 по мск
</Typography>
2024-02-12 16:39:30 +00:00
</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
2024-03-13 14:36:47 +00:00
onWheel={loadNewMessages}
onTouchMove={loadNewMessages}
2024-02-12 16:39:30 +00:00
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
}
/>
);
})}
{!ticket.sessionData?.ticketId && (
2024-05-27 15:43:38 +00:00
<ChatMessage
unAuthenticated
text={getGreetingMessage.message}
createdAt={getGreetingMessage.created_at}
isSelf={false}
/>
)}
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={() => {
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
}