feat: support chat upload file
This commit is contained in:
parent
c730993fef
commit
0f91f0037d
@ -9,26 +9,8 @@
|
|||||||
"ecmaVersion": "latest",
|
"ecmaVersion": "latest",
|
||||||
"sourceType": "module"
|
"sourceType": "module"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": ["@typescript-eslint", "react"],
|
||||||
"@typescript-eslint",
|
|
||||||
"react"
|
|
||||||
],
|
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [
|
"quotes": ["error", "double"]
|
||||||
"error",
|
|
||||||
"tab"
|
|
||||||
],
|
|
||||||
"linebreak-style": [
|
|
||||||
"error",
|
|
||||||
"unix"
|
|
||||||
],
|
|
||||||
"quotes": [
|
|
||||||
"error",
|
|
||||||
"double"
|
|
||||||
],
|
|
||||||
"semi": [
|
|
||||||
"error",
|
|
||||||
"never"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { makeRequest } from "@frontend/kitui"
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { parseAxiosError } from "@root/utils/parse-error"
|
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(
|
export async function sendTicketMessage(
|
||||||
ticketId: string,
|
ticketId: string,
|
||||||
@ -18,13 +18,13 @@ export async function sendTicketMessage(
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
useToken: true,
|
useToken: true,
|
||||||
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
|
body: { ticket: ticketId, message: message, lang: "ru", files: [] },
|
||||||
})
|
});
|
||||||
|
|
||||||
return [sendTicketMessageResponse]
|
return [sendTicketMessageResponse];
|
||||||
} catch (nativeError) {
|
} catch (nativeError) {
|
||||||
const [error] = parseAxiosError(nativeError)
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
return [null, `Не удалось отправить сообщение. ${error}`]
|
return [null, `Не удалось отправить сообщение. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -35,12 +35,12 @@ export async function shownMessage(id: string): Promise<[null, string?]> {
|
|||||||
method: "POST",
|
method: "POST",
|
||||||
useToken: true,
|
useToken: true,
|
||||||
body: { id },
|
body: { id },
|
||||||
})
|
});
|
||||||
|
|
||||||
return [shownMessageResponse]
|
return [shownMessageResponse];
|
||||||
} catch (nativeError) {
|
} catch (nativeError) {
|
||||||
const [error] = parseAxiosError(nativeError)
|
const [error] = parseAxiosError(nativeError);
|
||||||
|
|
||||||
return [null, `Не удалось прочесть сообщение. ${error}`]
|
return [null, `Не удалось прочесть сообщение. ${error}`];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
55
src/assets/Icons/download.tsx
Normal file
55
src/assets/Icons/download.tsx
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { Box, SxProps, Theme } from "@mui/material";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
color: string;
|
||||||
|
sx?: SxProps<Theme>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Download({ color, sx }: Props) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "38px",
|
||||||
|
width: "45px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
...sx,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="47"
|
||||||
|
height="42"
|
||||||
|
viewBox="0 0 47 42"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M33.8846 26.8076H44.2692C44.7283 26.8076 45.1685 26.9551 45.4931 27.2177C45.8177 27.4802 46 27.8363 46 28.2076V39.4076C46 39.7789 45.8177 40.135 45.4931 40.3976C45.1685 40.6601 44.7283 40.8076 44.2692 40.8076H2.73077C2.27174 40.8076 1.83151 40.6601 1.50693 40.3976C1.18235 40.135 1 39.7789 1 39.4076V28.2076C1 27.8363 1.18235 27.4802 1.50693 27.2177C1.83151 26.9551 2.27174 26.8076 2.73077 26.8076H13.1154"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M23.5 27V1"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M13.1155 11.3846L23.5001 1L33.8847 11.3846"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth="2"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M39.135 36.0776C40.3141 36.0776 41.27 35.1217 41.27 33.9426C41.27 32.7635 40.3141 31.8076 39.135 31.8076C37.9559 31.8076 37 32.7635 37 33.9426C37 35.1217 37.9559 36.0776 39.135 36.0776Z"
|
||||||
|
fill={color}
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -10,32 +10,52 @@ import {
|
|||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} 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 {
|
import {
|
||||||
|
TicketMessage,
|
||||||
|
makeRequest,
|
||||||
|
useTicketsFetcher,
|
||||||
useTicketMessages,
|
useTicketMessages,
|
||||||
getMessageFromFetchError,
|
getMessageFromFetchError,
|
||||||
useSSESubscription,
|
useSSESubscription,
|
||||||
useEventListener,
|
useEventListener,
|
||||||
createTicket,
|
createTicket,
|
||||||
} from "@frontend/kitui";
|
} 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 { sendTicketMessage, shownMessage } from "@root/api/ticket";
|
||||||
import { useSSETab } from "@root/utils/hooks/useSSETab";
|
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 {
|
interface Props {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
sx?: SxProps<Theme>;
|
sx?: SxProps<Theme>;
|
||||||
@ -45,23 +65,26 @@ export default function Chat({ open = false, sx }: Props) {
|
|||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
const [messageField, setMessageField] = useState<string>("");
|
const [messageField, setMessageField] = useState<string>("");
|
||||||
const sessionData = useUnauthTicketStore((state) => state.sessionData);
|
const [disableFileButton, setDisableFileButton] = useState(false);
|
||||||
const messages = useUnauthTicketStore((state) => state.messages);
|
const [modalWarningType, setModalWarningType] =
|
||||||
const messageApiPage = useUnauthTicketStore((state) => state.apiPage);
|
useState<ModalWarningType>(null);
|
||||||
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 chatBoxRef = useRef<HTMLDivElement>(null);
|
const chatBoxRef = useRef<HTMLDivElement>(null);
|
||||||
|
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;
|
||||||
|
|
||||||
const { isActiveSSETab, updateSSEValue } = useSSETab<TicketMessage[]>(
|
const { isActiveSSETab, updateSSEValue } = useSSETab<TicketMessage[]>(
|
||||||
"ticket",
|
"ticket",
|
||||||
addOrUpdateUnauthMessages
|
addOrUpdateUnauthMessages
|
||||||
@ -74,8 +97,10 @@ export default function Chat({ open = false, sx }: Props) {
|
|||||||
messagesPerPage,
|
messagesPerPage,
|
||||||
messageApiPage,
|
messageApiPage,
|
||||||
onSuccess: useCallback((messages) => {
|
onSuccess: useCallback((messages) => {
|
||||||
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1)
|
if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) {
|
||||||
chatBoxRef.current.scrollTop = 1;
|
chatBoxRef.current.scrollTop = 1;
|
||||||
|
}
|
||||||
|
|
||||||
addOrUpdateUnauthMessages(messages);
|
addOrUpdateUnauthMessages(messages);
|
||||||
}, []),
|
}, []),
|
||||||
onError: useCallback((error: Error) => {
|
onError: useCallback((error: Error) => {
|
||||||
@ -92,6 +117,7 @@ export default function Chat({ open = false, sx }: Props) {
|
|||||||
`/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
|
`/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`,
|
||||||
onNewData: (ticketMessages) => {
|
onNewData: (ticketMessages) => {
|
||||||
updateSSEValue(ticketMessages);
|
updateSSEValue(ticketMessages);
|
||||||
|
|
||||||
addOrUpdateUnauthMessages(ticketMessages);
|
addOrUpdateUnauthMessages(ticketMessages);
|
||||||
},
|
},
|
||||||
onDisconnect: useCallback(() => {
|
onDisconnect: useCallback(() => {
|
||||||
@ -100,6 +126,34 @@ export default function Chat({ open = false, sx }: Props) {
|
|||||||
marker: "ticket",
|
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(
|
const throttledScrollHandler = useMemo(
|
||||||
() =>
|
() =>
|
||||||
throttle(() => {
|
throttle(() => {
|
||||||
@ -149,7 +203,7 @@ export default function Chat({ open = false, sx }: Props) {
|
|||||||
async function handleSendMessage() {
|
async function handleSendMessage() {
|
||||||
if (!messageField || isMessageSending) return;
|
if (!messageField || isMessageSending) return;
|
||||||
|
|
||||||
if (!sessionData) {
|
if (!sessionData?.ticketId) {
|
||||||
setIsMessageSending(true);
|
setIsMessageSending(true);
|
||||||
createTicket({
|
createTicket({
|
||||||
url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
|
url: process.env.REACT_APP_DOMAIN + "/heruvym/create",
|
||||||
@ -157,10 +211,10 @@ export default function Chat({ open = false, sx }: Props) {
|
|||||||
Title: "Unauth title",
|
Title: "Unauth title",
|
||||||
Message: messageField,
|
Message: messageField,
|
||||||
},
|
},
|
||||||
useToken: false,
|
useToken: Boolean(user),
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setUnauthSessionData({
|
setTicketData({
|
||||||
ticketId: response.Ticket,
|
ticketId: response.Ticket,
|
||||||
sessionId: response.sess,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
{open && (
|
{open && (
|
||||||
@ -276,16 +390,87 @@ export default function Chat({ open = false, sx }: Props) {
|
|||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{sessionData &&
|
{ticket.sessionData?.ticketId &&
|
||||||
messages.map((message) => (
|
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
|
<ChatMessage
|
||||||
unAuthenticated
|
unAuthenticated
|
||||||
key={message.id}
|
key={message.id}
|
||||||
text={message.message}
|
text={message.message}
|
||||||
createdAt={message.created_at}
|
createdAt={message.created_at}
|
||||||
isSelf={sessionData.sessionId === message.user_id}
|
isSelf={
|
||||||
|
(ticket.sessionData?.sessionId || user) ===
|
||||||
|
message.user_id
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</Box>
|
</Box>
|
||||||
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
|
<FormControl fullWidth sx={{ borderTop: "1px solid black" }}>
|
||||||
<InputBase
|
<InputBase
|
||||||
@ -314,6 +499,26 @@ export default function Chat({ open = false, sx }: Props) {
|
|||||||
onChange={(e) => setMessageField(e.target.value)}
|
onChange={(e) => setMessageField(e.target.value)}
|
||||||
endAdornment={
|
endAdornment={
|
||||||
<InputAdornment position="end">
|
<InputAdornment position="end">
|
||||||
|
<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"
|
||||||
|
/>
|
||||||
<IconButton
|
<IconButton
|
||||||
disabled={isMessageSending}
|
disabled={isMessageSending}
|
||||||
onClick={handleSendMessage}
|
onClick={handleSendMessage}
|
||||||
|
113
src/components/FloatingSupportChat/ChatDocument.tsx
Normal file
113
src/components/FloatingSupportChat/ChatDocument.tsx
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
|
||||||
|
import { isDateToday } from "../../utils/date";
|
||||||
|
import Download from "@root/assets/Icons/download";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
unAuthenticated?: boolean;
|
||||||
|
isSelf: boolean;
|
||||||
|
file: string;
|
||||||
|
createdAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ChatDocument({
|
||||||
|
unAuthenticated = false,
|
||||||
|
isSelf,
|
||||||
|
file,
|
||||||
|
createdAt,
|
||||||
|
}: Props) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const upMd = useMediaQuery(theme.breakpoints.up("md"));
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "9px",
|
||||||
|
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
alignSelf: "end",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "14px",
|
||||||
|
lineHeight: "17px",
|
||||||
|
order: isSelf ? 1 : 2,
|
||||||
|
margin: isSelf ? "0 0 0 auto" : "0 auto 0 0",
|
||||||
|
color: "#434657",
|
||||||
|
mb: "-4px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{time}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: messageBackgroundColor,
|
||||||
|
border: unAuthenticated
|
||||||
|
? "1px solid #E3E3E3"
|
||||||
|
: `1px solid ${"#434657"}`,
|
||||||
|
order: isSelf ? 2 : 1,
|
||||||
|
p: upMd ? "18px" : "12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
color: isSelf || unAuthenticated ? "#434657" : "white",
|
||||||
|
position: "relative",
|
||||||
|
maxWidth: `calc(100% - ${today ? 45 : 110}px)`,
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-1px",
|
||||||
|
right: isSelf ? "-8px" : undefined,
|
||||||
|
left: isSelf ? undefined : "-8px",
|
||||||
|
transform: isSelf ? undefined : "scale(-1, 1)",
|
||||||
|
}}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 16 8"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0.5 0.5L15.5 0.500007
|
||||||
|
C10 0.500006 7.5 8 7.5 7.5H7.5H0.5V0.5Z"
|
||||||
|
fill={messageBackgroundColor}
|
||||||
|
stroke={unAuthenticated ? "#E3E3E3" : theme.palette.gray.main}
|
||||||
|
/>
|
||||||
|
<rect y="1" width="8" height="8" fill={messageBackgroundColor} />
|
||||||
|
</svg>
|
||||||
|
<Link
|
||||||
|
download
|
||||||
|
href={`https://storage.yandexcloud.net/pair/${file}`}
|
||||||
|
style={{
|
||||||
|
color: "#7E2AEA",
|
||||||
|
display: "flex",
|
||||||
|
gap: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Download color={theme.palette.purple.main} />
|
||||||
|
</Link>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
119
src/components/FloatingSupportChat/ChatImage.tsx
Normal file
119
src/components/FloatingSupportChat/ChatImage.tsx
Normal file
@ -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 (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "9px",
|
||||||
|
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
alignSelf: "end",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "14px",
|
||||||
|
lineHeight: "17px",
|
||||||
|
order: isSelf ? 1 : 2,
|
||||||
|
margin: isSelf ? "0 0 0 auto" : "0 auto 0 0",
|
||||||
|
color: "#434657",
|
||||||
|
mb: "-4px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{time}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: messageBackgroundColor,
|
||||||
|
border: unAuthenticated
|
||||||
|
? "1px solid #E3E3E3"
|
||||||
|
: `1px solid ${"#434657"}`,
|
||||||
|
order: isSelf ? 2 : 1,
|
||||||
|
p: upMd ? "18px" : "12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
color: isSelf || unAuthenticated ? "#434657" : "white",
|
||||||
|
position: "relative",
|
||||||
|
maxWidth: `calc(100% - ${today ? 45 : 110}px)`,
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-1px",
|
||||||
|
right: isSelf ? "-8px" : undefined,
|
||||||
|
left: isSelf ? undefined : "-8px",
|
||||||
|
transform: isSelf ? undefined : "scale(-1, 1)",
|
||||||
|
}}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 16 8"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0.5 0.5L15.5 0.500007
|
||||||
|
C10 0.500006 7.5 8 7.5 7.5H7.5H0.5V0.5Z"
|
||||||
|
fill={messageBackgroundColor}
|
||||||
|
stroke={unAuthenticated ? "#E3E3E3" : "#434657"}
|
||||||
|
/>
|
||||||
|
<rect y="1" width="8" height="8" fill={messageBackgroundColor} />
|
||||||
|
</svg>
|
||||||
|
<ButtonBase target="_blank" href={`/image/${file}`}>
|
||||||
|
<Box
|
||||||
|
component="img"
|
||||||
|
sx={{
|
||||||
|
height: "217px",
|
||||||
|
width: "217px",
|
||||||
|
}}
|
||||||
|
src={`https://storage.yandexcloud.net/pair/${file}`}
|
||||||
|
/>
|
||||||
|
</ButtonBase>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
123
src/components/FloatingSupportChat/ChatVideo.tsx
Normal file
123
src/components/FloatingSupportChat/ChatVideo.tsx
Normal file
@ -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 (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
gap: "9px",
|
||||||
|
padding: isSelf ? "0 8px 0 0" : "0 0 0 8px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
alignSelf: "end",
|
||||||
|
fontWeight: 400,
|
||||||
|
fontSize: "14px",
|
||||||
|
lineHeight: "17px",
|
||||||
|
order: isSelf ? 1 : 2,
|
||||||
|
margin: isSelf ? "0 0 0 auto" : "0 auto 0 0",
|
||||||
|
color: "#434657",
|
||||||
|
mb: "-4px",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{time}
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
backgroundColor: messageBackgroundColor,
|
||||||
|
border: unAuthenticated
|
||||||
|
? "1px solid #E3E3E3"
|
||||||
|
: `1px solid ${"#434657"}`,
|
||||||
|
order: isSelf ? 2 : 1,
|
||||||
|
p: upMd ? "18px" : "12px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
color: isSelf || unAuthenticated ? "#434657" : "white",
|
||||||
|
position: "relative",
|
||||||
|
maxWidth: `calc(100% - ${today ? 45 : 110}px)`,
|
||||||
|
overflowWrap: "break-word",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "-1px",
|
||||||
|
right: isSelf ? "-8px" : undefined,
|
||||||
|
left: isSelf ? undefined : "-8px",
|
||||||
|
transform: isSelf ? undefined : "scale(-1, 1)",
|
||||||
|
}}
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="8"
|
||||||
|
viewBox="0 0 16 8"
|
||||||
|
fill="none"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M0.5 0.5L15.5 0.500007
|
||||||
|
C10 0.500006 7.5 8 7.5 7.5H7.5H0.5V0.5Z"
|
||||||
|
fill={messageBackgroundColor}
|
||||||
|
stroke={unAuthenticated ? "#E3E3E3" : "#434657"}
|
||||||
|
/>
|
||||||
|
<rect y="1" width="8" height="8" fill={messageBackgroundColor} />
|
||||||
|
</svg>
|
||||||
|
<Box
|
||||||
|
component="video"
|
||||||
|
sx={{
|
||||||
|
height: "217px",
|
||||||
|
width: "217px",
|
||||||
|
}}
|
||||||
|
controls
|
||||||
|
>
|
||||||
|
<source src={`https://storage.yandexcloud.net/pair/${file}`} />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
@ -4,12 +4,16 @@ import { Box, Fab, Typography, Badge, useTheme } from "@mui/material";
|
|||||||
import CircleDoubleDown from "./CircleDoubleDownIcon";
|
import CircleDoubleDown from "./CircleDoubleDownIcon";
|
||||||
import Chat from "./Chat";
|
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() {
|
export default function FloatingSupportChat() {
|
||||||
const [isChatOpened, setIsChatOpened] = useState<boolean>(false);
|
const [isChatOpened, setIsChatOpened] = useState<boolean>(false);
|
||||||
const theme = useTheme();
|
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 = {
|
const animation = {
|
||||||
"@keyframes runningStripe": {
|
"@keyframes runningStripe": {
|
||||||
@ -35,6 +39,7 @@ export default function FloatingSupportChat() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
|
@ -1,7 +1,23 @@
|
|||||||
import { FetchState, Ticket } from "@frontend/kitui"
|
import { FetchState, Ticket, TicketMessage } from "@frontend/kitui";
|
||||||
import { create } from "zustand"
|
import { create } from "zustand";
|
||||||
import { devtools } from "zustand/middleware"
|
import { devtools, persist, createJSONStorage } from "zustand/middleware";
|
||||||
|
import { useUserStore } from "./user";
|
||||||
|
import { produce } from "immer";
|
||||||
|
|
||||||
|
type SessionData = {
|
||||||
|
ticketId: string;
|
||||||
|
sessionId: string;
|
||||||
|
};
|
||||||
|
interface AuthData {
|
||||||
|
sessionData: SessionData | null;
|
||||||
|
isMessageSending: boolean;
|
||||||
|
messages: TicketMessage[];
|
||||||
|
apiPage: number;
|
||||||
|
messagesPerPage: number;
|
||||||
|
lastMessageId: string | undefined;
|
||||||
|
isPreventAutoscroll: boolean;
|
||||||
|
unauthTicketMessageFetchState: FetchState;
|
||||||
|
}
|
||||||
|
|
||||||
interface TicketStore {
|
interface TicketStore {
|
||||||
ticketCount: number;
|
ticketCount: number;
|
||||||
@ -9,38 +25,160 @@ interface TicketStore {
|
|||||||
apiPage: number;
|
apiPage: number;
|
||||||
ticketsPerPage: number;
|
ticketsPerPage: number;
|
||||||
ticketsFetchState: FetchState;
|
ticketsFetchState: FetchState;
|
||||||
|
authData: AuthData;
|
||||||
|
unauthData: AuthData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const initAuthData = {
|
||||||
|
sessionData: null,
|
||||||
|
isMessageSending: false,
|
||||||
|
messages: [],
|
||||||
|
apiPage: 0,
|
||||||
|
messagesPerPage: 10,
|
||||||
|
lastMessageId: undefined,
|
||||||
|
isPreventAutoscroll: false,
|
||||||
|
unauthTicketMessageFetchState: "idle" as FetchState,
|
||||||
|
};
|
||||||
|
|
||||||
const initialState: TicketStore = {
|
const initialState: TicketStore = {
|
||||||
ticketCount: 0,
|
ticketCount: 0,
|
||||||
tickets: [],
|
tickets: [],
|
||||||
apiPage: 0,
|
apiPage: 0,
|
||||||
ticketsPerPage: 10,
|
ticketsPerPage: 10,
|
||||||
ticketsFetchState: "idle",
|
ticketsFetchState: "idle",
|
||||||
}
|
authData: initAuthData,
|
||||||
|
unauthData: initAuthData,
|
||||||
|
};
|
||||||
|
|
||||||
export const useTicketStore = create<TicketStore>()(
|
export const useTicketStore = create<TicketStore>()(
|
||||||
devtools(
|
persist(
|
||||||
(set, get) => initialState,
|
devtools((set, get) => initialState, {
|
||||||
|
name: "Unauth tickets",
|
||||||
|
}),
|
||||||
{
|
{
|
||||||
name: "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[]) => {
|
export const updateTickets = (receivedTickets: Ticket[]) => {
|
||||||
const state = useTicketStore.getState()
|
const state = useTicketStore.getState();
|
||||||
const ticketIdToTicketMap: { [ticketId: string]: Ticket; } = {};
|
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 = <T extends AuthData>(
|
||||||
|
recipe: (ticket: AuthData) => void
|
||||||
|
) =>
|
||||||
|
setProducedState(
|
||||||
|
(state) => {
|
||||||
|
//В зависимости от авторизованности вызывается изменение разных объектов
|
||||||
|
if (Boolean(useUserStore.getState().userId)) {
|
||||||
|
recipe(state.authData);
|
||||||
|
} else {
|
||||||
|
recipe(state.unauthData);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "updateTicket",
|
||||||
|
recipe,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
function setProducedState<A extends string | { type: unknown }>(
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
@ -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<UnauthTicketStore>()(
|
|
||||||
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
|
|
||||||
}
|
|
37
src/utils/checkAcceptableMediaType.ts
Normal file
37
src/utils/checkAcceptableMediaType.ts
Normal file
@ -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 "Не удалось отправить файл. Недопустимый тип";
|
||||||
|
}
|
||||||
|
};
|
Loading…
Reference in New Issue
Block a user