From 77071799a0541353d656e2dc3baa49551966f4e7 Mon Sep 17 00:00:00 2001 From: IlyaDoronin Date: Tue, 23 Jan 2024 17:18:22 +0300 Subject: [PATCH 1/4] feat: useSSETab --- src/components/FloatingSupportChat/Chat.tsx | 590 +++++++++---------- src/components/ProtectedLayout.tsx | 168 +++--- src/pages/Support/SupportChat.tsx | 598 ++++++++++---------- src/utils/hooks/useSSETab.ts | 93 +++ 4 files changed, 791 insertions(+), 658 deletions(-) create mode 100644 src/utils/hooks/useSSETab.ts diff --git a/src/components/FloatingSupportChat/Chat.tsx b/src/components/FloatingSupportChat/Chat.tsx index c84da71..8c9b375 100644 --- a/src/components/FloatingSupportChat/Chat.tsx +++ b/src/components/FloatingSupportChat/Chat.tsx @@ -1,319 +1,329 @@ import { - Box, - FormControl, - IconButton, - InputAdornment, - InputBase, - SxProps, - Theme, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material" -import { TicketMessage } from "@frontend/kitui" + Box, + FormControl, + IconButton, + InputAdornment, + InputBase, + SxProps, + Theme, + Typography, + 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" + 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 { - useTicketMessages, - getMessageFromFetchError, - useSSESubscription, - useEventListener, - createTicket, -} from "@frontend/kitui" -import { sendTicketMessage } from "@root/api/ticket" + useTicketMessages, + getMessageFromFetchError, + useSSESubscription, + useEventListener, + createTicket, +} from "@frontend/kitui"; +import { sendTicketMessage } from "@root/api/ticket"; +import { useSSETab } from "@root/utils/hooks/useSSETab"; interface Props { sx?: SxProps; } export default function Chat({ 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 chatBoxRef = useRef(null) + 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 chatBoxRef = useRef(null); + const { isActiveSSETab, updateSSEValue } = useSSETab( + "ticket", + addOrUpdateUnauthMessages + ); - useTicketMessages({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", - isUnauth: true, - ticketId: sessionData?.ticketId, - messagesPerPage, - messageApiPage, - onSuccess: useCallback((messages) => { - if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) - chatBoxRef.current.scrollTop = 1 - addOrUpdateUnauthMessages(messages) - }, []), - onError: useCallback((error: Error) => { - const message = getMessageFromFetchError(error) - if (message) enqueueSnackbar(message) - }, []), - onFetchStateChange: setUnauthTicketMessageFetchState, - }) + useTicketMessages({ + url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", + isUnauth: true, + ticketId: sessionData?.ticketId, + messagesPerPage, + messageApiPage, + onSuccess: useCallback((messages) => { + if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) + chatBoxRef.current.scrollTop = 1; + addOrUpdateUnauthMessages(messages); + }, []), + onError: useCallback((error: Error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }, []), + onFetchStateChange: setUnauthTicketMessageFetchState, + }); - useSSESubscription({ - enabled: Boolean(sessionData), - url: process.env.REACT_APP_DOMAIN + `/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`, - onNewData: addOrUpdateUnauthMessages, - onDisconnect: useCallback(() => { - setUnauthIsPreventAutoscroll(false) - }, []), - marker: "ticket", - }) + useSSESubscription({ + enabled: isActiveSSETab && Boolean(sessionData), + url: + process.env.REACT_APP_DOMAIN + + `/heruvym/ticket?ticket=${sessionData?.ticketId}&s=${sessionData?.sessionId}`, + onNewData: (ticketMessages) => { + updateSSEValue(ticketMessages); + addOrUpdateUnauthMessages(ticketMessages); + }, + onDisconnect: useCallback(() => { + setUnauthIsPreventAutoscroll(false); + }, []), + marker: "ticket", + }); - const throttledScrollHandler = useMemo( - () => - throttle(() => { - const chatBox = chatBoxRef.current - if (!chatBox) return + const throttledScrollHandler = useMemo( + () => + throttle(() => { + const chatBox = chatBoxRef.current; + if (!chatBox) return; - const scrollBottom = - chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight - const isPreventAutoscroll = scrollBottom > chatBox.clientHeight - setUnauthIsPreventAutoscroll(isPreventAutoscroll) + const scrollBottom = + chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; + const isPreventAutoscroll = scrollBottom > chatBox.clientHeight; + setUnauthIsPreventAutoscroll(isPreventAutoscroll); - if (fetchState !== "idle") return + if (fetchState !== "idle") return; - if (chatBox.scrollTop < chatBox.clientHeight) { - incrementUnauthMessageApiPage() - } - }, 200), - [fetchState] - ) + if (chatBox.scrollTop < chatBox.clientHeight) { + incrementUnauthMessageApiPage(); + } + }, 200), + [fetchState] + ); - useEventListener("scroll", throttledScrollHandler, chatBoxRef) + useEventListener("scroll", throttledScrollHandler, chatBoxRef); - useEffect( - function scrollOnNewMessage() { - if (!chatBoxRef.current) return + useEffect( + function scrollOnNewMessage() { + if (!chatBoxRef.current) return; - if (!isPreventAutoscroll) { - setTimeout(() => { - scrollToBottom() - }, 50) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, - [lastMessageId] - ) + if (!isPreventAutoscroll) { + setTimeout(() => { + scrollToBottom(); + }, 50); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, + [lastMessageId] + ); - async function handleSendMessage() { - if (!messageField || isMessageSending) return + async function handleSendMessage() { + if (!messageField || isMessageSending) return; - if (!sessionData) { - setIsMessageSending(true) - createTicket({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/create", - body: { - Title: "Unauth title", - Message: messageField, - }, - useToken: false, - }) - .then((response) => { - setUnauthSessionData({ - ticketId: response.Ticket, - sessionId: response.sess, - }) - }) - .catch((error) => { - const errorMessage = getMessageFromFetchError(error) - if (errorMessage) enqueueSnackbar(errorMessage) - }) - .finally(() => { - setMessageField("") - setIsMessageSending(false) - }) - } else { - setIsMessageSending(true) + if (!sessionData) { + setIsMessageSending(true); + createTicket({ + url: process.env.REACT_APP_DOMAIN + "/heruvym/create", + body: { + Title: "Unauth title", + Message: messageField, + }, + useToken: false, + }) + .then((response) => { + setUnauthSessionData({ + ticketId: response.Ticket, + sessionId: response.sess, + }); + }) + .catch((error) => { + const errorMessage = getMessageFromFetchError(error); + if (errorMessage) enqueueSnackbar(errorMessage); + }) + .finally(() => { + setMessageField(""); + setIsMessageSending(false); + }); + } else { + setIsMessageSending(true); - const [_, sendTicketMessageError] = await sendTicketMessage( - sessionData.ticketId, - messageField - ) + const [_, sendTicketMessageError] = await sendTicketMessage( + sessionData.ticketId, + messageField + ); - if (sendTicketMessageError) { - enqueueSnackbar(sendTicketMessageError) - } + if (sendTicketMessageError) { + enqueueSnackbar(sendTicketMessageError); + } - setMessageField("") - setIsMessageSending(false) - } - } - - function scrollToBottom(behavior?: ScrollBehavior) { - if (!chatBoxRef.current) return - - const chatBox = chatBoxRef.current - chatBox.scroll({ - left: 0, - top: chatBox.scrollHeight, - behavior, - }) - } - - const handleTextfieldKeyPress: React.KeyboardEventHandler< - HTMLInputElement | HTMLTextAreaElement - > = (e) => { - if (e.key === "Enter" && !e.shiftKey) { - e.preventDefault() - handleSendMessage() - } + setMessageField(""); + setIsMessageSending(false); + } } - return ( - - - - - Мария - + function scrollToBottom(behavior?: ScrollBehavior) { + if (!chatBoxRef.current) return; + + const chatBox = chatBoxRef.current; + chatBox.scroll({ + left: 0, + top: chatBox.scrollHeight, + behavior, + }); + } + + const handleTextfieldKeyPress: React.KeyboardEventHandler< + HTMLInputElement | HTMLTextAreaElement + > = (e) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSendMessage(); + } + }; + + return ( + + + + + Мария + онлайн-консультант - - - - - - {sessionData && + + + + + + {sessionData && messages.map((message) => ( - + ))} - - - setMessageField(e.target.value)} - endAdornment={ - - - - - - } - /> - - - - ) + + + setMessageField(e.target.value)} + endAdornment={ + + + + + + } + /> + + + + ); } diff --git a/src/components/ProtectedLayout.tsx b/src/components/ProtectedLayout.tsx index 1ea76db..271de1b 100644 --- a/src/components/ProtectedLayout.tsx +++ b/src/components/ProtectedLayout.tsx @@ -1,88 +1,104 @@ -import { Outlet } from "react-router-dom" -import Navbar from "./NavbarSite/Navbar" +import { Outlet } from "react-router-dom"; +import Navbar from "./NavbarSite/Navbar"; import { - Ticket, - getMessageFromFetchError, - useAllTariffsFetcher, - usePrivilegeFetcher, - useSSESubscription, - useTicketsFetcher, - useToken, -} from "@frontend/kitui" -import { updateTickets, setTicketCount, useTicketStore, setTicketsFetchState } from "@root/stores/tickets" -import { enqueueSnackbar } from "notistack" -import { updateTariffs } from "@root/stores/tariffs" -import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs" -import { setCustomTariffs } from "@root/stores/customTariffs" -import { useDiscounts } from "@root/utils/hooks/useDiscounts" -import { setDiscounts } from "@root/stores/discounts" -import { setPrivileges } from "@root/stores/privileges" + Ticket, + getMessageFromFetchError, + useAllTariffsFetcher, + usePrivilegeFetcher, + useSSESubscription, + useTicketsFetcher, + useToken, +} from "@frontend/kitui"; +import { + updateTickets, + setTicketCount, + useTicketStore, + setTicketsFetchState, +} from "@root/stores/tickets"; +import { enqueueSnackbar } from "notistack"; +import { updateTariffs } from "@root/stores/tariffs"; +import { useCustomTariffs } from "@root/utils/hooks/useCustomTariffs"; +import { setCustomTariffs } from "@root/stores/customTariffs"; +import { useDiscounts } from "@root/utils/hooks/useDiscounts"; +import { setDiscounts } from "@root/stores/discounts"; +import { setPrivileges } from "@root/stores/privileges"; import { useHistoryData } from "@root/utils/hooks/useHistoryData"; +import { useSSETab } from "@root/utils/hooks/useSSETab"; export default function ProtectedLayout() { - const token = useToken() - const ticketApiPage = useTicketStore((state) => state.apiPage) - const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage) + const token = useToken(); + const ticketApiPage = useTicketStore((state) => state.apiPage); + const ticketsPerPage = useTicketStore((state) => state.ticketsPerPage); + const { isActiveSSETab, updateSSEValue } = useSSETab( + "auth", + (data) => { + updateTickets(data.filter((d) => Boolean(d.id))); + setTicketCount(data.length); + } + ); + useSSESubscription({ + enabled: isActiveSSETab, + url: + process.env.REACT_APP_DOMAIN + + `/heruvym/subscribe?Authorization=${token}`, + onNewData: (data) => { + updateSSEValue(data); + updateTickets(data.filter((d) => Boolean(d.id))); + setTicketCount(data.length); + }, + marker: "ticket", + }); - useSSESubscription({ - url: process.env.REACT_APP_DOMAIN + `/heruvym/subscribe?Authorization=${token}`, - onNewData: (data) => { - updateTickets(data.filter((d) => Boolean(d.id))) - setTicketCount(data.length) - }, - marker: "ticket", - }) + useTicketsFetcher({ + url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets", + ticketsPerPage, + ticketApiPage, + onSuccess: (result) => { + if (result.data) updateTickets(result.data); + setTicketCount(result.count); + }, + onError: (error: Error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }, + onFetchStateChange: setTicketsFetchState, + }); - useTicketsFetcher({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/getTickets", - ticketsPerPage, - ticketApiPage, - onSuccess: (result) => { - if (result.data) updateTickets(result.data) - setTicketCount(result.count) - }, - onError: (error: Error) => { - const message = getMessageFromFetchError(error) - if (message) enqueueSnackbar(message) - }, - onFetchStateChange: setTicketsFetchState, - }) + useAllTariffsFetcher({ + onSuccess: updateTariffs, + onError: (error) => { + const errorMessage = getMessageFromFetchError(error); + if (errorMessage) enqueueSnackbar(errorMessage); + }, + }); - useAllTariffsFetcher({ - onSuccess: updateTariffs, - onError: (error) => { - const errorMessage = getMessageFromFetchError(error) - if (errorMessage) enqueueSnackbar(errorMessage) - }, - }) + useCustomTariffs({ + onNewUser: setCustomTariffs, + onError: (error) => { + if (error) enqueueSnackbar(error); + }, + }); - useCustomTariffs({ - onNewUser: setCustomTariffs, - onError: (error) => { - if (error) enqueueSnackbar(error) - }, - }) + useDiscounts({ + onNewDiscounts: setDiscounts, + onError: (error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }, + }); - useDiscounts({ - onNewDiscounts: setDiscounts, - onError: (error) => { - const message = getMessageFromFetchError(error) - if (message) enqueueSnackbar(message) - }, - }) + usePrivilegeFetcher({ + onSuccess: setPrivileges, + onError: (error) => { + console.log("usePrivilegeFetcher error :>> ", error); + }, + }); - usePrivilegeFetcher({ - onSuccess: setPrivileges, - onError: (error) => { - console.log("usePrivilegeFetcher error :>> ", error) - }, - }) - - useHistoryData(); + useHistoryData(); - return ( - - - - ) + return ( + + + + ); } diff --git a/src/pages/Support/SupportChat.tsx b/src/pages/Support/SupportChat.tsx index f51136e..d7a7e0e 100644 --- a/src/pages/Support/SupportChat.tsx +++ b/src/pages/Support/SupportChat.tsx @@ -1,327 +1,341 @@ import { - Box, - Button, - Fab, - FormControl, - IconButton, - InputAdornment, - InputBase, - Typography, - useMediaQuery, - useTheme, -} from "@mui/material" -import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward" -import { useCallback, useEffect, useMemo, useRef, useState } from "react" -import { useParams } from "react-router-dom" -import SendIcon from "@components/icons/SendIcon" -import { throttle, useToken } from "@frontend/kitui" -import { enqueueSnackbar } from "notistack" -import { useTicketStore } from "@root/stores/tickets" + Box, + Button, + Fab, + FormControl, + IconButton, + InputAdornment, + InputBase, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { useParams } from "react-router-dom"; +import SendIcon from "@components/icons/SendIcon"; +import { throttle, useToken } from "@frontend/kitui"; +import { enqueueSnackbar } from "notistack"; +import { useTicketStore } from "@root/stores/tickets"; import { - addOrUpdateMessages, - clearMessageState, - incrementMessageApiPage, - setIsPreventAutoscroll, - setTicketMessageFetchState, - useMessageStore, -} from "@root/stores/messages" -import { TicketMessage } from "@frontend/kitui" -import ChatMessage from "@root/components/ChatMessage" -import { cardShadow } from "@root/utils/theme" + addOrUpdateMessages, + clearMessageState, + incrementMessageApiPage, + setIsPreventAutoscroll, + setTicketMessageFetchState, + useMessageStore, +} from "@root/stores/messages"; +import { TicketMessage } from "@frontend/kitui"; +import ChatMessage from "@root/components/ChatMessage"; +import { cardShadow } from "@root/utils/theme"; import { - getMessageFromFetchError, - useEventListener, - useSSESubscription, - useTicketMessages, -} from "@frontend/kitui" -import { shownMessage, sendTicketMessage } from "@root/api/ticket" -import { withErrorBoundary } from "react-error-boundary" -import { handleComponentError } from "@root/utils/handleComponentError" + getMessageFromFetchError, + useEventListener, + useSSESubscription, + useTicketMessages, +} from "@frontend/kitui"; +import { shownMessage, sendTicketMessage } from "@root/api/ticket"; +import { withErrorBoundary } from "react-error-boundary"; +import { handleComponentError } from "@root/utils/handleComponentError"; +import { useSSETab } from "@root/utils/hooks/useSSETab"; function SupportChat() { - const theme = useTheme() - const upMd = useMediaQuery(theme.breakpoints.up("md")) - const isMobile = useMediaQuery(theme.breakpoints.up(460)) - const [messageField, setMessageField] = useState("") - const tickets = useTicketStore((state) => state.tickets) - const messages = useMessageStore((state) => state.messages) - const messageApiPage = useMessageStore((state) => state.apiPage) - const lastMessageId = useMessageStore((state) => state.lastMessageId) - const messagesPerPage = useMessageStore((state) => state.messagesPerPage) - const isPreventAutoscroll = useMessageStore( - (state) => state.isPreventAutoscroll - ) - const token = useToken() - const ticketId = useParams().ticketId - const ticket = tickets.find((ticket) => ticket.id === ticketId) - const chatBoxRef = useRef(null) - const fetchState = useMessageStore((state) => state.ticketMessageFetchState) + const theme = useTheme(); + const upMd = useMediaQuery(theme.breakpoints.up("md")); + const isMobile = useMediaQuery(theme.breakpoints.up(460)); + const [messageField, setMessageField] = useState(""); + const tickets = useTicketStore((state) => state.tickets); + const messages = useMessageStore((state) => state.messages); + const messageApiPage = useMessageStore((state) => state.apiPage); + const lastMessageId = useMessageStore((state) => state.lastMessageId); + const messagesPerPage = useMessageStore((state) => state.messagesPerPage); + const isPreventAutoscroll = useMessageStore( + (state) => state.isPreventAutoscroll + ); + const token = useToken(); + const ticketId = useParams().ticketId; + const ticket = tickets.find((ticket) => ticket.id === ticketId); + const chatBoxRef = useRef(null); + const fetchState = useMessageStore((state) => state.ticketMessageFetchState); + const { isActiveSSETab, updateSSEValue } = useSSETab( + "supportChat", + addOrUpdateMessages + ); - useTicketMessages({ - url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", - ticketId, - messagesPerPage, - messageApiPage, - onSuccess: (messages) => { - if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) - chatBoxRef.current.scrollTop = 1 - addOrUpdateMessages(messages) - }, - onError: (error: Error) => { - const message = getMessageFromFetchError(error) - if (message) enqueueSnackbar(message) - }, - onFetchStateChange: setTicketMessageFetchState, - }) + useTicketMessages({ + url: process.env.REACT_APP_DOMAIN + "/heruvym/getMessages", + ticketId, + messagesPerPage, + messageApiPage, + onSuccess: (messages) => { + if (chatBoxRef.current && chatBoxRef.current.scrollTop < 1) + chatBoxRef.current.scrollTop = 1; + addOrUpdateMessages(messages); + }, + onError: (error: Error) => { + const message = getMessageFromFetchError(error); + if (message) enqueueSnackbar(message); + }, + onFetchStateChange: setTicketMessageFetchState, + }); - useSSESubscription({ - enabled: Boolean(token) && Boolean(ticketId), - url: process.env.REACT_APP_DOMAIN + `/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`, - onNewData: addOrUpdateMessages, - onDisconnect: useCallback(() => { - clearMessageState() - setIsPreventAutoscroll(false) - }, []), - marker: "ticket message", - }) + useSSESubscription({ + enabled: isActiveSSETab && Boolean(token) && Boolean(ticketId), + url: + process.env.REACT_APP_DOMAIN + + `/heruvym/ticket?ticket=${ticketId}&Authorization=${token}`, + onNewData: (ticketMessages) => { + updateSSEValue(ticketMessages); + addOrUpdateMessages(ticketMessages); + }, + onDisconnect: useCallback(() => { + clearMessageState(); + setIsPreventAutoscroll(false); + }, []), + marker: "ticket message", + }); - const throttledScrollHandler = useMemo( - () => - throttle(() => { - const chatBox = chatBoxRef.current - if (!chatBox) return + const throttledScrollHandler = useMemo( + () => + throttle(() => { + const chatBox = chatBoxRef.current; + if (!chatBox) return; - const scrollBottom = - chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight - const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20 - setIsPreventAutoscroll(isPreventAutoscroll) + const scrollBottom = + chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight; + const isPreventAutoscroll = scrollBottom > chatBox.clientHeight * 20; + setIsPreventAutoscroll(isPreventAutoscroll); - if (fetchState !== "idle") return + if (fetchState !== "idle") return; - if (chatBox.scrollTop < chatBox.clientHeight) { - incrementMessageApiPage() - } - }, 200), - [fetchState] - ) + if (chatBox.scrollTop < chatBox.clientHeight) { + incrementMessageApiPage(); + } + }, 200), + [fetchState] + ); - useEventListener("scroll", throttledScrollHandler, chatBoxRef) + useEventListener("scroll", throttledScrollHandler, chatBoxRef); - useEffect( - function scrollOnNewMessage() { - if (!chatBoxRef.current) return + useEffect( + function scrollOnNewMessage() { + if (!chatBoxRef.current) return; - if (!isPreventAutoscroll) { - setTimeout(() => { - scrollToBottom() - }, 50) - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [lastMessageId] - ) + if (!isPreventAutoscroll) { + setTimeout(() => { + scrollToBottom(); + }, 50); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [lastMessageId] + ); - useEffect(() => { - if (ticket) { - shownMessage(ticket.top_message.id) - } - }, [ticket]) + useEffect(() => { + if (ticket) { + shownMessage(ticket.top_message.id); + } + }, [ticket]); - async function handleSendMessage() { - if (!ticket || !messageField) return + async function handleSendMessage() { + if (!ticket || !messageField) return; - const [, sendTicketMessageError] = await sendTicketMessage( - ticket.id, - messageField - ) + const [, sendTicketMessageError] = await sendTicketMessage( + ticket.id, + messageField + ); - if (sendTicketMessageError) { - return enqueueSnackbar(sendTicketMessageError) - } + if (sendTicketMessageError) { + return enqueueSnackbar(sendTicketMessageError); + } - setMessageField("") - } + setMessageField(""); + } - function scrollToBottom(behavior?: ScrollBehavior) { - if (!chatBoxRef.current) return + function scrollToBottom(behavior?: ScrollBehavior) { + if (!chatBoxRef.current) return; - const chatBox = chatBoxRef.current - chatBox.scroll({ - left: 0, - top: chatBox.scrollHeight, - behavior, - }) - } + const chatBox = chatBoxRef.current; + chatBox.scroll({ + left: 0, + top: chatBox.scrollHeight, + behavior, + }); + } - const createdAt = ticket && new Date(ticket.created_at) - const createdAtString = + const createdAt = ticket && new Date(ticket.created_at); + const createdAtString = createdAt && createdAt.toLocaleDateString(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", + year: "numeric", + month: "2-digit", + day: "2-digit", }) + " " + createdAt.toLocaleTimeString(undefined, { - hour: "2-digit", - minute: "2-digit", - }) + hour: "2-digit", + minute: "2-digit", + }); - return ( - - - - {ticket?.title} - - + return ( + + + + {ticket?.title} + + Создан: {createdAtString} - - - - {isPreventAutoscroll && ( - scrollToBottom("smooth")} - sx={{ - position: "absolute", - left: "10px", - bottom: "10px", - }} - > - - - )} - - {ticket && + + + + {isPreventAutoscroll && ( + scrollToBottom("smooth")} + sx={{ + position: "absolute", + left: "10px", + bottom: "10px", + }} + > + + + )} + + {ticket && messages.map((message) => ( - + ))} - - - - setMessageField(e.target.value)} - endAdornment={ - !upMd && ( - - - - - - ) - } - /> - - - - {upMd && ( - - - - )} - - ) + + + )} + + ); } export default withErrorBoundary(SupportChat, { - fallback: Не удалось отобразить чат, - onError: handleComponentError, -}) + fallback: ( + + Не удалось отобразить чат + + ), + onError: handleComponentError, +}); diff --git a/src/utils/hooks/useSSETab.ts b/src/utils/hooks/useSSETab.ts new file mode 100644 index 0000000..2cbd4a7 --- /dev/null +++ b/src/utils/hooks/useSSETab.ts @@ -0,0 +1,93 @@ +import { useState, useEffect, useRef } from "react"; + +type UseSSETabResult = { + isActiveSSETab: boolean; + updateSSEValue: (value: T) => void; +}; + +export const useSSETab = ( + sseName: string, + onUpdateValue?: (value: T) => void +): UseSSETabResult => { + const [openTimeSetted, seteOpenTimeSetted] = useState(false); + const [activeSSETab, setActiveSSETab] = useState(false); + const updateTimeIntervalId = useRef(null); + const checkConnectionIntervalId = useRef(null); + + useEffect(() => { + setOpenTime(); + checkConnectionIntervalId.current = setInterval(checkConnection, 15000); + + const onUpdate = (event: StorageEvent) => { + if (event.key === `sse-${sseName}-update` && event.newValue) { + onUpdateValue?.(JSON.parse(event.newValue)); + } + }; + + window.addEventListener("storage", onUpdate); + + return () => { + if (checkConnectionIntervalId.current) { + clearInterval(checkConnectionIntervalId.current); + } + + window.removeEventListener("storage", onUpdate); + }; + }, []); + + useEffect(() => { + if (activeSSETab) { + if (updateTimeIntervalId.current) { + clearInterval(updateTimeIntervalId.current); + } + + updateTime(); + updateTimeIntervalId.current = setInterval(updateTime, 5000); + + return () => { + setActiveSSETab(false); + + if (updateTimeIntervalId.current) { + clearInterval(updateTimeIntervalId.current); + } + }; + } + }, [activeSSETab]); + + const updateTime = () => { + const time = new Date().getTime(); + + localStorage.setItem(`sse-${sseName}`, String(time)); + }; + + const checkConnection = (): boolean => { + const time = new Date().getTime(); + const lastMessageTime = Number(localStorage.getItem(`sse-${sseName}`)); + + if (time - lastMessageTime > 15000) { + setActiveSSETab(true); + + return false; + } + + return true; + }; + + const setOpenTime = () => { + if (openTimeSetted) { + return; + } + + if (!checkConnection()) { + setActiveSSETab(true); + } + + seteOpenTimeSetted(true); + }; + + const updateSSEValue = (value: T) => { + localStorage.setItem(`sse-${sseName}-update`, JSON.stringify(value)); + }; + + return { isActiveSSETab: activeSSETab, updateSSEValue }; +}; From eb7b6064abfc45e63441db432209adfcdee2edcb Mon Sep 17 00:00:00 2001 From: IlyaDoronin Date: Wed, 14 Feb 2024 17:22:12 +0300 Subject: [PATCH 2/4] feat: rs-pay --- src/api/wallet.ts | 93 +++-- src/assets/bank-logo/rs-pay.png | Bin 0 -> 2108 bytes src/pages/Payment/Payment.tsx | 490 +++++++++++++----------- src/pages/Payment/PaymentMethodCard.tsx | 86 +++-- src/pages/Payment/WarnModal.tsx | 61 +++ 5 files changed, 433 insertions(+), 297 deletions(-) create mode 100644 src/assets/bank-logo/rs-pay.png create mode 100644 src/pages/Payment/WarnModal.tsx diff --git a/src/api/wallet.ts b/src/api/wallet.ts index 9b9d0c5..73d57d4 100644 --- a/src/api/wallet.ts +++ b/src/api/wallet.ts @@ -1,46 +1,67 @@ -import { makeRequest } from "@frontend/kitui" -import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet" -import { parseAxiosError } from "@root/utils/parse-error" +import { makeRequest } from "@frontend/kitui"; +import { SendPaymentRequest, SendPaymentResponse } from "@root/model/wallet"; +import { parseAxiosError } from "@root/utils/parse-error"; -const apiUrl = process.env.REACT_APP_DOMAIN + "/customer" +const apiUrl = process.env.REACT_APP_DOMAIN + "/customer"; const testPaymentBody: SendPaymentRequest = { - type: "bankCard", - amount: 15020, - currency: "RUB", - bankCard: { - number: "RUB", - expiryYear: "2021", - expiryMonth: "05", - csc: "05", - cardholder: "IVAN IVANOV", - }, - phoneNumber: "79000000000", - login: "login_test", - returnUrl: window.location.origin + "/wallet", -} + type: "bankCard", + amount: 15020, + currency: "RUB", + bankCard: { + number: "RUB", + expiryYear: "2021", + expiryMonth: "05", + csc: "05", + cardholder: "IVAN IVANOV", + }, + phoneNumber: "79000000000", + login: "login_test", + returnUrl: window.location.origin + "/wallet", +}; -export async function sendPayment( - {body = testPaymentBody, fromSquiz = false}: {body?: SendPaymentRequest, fromSquiz:boolean} -): Promise<[SendPaymentResponse | null, string?]> { - if (fromSquiz) body.returnUrl = "squiz.pena.digital/list?action=fromhub" - try { - const sendPaymentResponse = await makeRequest< +export async function sendPayment({ + body = testPaymentBody, + fromSquiz = false, +}: { + body?: SendPaymentRequest; + fromSquiz: boolean; +}): Promise<[SendPaymentResponse | null, string?]> { + if (fromSquiz) body.returnUrl = "squiz.pena.digital/list?action=fromhub"; + try { + const sendPaymentResponse = await makeRequest< SendPaymentRequest, SendPaymentResponse >({ - url: apiUrl + "/wallet", - contentType: true, - method: "POST", - useToken: true, - withCredentials: false, - body, - }) + url: apiUrl + "/wallet", + contentType: true, + method: "POST", + useToken: true, + withCredentials: false, + body, + }); - return [sendPaymentResponse] - } catch (nativeError) { - const [error] = parseAxiosError(nativeError) + return [sendPaymentResponse]; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); - return [null, `Ошибка оплаты. ${error}`] - } + return [null, `Ошибка оплаты. ${error}`]; + } } + +export const sendRSPayment = async (): Promise => { + try { + await makeRequest({ + url: apiUrl + "/wallet/rspay", + method: "POST", + useToken: true, + withCredentials: false, + }); + + return null; + } catch (nativeError) { + const [error] = parseAxiosError(nativeError); + + return `Ошибка оплаты. ${error}`; + } +}; diff --git a/src/assets/bank-logo/rs-pay.png b/src/assets/bank-logo/rs-pay.png new file mode 100644 index 0000000000000000000000000000000000000000..95270e8032911ff7d2df55430d806302c668dcc5 GIT binary patch literal 2108 zcmZ{lc{tSj7stP|lCoY)Vg@C$Wsop&ZNpfzj8L|5?KCxFqOpBdF1prVa!W+grJFK? zG?(a0G_qwakuev)YhSWvojdoR-}C(bxX*dcc|XrNpVxEFAD`!(bm#L#VWDF}000Qv z+7Mj%iu=a}(fsHE-%jO=Vt|dSBLL7<006uV06Y8?I0FDQ{=+#k0O0QffDE;;-o=<- zK>uVJFf`;J5)ea-F-b!G94QW{e z_VR>WG7+poq`UZP0j4Od9sN4bO$gVaXet^j!iqSuTJpKon){q&i+#V1@+S7c5)Z{Q zyEa7;2C62ztvm;CzFiiofiOQTOh$5>%0ix(G+L8??9E?a=n6z-;8VPbm!L*y@$O83 z3FAztHrg1y0J~D8xvhgj79|zipKvr{Nw7L;Ww29R<6Z zueAe|uJVHY?Rw?DAp zWZIVgRpHx%94l8d4Qh7YwejAY%`Vq5VNWLaCr*9vJ9Ti^wA_1ScMEB(GAtP{=y}n{ zXDt7G`KW-<&x!9hLPKQ5ZBL zl~pKed}!F^5YU#~q$S^da3HZNlwyV(@U|gMSo5yUPip(Ef80}^-+B|6Hmpa7m>?wp>9JFfIS*zoRxqe<7F3nCdxwJ&q{6u* zgiEzkfv0BGj)-Hr{w)i8oE(c?&KjUiZJ4)i!+NFqhg89PXogTl_Ia?RhMHEZo!XsX zmHWrHc@{oWFMR!#I_L!ZXgU?G^{Z>so$IKl73d~1P@Hk+jmqza9)=dVdyHwcj&$4S z95KD4A%;Gc&P#_o-JX`RFci3bsl@->={x3bB zCdF9;H*;3E(R7ZLs+rV`hnii0llP#j{~+nFVE1{_<lY9YeCl!b;d;pTr}iX=VX7H>zL`mS-JvNHQ~;^6CcS zuXcMn=SJqbzbIH0W9%aPft}xcL6IlysP6S=kL)*7#i`C@<9hPj71Vo*8KwQ)3v{mu z298t-SrhnHJ$fsF>?W=S87OQ9D=_C5y?TlLuHHa_FxPKD34qGmo4cx3s?Rw~w#bJc zi#@F};b%|ZgpJmDAe;c2QuiXl*8r@Wl~5F$8nn99@3can)8S|wAc~KpgPWmnCSizv zIAHSQlyT7LW&-5i((R2NKK^te6J^)bJUrXdPLOo_`EAJSJ`V3#<|p0o3?trB`*P9? zJ>I4T$@49P&?ZC{KS69?gfvP-2X?^$^OUeX>6>}uADG^H&^`EMBB_;KQ||>5%knPv zns57%m)CO@PQQa%AxXFncJ(TJ{GRJ0`B*aY!*7d(JTCe4dZ&vL<~HG@avcnO~@i->@2KPRZCIK z#eu&03-oKBOq@yI3bs&513qEd2lnJR_(Ebz%fL9|3M?^`!z7vv3bljUDLvng?A325 zrX|-;TmCNHCsL!?`kt=DnjV|SSEDGE?j?G%9s*DD&Da}QE<^0E&VBWSLi+@VEZ(K9 zqM38mD9aA7zL=08e~To2!2<=CWRchIIJ&Ij-SvgYrl?{DsJTZpR>=z=N6w?nc zwV+1G3RF)@9VDgl00M=UASxq>;vdtK(N><(f&S4!_<(Ccd;xTEx@WX-x>`DC+;nvD zXLRwnvl=)Y9*6T`)+PNPK)D(i7JTF10nG)GcYNT~|2jm6QG%kP{VCLcXX3WC<@pR? MYjvLR(t>>RZ~Pa)(null) - const [paymentValueField, setPaymentValueField] = useState("0") - const [paymentLink, setPaymentLink] = useState("") - const [fromSquiz, setIsFromSquiz] = useState(false) - const location = useLocation() + const [selectedPaymentMethod, setSelectedPaymentMethod] = + useState(null); + const [warnModalOpen, setWarnModalOpen] = useState(false); + const [paymentValueField, setPaymentValueField] = useState("0"); + const [paymentLink, setPaymentLink] = useState(""); + const [fromSquiz, setIsFromSquiz] = useState(false); + const location = useLocation(); + const verificationStatus = useUserStore((state) => state.verificationStatus); + const navigate = useNavigate(); - const notEnoughMoneyAmount = - (location.state?.notEnoughMoneyAmount as number) ?? 0 + const notEnoughMoneyAmount = + (location.state?.notEnoughMoneyAmount as number) ?? 0; - const paymentValue = parseFloat(paymentValueField) * 100 - - useLayoutEffect(() => { - // eslint-disable-next-line react-hooks/exhaustive-deps - setPaymentValueField((notEnoughMoneyAmount / 100).toString()) - const params = new URLSearchParams(window.location.search) - const fromSquiz = params.get("action") - if (fromSquiz === "squizpay") { - setIsFromSquiz(true) - setPaymentValueField((Number(params.get("dif") || "0") / 100).toString()) - } - history.pushState(null, document.title, "/payment"); - console.log(fromSquiz) - }, []) + const paymentValue = parseFloat(paymentValueField) * 100; - useEffect(() => { - setPaymentLink("") - }, [selectedPaymentMethod]) + useLayoutEffect(() => { + setPaymentValueField((notEnoughMoneyAmount / 100).toString()); + const params = new URLSearchParams(window.location.search); + const fromSquiz = params.get("action"); + if (fromSquiz === "squizpay") { + setIsFromSquiz(true); + setPaymentValueField((Number(params.get("dif") || "0") / 100).toString()); + } + history.pushState(null, document.title, "/payment"); + console.log(fromSquiz); + }, []); - async function handleChoosePaymentClick() { - if (Number(paymentValueField) !== 0) { - const [sendPaymentResponse, sendPaymentError] = await sendPayment({fromSquiz}) + useEffect(() => { + setPaymentLink(""); + }, [selectedPaymentMethod]); - if (sendPaymentError) { - return enqueueSnackbar(sendPaymentError) - } + async function handleChoosePaymentClick() { + if (Number(paymentValueField) === 0) { + return; + } - if (sendPaymentResponse) { - setPaymentLink(sendPaymentResponse.link) - } - } - } + if (selectedPaymentMethod !== "rspay") { + const [sendPaymentResponse, sendPaymentError] = await sendPayment({ + fromSquiz, + }); - const handleCustomBackNavigation = useHistoryTracker() + if (sendPaymentError) { + return enqueueSnackbar(sendPaymentError); + } - return ( - - - {!upMd && ( - - - - )} - Способ оплаты - - {!upMd && ( - + if (sendPaymentResponse) { + setPaymentLink(sendPaymentResponse.link); + } + + return; + } + + if (verificationStatus !== VerificationStatus.VERIFICATED) { + setWarnModalOpen(true); + + return; + } + + const sendRSPaymentError = await sendRSPayment(); + + if (sendRSPaymentError) { + return enqueueSnackbar(sendRSPaymentError); + } + + enqueueSnackbar( + "Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг." + ); + + navigate("/settings"); + } + + const handleCustomBackNavigation = useHistoryTracker(); + + return ( + + + {!upMd && ( + + + + )} + Способ оплаты + + {!upMd && ( + Выберите способ оплаты - - )} - - - {paymentMethods.map((method) => ( - setSelectedPaymentMethod(method.name)} - /> - ))} - - - - {upMd && Выберите способ оплаты} - К оплате - {paymentLink ? ( - - {currencyFormatter.format(paymentValue / 100)} - - ) : ( - setPaymentValueField(e.target.value)} - id="payment-amount" - gap={upMd ? "16px" : "10px"} - color={"#F2F3F7"} - FormInputSx={{ mb: "28px" }} - /> - )} - - {paymentLink ? ( - - ) : ( - + ) : ( + - )} - - - - ) + + )} + + + + + ); } diff --git a/src/pages/Payment/PaymentMethodCard.tsx b/src/pages/Payment/PaymentMethodCard.tsx index 2ed39a1..42c53d9 100644 --- a/src/pages/Payment/PaymentMethodCard.tsx +++ b/src/pages/Payment/PaymentMethodCard.tsx @@ -1,43 +1,55 @@ -import { Button, Typography, useMediaQuery, useTheme } from "@mui/material" +import { Button, Typography, useMediaQuery, useTheme } from "@mui/material"; interface Props { - name: string; - image: string; - isSelected?: boolean; - onClick: () => void; + label: string; + image: string; + isSelected?: boolean; + unpopular?: boolean; + onClick: () => void; } -export default function PaymentMethodCard({ name, image, isSelected, onClick }: Props) { - const theme = useTheme() - const upSm = useMediaQuery(theme.breakpoints.up("sm")) +export default function PaymentMethodCard({ + label, + image, + isSelected, + unpopular, + onClick, +}: Props) { + const theme = useTheme(); + const upSm = useMediaQuery(theme.breakpoints.up("sm")); - return ( - - ) + return ( + + ); } diff --git a/src/pages/Payment/WarnModal.tsx b/src/pages/Payment/WarnModal.tsx new file mode 100644 index 0000000..493d4a8 --- /dev/null +++ b/src/pages/Payment/WarnModal.tsx @@ -0,0 +1,61 @@ +import { Modal, Box, Typography, Button, useTheme } from "@mui/material"; +import { useNavigate } from "react-router-dom"; + +type WarnModalProps = { + open: boolean; + setOpen: (isOpen: boolean) => void; +}; + +export const WarnModal = ({ open, setOpen }: WarnModalProps) => { + const theme = useTheme(); + const navigate = useNavigate(); + + return ( + setOpen(false)} + sx={{ + display: "flex", + alignItems: "center", + justifyContent: "center", + }} + > + + + + Верификация не пройдена. + + + + + + + + + ); +}; From 4cf457a5f0c760f830f6ee6f7db2f639ef2e70b5 Mon Sep 17 00:00:00 2001 From: Nastya Date: Wed, 14 Feb 2024 23:42:50 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D0=B0=20?= =?UTF-8?q?=D0=A0=D0=A1=20=D0=BF=D0=BE=20=D0=BF=D0=B5=D1=80=D0=B2=D0=BE?= =?UTF-8?q?=D0=BC=D1=83=20=D0=BA=D0=BB=D0=B8=D0=BA=D1=83=20=D0=B1=D0=B5?= =?UTF-8?q?=D0=B7=20=D1=81=D1=83=D0=BC=D0=BC=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pages/Payment/Payment.tsx | 44 +++++++++++++++++++++-------------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/pages/Payment/Payment.tsx b/src/pages/Payment/Payment.tsx index 4bb82aa..bcf7e23 100644 --- a/src/pages/Payment/Payment.tsx +++ b/src/pages/Payment/Payment.tsx @@ -41,7 +41,6 @@ const paymentMethods: PaymentMethod[] = [ { label: "QIWI Кошелек", name: "qiwi", image: qiwiLogo }, { label: "Мир", name: "mir", image: mirLogo }, { label: "Тинькофф", name: "tinkoff", image: tinkoffLogo }, - { label: "Расчётный счёт", name: "rspay", image: rsPayLogo, unpopular: true }, ]; type PaymentMethodType = (typeof paymentMethods)[number]["name"]; @@ -104,23 +103,6 @@ export default function Payment() { return; } - if (verificationStatus !== VerificationStatus.VERIFICATED) { - setWarnModalOpen(true); - - return; - } - - const sendRSPaymentError = await sendRSPayment(); - - if (sendRSPaymentError) { - return enqueueSnackbar(sendRSPaymentError); - } - - enqueueSnackbar( - "Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг." - ); - - navigate("/settings"); } const handleCustomBackNavigation = useHistoryTracker(); @@ -187,6 +169,32 @@ export default function Payment() { unpopular={unpopular} /> ))} + { + + if (verificationStatus !== VerificationStatus.VERIFICATED) { + setWarnModalOpen(true); + + return; + } + + const sendRSPaymentError = await sendRSPayment(); + + if (sendRSPaymentError) { + return enqueueSnackbar(sendRSPaymentError); + } + + enqueueSnackbar( + "Cпасибо за заявку, в течении 24 часов вам будет выставлен счёт для оплаты услуг." + ); + + navigate("/settings"); + }} + unpopular={true} + /> Date: Thu, 15 Feb 2024 11:51:21 +0300 Subject: [PATCH 4/4] fix: useSSETab timeout --- src/utils/hooks/useSSETab.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/hooks/useSSETab.ts b/src/utils/hooks/useSSETab.ts index 2cbd4a7..69fa6e5 100644 --- a/src/utils/hooks/useSSETab.ts +++ b/src/utils/hooks/useSSETab.ts @@ -16,7 +16,7 @@ export const useSSETab = ( useEffect(() => { setOpenTime(); - checkConnectionIntervalId.current = setInterval(checkConnection, 15000); + checkConnectionIntervalId.current = setInterval(checkConnection, 5000); const onUpdate = (event: StorageEvent) => { if (event.key === `sse-${sseName}-update` && event.newValue) {