add message fetch on scroll

This commit is contained in:
nflnkr 2023-04-06 18:10:22 +03:00
parent 7fa15d9b98
commit ef7685cfec
3 changed files with 95 additions and 41 deletions

@ -8,7 +8,7 @@ import Message from "./Message";
import { throttle } from "@utils/decorators";
import { enqueueSnackbar } from "notistack";
import { useTicketStore } from "@root/stores/tickets";
import { addOrUpdateMessages, clearMessages, setMessages, useMessageStore } from "@root/stores/messages";
import { addOrUpdateMessages, clearMessageState, incrementMessageApiPage, setMessageFetchState, useMessageStore } from "@root/stores/messages";
import { getTicketMessages, sendTicketMessage, subscribeToTicketMessages } from "@root/api/tickets";
import { GetMessagesRequest, TicketMessage } from "@root/model/ticket";
import { authStore } from "@root/stores/makeRequest";
@ -20,6 +20,10 @@ export default function SupportChat() {
const [messageField, setMessageField] = useState<string>("");
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 fetchStateRef = useRef(useMessageStore.getState().fetchState);
const token = authStore(state => state.token);
const ticketId = useParams().ticketId;
const ticket = tickets.find(ticket => ticket.id === ticketId);
@ -30,18 +34,22 @@ export default function SupportChat() {
if (!ticketId) return;
const getTicketsBody: GetMessagesRequest = {
amt: 100, // TODO use pagination
page: 0,
amt: messagesPerPage,
page: messageApiPage,
ticket: ticketId,
};
const controller = new AbortController();
setMessageFetchState("fetching");
getTicketMessages({
body: getTicketsBody,
signal: controller.signal,
}).then(result => {
console.log("GetMessagesResponse", result);
setMessages(result);
if (result?.length > 0) {
addOrUpdateMessages(result);
setMessageFetchState("idle");
} else setMessageFetchState("all fetched");
}).catch(error => {
console.log("Error fetching tickets", error);
enqueueSnackbar(error.message);
@ -49,9 +57,8 @@ export default function SupportChat() {
return () => {
controller.abort();
clearMessages();
};
}, [ticketId]);
}, [messageApiPage, messagesPerPage, ticketId]);
useEffect(function subscribeToMessages() {
if (!ticketId || !token) return;
@ -76,15 +83,26 @@ export default function SupportChat() {
return () => {
unsubscribe();
clearMessageState();
};
}, [ticketId, token]);
useEffect(function refreshChatScrollTop() {
useEffect(function attachScrollHandler() {
if (!chatBoxRef.current) return;
const chatBox = chatBoxRef.current;
const scrollHandler = () =>
setIsPreventAutoscroll(chatBox.scrollTop + chatBox.clientHeight * 2 < chatBox.scrollHeight);
const scrollHandler = () => {
const scrollBottom = chatBox.scrollHeight - chatBox.scrollTop - chatBox.clientHeight;
const isPreventAutoscroll = scrollBottom > chatBox.clientHeight;
setIsPreventAutoscroll(isPreventAutoscroll);
if (fetchStateRef.current !== "idle") return;
if (chatBox.scrollTop < chatBox.clientHeight) {
if (chatBox.scrollTop < 1) chatBox.scrollTop = 1;
incrementMessageApiPage();
}
};
const throttledScrollHandler = throttle(scrollHandler, 200);
chatBox.addEventListener("scroll", throttledScrollHandler);
@ -94,12 +112,18 @@ export default function SupportChat() {
};
}, []);
useEffect(function scrollOnMessage() {
useEffect(function scrollOnNewMessage() {
if (!chatBoxRef.current) return;
if (!isPreventAutoscroll) scrollToBottom();
if (!isPreventAutoscroll) {
setTimeout(() => {
scrollToBottom();
}, 50);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [messages]);
}, [lastMessageId]);
useEffect(() => useMessageStore.subscribe(state => (fetchStateRef.current = state.fetchState)), []);
async function handleSendMessage() {
if (!ticket || !messageField) return;
@ -113,21 +137,19 @@ export default function SupportChat() {
setMessageField("");
}
function scrollToBottom() {
function scrollToBottom(behavior?: ScrollBehavior) {
if (!chatBoxRef.current) return;
chatBoxRef.current.scroll({
const chatBox = chatBoxRef.current;
chatBox.scroll({
left: 0,
top: chatBoxRef.current.scrollHeight,
behavior: "smooth",
top: chatBox.scrollHeight,
behavior,
});
}
if (!ticket) return null;
const sortedMessages = messages.sort(sortMessagesTime);
const createdAt = new Date(ticket.created_at);
const createdAtString = createdAt.toLocaleDateString(undefined, {
const createdAt = ticket && new Date(ticket.created_at);
const createdAtString = createdAt && createdAt.toLocaleDateString(undefined, {
year: "numeric",
month: "2-digit",
day: "2-digit",
@ -163,7 +185,7 @@ export default function SupportChat() {
}}
>
<Typography variant={upMd ? "h5" : "body2"} mb={"4px"}>
Заголовок
{ticket?.title}
</Typography>
<Typography
sx={{
@ -201,7 +223,7 @@ export default function SupportChat() {
{isPreventAutoscroll && (
<Fab
size="small"
onClick={scrollToBottom}
onClick={() => scrollToBottom("smooth")}
sx={{
position: "absolute",
left: "10px",
@ -224,7 +246,7 @@ export default function SupportChat() {
height: "100%",
}}
>
{sortedMessages.map((message) => (
{ticket && messages.map((message) => (
<Message
key={message.id}
text={message.message}
@ -294,10 +316,4 @@ export default function SupportChat() {
)}
</Box>
);
}
function sortMessagesTime(ticket1: TicketMessage, ticket2: TicketMessage) {
const date1 = new Date(ticket1.created_at).getTime();
const date2 = new Date(ticket2.created_at).getTime();
return date1 - date2;
}

@ -9,7 +9,7 @@ import TicketList from "./TicketList/TicketList";
import { useEffect } from "react";
import { getTickets, subscribeToAllTickets } from "@root/api/tickets";
import { GetTicketsRequest, Ticket } from "@root/model/ticket";
import { updateTickets, setTicketCount, clearTickets, useTicketStore, setFetchState } from "@root/stores/tickets";
import { updateTickets, setTicketCount, clearTickets, useTicketStore, setTicketsFetchState } from "@root/stores/tickets";
import { enqueueSnackbar } from "notistack";
import { authStore } from "@root/stores/makeRequest";
@ -18,19 +18,19 @@ export default function Support() {
const theme = useTheme();
const upMd = useMediaQuery(theme.breakpoints.up("md"));
const ticketId = useParams().ticketId;
const currentPage = useTicketStore(state => state.currentPage);
const ticketApiPage = useTicketStore(state => state.apiPage);
const ticketsPerPage = useTicketStore(state => state.ticketsPerPage);
const token = authStore(state => state.token);
useEffect(function fetchTickets() {
const getTicketsBody: GetTicketsRequest = {
amt: ticketsPerPage,
page: currentPage,
page: ticketApiPage,
status: "open",
};
const controller = new AbortController();
setFetchState("fetching");
setTicketsFetchState("fetching");
getTickets({
body: getTicketsBody,
signal: controller.signal,
@ -39,15 +39,15 @@ export default function Support() {
if (result.data) {
updateTickets(result.data);
setTicketCount(result.count);
setFetchState("idle");
} else setFetchState("all fetched");
setTicketsFetchState("idle");
} else setTicketsFetchState("all fetched");
}).catch(error => {
console.log("Error fetching tickets", error);
enqueueSnackbar(error.message);
});
return () => controller.abort();
}, [currentPage, ticketsPerPage]);
}, [ticketApiPage, ticketsPerPage]);
useEffect(function subscribeToTickets() {
if (!token) return;

@ -5,20 +5,34 @@ import { devtools } from "zustand/middleware";
interface MessageStore {
messages: TicketMessage[];
fetchState: "idle" | "fetching" | "all fetched";
apiPage: number;
messagesPerPage: number;
lastMessageId: string | undefined;
}
export const useMessageStore = create<MessageStore>()(
devtools(
(set, get) => ({
messages: [],
messagesPerPage: 10,
fetchState: "idle",
apiPage: 0,
lastMessageId: undefined,
}),
{
name: "Message store (client)"
name: "Message store (marketplace)"
}
)
);
export const setMessages = (messages: TicketMessage[]) => useMessageStore.setState(({ messages }));
export const setMessages = (messages: TicketMessage[]) => {
const sortedMessages = messages.sort(sortMessagesByTime);
useMessageStore.setState(({
messages: sortedMessages,
lastMessageId: sortedMessages.at(-1)?.id,
}));
};
export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => {
const state = useMessageStore.getState();
@ -26,7 +40,31 @@ export const addOrUpdateMessages = (receivedMessages: TicketMessage[]) => {
[...state.messages, ...receivedMessages].forEach(message => messageIdToMessageMap[message.id] = message);
useMessageStore.setState({ messages: Object.values(messageIdToMessageMap) });
const sortedMessages = Object.values(messageIdToMessageMap).sort(sortMessagesByTime);
useMessageStore.setState({
messages: sortedMessages,
lastMessageId: sortedMessages.at(-1)?.id,
});
};
export const clearMessages = () => useMessageStore.setState({ messages: [] });
export const clearMessageState = () => useMessageStore.setState({
messages: [],
apiPage: 0,
lastMessageId: undefined,
fetchState: "idle",
});
export const setMessageFetchState = (fetchState: MessageStore["fetchState"]) => useMessageStore.setState({ fetchState });
export const incrementMessageApiPage = () => {
const state = useMessageStore.getState();
useMessageStore.setState({ apiPage: state.apiPage + 1 });
};
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;
}