feat: creating chats for cards and clients, sending and deleting text messages

This commit is contained in:
2025-03-27 15:15:06 +04:00
parent 3bbdacdd68
commit f6c55012bd
37 changed files with 967 additions and 21 deletions

View File

@@ -7,6 +7,8 @@ import { Button } from "@mantine/core";
import { modals } from "@mantine/modals";
import { ClientSchema, ClientService } from "../../client";
import { notifications } from "../../shared/lib/notifications.ts";
import { ChatContextProvider } from "./contexts/ChatContext.tsx";
import ClientChatDrawer from "./drawers/ClientChatDrawer.tsx";
const ClientsPage: FC = () => {
const { clients, refetch } = useClientsList();
@@ -62,11 +64,15 @@ const ClientsPage: FC = () => {
</div>
</PageBlock>
<PageBlock>
<ClientsTable
onChange={onChange}
onDelete={onDelete}
items={clients}
/>
<ChatContextProvider>
<ClientsTable
onChange={onChange}
onDelete={onDelete}
items={clients}
refetch={refetch}
/>
<ClientChatDrawer />
</ChatContextProvider>
</PageBlock>
</div>
);

View File

@@ -0,0 +1,68 @@
import { ChatService, ClientSchema } from "../../../../client";
import { ActionIcon, Text, Tooltip } from "@mantine/core";
import { IconMessage, IconMessagePlus } from "@tabler/icons-react";
import { useChatContext } from "../../contexts/ChatContext.tsx";
import { modals } from "@mantine/modals";
import { notifications } from "../../../../shared/lib/notifications.ts";
type Props = {
client: ClientSchema;
refetch: () => void;
}
const ClientChatButton = ({ client, refetch }: Props) => {
const { setChat } = useChatContext();
const createChat = () => {
console.log("Creating chat")
ChatService.createChat({
requestBody: {
clientId: client.id,
cardId: null,
},
})
.then(({ ok, message }) => {
notifications.guess(ok, { message });
refetch();
})
.catch(err => console.log(err));
};
const onCreateChatClick = () => {
modals.openConfirmModal({
title: "Создание чата",
children: (
<Text size="sm">
Вы уверены что хотите создать чат с {client.name}?
</Text>
),
labels: { confirm: "Да", cancel: "Нет" },
confirmProps: { color: "green" },
onConfirm: createChat,
});
};
if (client.chat) {
return (
<Tooltip label="Открыть чат">
<ActionIcon
onClick={() => setChat(client.chat!)}
variant={"default"}>
<IconMessage />
</ActionIcon>
</Tooltip>
);
}
return (
<Tooltip label="Создать чат">
<ActionIcon
onClick={onCreateChatClick}
variant={"default"}>
<IconMessagePlus />
</ActionIcon>
</Tooltip>
);
};
export default ClientChatButton;

View File

@@ -7,13 +7,20 @@ import { ActionIcon, Flex, Tooltip } from "@mantine/core";
import { IconEdit, IconTrash } from "@tabler/icons-react";
import { CRUDTableProps } from "../../../../types/CRUDTable.tsx";
import { modals } from "@mantine/modals";
import ClientChatButton from "../ClientChatButton/ClientChatButton.tsx";
const ClientsTable: FC<CRUDTableProps<ClientSchema>> = ({
items,
onDelete,
onChange,
}) => {
type RefetchProps = {
refetch: () => void;
}
const ClientsTable: FC<CRUDTableProps<ClientSchema> & RefetchProps> = ({
items,
onDelete,
onChange,
refetch,
}) => {
const columns = useClientsTableColumns();
const onEditClick = (client: ClientSchema) => {
if (!onChange) return;
modals.openContextModal({
@@ -26,6 +33,7 @@ const ClientsTable: FC<CRUDTableProps<ClientSchema>> = ({
},
});
};
return (
<>
<BaseTable
@@ -38,6 +46,7 @@ const ClientsTable: FC<CRUDTableProps<ClientSchema>> = ({
enableRowActions: true,
renderRowActions: ({ row }) => (
<Flex gap="md">
<ClientChatButton client={row.original} refetch={refetch}/>
<Tooltip label="Редактировать">
<ActionIcon
onClick={() =>
@@ -50,8 +59,7 @@ const ClientsTable: FC<CRUDTableProps<ClientSchema>> = ({
<Tooltip label="Удалить">
<ActionIcon
onClick={() => {
if (onDelete)
onDelete(row.original);
if (onDelete) onDelete(row.original);
}}
variant={"default"}>
<IconTrash />

View File

@@ -0,0 +1,161 @@
import React, { createContext, FC, MutableRefObject, useContext, useEffect, useRef, useState } from "react";
import { ChatSchema, ChatService, MessageSchema } from "../../../client";
import { notifications } from "../../../shared/lib/notifications.ts";
import { MessageForm } from "../../../components/Chat/components/MessageInput/MessageInput.tsx";
import { UseFormReturnType } from "@mantine/form";
import { useDebouncedState } from "@mantine/hooks";
type ChatContextState = {
chat: ChatSchema | null;
setChat: (chat: ChatSchema | null) => void;
messages: MessageSchema[];
onScrollPositionChange: (values: { x: number, y: number }) => void;
scrollRef: MutableRefObject<HTMLDivElement | null>;
sendMessage: (values: MessageForm, form: UseFormReturnType<MessageForm>) => void;
};
const ChatContext = createContext<ChatContextState | undefined>(undefined);
const useChatContextState = () => {
const [chat, setChat] = useState<ChatSchema | null>(null);
const [messages, setMessages] = useState<MessageSchema[]>([]);
const [offset, setOffset] = useState(0);
const limit = 20;
const [hasMore, setHasMore] = useState(true);
const scrollRef = useRef<HTMLDivElement | null>(null);
const [isScrollToBottom, setIsScrollToBottom] = useState(true);
const [scrollPosition, setScrollPosition] = useDebouncedState<{ x: number, y: number }>({ x: 0, y: 0 }, 400);
const setChatValue = (chat: ChatSchema | null) => {
if (chat) {
setOffset(0);
setHasMore(true);
setScrollPosition({ x: 0, y: 0 });
}
setChat(chat);
};
console.log(`has more = ${hasMore}`);
const fetchMessages = () => {
if (!chat) return;
ChatService.getMessages({
requestBody: {
chatId: chat.id,
offset,
limit,
},
})
.then(({ messages: newMessages }) => {
if (newMessages.length < limit) setHasMore(false);
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) {
console.log("FETCH ON SCROLL")
const prevPosition = limit / offset * scrollRef.current.scrollHeight;
console.log(`PREV POSITION = ${prevPosition}`)
scrollRef.current.scrollTo({ top: prevPosition, behavior: "instant" });
}
})
.catch(err => console.log(err));
};
useEffect(() => {
fetchMessages();
const interval = setInterval(fetchMessages, 2000);
return () => clearInterval(interval);
}, [chat]);
useEffect(() => {
if (scrollRef.current && isScrollToBottom && chat) {
setIsScrollToBottom(false);
console.log("Scroll to the bottom");
console.log(`scrollHeight = ${scrollRef.current.scrollHeight}`);
scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
}
}, [messages, isScrollToBottom]);
useEffect(() => {
if (!chat) return;
console.log(`handleScroll ${Math.abs(scrollPosition.y - 200)}`);
if (Math.abs(scrollPosition.y - 200) <= 200 && hasMore) {
console.log("handleScroll WORK");
fetchMessagesOnScroll();
}
}, [scrollPosition]);
const sendMessage = (values: MessageForm, form: UseFormReturnType<MessageForm>) => {
if (!chat) return;
ChatService.sendMessage({
requestBody: {
message: {
text: values.message,
chatId: chat.id,
},
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
form.reset();
setIsScrollToBottom(true);
})
.catch(err => console.log(err));
};
return {
chat,
setChat: setChatValue,
messages,
onScrollPositionChange: setScrollPosition,
scrollRef,
sendMessage,
};
};
type ChatContextProviderProps = {
children: React.ReactNode;
};
export const ChatContextProvider: FC<ChatContextProviderProps> = ({ children }) => {
const state = useChatContextState();
return (
<ChatContext.Provider value={state}>
{children}
</ChatContext.Provider>
);
};
export const useChatContext = () => {
const context = useContext(ChatContext);
if (!context) {
throw new Error(
"useChatContext must be used within a ChatContextProvider",
);
}
return context;
};

View File

@@ -0,0 +1,24 @@
import { useChatContext } from "../contexts/ChatContext.tsx";
import { Drawer } from "@mantine/core";
import Chat from "../../../components/Chat/Chat.tsx";
const ClientChatDrawer = () => {
const { chat, setChat } = useChatContext();
if (!chat) return;
return (
<Drawer
opened={!!chat}
onClose={() => setChat(null)}
position={"right"}
size={"calc(50vw)"}
withCloseButton={false}
>
<Chat />
</Drawer>
);
};
export default ClientChatDrawer;