From 0f91f0037df8926e711387db3adda54fa1978143 Mon Sep 17 00:00:00 2001 From: IlyaDoronin Date: Mon, 11 Mar 2024 19:02:37 +0300 Subject: [PATCH] feat: support chat upload file --- .eslintrc.json | 46 +-- src/api/ticket.ts | 60 ++-- src/assets/Icons/download.tsx | 55 ++++ src/components/FloatingSupportChat/Chat.tsx | 297 +++++++++++++++--- .../FloatingSupportChat/ChatDocument.tsx | 113 +++++++ .../FloatingSupportChat/ChatImage.tsx | 119 +++++++ .../FloatingSupportChat/ChatVideo.tsx | 123 ++++++++ .../FloatingSupportChat.tsx | 9 +- src/stores/tickets.ts | 196 ++++++++++-- src/stores/unauthTicket.ts | 79 ----- src/utils/checkAcceptableMediaType.ts | 37 +++ 11 files changed, 916 insertions(+), 218 deletions(-) create mode 100644 src/assets/Icons/download.tsx create mode 100644 src/components/FloatingSupportChat/ChatDocument.tsx create mode 100644 src/components/FloatingSupportChat/ChatImage.tsx create mode 100644 src/components/FloatingSupportChat/ChatVideo.tsx delete mode 100644 src/stores/unauthTicket.ts create mode 100644 src/utils/checkAcceptableMediaType.ts diff --git a/.eslintrc.json b/.eslintrc.json index b510487..0ef1b10 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,34 +1,16 @@ { - "env": { - "browser": true, - "es2021": true - }, - "extends": [], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "react" - ], - "rules": { - "indent": [ - "error", - "tab" - ], - "linebreak-style": [ - "error", - "unix" - ], - "quotes": [ - "error", - "double" - ], - "semi": [ - "error", - "never" - ] - } + "env": { + "browser": true, + "es2021": true + }, + "extends": [], + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": ["@typescript-eslint", "react"], + "rules": { + "quotes": ["error", "double"] + } } diff --git a/src/api/ticket.ts b/src/api/ticket.ts index ea95931..bfa0d34 100644 --- a/src/api/ticket.ts +++ b/src/api/ticket.ts @@ -1,46 +1,46 @@ -import { makeRequest } from "@frontend/kitui" -import { parseAxiosError } from "@root/utils/parse-error" +import { makeRequest } from "@frontend/kitui"; +import { parseAxiosError } from "@root/utils/parse-error"; -import { SendTicketMessageRequest } from "@frontend/kitui" +import { SendTicketMessageRequest } from "@frontend/kitui"; -const apiUrl = process.env.REACT_APP_DOMAIN + "/heruvym" +const apiUrl = process.env.REACT_APP_DOMAIN + "/heruvym"; export async function sendTicketMessage( - ticketId: string, - message: string + ticketId: string, + message: string ): Promise<[null, string?]> { - try { - const sendTicketMessageResponse = await makeRequest< + try { + const sendTicketMessageResponse = await makeRequest< SendTicketMessageRequest, null >({ - url: `${apiUrl}/send`, - method: "POST", - useToken: true, - body: { ticket: ticketId, message: message, lang: "ru", files: [] }, - }) + url: `${apiUrl}/send`, + method: "POST", + useToken: true, + body: { ticket: ticketId, message: message, lang: "ru", files: [] }, + }); - return [sendTicketMessageResponse] - } catch (nativeError) { - const [error] = parseAxiosError(nativeError) + return [sendTicketMessageResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); - return [null, `Не удалось отправить сообщение. ${error}`] - } + return [null, `Не удалось отправить сообщение. ${error}`]; + } } export async function shownMessage(id: string): Promise<[null, string?]> { - try { - const shownMessageResponse = await makeRequest<{ id: string }, null>({ - url: apiUrl + "/shown", - method: "POST", - useToken: true, - body: { id }, - }) + try { + const shownMessageResponse = await makeRequest<{ id: string }, null>({ + url: apiUrl + "/shown", + method: "POST", + useToken: true, + body: { id }, + }); - return [shownMessageResponse] - } catch (nativeError) { - const [error] = parseAxiosError(nativeError) + return [shownMessageResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); - return [null, `Не удалось прочесть сообщение. ${error}`] - } + return [null, `Не удалось прочесть сообщение. ${error}`]; + } } diff --git a/src/assets/Icons/download.tsx b/src/assets/Icons/download.tsx new file mode 100644 index 0000000..edba284 --- /dev/null +++ b/src/assets/Icons/download.tsx @@ -0,0 +1,55 @@ +import { Box, SxProps, Theme } from "@mui/material"; + +interface Props { + color: string; + sx?: SxProps; +} + +export default function Download({ color, sx }: Props) { + return ( + + + + + + + + + ); +} diff --git a/src/components/FloatingSupportChat/Chat.tsx b/src/components/FloatingSupportChat/Chat.tsx index eee11d2..95cf39c 100644 --- a/src/components/FloatingSupportChat/Chat.tsx +++ b/src/components/FloatingSupportChat/Chat.tsx @@ -10,32 +10,52 @@ import { useMediaQuery, useTheme, } from "@mui/material"; -import { TicketMessage } from "@frontend/kitui"; -import { - addOrUpdateUnauthMessages, - useUnauthTicketStore, - incrementUnauthMessageApiPage, - setUnauthIsPreventAutoscroll, - setUnauthSessionData, - setIsMessageSending, - setUnauthTicketMessageFetchState, -} from "@root/stores/unauthTicket"; -import { enqueueSnackbar } from "notistack"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import ChatMessage from "../ChatMessage"; -import SendIcon from "../icons/SendIcon"; -import UserCircleIcon from "./UserCircleIcon"; -import { throttle } from "@frontend/kitui"; import { + TicketMessage, + makeRequest, + useTicketsFetcher, useTicketMessages, getMessageFromFetchError, useSSESubscription, useEventListener, createTicket, } from "@frontend/kitui"; +import { enqueueSnackbar } from "notistack"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import ChatMessage from "../ChatMessage"; +import SendIcon from "../icons/SendIcon"; +import UserCircleIcon from "./UserCircleIcon"; +import { throttle } from "@frontend/kitui"; import { sendTicketMessage, shownMessage } from "@root/api/ticket"; import { useSSETab } from "@root/utils/hooks/useSSETab"; +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"; +type ModalWarningType = + | "errorType" + | "errorSize" + | "picture" + | "video" + | "audio" + | "document" + | null; interface Props { open: boolean; sx?: SxProps; @@ -45,23 +65,26 @@ export default function Chat({ open = false, sx }: Props) { const theme = useTheme(); const upMd = useMediaQuery(theme.breakpoints.up("md")); const [messageField, setMessageField] = useState(""); - const sessionData = useUnauthTicketStore((state) => state.sessionData); - const messages = useUnauthTicketStore((state) => state.messages); - const messageApiPage = useUnauthTicketStore((state) => state.apiPage); - const messagesPerPage = useUnauthTicketStore( - (state) => state.messagesPerPage - ); - const isMessageSending = useUnauthTicketStore( - (state) => state.isMessageSending - ); - const isPreventAutoscroll = useUnauthTicketStore( - (state) => state.isPreventAutoscroll - ); - const lastMessageId = useUnauthTicketStore((state) => state.lastMessageId); - const fetchState = useUnauthTicketStore( - (state) => state.unauthTicketMessageFetchState - ); + const [disableFileButton, setDisableFileButton] = useState(false); + const [modalWarningType, setModalWarningType] = + useState(null); const chatBoxRef = useRef(null); + const fileInputRef = useRef(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; + const { isActiveSSETab, updateSSEValue } = useSSETab( "ticket", addOrUpdateUnauthMessages @@ -74,8 +97,10 @@ export default function Chat({ open = false, sx }: Props) { messagesPerPage, messageApiPage, onSuccess: useCallback((messages) => { - if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) + if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) { chatBoxRef.current.scrollTop = 1; + } + addOrUpdateUnauthMessages(messages); }, []), onError: useCallback((error: Error) => { @@ -92,6 +117,7 @@ export default function Chat({ open = false, sx }: Props) { `/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`, onNewData: (ticketMessages) => { updateSSEValue(ticketMessages); + addOrUpdateUnauthMessages(ticketMessages); }, onDisconnect: useCallback(() => { @@ -100,6 +126,34 @@ export default function Chat({ open = false, sx }: Props) { marker: "ticket", }); + 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), + }); + const throttledScrollHandler = useMemo( () => throttle(() => { @@ -149,7 +203,7 @@ export default function Chat({ open = false, sx }: Props) { async function handleSendMessage() { if (!messageField || isMessageSending) return; - if (!sessionData) { + if (!sessionData?.ticketId) { setIsMessageSending(true); createTicket({ url: process.env.REACT_APP_DOMAIN + "/heruvym/create", @@ -157,10 +211,10 @@ export default function Chat({ open = false, sx }: Props) { Title: "Unauth title", Message: messageField, }, - useToken: false, + useToken: Boolean(user), }) .then((response) => { - setUnauthSessionData({ + setTicketData({ ticketId: response.Ticket, sessionId: response.sess, }); @@ -210,6 +264,66 @@ export default function Chat({ open = false, sx }: Props) { } }; + 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); + }; + return ( <> {open && ( @@ -276,16 +390,87 @@ export default function Chat({ open = false, sx }: Props) { flexGrow: 1, }} > - {sessionData && - messages.map((message) => ( - - ))} + {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 ( + + ); + } + if (message.files.length > 0 && isFileVideo()) { + return ( + + ); + } + if (message.files.length > 0 && isFileDocument()) { + return ( + + ); + } + return ( + + ); + })} setMessageField(e.target.value)} endAdornment={ + { + console.log(disableFileButton); + if (!disableFileButton) fileInputRef.current?.click(); + }} + > + + + { + if (target.files?.[0]) { + sendFileHC(target.files?.[0]); + } + }} + style={{ display: "none" }} + type="file" + /> + + {time} + + + + + + + + + + + + ); +} diff --git a/src/components/FloatingSupportChat/ChatImage.tsx b/src/components/FloatingSupportChat/ChatImage.tsx new file mode 100644 index 0000000..8d790aa --- /dev/null +++ b/src/components/FloatingSupportChat/ChatImage.tsx @@ -0,0 +1,119 @@ +import { + Box, + ButtonBase, + Link, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { isDateToday } from "../../utils/date"; +import { useNavigate } from "react-router-dom"; + +interface Props { + unAuthenticated?: boolean; + isSelf: boolean; + file: string; + createdAt: string; +} + +export default function ChatImage({ + unAuthenticated = false, + isSelf, + file, + createdAt, +}: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const navigate = useNavigate(); + const messageBackgroundColor = isSelf + ? "white" + : unAuthenticated + ? "#EFF0F5" + : "#434657"; + + const date = new Date(createdAt); + const today = isDateToday(date); + const time = date.toLocaleString([], { + hour: "2-digit", + minute: "2-digit", + ...(!today && { + year: "2-digit", + month: "2-digit", + day: "2-digit", + }), + }); + + return ( + + + {time} + + + + + + + + + + + + ); +} diff --git a/src/components/FloatingSupportChat/ChatVideo.tsx b/src/components/FloatingSupportChat/ChatVideo.tsx new file mode 100644 index 0000000..3f4526c --- /dev/null +++ b/src/components/FloatingSupportChat/ChatVideo.tsx @@ -0,0 +1,123 @@ +import { + Box, + ButtonBase, + Link, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import { isDateToday } from "../../utils/date"; +import { useNavigate } from "react-router-dom"; +import { useEffect } from "react"; + +interface Props { + unAuthenticated?: boolean; + isSelf: boolean; + file: string; + createdAt: string; +} + +export default function ChatImage({ + unAuthenticated = false, + isSelf, + file, + createdAt, +}: Props) { + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const navigate = useNavigate(); + useEffect(() => { + () => console.log("delete"); + }); + const messageBackgroundColor = isSelf + ? "white" + : unAuthenticated + ? "#EFF0F5" + : "#434657"; + + const date = new Date(createdAt); + const today = isDateToday(date); + const time = date.toLocaleString([], { + hour: "2-digit", + minute: "2-digit", + ...(!today && { + year: "2-digit", + month: "2-digit", + day: "2-digit", + }), + }); + + return ( + + + {time} + + + + + + + + + + + + ); +} diff --git a/src/components/FloatingSupportChat/FloatingSupportChat.tsx b/src/components/FloatingSupportChat/FloatingSupportChat.tsx index cd1065d..bbf821d 100644 --- a/src/components/FloatingSupportChat/FloatingSupportChat.tsx +++ b/src/components/FloatingSupportChat/FloatingSupportChat.tsx @@ -4,12 +4,16 @@ import { Box, Fab, Typography, Badge, useTheme } from "@mui/material"; import CircleDoubleDown from "./CircleDoubleDownIcon"; import Chat from "./Chat"; -import { useUnauthTicketStore } from "@root/stores/unauthTicket"; +import { useUserStore } from "@root/stores/user"; +import { useTicketStore } from "@root/stores/tickets"; export default function FloatingSupportChat() { const [isChatOpened, setIsChatOpened] = useState(false); const theme = useTheme(); - const { messages } = useUnauthTicketStore((state) => state); + const user = useUserStore((state) => state.user?._id); + const { messages } = useTicketStore( + (state) => state[user ? "authData" : "unauthData"] + ); const animation = { "@keyframes runningStripe": { @@ -35,6 +39,7 @@ export default function FloatingSupportChat() { }, }, }; + return ( ()( - devtools( - (set, get) => initialState, - { - name: "Tickets" - } - ) -) + persist( + devtools((set, get) => initialState, { + name: "Unauth tickets", + }), + { + version: 0, + name: "unauth-ticket", + storage: createJSONStorage(() => localStorage), + } + ) +); -export const setTicketCount = (ticketCount: number) => useTicketStore.setState({ ticketCount }) +export const setTicketCount = (ticketCount: number) => + useTicketStore.setState({ ticketCount }); -export const setTicketApiPage = (apiPage: number) => useTicketStore.setState({ apiPage: apiPage }) +export const setTicketApiPage = (apiPage: number) => + useTicketStore.setState({ apiPage: apiPage }); export const updateTickets = (receivedTickets: Ticket[]) => { - const state = useTicketStore.getState() - const ticketIdToTicketMap: { [ticketId: string]: Ticket; } = {}; + const state = useTicketStore.getState(); + const ticketIdToTicketMap: { [ticketId: string]: Ticket } = {}; - [...state.tickets, ...receivedTickets].forEach(ticket => ticketIdToTicketMap[ticket.id] = ticket) + [...state.tickets, ...receivedTickets].forEach( + (ticket) => (ticketIdToTicketMap[ticket.id] = ticket) + ); - useTicketStore.setState({ tickets: Object.values(ticketIdToTicketMap) }) + useTicketStore.setState({ tickets: Object.values(ticketIdToTicketMap) }); +}; + +export const clearTickets = () => useTicketStore.setState({ ...initialState }); + +export const setTicketsFetchState = (ticketsFetchState: FetchState) => + useTicketStore.setState({ ticketsFetchState }); + +export const setTicketData = (sessionData: SessionData) => + updateTicket((ticket) => { + ticket.sessionData = sessionData; + }); + +export const updateTicket = ( + recipe: (ticket: AuthData) => void +) => + setProducedState( + (state) => { + //В зависимости от авторизованности вызывается изменение разных объектов + if (Boolean(useUserStore.getState().userId)) { + recipe(state.authData); + } else { + recipe(state.unauthData); + } + }, + { + type: "updateTicket", + recipe, + } + ); + +function setProducedState( + recipe: (state: TicketStore) => void, + action?: A +) { + useTicketStore.setState((state) => produce(state, recipe), false, action); } -export const clearTickets = () => useTicketStore.setState({ ...initialState }) +function filterMessageUncompleteness(messages: TicketMessage[]) { + return messages.filter( + (message) => + "id" in message && + "ticket_id" in message && + "user_id" in message && + "session_id" in message && + "message" in message && + "files" in message && + "shown" in message && + "request_screenshot" in message && + "created_at" in message && + ((message.files !== null && message.files.length > 0) || + message.message.length > 0) + ); +} -export const setTicketsFetchState = (ticketsFetchState: FetchState) => useTicketStore.setState({ ticketsFetchState }) +function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) { + const date1 = new Date(ticket1.created_at).getTime(); + const date2 = new Date(ticket2.created_at).getTime(); + return date1 - date2; +} + +export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) => + updateTicket((ticket) => { + const filtered = filterMessageUncompleteness(receivedMessages); + if (filtered.length === 0) return; + + const messageIdToMessageMap: { [messageId: string]: TicketMessage } = {}; + + [...ticket.messages, ...filtered].forEach( + (message) => (messageIdToMessageMap[message.id] = message) + ); + + const sortedMessages = Object.values(messageIdToMessageMap).sort( + sortMessagesByTime + ); + + ticket.messages = sortedMessages; + ticket.lastMessageId = sortedMessages.at(-1)?.id; + }); + +export const setUnauthTicketMessageFetchState = ( + unauthTicketMessageFetchState: FetchState +) => + updateTicket((ticket) => { + ticket.unauthTicketMessageFetchState = unauthTicketMessageFetchState; + }); + +export const setUnauthIsPreventAutoscroll = (isPreventAutoscroll: boolean) => + updateTicket((ticket) => { + ticket.isPreventAutoscroll = isPreventAutoscroll; + }); + +export const incrementUnauthMessageApiPage = () => { + const state = useTicketStore.getState(); + + useTicketStore.setState({ apiPage: state.apiPage + 1 }); +}; + +export const setIsMessageSending = ( + isMessageSending: AuthData["isMessageSending"] +) => { + updateTicket((ticket) => { + ticket.isMessageSending = isMessageSending; + }); +}; diff --git a/src/stores/unauthTicket.ts b/src/stores/unauthTicket.ts deleted file mode 100644 index e16d0e6..0000000 --- a/src/stores/unauthTicket.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { FetchState, TicketMessage } from "@frontend/kitui" -import { create } from "zustand" -import { createJSONStorage, devtools, persist } from "zustand/middleware" - - -interface UnauthTicketStore { - sessionData: { - ticketId: string; - sessionId: string; - } | null; - isMessageSending: boolean; - messages: TicketMessage[]; - apiPage: number; - messagesPerPage: number; - lastMessageId: string | undefined; - isPreventAutoscroll: boolean; - unauthTicketMessageFetchState: FetchState; -} - -export const useUnauthTicketStore = create()( - persist( - devtools( - (set, get) => ({ - sessionData: null, - isMessageSending: false, - messages: [], - apiPage: 0, - messagesPerPage: 10, - lastMessageId: undefined, - isPreventAutoscroll: false, - unauthTicketMessageFetchState: "idle", - }), - { - name: "Unauth tickets" - } - ), - { - version: 0, - name: "unauth-ticket", - storage: createJSONStorage(() => localStorage), - partialize: state => ({ - sessionData: state.sessionData, - }) - } - ) -) - -export const setUnauthSessionData = (sessionData: UnauthTicketStore["sessionData"]) => useUnauthTicketStore.setState({ sessionData }) -export const setIsMessageSending = (isMessageSending: UnauthTicketStore["isMessageSending"]) => useUnauthTicketStore.setState({ isMessageSending }) - -export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) => { - const state = useUnauthTicketStore.getState() - const messageIdToMessageMap: { [messageId: string]: TicketMessage; } = {}; - - [...state.messages, ...receivedMessages].forEach(message => messageIdToMessageMap[message.id] = message) - - const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime) - - useUnauthTicketStore.setState({ - messages: sortedMessages, - lastMessageId: sortedMessages.at(-1)?.id, - }) -} - -export const incrementUnauthMessageApiPage = () => { - const state = useUnauthTicketStore.getState() - - useUnauthTicketStore.setState({ apiPage: state.apiPage + 1 }) -} - -export const setUnauthIsPreventAutoscroll = (isPreventAutoscroll: boolean) => useUnauthTicketStore.setState({ isPreventAutoscroll }) - -export const setUnauthTicketMessageFetchState = (unauthTicketMessageFetchState: FetchState) => useUnauthTicketStore.setState({ unauthTicketMessageFetchState }) - -function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) { - const date1 = new Date(ticket1.created_at).getTime() - const date2 = new Date(ticket2.created_at).getTime() - return date1 - date2 -} diff --git a/src/utils/checkAcceptableMediaType.ts b/src/utils/checkAcceptableMediaType.ts new file mode 100644 index 0000000..95f56e4 --- /dev/null +++ b/src/utils/checkAcceptableMediaType.ts @@ -0,0 +1,37 @@ +export const MAX_FILE_SIZE = 10485760; +const MAX_PHOTO_SIZE = 5242880; +const MAX_VIDEO_SIZE = 52428800; + +export const ACCEPT_SEND_MEDIA_TYPES_MAP = { + picture: ["jpg", "png"], + video: ["mp4"], + document: ["doc", "docx", "pdf", "txt", "xlsx", "csv"], +} as const; + +const TOO_LARGE_TEXT = "Файл слишком большой"; + +export const checkAcceptableMediaType = (file: File) => { + if (file === null) return ""; + + const segments = file?.name.split("."); + const extension = segments[segments.length - 1]; + const type = extension.toLowerCase(); + + console.log(type); + switch (type) { + case ACCEPT_SEND_MEDIA_TYPES_MAP.document.find((name) => name === type): + if (file.size > MAX_FILE_SIZE) return TOO_LARGE_TEXT; + return ""; + + case ACCEPT_SEND_MEDIA_TYPES_MAP.picture.find((name) => name === type): + if (file.size > MAX_PHOTO_SIZE) return TOO_LARGE_TEXT; + return ""; + + case ACCEPT_SEND_MEDIA_TYPES_MAP.video.find((name) => name === type): + if (file.size > MAX_VIDEO_SIZE) return TOO_LARGE_TEXT; + return ""; + + default: + return "Не удалось отправить файл. Недопустимый тип"; + } +};