feat: creating chats for cards and clients, sending and deleting text messages
This commit is contained in:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
@@ -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 />
|
||||
|
||||
161
src/pages/ClientsPage/contexts/ChatContext.tsx
Normal file
161
src/pages/ClientsPage/contexts/ChatContext.tsx
Normal 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;
|
||||
};
|
||||
24
src/pages/ClientsPage/drawers/ClientChatDrawer.tsx
Normal file
24
src/pages/ClientsPage/drawers/ClientChatDrawer.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user