fix requests, add fetch on scroll

This commit is contained in:
nflnkr 2023-05-03 19:24:15 +03:00
parent 5ebea0abbd
commit 50e462da6e
2 changed files with 93 additions and 35 deletions

@ -1,13 +1,13 @@
import { Box, FormControl, IconButton, InputAdornment, InputBase, SxProps, Theme, Typography, useMediaQuery, useTheme } from "@mui/material";
import { createTicket, getUnauthTicketMessages, sendTicketMessage, subscribeToUnauthTicketMessages } from "@root/api/tickets";
import { GetMessagesRequest, TicketMessage } from "@root/model/ticket";
import { useMessageStore } from "@root/stores/messages";
import { addOrUpdateUnauthMessages, setUnauthTicketFetchState, setUnauthTicketSessionId, useUnauthTicketStore } from "@root/stores/unauthTicket";
import { addOrUpdateUnauthMessages, setUnauthTicketFetchState, useUnauthTicketStore, incrementUnauthMessageApiPage, setUnauthIsPreventAutoscroll, setUnauthSessionData } from "@root/stores/unauthTicket";
import { enqueueSnackbar } from "notistack";
import { useEffect, useRef, useState } from "react";
import ChatMessage from "../ChatMessage";
import SendIcon from "../icons/SendIcon";
import UserCircleIcon from "./UserCircleIcon";
import { throttle } from "@root/utils/decorators";
interface Props {
@ -18,20 +18,22 @@ export default function Chat({ sx }: Props) {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const [messageField, setMessageField] = useState<string>("");
const sessionId = useUnauthTicketStore(state => state.sessionId);
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 messagesFetchStateRef = useRef(useMessageStore.getState().fetchState);
const messagesFetchStateRef = useRef(useUnauthTicketStore.getState().fetchState);
const isPreventAutoscroll = useUnauthTicketStore(state => state.isPreventAutoscroll);
const lastMessageId = useUnauthTicketStore(state => state.lastMessageId);
const chatBoxRef = useRef<HTMLDivElement>();
useEffect(function fetchTicketMessages() {
if (!sessionId) return;
if (!sessionData) return;
const getTicketsBody: GetMessagesRequest = {
amt: messagesPerPage,
page: messageApiPage,
ticket: sessionId,
ticket: sessionData.ticketId,
};
const controller = new AbortController();
@ -47,19 +49,19 @@ export default function Chat({ sx }: Props) {
} else setUnauthTicketFetchState("all fetched");
}).catch(error => {
console.log("Error fetching messages", error);
enqueueSnackbar(error.message);
if (error.code !== "ERR_CANCELED") enqueueSnackbar(error.message);
});
return () => {
controller.abort();
};
}, [messageApiPage, messagesPerPage, sessionId]);
}, [messageApiPage, messagesPerPage, sessionData]);
useEffect(function subscribeToMessages() {
if (!sessionId) return;
if (!sessionData) return;
const unsubscribe = subscribeToUnauthTicketMessages({
sessionId,
sessionId: sessionData.ticketId,
onMessage(event) {
try {
const newMessage = JSON.parse(event.data) as TicketMessage;
@ -79,33 +81,87 @@ export default function Chat({ sx }: Props) {
return () => {
unsubscribe();
// clearUnauthTicketState();
setUnauthIsPreventAutoscroll(false);
};
}, [sessionId]);
}, [sessionData]);
useEffect(() => useMessageStore.subscribe(state => (messagesFetchStateRef.current = state.fetchState)), []);
useEffect(function attachScrollHandler() {
if (!chatBoxRef.current) return;
const chatBox = chatBoxRef.current;
const scrollHandler = () => {
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
setUnauthIsPreventAutoscroll(isPreventAutoscroll);
if (messagesFetchStateRef.current !== "idle") return;
if (chatBox.scrollTop < chatBox.clientHeight) {
if (chatBox.scrollTop < 1) chatBox.scrollTop = 1;
incrementUnauthMessageApiPage();
}
};
const throttledScrollHandler = throttle(scrollHandler, 200);
chatBox.addEventListener("scroll", throttledScrollHandler);
return () => {
chatBox.removeEventListener("scroll", throttledScrollHandler);
};
}, []);
useEffect(function scrollOnNewMessage() {
if (!chatBoxRef.current) return;
if (!isPreventAutoscroll) {
setTimeout(() => {
scrollToBottom();
}, 50);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [lastMessageId]);
useEffect(() => useUnauthTicketStore.subscribe(state => (messagesFetchStateRef.current = state.fetchState)), []);
async function handleSendMessage() {
if (!messageField) return;
if (!sessionId) {
if (!sessionData) {
const response = await createTicket({
Title: "Unauth title",
Message: messageField,
}, false);
setUnauthTicketSessionId(response.sess);
setUnauthSessionData({
ticketId: response.Ticket,
sessionId: response.sess,
});
} else {
sendTicketMessage({
ticket: sessionId,
ticket: sessionData.ticketId,
message: messageField,
lang: "ru",
files: [],
}, true);
}, true).catch(error => {
console.log("Coudn't send message", error);
enqueueSnackbar(error.message);
});
}
setMessageField("");
}
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();
@ -157,6 +213,7 @@ export default function Chat({ sx }: Props) {
sx={{
display: "flex",
width: "100%",
flexBasis: 0,
flexDirection: "column",
gap: upMd ? "20px" : "16px",
px: upMd ? "20px" : "5px",
@ -165,13 +222,13 @@ export default function Chat({ sx }: Props) {
flexGrow: 1,
}}
>
{messages.map((message) => (
{sessionData && messages.map((message) => (
<ChatMessage
unAuthenticated
key={message.id}
text={message.message}
time={new Date(message.created_at).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })}
isSelf={sessionId === message.user_id}
isSelf={sessionData.sessionId === message.user_id}
/>
))}
</Box>

@ -1,27 +1,32 @@
import { Ticket, TicketMessage } from "@root/model/ticket";
import { TicketMessage } from "@root/model/ticket";
import { create } from "zustand";
import { createJSONStorage, devtools, persist } from "zustand/middleware";
interface UnauthTicketStore {
ticket: Ticket | null; // TODO delete if unused
sessionId: string | null;
sessionData: {
ticketId: string;
sessionId: string;
} | null;
messages: TicketMessage[];
fetchState: "idle" | "fetching" | "all fetched";
apiPage: number;
messagesPerPage: number;
lastMessageId: string | undefined;
isPreventAutoscroll: boolean;
}
export const useUnauthTicketStore = create<UnauthTicketStore>()(
persist(
devtools(
(set, get) => ({
ticket: null,
sessionId: null,
sessionData: null,
messages: [],
fetchState: "idle",
apiPage: 0,
messagesPerPage: 10,
lastMessageId: undefined,
isPreventAutoscroll: false,
}),
{
name: "Unauth ticket store"
@ -29,16 +34,16 @@ export const useUnauthTicketStore = create<UnauthTicketStore>()(
),
{
version: 0,
name: "session",
name: "unauth-ticket",
storage: createJSONStorage(() => localStorage),
partialize: state => ({
sessionId: state.sessionId,
sessionData: state.sessionData,
})
}
)
);
export const setUnauthTicket = (ticket: Ticket) => useUnauthTicketStore.setState({ ticket });
export const setUnauthSessionData = (sessionData: UnauthTicketStore["sessionData"]) => useUnauthTicketStore.setState({ sessionData });
export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) => {
const state = useUnauthTicketStore.getState();
@ -48,16 +53,12 @@ export const addOrUpdateUnauthMessages = (receivedMessages: TicketMessage[]) =>
const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime);
useUnauthTicketStore.setState({ messages: sortedMessages });
useUnauthTicketStore.setState({
messages: sortedMessages,
lastMessageId: sortedMessages.at(-1)?.id,
});
};
export const clearUnauthTicketState = () => useUnauthTicketStore.setState({
ticket: null,
messages: [],
apiPage: 0,
fetchState: "idle",
});
export const setUnauthTicketFetchState = (fetchState: UnauthTicketStore["fetchState"]) => useUnauthTicketStore.setState({ fetchState });
export const incrementUnauthMessageApiPage = () => {
@ -66,7 +67,7 @@ export const incrementUnauthMessageApiPage = () => {
useUnauthTicketStore.setState({ apiPage: state.apiPage + 1 });
};
export const setUnauthTicketSessionId = (sessionId: string | null) => useUnauthTicketStore.setState({ sessionId });
export const setUnauthIsPreventAutoscroll = (isPreventAutoscroll: boolean) => useUnauthTicketStore.setState({ isPreventAutoscroll });
function sortMessagesByTime(ticket1: TicketMessage, ticket2: TicketMessage) {
const date1 = new Date(ticket1.created_at).getTime();