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