add message fetch on scroll
This commit is contained in:
parent
7fa15d9b98
commit
ef7685cfec
@ -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;
|
||||
}
|
Loading…
Reference in New Issue
Block a user