feat: chat with infinite scroll
This commit is contained in:
@@ -54,6 +54,7 @@
|
|||||||
"react-imask": "^7.6.1",
|
"react-imask": "^7.6.1",
|
||||||
"react-redux": "^9.1.2",
|
"react-redux": "^9.1.2",
|
||||||
"react-to-print": "^2.15.1",
|
"react-to-print": "^2.15.1",
|
||||||
|
"react-virtuoso": "^4.12.6",
|
||||||
"reactflow": "^11.11.4",
|
"reactflow": "^11.11.4",
|
||||||
"recharts": "^2.13.3",
|
"recharts": "^2.13.3",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import type { MessageFileSchema } from './MessageFileSchema';
|
import type { MessageFileSchema } from './MessageFileSchema';
|
||||||
import type { TgUserSchema } from './TgUserSchema';
|
import type { TgUserSchema } from './TgUserSchema';
|
||||||
|
import type { UserSchema } from './UserSchema';
|
||||||
export type MessageSchema = {
|
export type MessageSchema = {
|
||||||
text: string;
|
text: string;
|
||||||
chatId: number;
|
chatId: number;
|
||||||
id: number;
|
id: number;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
tgSender: (TgUserSchema | null);
|
tgSender: (TgUserSchema | null);
|
||||||
|
crmSender: (UserSchema | null);
|
||||||
status: string;
|
status: string;
|
||||||
isEdited: boolean;
|
isEdited: boolean;
|
||||||
file?: (MessageFileSchema | null);
|
file?: (MessageFileSchema | null);
|
||||||
|
|||||||
@@ -1,53 +1,82 @@
|
|||||||
import { ScrollArea, Stack } from "@mantine/core";
|
|
||||||
import Message from "./components/Message/Message.tsx";
|
import Message from "./components/Message/Message.tsx";
|
||||||
import MessageInput from "./components/MessageInput/MessageInput.tsx";
|
|
||||||
import { useChatContext } from "../../pages/ClientsPage/contexts/ChatContext.tsx";
|
import { useChatContext } from "../../pages/ClientsPage/contexts/ChatContext.tsx";
|
||||||
import { MessageSchema } from "../../client";
|
import { MessageSchema } from "../../client";
|
||||||
import { ReactNode } from "react";
|
import { ReactNode, useCallback } from "react";
|
||||||
import ChatDate from "./components/ChatDate/ChatDate.tsx";
|
import ChatDate from "./components/ChatDate/ChatDate.tsx";
|
||||||
|
import MessageInput from "./components/MessageInput/MessageInput.tsx";
|
||||||
|
import { Virtuoso } from "react-virtuoso";
|
||||||
|
import { Stack } from "@mantine/core";
|
||||||
|
|
||||||
const Chat = () => {
|
const Chat = () => {
|
||||||
const {
|
const {
|
||||||
messages,
|
messages,
|
||||||
scrollRef,
|
lastMessage,
|
||||||
onScrollPositionChange,
|
firstItemIndex,
|
||||||
|
fetchMoreMessages,
|
||||||
} = useChatContext();
|
} = useChatContext();
|
||||||
|
|
||||||
const getChatElements = (): ReactNode[] => {
|
const onFollowOutputHandler = useCallback(
|
||||||
const elements: ReactNode[] = [];
|
(atBottom: boolean) => {
|
||||||
let prevMessage: MessageSchema | null = null;
|
if (atBottom || lastMessage?.crmSender) {
|
||||||
|
return "auto";
|
||||||
for (let i = messages.length - 1; i >= 0; i--) {
|
} else {
|
||||||
const currMessage = messages[i];
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[lastMessage],
|
||||||
|
);
|
||||||
|
|
||||||
|
const itemContent = useCallback(
|
||||||
|
(index: number, sessionData: MessageSchema) => {
|
||||||
|
let dateComponent: ReactNode | null = null;
|
||||||
|
const msgArrayIdx = index - firstItemIndex;
|
||||||
|
const currMessage = messages[msgArrayIdx];
|
||||||
|
let prevMessage = null;
|
||||||
|
if (msgArrayIdx > 0) {
|
||||||
|
prevMessage = messages[msgArrayIdx - 1];
|
||||||
|
}
|
||||||
if (!prevMessage || prevMessage.createdAt.substring(5, 10) != currMessage.createdAt.substring(5, 10)) {
|
if (!prevMessage || prevMessage.createdAt.substring(5, 10) != currMessage.createdAt.substring(5, 10)) {
|
||||||
elements.push((
|
dateComponent = (
|
||||||
<ChatDate
|
<ChatDate
|
||||||
key={currMessage.id + "date"}
|
key={currMessage.id + "date"}
|
||||||
date={new Date(currMessage.createdAt)}
|
date={new Date(currMessage.createdAt)}
|
||||||
/>
|
/>
|
||||||
));
|
);
|
||||||
}
|
}
|
||||||
elements.push(
|
return (
|
||||||
<Message key={currMessage.id + "msg"} message={currMessage} />
|
<Stack mb={"xs"} mr={"xs"}>
|
||||||
|
{dateComponent}
|
||||||
|
<Message
|
||||||
|
key={`${sessionData.id}${index}`}
|
||||||
|
message={sessionData}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
);
|
);
|
||||||
prevMessage = currMessage;
|
},
|
||||||
}
|
[messages],
|
||||||
|
);
|
||||||
return elements;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
if (messages.length === 0) {
|
||||||
|
return (
|
||||||
|
<Stack h={"96vh"} justify={"flex-end"}>
|
||||||
|
<MessageInput />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack h={"96vh"}>
|
<Stack h={"96vh"}>
|
||||||
<ScrollArea
|
<Virtuoso
|
||||||
h={"100%"}
|
data={messages}
|
||||||
viewportRef={scrollRef}
|
followOutput={onFollowOutputHandler}
|
||||||
onScrollPositionChange={onScrollPositionChange}
|
firstItemIndex={firstItemIndex}
|
||||||
>
|
initialTopMostItemIndex={messages.length - 1}
|
||||||
<Stack pr={"md"} gap={"sm"}>
|
itemContent={itemContent}
|
||||||
{getChatElements()}
|
startReached={fetchMoreMessages}
|
||||||
</Stack>
|
height={"100%"}
|
||||||
</ScrollArea>
|
increaseViewportBy={100}
|
||||||
|
alignToBottom
|
||||||
|
/>
|
||||||
<MessageInput />
|
<MessageInput />
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -53,19 +53,19 @@ const Message = ({ message }: Props) => {
|
|||||||
title: "Повторить отправку",
|
title: "Повторить отправку",
|
||||||
icon: <IconBrandTelegram />,
|
icon: <IconBrandTelegram />,
|
||||||
},
|
},
|
||||||
])
|
]);
|
||||||
|
|
||||||
const getContext = () => {
|
const getContext = () => {
|
||||||
if (!isMine) return;
|
if (!isMine || message.file) return;
|
||||||
|
|
||||||
if (isSuccess) return contextMenuSuccessMsg();
|
if (isSuccess) return contextMenuSuccessMsg();
|
||||||
if (isError) return contextMenuErrorMsg();
|
if (isError) return contextMenuErrorMsg();
|
||||||
}
|
};
|
||||||
|
|
||||||
const getStatusIcon = () => {
|
const getStatusIcon = () => {
|
||||||
const size = em(18);
|
const size = em(18);
|
||||||
if (message.status == MessageStatuses.ERROR) {
|
if (message.status == MessageStatuses.ERROR) {
|
||||||
return <IconAlertCircle size={size} />;
|
return <IconAlertCircle size={size} color={"red"}/>;
|
||||||
}
|
}
|
||||||
if (message.status == MessageStatuses.SENDING) {
|
if (message.status == MessageStatuses.SENDING) {
|
||||||
return <IconClock size={size} />;
|
return <IconClock size={size} />;
|
||||||
@@ -86,7 +86,9 @@ const Message = ({ message }: Props) => {
|
|||||||
<div>{message.tgSender!.lastName} {message.tgSender!.firstName}</div>
|
<div>{message.tgSender!.lastName} {message.tgSender!.firstName}</div>
|
||||||
)}
|
)}
|
||||||
{message.file && (
|
{message.file && (
|
||||||
<ChatFile file={message.file} />
|
<Group>
|
||||||
|
<ChatFile file={message.file} />
|
||||||
|
</Group>
|
||||||
)}
|
)}
|
||||||
<div>{message.text}</div>
|
<div>{message.text}</div>
|
||||||
<Group
|
<Group
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { ActionIcon, Button, Divider, Group, Stack, TextInput, Tooltip } from "@mantine/core";
|
import { ActionIcon, Button, Divider, Group, Stack, Textarea, Tooltip } from "@mantine/core";
|
||||||
import { IconCheck, IconPaperclip, IconSend2, IconX } from "@tabler/icons-react";
|
import { IconCheck, IconPaperclip, IconSend2, IconX } from "@tabler/icons-react";
|
||||||
import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx";
|
import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx";
|
||||||
import ActionIconCopy from "../../../ActionIconCopy/ActionIconCopy.tsx";
|
import ActionIconCopy from "../../../ActionIconCopy/ActionIconCopy.tsx";
|
||||||
import SelectedFile from "../SelectedFile/SelectedFile.tsx";
|
import SelectedFile from "../SelectedFile/SelectedFile.tsx";
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
|
|
||||||
const MessageInput = () => {
|
const MessageInput = () => {
|
||||||
@@ -13,20 +12,19 @@ const MessageInput = () => {
|
|||||||
form,
|
form,
|
||||||
files,
|
files,
|
||||||
fileDialog,
|
fileDialog,
|
||||||
|
isMessageSending,
|
||||||
} = useChatContext();
|
} = useChatContext();
|
||||||
|
|
||||||
const getFiles = useMemo(() => {
|
const getFiles = files.map(file => (
|
||||||
return files.map(file => (
|
<SelectedFile key={file.name} file={file} />
|
||||||
<SelectedFile key={file.name} file={file} />
|
));
|
||||||
));
|
|
||||||
}, [files]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit(values => submitMessage(values))}>
|
<form onSubmit={form.onSubmit(values => submitMessage(values))}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
<Divider />
|
<Divider />
|
||||||
{getFiles}
|
{getFiles}
|
||||||
<Group wrap={"nowrap"} align={"center"}>
|
<Group wrap={"nowrap"} align={"flex-end"}>
|
||||||
{chat?.tgGroup?.tgInviteLink && (
|
{chat?.tgGroup?.tgInviteLink && (
|
||||||
<ActionIconCopy
|
<ActionIconCopy
|
||||||
onCopiedLabel={"Ссылка на чат скопирована в буфер обмена"}
|
onCopiedLabel={"Ссылка на чат скопирована в буфер обмена"}
|
||||||
@@ -42,9 +40,12 @@ const MessageInput = () => {
|
|||||||
<IconPaperclip />
|
<IconPaperclip />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<TextInput
|
<Textarea
|
||||||
{...form.getInputProps("message")}
|
{...form.getInputProps("message")}
|
||||||
w={"100%"}
|
w={"100%"}
|
||||||
|
minRows={1}
|
||||||
|
maxRows={4}
|
||||||
|
autosize
|
||||||
/>
|
/>
|
||||||
{form.values.messageId && (
|
{form.values.messageId && (
|
||||||
<Button
|
<Button
|
||||||
@@ -57,6 +58,7 @@ const MessageInput = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant={"default"}
|
variant={"default"}
|
||||||
type="submit"
|
type="submit"
|
||||||
|
disabled={isMessageSending}
|
||||||
>
|
>
|
||||||
{form.values.messageId ? (
|
{form.values.messageId ? (
|
||||||
<IconCheck />
|
<IconCheck />
|
||||||
|
|||||||
@@ -8,13 +8,14 @@ import useChatTab from "./hooks/useChatTab.tsx";
|
|||||||
|
|
||||||
const ChatTab = () => {
|
const ChatTab = () => {
|
||||||
const { selectedCard } = useCardPageContext();
|
const { selectedCard } = useCardPageContext();
|
||||||
const { onChatCreateClick } = useChatTab();
|
const { onChatCreateClick, isRequestSending } = useChatTab();
|
||||||
|
|
||||||
if (!selectedCard?.chat) {
|
if (!selectedCard?.chat) {
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
<InlineButton
|
<InlineButton
|
||||||
onClick={onChatCreateClick}
|
onClick={onChatCreateClick}
|
||||||
|
disabled={isRequestSending}
|
||||||
>
|
>
|
||||||
<IconMessagePlus />
|
<IconMessagePlus />
|
||||||
Создать чат
|
Создать чат
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ import { ChatService } from "../../../../../client";
|
|||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { Text } from "@mantine/core";
|
import { Text } from "@mantine/core";
|
||||||
import { useCardPageContext } from "../../../../../pages/CardsPage/contexts/CardPageContext.tsx";
|
import { useCardPageContext } from "../../../../../pages/CardsPage/contexts/CardPageContext.tsx";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
|
||||||
const useChatTab = () => {
|
const useChatTab = () => {
|
||||||
const { selectedCard, refetchCard } = useCardPageContext();
|
const { selectedCard, refetchCard } = useCardPageContext();
|
||||||
|
const [isRequestSending, setIsRequestSending] = useState<boolean>(false);
|
||||||
|
|
||||||
const createChat = () => {
|
const createChat = () => {
|
||||||
if (!selectedCard?.clientId) {
|
if (!selectedCard?.clientId) {
|
||||||
@@ -14,6 +16,7 @@ const useChatTab = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setIsRequestSending(true);
|
||||||
ChatService.createChat({
|
ChatService.createChat({
|
||||||
requestBody: {
|
requestBody: {
|
||||||
clientId: selectedCard.clientId,
|
clientId: selectedCard.clientId,
|
||||||
@@ -24,7 +27,8 @@ const useChatTab = () => {
|
|||||||
notifications.guess(ok, { message });
|
notifications.guess(ok, { message });
|
||||||
refetchCard();
|
refetchCard();
|
||||||
})
|
})
|
||||||
.catch(err => console.log(err));
|
.catch(err => console.log(err))
|
||||||
|
.finally(() => setIsRequestSending(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChatCreateClick = () => {
|
const onChatCreateClick = () => {
|
||||||
@@ -43,6 +47,7 @@ const useChatTab = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
onChatCreateClick,
|
onChatCreateClick,
|
||||||
|
isRequestSending,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { IconMessage, IconMessagePlus } from "@tabler/icons-react";
|
|||||||
import { useChatContext } from "../../contexts/ChatContext.tsx";
|
import { useChatContext } from "../../contexts/ChatContext.tsx";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import { notifications } from "../../../../shared/lib/notifications.ts";
|
import { notifications } from "../../../../shared/lib/notifications.ts";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
client: ClientSchema;
|
client: ClientSchema;
|
||||||
@@ -12,9 +13,10 @@ type Props = {
|
|||||||
|
|
||||||
const ClientChatButton = ({ client, refetch }: Props) => {
|
const ClientChatButton = ({ client, refetch }: Props) => {
|
||||||
const { setChat } = useChatContext();
|
const { setChat } = useChatContext();
|
||||||
|
const [isRequestSending, setIsRequestSending] = useState<boolean>(false);
|
||||||
|
|
||||||
const createChat = () => {
|
const createChat = () => {
|
||||||
console.log("Creating chat")
|
setIsRequestSending(true);
|
||||||
ChatService.createChat({
|
ChatService.createChat({
|
||||||
requestBody: {
|
requestBody: {
|
||||||
clientId: client.id,
|
clientId: client.id,
|
||||||
@@ -25,7 +27,8 @@ const ClientChatButton = ({ client, refetch }: Props) => {
|
|||||||
notifications.guess(ok, { message });
|
notifications.guess(ok, { message });
|
||||||
refetch();
|
refetch();
|
||||||
})
|
})
|
||||||
.catch(err => console.log(err));
|
.catch(err => console.log(err))
|
||||||
|
.finally(() => setIsRequestSending(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onCreateChatClick = () => {
|
const onCreateChatClick = () => {
|
||||||
@@ -58,7 +61,9 @@ const ClientChatButton = ({ client, refetch }: Props) => {
|
|||||||
<Tooltip label="Создать чат">
|
<Tooltip label="Создать чат">
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
onClick={onCreateChatClick}
|
onClick={onCreateChatClick}
|
||||||
variant={"default"}>
|
variant={"default"}
|
||||||
|
disabled={isRequestSending}
|
||||||
|
>
|
||||||
<IconMessagePlus />
|
<IconMessagePlus />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import React, { createContext, FC, MutableRefObject, useContext, useEffect, useRef, useState } from "react";
|
import React, { createContext, FC, useCallback, useContext, useEffect, useState } from "react";
|
||||||
import { ChatSchema, ChatService, MessageSchema } from "../../../client";
|
import { ChatSchema, ChatService, MessageSchema } from "../../../client";
|
||||||
import { notifications } from "../../../shared/lib/notifications.ts";
|
import { notifications } from "../../../shared/lib/notifications.ts";
|
||||||
import { useForm, UseFormReturnType } from "@mantine/form";
|
import { useForm, UseFormReturnType } from "@mantine/form";
|
||||||
import { useDebouncedState, useFileDialog, useListState, UseListStateHandlers } from "@mantine/hooks";
|
import { useFileDialog, useListState, UseListStateHandlers } from "@mantine/hooks";
|
||||||
|
|
||||||
export type MessageForm = {
|
export type MessageForm = {
|
||||||
message: string;
|
message: string;
|
||||||
@@ -19,13 +19,16 @@ type ChatContextState = {
|
|||||||
chat: ChatSchema | null;
|
chat: ChatSchema | null;
|
||||||
setChat: (chat: ChatSchema | null) => void;
|
setChat: (chat: ChatSchema | null) => void;
|
||||||
messages: MessageSchema[];
|
messages: MessageSchema[];
|
||||||
|
lastMessage: MessageSchema | null;
|
||||||
|
firstItemIndex: number;
|
||||||
form: UseFormReturnType<MessageForm>;
|
form: UseFormReturnType<MessageForm>;
|
||||||
onScrollPositionChange: (values: { x: number, y: number }) => void;
|
|
||||||
scrollRef: MutableRefObject<HTMLDivElement | null>;
|
|
||||||
submitMessage: (values: MessageForm) => void;
|
submitMessage: (values: MessageForm) => void;
|
||||||
|
fetchMoreMessages: () => void;
|
||||||
files: Array<File>;
|
files: Array<File>;
|
||||||
filesHandlers: UseListStateHandlers<File>;
|
filesHandlers: UseListStateHandlers<File>;
|
||||||
fileDialog: FileDialog;
|
fileDialog: FileDialog;
|
||||||
|
isMessageSending: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const ChatContext = createContext<ChatContextState | undefined>(undefined);
|
const ChatContext = createContext<ChatContextState | undefined>(undefined);
|
||||||
@@ -33,18 +36,18 @@ const ChatContext = createContext<ChatContextState | undefined>(undefined);
|
|||||||
const useChatContextState = () => {
|
const useChatContextState = () => {
|
||||||
const [chat, setChat] = useState<ChatSchema | null>(null);
|
const [chat, setChat] = useState<ChatSchema | null>(null);
|
||||||
const [messages, setMessages] = useState<MessageSchema[]>([]);
|
const [messages, setMessages] = useState<MessageSchema[]>([]);
|
||||||
const [offset, setOffset] = useState(0);
|
const lastMessage = messages?.length ? messages[messages?.length - 1] : null;
|
||||||
const limit = 20;
|
const [firstItemIndex, setFirstItemIndex] = useState(10000);
|
||||||
const [hasMore, setHasMore] = useState(true);
|
const limit: number = 30;
|
||||||
const scrollRef = useRef<HTMLDivElement | null>(null);
|
let offset: number = 0;
|
||||||
const [isScrollToBottom, setIsScrollToBottom] = useState(true);
|
const [hasMore, setHasMore] = useState(false);
|
||||||
const [scrollPosition, setScrollPosition] = useDebouncedState<{ x: number, y: number }>({ x: 0, y: 0 }, 400);
|
|
||||||
|
|
||||||
const [files, filesHandlers] = useListState<File>([]);
|
const [files, filesHandlers] = useListState<File>([]);
|
||||||
const fileDialog = useFileDialog();
|
const fileDialog = useFileDialog();
|
||||||
|
const [isMessageSending, setIsMessageSending] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
filesHandlers.setState(Array.from(fileDialog.files ?? []));
|
filesHandlers.append(...Array.from(fileDialog.files ?? []));
|
||||||
}, [fileDialog.files]);
|
}, [fileDialog.files]);
|
||||||
|
|
||||||
const form = useForm<MessageForm>({
|
const form = useForm<MessageForm>({
|
||||||
@@ -55,52 +58,49 @@ const useChatContextState = () => {
|
|||||||
|
|
||||||
const setChatValue = (chat: ChatSchema | null) => {
|
const setChatValue = (chat: ChatSchema | null) => {
|
||||||
if (chat) {
|
if (chat) {
|
||||||
setOffset(0);
|
setFirstItemIndex(10000);
|
||||||
|
offset = 0;
|
||||||
setHasMore(true);
|
setHasMore(true);
|
||||||
setScrollPosition({ x: 0, y: 0 });
|
|
||||||
filesHandlers.setState([]);
|
filesHandlers.setState([]);
|
||||||
form.reset();
|
form.reset();
|
||||||
}
|
}
|
||||||
setChat(chat);
|
setChat(chat);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchMoreMessages = useCallback(() => {
|
||||||
|
return setTimeout(() => {
|
||||||
|
if (!chat || !hasMore) return;
|
||||||
|
|
||||||
|
offset += limit;
|
||||||
|
|
||||||
|
ChatService.getMessages({
|
||||||
|
requestBody: {
|
||||||
|
chatId: chat.id,
|
||||||
|
offset,
|
||||||
|
limit,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(({ messages: newMessages }) => {
|
||||||
|
setFirstItemIndex(prev => prev - newMessages.length);
|
||||||
|
setMessages(prev => [...newMessages.reverse(), ...prev]);
|
||||||
|
setHasMore(newMessages.length === limit);
|
||||||
|
})
|
||||||
|
.catch(err => console.log(err));
|
||||||
|
}, 500);
|
||||||
|
}, [setMessages, chat, offset, hasMore, setHasMore]);
|
||||||
|
|
||||||
const fetchMessages = () => {
|
const fetchMessages = () => {
|
||||||
if (!chat) return;
|
if (!chat) return;
|
||||||
|
|
||||||
ChatService.getMessages({
|
ChatService.getMessages({
|
||||||
requestBody: {
|
requestBody: {
|
||||||
chatId: chat.id,
|
chatId: chat.id,
|
||||||
offset,
|
offset: 0,
|
||||||
limit,
|
limit: limit + offset,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(({ messages: newMessages }) => {
|
.then(({ messages: newMessages }) => {
|
||||||
if (newMessages.length < limit) setHasMore(false);
|
setMessages(newMessages.reverse());
|
||||||
setMessages([...newMessages]);
|
|
||||||
setOffset((prev) => prev + limit);
|
|
||||||
})
|
|
||||||
.catch(err => console.log(err));
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchMessagesOnScroll = () => {
|
|
||||||
if (!chat) return;
|
|
||||||
|
|
||||||
ChatService.getMessages({
|
|
||||||
requestBody: {
|
|
||||||
chatId: chat.id,
|
|
||||||
offset,
|
|
||||||
limit,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(({ messages: newMessages }) => {
|
|
||||||
console.log(newMessages);
|
|
||||||
if (newMessages.length < limit) setHasMore(false);
|
|
||||||
setMessages((prev) => [...prev, ...newMessages]);
|
|
||||||
setOffset((prev) => prev + limit);
|
|
||||||
if (scrollRef.current) {
|
|
||||||
const prevPosition = limit / offset * scrollRef.current.scrollHeight;
|
|
||||||
scrollRef.current.scrollTo({ top: prevPosition, behavior: "instant" });
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.catch(err => console.log(err));
|
.catch(err => console.log(err));
|
||||||
};
|
};
|
||||||
@@ -113,22 +113,9 @@ const useChatContextState = () => {
|
|||||||
return () => clearInterval(interval);
|
return () => clearInterval(interval);
|
||||||
}, [chat]);
|
}, [chat]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (scrollRef.current && isScrollToBottom && chat) {
|
|
||||||
setIsScrollToBottom(false);
|
|
||||||
scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
|
|
||||||
}
|
|
||||||
}, [messages, isScrollToBottom]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!chat) return;
|
|
||||||
if (Math.abs(scrollPosition.y - 200) <= 200 && hasMore) {
|
|
||||||
fetchMessagesOnScroll();
|
|
||||||
}
|
|
||||||
}, [scrollPosition]);
|
|
||||||
|
|
||||||
const sendMessageWithFiles = (values: MessageForm) => {
|
const sendMessageWithFiles = (values: MessageForm) => {
|
||||||
if (!chat) return;
|
if (!chat) return;
|
||||||
|
setIsMessageSending(true);
|
||||||
|
|
||||||
ChatService.sendMessagesWithFiles({
|
ChatService.sendMessagesWithFiles({
|
||||||
formData: {
|
formData: {
|
||||||
@@ -145,11 +132,13 @@ const useChatContextState = () => {
|
|||||||
filesHandlers.setState([]);
|
filesHandlers.setState([]);
|
||||||
form.reset();
|
form.reset();
|
||||||
})
|
})
|
||||||
.catch(err => console.log(err));
|
.catch(err => console.log(err))
|
||||||
|
.finally(() => setIsMessageSending(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const sendTextMessage = (values: MessageForm) => {
|
const sendTextMessage = (values: MessageForm) => {
|
||||||
if (!chat) return;
|
if (!chat) return;
|
||||||
|
setIsMessageSending(true);
|
||||||
|
|
||||||
ChatService.sendTextMessage({
|
ChatService.sendTextMessage({
|
||||||
requestBody: {
|
requestBody: {
|
||||||
@@ -164,13 +153,15 @@ const useChatContextState = () => {
|
|||||||
notifications.error({ message });
|
notifications.error({ message });
|
||||||
}
|
}
|
||||||
form.reset();
|
form.reset();
|
||||||
setIsScrollToBottom(true);
|
setIsMessageSending(false);
|
||||||
})
|
})
|
||||||
.catch(err => console.log(err));
|
.catch(err => console.log(err))
|
||||||
|
.finally(() => setIsMessageSending(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const editMessage = (values: MessageForm) => {
|
const editMessage = (values: MessageForm) => {
|
||||||
if (!chat) return;
|
if (!chat) return;
|
||||||
|
setIsMessageSending(true);
|
||||||
|
|
||||||
ChatService.editMessage({
|
ChatService.editMessage({
|
||||||
requestBody: {
|
requestBody: {
|
||||||
@@ -186,9 +177,10 @@ const useChatContextState = () => {
|
|||||||
notifications.error({ message });
|
notifications.error({ message });
|
||||||
}
|
}
|
||||||
form.reset();
|
form.reset();
|
||||||
setIsScrollToBottom(true);
|
setIsMessageSending(false);
|
||||||
})
|
})
|
||||||
.catch(err => console.log(err));
|
.catch(err => console.log(err))
|
||||||
|
.finally(() => setIsMessageSending(false));
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitMessage = (values: MessageForm) => {
|
const submitMessage = (values: MessageForm) => {
|
||||||
@@ -205,13 +197,16 @@ const useChatContextState = () => {
|
|||||||
chat,
|
chat,
|
||||||
setChat: setChatValue,
|
setChat: setChatValue,
|
||||||
messages,
|
messages,
|
||||||
|
lastMessage,
|
||||||
|
firstItemIndex,
|
||||||
form,
|
form,
|
||||||
onScrollPositionChange: setScrollPosition,
|
|
||||||
scrollRef,
|
|
||||||
submitMessage,
|
submitMessage,
|
||||||
|
fetchMoreMessages,
|
||||||
files,
|
files,
|
||||||
filesHandlers,
|
filesHandlers,
|
||||||
fileDialog,
|
fileDialog,
|
||||||
|
isMessageSending,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user