feat: creating chats for cards and clients, sending and deleting text messages
This commit is contained in:
45
src/components/ActionIconCopy/ActionIconCopy.tsx
Normal file
45
src/components/ActionIconCopy/ActionIconCopy.tsx
Normal 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;
|
||||
20
src/components/Chat/Chat.module.css
Normal file
20
src/components/Chat/Chat.module.css
Normal 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;
|
||||
}
|
||||
55
src/components/Chat/Chat.tsx
Normal file
55
src/components/Chat/Chat.tsx
Normal 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;
|
||||
20
src/components/Chat/components/ChatDate/ChatDate.tsx
Normal file
20
src/components/Chat/components/ChatDate/ChatDate.tsx
Normal 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;
|
||||
78
src/components/Chat/components/Message/Message.tsx
Normal file
78
src/components/Chat/components/Message/Message.tsx
Normal 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;
|
||||
23
src/components/Chat/components/Message/hooks/useMessage.tsx
Normal file
23
src/components/Chat/components/Message/hooks/useMessage.tsx
Normal 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;
|
||||
45
src/components/Chat/components/MessageInput/MessageInput.tsx
Normal file
45
src/components/Chat/components/MessageInput/MessageInput.tsx
Normal 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;
|
||||
Reference in New Issue
Block a user