feat: chat with infinite scroll
This commit is contained in:
@@ -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 = (
|
||||
<ChatDate
|
||||
key={currMessage.id + "date"}
|
||||
date={new Date(currMessage.createdAt)}
|
||||
/>
|
||||
));
|
||||
);
|
||||
}
|
||||
elements.push(
|
||||
<Message key={currMessage.id + "msg"} message={currMessage} />
|
||||
return (
|
||||
<Stack mb={"xs"} mr={"xs"}>
|
||||
{dateComponent}
|
||||
<Message
|
||||
key={`${sessionData.id}${index}`}
|
||||
message={sessionData}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
prevMessage = currMessage;
|
||||
}
|
||||
|
||||
return elements;
|
||||
};
|
||||
},
|
||||
[messages],
|
||||
);
|
||||
|
||||
if (messages.length === 0) {
|
||||
return (
|
||||
<Stack h={"96vh"} justify={"flex-end"}>
|
||||
<MessageInput />
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack h={"96vh"}>
|
||||
<ScrollArea
|
||||
h={"100%"}
|
||||
viewportRef={scrollRef}
|
||||
onScrollPositionChange={onScrollPositionChange}
|
||||
>
|
||||
<Stack pr={"md"} gap={"sm"}>
|
||||
{getChatElements()}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
<Virtuoso
|
||||
data={messages}
|
||||
followOutput={onFollowOutputHandler}
|
||||
firstItemIndex={firstItemIndex}
|
||||
initialTopMostItemIndex={messages.length - 1}
|
||||
itemContent={itemContent}
|
||||
startReached={fetchMoreMessages}
|
||||
height={"100%"}
|
||||
increaseViewportBy={100}
|
||||
alignToBottom
|
||||
/>
|
||||
<MessageInput />
|
||||
</Stack>
|
||||
);
|
||||
|
||||
@@ -53,19 +53,19 @@ const Message = ({ message }: Props) => {
|
||||
title: "Повторить отправку",
|
||||
icon: <IconBrandTelegram />,
|
||||
},
|
||||
])
|
||||
]);
|
||||
|
||||
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 <IconAlertCircle size={size} />;
|
||||
return <IconAlertCircle size={size} color={"red"}/>;
|
||||
}
|
||||
if (message.status == MessageStatuses.SENDING) {
|
||||
return <IconClock size={size} />;
|
||||
@@ -86,7 +86,9 @@ const Message = ({ message }: Props) => {
|
||||
<div>{message.tgSender!.lastName} {message.tgSender!.firstName}</div>
|
||||
)}
|
||||
{message.file && (
|
||||
<ChatFile file={message.file} />
|
||||
<Group>
|
||||
<ChatFile file={message.file} />
|
||||
</Group>
|
||||
)}
|
||||
<div>{message.text}</div>
|
||||
<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 { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx";
|
||||
import ActionIconCopy from "../../../ActionIconCopy/ActionIconCopy.tsx";
|
||||
import SelectedFile from "../SelectedFile/SelectedFile.tsx";
|
||||
import { useMemo } from "react";
|
||||
|
||||
|
||||
const MessageInput = () => {
|
||||
@@ -13,20 +12,19 @@ const MessageInput = () => {
|
||||
form,
|
||||
files,
|
||||
fileDialog,
|
||||
isMessageSending,
|
||||
} = useChatContext();
|
||||
|
||||
const getFiles = useMemo(() => {
|
||||
return files.map(file => (
|
||||
<SelectedFile key={file.name} file={file} />
|
||||
));
|
||||
}, [files]);
|
||||
const getFiles = files.map(file => (
|
||||
<SelectedFile key={file.name} file={file} />
|
||||
));
|
||||
|
||||
return (
|
||||
<form onSubmit={form.onSubmit(values => submitMessage(values))}>
|
||||
<Stack gap={"xs"}>
|
||||
<Divider />
|
||||
{getFiles}
|
||||
<Group wrap={"nowrap"} align={"center"}>
|
||||
<Group wrap={"nowrap"} align={"flex-end"}>
|
||||
{chat?.tgGroup?.tgInviteLink && (
|
||||
<ActionIconCopy
|
||||
onCopiedLabel={"Ссылка на чат скопирована в буфер обмена"}
|
||||
@@ -42,9 +40,12 @@ const MessageInput = () => {
|
||||
<IconPaperclip />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
<TextInput
|
||||
<Textarea
|
||||
{...form.getInputProps("message")}
|
||||
w={"100%"}
|
||||
minRows={1}
|
||||
maxRows={4}
|
||||
autosize
|
||||
/>
|
||||
{form.values.messageId && (
|
||||
<Button
|
||||
@@ -57,6 +58,7 @@ const MessageInput = () => {
|
||||
<Button
|
||||
variant={"default"}
|
||||
type="submit"
|
||||
disabled={isMessageSending}
|
||||
>
|
||||
{form.values.messageId ? (
|
||||
<IconCheck />
|
||||
|
||||
Reference in New Issue
Block a user