From 2f4f3c312524617f7ec25eb5a31e2d8aacd1930d Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Sun, 6 Apr 2025 11:43:45 +0400 Subject: [PATCH] feat: chat with infinite scroll --- package.json | 1 + src/client/models/MessageSchema.ts | 2 + src/components/Chat/Chat.tsx | 87 ++++++++----- .../Chat/components/Message/Message.tsx | 12 +- .../components/MessageInput/MessageInput.tsx | 20 +-- .../cardEditorTabs/ChatTab/ChatTab.tsx | 3 +- .../ChatTab/hooks/useChatTab.tsx | 7 +- .../ClientChatButton/ClientChatButton.tsx | 11 +- .../ClientsPage/contexts/ChatContext.tsx | 119 +++++++++--------- 9 files changed, 152 insertions(+), 110 deletions(-) diff --git a/package.json b/package.json index f7524e6..160813a 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "react-imask": "^7.6.1", "react-redux": "^9.1.2", "react-to-print": "^2.15.1", + "react-virtuoso": "^4.12.6", "reactflow": "^11.11.4", "recharts": "^2.13.3", "zod": "^3.23.8" diff --git a/src/client/models/MessageSchema.ts b/src/client/models/MessageSchema.ts index f465e78..2fd8a49 100644 --- a/src/client/models/MessageSchema.ts +++ b/src/client/models/MessageSchema.ts @@ -4,12 +4,14 @@ /* eslint-disable */ import type { MessageFileSchema } from './MessageFileSchema'; import type { TgUserSchema } from './TgUserSchema'; +import type { UserSchema } from './UserSchema'; export type MessageSchema = { text: string; chatId: number; id: number; createdAt: string; tgSender: (TgUserSchema | null); + crmSender: (UserSchema | null); status: string; isEdited: boolean; file?: (MessageFileSchema | null); diff --git a/src/components/Chat/Chat.tsx b/src/components/Chat/Chat.tsx index 0f5e682..e385999 100644 --- a/src/components/Chat/Chat.tsx +++ b/src/components/Chat/Chat.tsx @@ -1,53 +1,82 @@ -import { ScrollArea, Stack } from "@mantine/core"; import Message from "./components/Message/Message.tsx"; -import MessageInput from "./components/MessageInput/MessageInput.tsx"; import { useChatContext } from "../../pages/ClientsPage/contexts/ChatContext.tsx"; import { MessageSchema } from "../../client"; -import { ReactNode } from "react"; +import { ReactNode, useCallback } from "react"; 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 { messages, - scrollRef, - onScrollPositionChange, + lastMessage, + firstItemIndex, + fetchMoreMessages, } = useChatContext(); - const getChatElements = (): ReactNode[] => { - const elements: ReactNode[] = []; - let prevMessage: MessageSchema | null = null; - - for (let i = messages.length - 1; i >= 0; i--) { - const currMessage = messages[i]; + const onFollowOutputHandler = useCallback( + (atBottom: boolean) => { + if (atBottom || lastMessage?.crmSender) { + return "auto"; + } else { + 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)) { - elements.push(( + dateComponent = ( - )); + ); } - elements.push( - + return ( + + {dateComponent} + + ); - prevMessage = currMessage; - } - - return elements; - }; + }, + [messages], + ); + if (messages.length === 0) { + return ( + + + + ); + } + return ( - - - {getChatElements()} - - + ); diff --git a/src/components/Chat/components/Message/Message.tsx b/src/components/Chat/components/Message/Message.tsx index a7e634e..56e4b8b 100644 --- a/src/components/Chat/components/Message/Message.tsx +++ b/src/components/Chat/components/Message/Message.tsx @@ -53,19 +53,19 @@ const Message = ({ message }: Props) => { title: "Повторить отправку", icon: , }, - ]) + ]); const getContext = () => { - if (!isMine) return; + if (!isMine || message.file) return; if (isSuccess) return contextMenuSuccessMsg(); if (isError) return contextMenuErrorMsg(); - } + }; const getStatusIcon = () => { const size = em(18); if (message.status == MessageStatuses.ERROR) { - return ; + return ; } if (message.status == MessageStatuses.SENDING) { return ; @@ -86,7 +86,9 @@ const Message = ({ message }: Props) => {
{message.tgSender!.lastName} {message.tgSender!.firstName}
)} {message.file && ( - + + + )}
{message.text}
{ @@ -13,20 +12,19 @@ const MessageInput = () => { form, files, fileDialog, + isMessageSending, } = useChatContext(); - const getFiles = useMemo(() => { - return files.map(file => ( - - )); - }, [files]); + const getFiles = files.map(file => ( + + )); return (
submitMessage(values))}> {getFiles} - + {chat?.tgGroup?.tgInviteLink && ( { - {form.values.messageId && (