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

@@ -0,0 +1,45 @@
import { ActionIcon, rem, Tooltip } from "@mantine/core";
import { IconCheck, IconCopy } from "@tabler/icons-react";
import { FC } from "react";
import { useClipboard } from "@mantine/hooks";
type Props = {
value: string;
onCopiedLabel: string;
};
export const ActionIconCopy: FC<Props> = ({ onCopiedLabel, value }) => {
const clipboard = useClipboard();
return (
<Tooltip
label={onCopiedLabel}
offset={5}
position="bottom"
radius="xl"
transitionProps={{ duration: 100, transition: "slide-down" }}
opened={clipboard.copied}>
<ActionIcon
variant={"default"}
size="lg"
onClick={() => {
console.log("AKLSKLSKSM");
return clipboard.copy(value);
}}
>
{clipboard.copied ? (
<IconCheck
style={{ width: rem(20), height: rem(20) }}
stroke={1.5}
/>
) : (
<IconCopy
style={{ width: rem(20), height: rem(20) }}
stroke={1.5}
/>
)}
</ActionIcon>
</Tooltip>
);
};
export default ActionIconCopy;

View File

@@ -0,0 +1,20 @@
.message {
@mixin light {
background-color: var(--mantine-color-gray-2);
}
@mixin dark {
background-color: var(--mantine-color-dark-5);
}
border-radius: 1em;
}
.other-message {
@mixin light {
background-color: var(--mantine-color-gray-3);
}
@mixin dark {
background-color: var(--mantine-color-dark-6);
}
border-radius: 1em;
}

View File

@@ -0,0 +1,55 @@
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 ChatDate from "./components/ChatDate/ChatDate.tsx";
const Chat = () => {
const {
messages,
scrollRef,
onScrollPositionChange,
} = 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];
if (!prevMessage || prevMessage.createdAt.substring(5, 10) != currMessage.createdAt.substring(5, 10)) {
elements.push((
<ChatDate
key={currMessage.id + "date"}
date={new Date(currMessage.createdAt)}
/>
));
}
elements.push((
<Message key={currMessage.id + "msg"} message={currMessage} />
));
prevMessage = currMessage;
}
return elements;
};
return (
<Stack>
<ScrollArea
viewportRef={scrollRef}
onScrollPositionChange={onScrollPositionChange}
>
<Stack h={"91vh"} pr={"md"} gap={"sm"}>
{getChatElements()}
</Stack>
</ScrollArea>
<MessageInput />
</Stack>
);
};
export default Chat;

View File

@@ -0,0 +1,20 @@
import { Box, Center, Pill } from "@mantine/core";
import styles from "../../Chat.module.css";
type Props = {
date: Date;
}
const ChatDate = ({ date }: Props) => {
return (
<Center>
<Pill size={"md"} className={styles["message"]}>
<Box>
{date.toLocaleDateString("ru-RU", { day: "numeric", month: "long" })}
</Box>
</Pill>
</Center>
);
};
export default ChatDate;

View File

@@ -0,0 +1,78 @@
import { MessageSchema } from "../../../../client";
import { Box, Center, em, Flex, Group, rem } from "@mantine/core";
import { formatDateTime } from "../../../../types/utils.ts";
import { IconAlertCircle, IconCheck, IconClock, IconTrash } from "@tabler/icons-react";
import { useContextMenu } from "mantine-contextmenu";
import useMessage from "./hooks/useMessage.tsx";
import styles from "../../Chat.module.css";
import classNames from "classnames";
type Props = {
message: MessageSchema;
}
enum MessageStatuses {
SUCCESS = "SUCCESS",
ERROR = "ERROR",
SENDING = "SENDING",
}
const Message = ({ message }: Props) => {
const isMine = !message.tgSender;
const isSuccess = message.status == MessageStatuses.SUCCESS;
const { onDeleteMessageClick } = useMessage();
const { showContextMenu } = useContextMenu();
const contextMenu = () => showContextMenu([
{
key: "delete",
onClick: () => onDeleteMessageClick(message),
title: "Удалить",
icon: <IconTrash />,
},
]);
const getStatusIcon = () => {
const size = em(18);
if (message.status == MessageStatuses.ERROR) {
return <IconAlertCircle size={size} />;
}
if (message.status == MessageStatuses.SENDING) {
return <IconClock size={size} />;
}
return <IconCheck size={size} />;
};
return (
<Flex justify={isMine ? "flex-end" : "flex-start"}>
<Box
py={rem(5)}
px={rem(15)}
className={classNames(isMine ? styles["message"] : styles["other-message"])}
onContextMenu={isMine && isSuccess ? contextMenu() : undefined}
>
{!isMine && (
<div>{message.tgSender!.lastName} {message.tgSender!.firstName}</div>
)}
<div>{message.text}</div>
<Group
gap={em(5)}
justify={"flex-end"}
align={"center"}
wrap={"nowrap"}
>
<Center>
{formatDateTime(message.createdAt).substring(11, 16)}
</Center>
{isMine && (
<Center>
{getStatusIcon()}
</Center>
)}
</Group>
</Box>
</Flex>
);
};
export default Message;

View File

@@ -0,0 +1,23 @@
import { ChatService, MessageSchema } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts";
const useMessage = () => {
const onDeleteMessageClick = (message: MessageSchema) => {
ChatService.deleteMessage({
messageId: message.id,
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
})
.catch(err => console.log(err));
};
return {
onDeleteMessageClick,
};
};
export default useMessage;

View File

@@ -0,0 +1,45 @@
import { Button, Group, TextInput, Tooltip } from "@mantine/core";
import { IconSend2 } from "@tabler/icons-react";
import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx";
import { useForm } from "@mantine/form";
import ActionIconCopy from "../../../ActionIconCopy/ActionIconCopy.tsx";
export type MessageForm = {
message: string;
}
const MessageInput = () => {
const { sendMessage, chat } = useChatContext();
const form = useForm<MessageForm>({
initialValues: {
message: "",
},
});
return (
<form onSubmit={form.onSubmit(values => sendMessage(values, form))}>
<Group wrap={"nowrap"} align={"center"}>
{chat?.tgGroup?.tgInviteLink && (
<Tooltip label={"Ссылка-приглашение"}>
<ActionIconCopy
onCopiedLabel={"Ссылка скопирована в буфер обмена"}
value={chat.tgGroup.tgInviteLink}
/>
</Tooltip>
)}
<TextInput
{...form.getInputProps("message")}
w={"100%"}
/>
<Button
variant={"default"}
type="submit"
>
<IconSend2 />
</Button>
</Group>
</form>
);
};
export default MessageInput;