feat: sending and receiving messages with files, editing text messages

This commit is contained in:
2025-04-02 15:27:57 +04:00
parent f6c55012bd
commit 23f0cdc081
29 changed files with 615 additions and 72 deletions

View File

@@ -40,6 +40,7 @@ export type { BillPaymentStatus } from './models/BillPaymentStatus';
export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest'; export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest';
export type { BoardSchema } from './models/BoardSchema'; export type { BoardSchema } from './models/BoardSchema';
export type { Body_parse_deals_excel } from './models/Body_parse_deals_excel'; export type { Body_parse_deals_excel } from './models/Body_parse_deals_excel';
export type { Body_send_messages_with_files } from './models/Body_send_messages_with_files';
export type { Body_upload_passport_image } from './models/Body_upload_passport_image'; export type { Body_upload_passport_image } from './models/Body_upload_passport_image';
export type { Body_upload_product_barcode_image } from './models/Body_upload_product_barcode_image'; export type { Body_upload_product_barcode_image } from './models/Body_upload_product_barcode_image';
export type { Body_upload_product_image } from './models/Body_upload_product_image'; export type { Body_upload_product_image } from './models/Body_upload_product_image';
@@ -216,6 +217,9 @@ export type { DepartmentSchema } from './models/DepartmentSchema';
export type { DepartmentSectionBaseSchema } from './models/DepartmentSectionBaseSchema'; export type { DepartmentSectionBaseSchema } from './models/DepartmentSectionBaseSchema';
export type { DepartmentSectionBriefSchema } from './models/DepartmentSectionBriefSchema'; export type { DepartmentSectionBriefSchema } from './models/DepartmentSectionBriefSchema';
export type { DepartmentSectionSchema } from './models/DepartmentSectionSchema'; export type { DepartmentSectionSchema } from './models/DepartmentSectionSchema';
export type { EditMessageRequest } from './models/EditMessageRequest';
export type { EditMessageResponse } from './models/EditMessageResponse';
export type { EditMessageSchema } from './models/EditMessageSchema';
export type { FinishPauseByShiftIdResponse } from './models/FinishPauseByShiftIdResponse'; export type { FinishPauseByShiftIdResponse } from './models/FinishPauseByShiftIdResponse';
export type { FinishPauseByUserIdResponse } from './models/FinishPauseByUserIdResponse'; export type { FinishPauseByUserIdResponse } from './models/FinishPauseByUserIdResponse';
export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse'; export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse';
@@ -279,12 +283,14 @@ export type { GetWorkShiftsPlanningDataRequest } from './models/GetWorkShiftsPla
export type { GetWorkShiftsResponse } from './models/GetWorkShiftsResponse'; export type { GetWorkShiftsResponse } from './models/GetWorkShiftsResponse';
export type { GroupBillRequestSchema } from './models/GroupBillRequestSchema'; export type { GroupBillRequestSchema } from './models/GroupBillRequestSchema';
export type { HTTPValidationError } from './models/HTTPValidationError'; export type { HTTPValidationError } from './models/HTTPValidationError';
export type { LoadMessagesResponse } from './models/LoadMessagesResponse';
export type { LoadReceiptRequest } from './models/LoadReceiptRequest'; export type { LoadReceiptRequest } from './models/LoadReceiptRequest';
export type { LoadReceiptResponse } from './models/LoadReceiptResponse'; export type { LoadReceiptResponse } from './models/LoadReceiptResponse';
export type { ManageEmployeeRequest } from './models/ManageEmployeeRequest'; export type { ManageEmployeeRequest } from './models/ManageEmployeeRequest';
export type { ManageEmployeeResponse } from './models/ManageEmployeeResponse'; export type { ManageEmployeeResponse } from './models/ManageEmployeeResponse';
export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema'; export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema';
export type { MarketplaceSchema } from './models/MarketplaceSchema'; export type { MarketplaceSchema } from './models/MarketplaceSchema';
export type { MessageFileSchema } from './models/MessageFileSchema';
export type { MessageSchema } from './models/MessageSchema'; export type { MessageSchema } from './models/MessageSchema';
export type { ModuleSchema } from './models/ModuleSchema'; export type { ModuleSchema } from './models/ModuleSchema';
export type { NotificationChannel } from './models/NotificationChannel'; export type { NotificationChannel } from './models/NotificationChannel';
@@ -334,12 +340,15 @@ export type { ProjectGeneralInfoSchema } from './models/ProjectGeneralInfoSchema
export type { ProjectSchema } from './models/ProjectSchema'; export type { ProjectSchema } from './models/ProjectSchema';
export type { ReceiptBoxSchema } from './models/ReceiptBoxSchema'; export type { ReceiptBoxSchema } from './models/ReceiptBoxSchema';
export type { ReceiptPalletSchema } from './models/ReceiptPalletSchema'; export type { ReceiptPalletSchema } from './models/ReceiptPalletSchema';
export type { RepeatSendingMessageSchema } from './models/RepeatSendingMessageSchema';
export type { RepeatSendingTextMessageRequest } from './models/RepeatSendingTextMessageRequest';
export type { RepeatSendingTextMessageResponse } from './models/RepeatSendingTextMessageResponse';
export type { ResidualBoxSchema } from './models/ResidualBoxSchema'; export type { ResidualBoxSchema } from './models/ResidualBoxSchema';
export type { ResidualPalletSchema } from './models/ResidualPalletSchema'; export type { ResidualPalletSchema } from './models/ResidualPalletSchema';
export type { ResidualProductSchema } from './models/ResidualProductSchema'; export type { ResidualProductSchema } from './models/ResidualProductSchema';
export type { RoleSchema } from './models/RoleSchema'; export type { RoleSchema } from './models/RoleSchema';
export type { SendMessageRequest } from './models/SendMessageRequest'; export type { SendTextMessageRequest } from './models/SendTextMessageRequest';
export type { SendMessageResponse } from './models/SendMessageResponse'; export type { SendTextMessageResponse } from './models/SendTextMessageResponse';
export type { ServiceCategoryReorderRequest } from './models/ServiceCategoryReorderRequest'; export type { ServiceCategoryReorderRequest } from './models/ServiceCategoryReorderRequest';
export type { ServiceCategoryReorderResponse } from './models/ServiceCategoryReorderResponse'; export type { ServiceCategoryReorderResponse } from './models/ServiceCategoryReorderResponse';
export type { ServiceCategorySchema } from './models/ServiceCategorySchema'; export type { ServiceCategorySchema } from './models/ServiceCategorySchema';

View File

@@ -0,0 +1,8 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type Body_send_messages_with_files = {
files: Array<Blob>;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { EditMessageSchema } from './EditMessageSchema';
export type EditMessageRequest = {
message: EditMessageSchema;
};

View File

@@ -2,7 +2,7 @@
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
export type SendMessageResponse = { export type EditMessageResponse = {
ok: boolean; ok: boolean;
message: string; message: string;
}; };

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type EditMessageSchema = {
text: string;
chatId: number;
id: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type LoadMessagesResponse = {
ok: boolean;
message: string;
};

View File

@@ -0,0 +1,12 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type MessageFileSchema = {
id: number;
filePath: string;
type: string;
fileName: string;
fileSize: number;
};

View File

@@ -2,6 +2,7 @@
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { MessageFileSchema } from './MessageFileSchema';
import type { TgUserSchema } from './TgUserSchema'; import type { TgUserSchema } from './TgUserSchema';
export type MessageSchema = { export type MessageSchema = {
text: string; text: string;
@@ -10,5 +11,7 @@ export type MessageSchema = {
createdAt: string; createdAt: string;
tgSender: (TgUserSchema | null); tgSender: (TgUserSchema | null);
status: string; status: string;
isEdited: boolean;
file?: (MessageFileSchema | null);
}; };

View File

@@ -0,0 +1,10 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type RepeatSendingMessageSchema = {
text: string;
chatId: number;
id: number;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import type { RepeatSendingMessageSchema } from './RepeatSendingMessageSchema';
export type RepeatSendingTextMessageRequest = {
message: RepeatSendingMessageSchema;
};

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type RepeatSendingTextMessageResponse = {
ok: boolean;
message: string;
};

View File

@@ -3,7 +3,7 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { BaseMessageSchema } from './BaseMessageSchema'; import type { BaseMessageSchema } from './BaseMessageSchema';
export type SendMessageRequest = { export type SendTextMessageRequest = {
message: BaseMessageSchema; message: BaseMessageSchema;
}; };

View File

@@ -0,0 +1,9 @@
/* generated using openapi-typescript-codegen -- do not edit */
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
export type SendTextMessageResponse = {
ok: boolean;
message: string;
};

View File

@@ -2,29 +2,35 @@
/* istanbul ignore file */ /* istanbul ignore file */
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
import type { Body_send_messages_with_files } from '../models/Body_send_messages_with_files';
import type { CreateChatRequest } from '../models/CreateChatRequest'; import type { CreateChatRequest } from '../models/CreateChatRequest';
import type { CreateChatResponse } from '../models/CreateChatResponse'; import type { CreateChatResponse } from '../models/CreateChatResponse';
import type { DeleteMessageResponse } from '../models/DeleteMessageResponse'; import type { DeleteMessageResponse } from '../models/DeleteMessageResponse';
import type { EditMessageRequest } from '../models/EditMessageRequest';
import type { EditMessageResponse } from '../models/EditMessageResponse';
import type { GetChatRequest } from '../models/GetChatRequest'; import type { GetChatRequest } from '../models/GetChatRequest';
import type { GetChatResponse } from '../models/GetChatResponse'; import type { GetChatResponse } from '../models/GetChatResponse';
import type { GetMessagesRequest } from '../models/GetMessagesRequest'; import type { GetMessagesRequest } from '../models/GetMessagesRequest';
import type { GetMessagesResponse } from '../models/GetMessagesResponse'; import type { GetMessagesResponse } from '../models/GetMessagesResponse';
import type { SendMessageRequest } from '../models/SendMessageRequest'; import type { LoadMessagesResponse } from '../models/LoadMessagesResponse';
import type { SendMessageResponse } from '../models/SendMessageResponse'; import type { RepeatSendingTextMessageRequest } from '../models/RepeatSendingTextMessageRequest';
import type { RepeatSendingTextMessageResponse } from '../models/RepeatSendingTextMessageResponse';
import type { SendTextMessageRequest } from '../models/SendTextMessageRequest';
import type { SendTextMessageResponse } from '../models/SendTextMessageResponse';
import type { CancelablePromise } from '../core/CancelablePromise'; import type { CancelablePromise } from '../core/CancelablePromise';
import { OpenAPI } from '../core/OpenAPI'; import { OpenAPI } from '../core/OpenAPI';
import { request as __request } from '../core/request'; import { request as __request } from '../core/request';
export class ChatService { export class ChatService {
/** /**
* Send Message * Send Text Message
* @returns SendMessageResponse Successful Response * @returns SendTextMessageResponse Successful Response
* @throws ApiError * @throws ApiError
*/ */
public static sendMessage({ public static sendTextMessage({
requestBody, requestBody,
}: { }: {
requestBody: SendMessageRequest, requestBody: SendTextMessageRequest,
}): CancelablePromise<SendMessageResponse> { }): CancelablePromise<SendTextMessageResponse> {
return __request(OpenAPI, { return __request(OpenAPI, {
method: 'POST', method: 'POST',
url: '/chat/message', url: '/chat/message',
@@ -35,6 +41,74 @@ export class ChatService {
}, },
}); });
} }
/**
* Edit Message
* @returns EditMessageResponse Successful Response
* @throws ApiError
*/
public static editMessage({
requestBody,
}: {
requestBody: EditMessageRequest,
}): CancelablePromise<EditMessageResponse> {
return __request(OpenAPI, {
method: 'PATCH',
url: '/chat/message',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Repeat Sending Text Message
* @returns RepeatSendingTextMessageResponse Successful Response
* @throws ApiError
*/
public static repeatSendingTextMessage({
requestBody,
}: {
requestBody: RepeatSendingTextMessageRequest,
}): CancelablePromise<RepeatSendingTextMessageResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/chat/message/repeat',
body: requestBody,
mediaType: 'application/json',
errors: {
422: `Validation Error`,
},
});
}
/**
* Send Messages With Files
* @returns LoadMessagesResponse Successful Response
* @throws ApiError
*/
public static sendMessagesWithFiles({
chatId,
caption,
formData,
}: {
chatId: number,
caption: string,
formData: Body_send_messages_with_files,
}): CancelablePromise<LoadMessagesResponse> {
return __request(OpenAPI, {
method: 'POST',
url: '/chat/message/files',
query: {
'chat_id': chatId,
'caption': caption,
},
formData: formData,
mediaType: 'multipart/form-data',
errors: {
422: `Validation Error`,
},
});
}
/** /**
* Delete Message * Delete Message
* @returns DeleteMessageResponse Successful Response * @returns DeleteMessageResponse Successful Response
@@ -116,4 +190,25 @@ export class ChatService {
}, },
}); });
} }
/**
* Get Tg File
* @returns any Successful Response
* @throws ApiError
*/
public static getTgFileChatTgFileFileIdGet({
fileId,
}: {
fileId: number,
}): CancelablePromise<any> {
return __request(OpenAPI, {
method: 'GET',
url: '/chat/tg-file/{file_id}',
path: {
'file_id': fileId,
},
errors: {
422: `Validation Error`,
},
});
}
} }

View File

@@ -15,17 +15,13 @@ export const ActionIconCopy: FC<Props> = ({ onCopiedLabel, value }) => {
<Tooltip <Tooltip
label={onCopiedLabel} label={onCopiedLabel}
offset={5} offset={5}
position="bottom"
radius="xl" radius="xl"
transitionProps={{ duration: 100, transition: "slide-down" }} transitionProps={{ duration: 100, transition: "slide-down" }}
opened={clipboard.copied}> opened={clipboard.copied}>
<ActionIcon <ActionIcon
variant={"default"} variant={"default"}
size="lg" size="lg"
onClick={() => { onClick={() => clipboard.copy(value)}
console.log("AKLSKLSKSM");
return clipboard.copy(value);
}}
> >
{clipboard.copied ? ( {clipboard.copied ? (
<IconCheck <IconCheck

View File

@@ -28,9 +28,9 @@ const Chat = () => {
/> />
)); ));
} }
elements.push(( elements.push(
<Message key={currMessage.id + "msg"} message={currMessage} /> <Message key={currMessage.id + "msg"} message={currMessage} />
)); );
prevMessage = currMessage; prevMessage = currMessage;
} }
@@ -38,12 +38,13 @@ const Chat = () => {
}; };
return ( return (
<Stack> <Stack h={"96vh"}>
<ScrollArea <ScrollArea
h={"100%"}
viewportRef={scrollRef} viewportRef={scrollRef}
onScrollPositionChange={onScrollPositionChange} onScrollPositionChange={onScrollPositionChange}
> >
<Stack h={"91vh"} pr={"md"} gap={"sm"}> <Stack pr={"md"} gap={"sm"}>
{getChatElements()} {getChatElements()}
</Stack> </Stack>
</ScrollArea> </ScrollArea>

View File

@@ -0,0 +1,15 @@
.file-circe {
@mixin light {
background-color: var(--mantine-color-gray-0);
}
@mixin dark {
background-color: var(--mantine-color-dark-4);
}
border-radius: 50%;
align-items: center;
justify-items: center;
padding: 0.8rem;
width: 3rem;
height: 3rem;
cursor: pointer;
}

View File

@@ -0,0 +1,21 @@
import { MessageFileSchema } from "../../../../client";
import ChatDocument from "./components/ChatDocument.tsx";
import ChatPhoto from "./components/ChatPhoto.tsx";
type Props = {
file: MessageFileSchema;
}
const ChatFile = ({ file }: Props) => {
if (file.type === "photo") {
return (
<ChatPhoto file={file} />
);
}
return (
<ChatDocument file={file} />
);
};
export default ChatFile;

View File

@@ -0,0 +1,48 @@
import { Center, Group, rem, Stack, Text } from "@mantine/core";
import { IconFileFilled } from "@tabler/icons-react";
import { MessageFileSchema } from "../../../../../client";
import styles from "../ChatFile.module.css";
import classNames from "classnames";
import downloadFile from "../utils/downloadFile.ts";
type Props = {
file: MessageFileSchema;
}
const ChatDocument = ({ file }: Props) => {
const round = (value: number) => Math.round(value * 10) / 10;
const getFileSize = () => {
const BYTES_IN_KB = 1_024;
const BYTES_IN_MB = 1_048_576;
const BYTES_IN_GB = 1_073_741_824;
if (file.fileSize < BYTES_IN_KB) {
return `${file.fileSize} B`;
}
if (file.fileSize < BYTES_IN_MB) {
return `${round(file.fileSize / BYTES_IN_KB)} KB`;
}
if (file.fileSize < BYTES_IN_GB) {
return `${round(file.fileSize / BYTES_IN_MB)} MB`;
}
return `${round(file.fileSize / BYTES_IN_GB)} GB`;
};
return (
<Group justify={"center"} my={"sm"}>
<Center
className={classNames(styles["file-circe"])}
onClick={() => downloadFile(file)}
>
<IconFileFilled />
</Center>
<Stack gap={rem(1)} my={rem(1)}>
<Text>{file.fileName}</Text>
<Text>{getFileSize()}</Text>
</Stack>
</Group>
);
};
export default ChatDocument;

View File

@@ -0,0 +1,44 @@
import { MessageFileSchema } from "../../../../../client";
import { Box, Image, Modal } from "@mantine/core";
import getDocLink from "../utils/getDocLink.ts";
import { useDisclosure } from "@mantine/hooks";
type Props = {
file: MessageFileSchema;
}
const ChatPhoto = ({ file }: Props) => {
const [opened, { open, close }] = useDisclosure(false);
const image = (
<Image
src={getDocLink(file.id)}
radius={"md"}
/>
);
return (
<>
<Modal
opened={opened}
onClose={close}
size={"auto"}
withCloseButton={false}
>
<Box mah={"90vh"} maw={"90vw"}>
{image}
</Box>
</Modal>
<Box
my={"sm"}
onClick={open}
style={{ cursor: "pointer" }}
>
{image}
</Box>
</>
);
};
export default ChatPhoto;

View File

@@ -0,0 +1,14 @@
import { MessageFileSchema } from "../../../../../client";
import getDocLink from "./getDocLink.ts";
const downloadFile = (file: MessageFileSchema) => {
const link = document.createElement("a");
link.href = getDocLink(file.id);
link.download = file.fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
export default downloadFile;

View File

@@ -0,0 +1,5 @@
const getDocLink = (fileId: number) => {
return `${import.meta.env.VITE_API_URL}/chat/tg-file/${fileId}`;
};
export default getDocLink;

View File

@@ -1,11 +1,12 @@
import { MessageSchema } from "../../../../client"; import { MessageSchema } from "../../../../client";
import { Box, Center, em, Flex, Group, rem } from "@mantine/core"; import { Box, Center, em, Flex, Group, rem } from "@mantine/core";
import { formatDateTime } from "../../../../types/utils.ts"; import { formatDateTime } from "../../../../types/utils.ts";
import { IconAlertCircle, IconCheck, IconClock, IconTrash } from "@tabler/icons-react"; import { IconAlertCircle, IconBrandTelegram, IconCheck, IconClock, IconEdit, IconTrash } from "@tabler/icons-react";
import { useContextMenu } from "mantine-contextmenu"; import { useContextMenu } from "mantine-contextmenu";
import useMessage from "./hooks/useMessage.tsx"; import useMessage from "./hooks/useMessage.tsx";
import styles from "../../Chat.module.css"; import styles from "../../Chat.module.css";
import classNames from "classnames"; import classNames from "classnames";
import ChatFile from "../ChatFile/ChatFile.tsx";
type Props = { type Props = {
message: MessageSchema; message: MessageSchema;
@@ -20,18 +21,47 @@ enum MessageStatuses {
const Message = ({ message }: Props) => { const Message = ({ message }: Props) => {
const isMine = !message.tgSender; const isMine = !message.tgSender;
const isSuccess = message.status == MessageStatuses.SUCCESS; const isSuccess = message.status == MessageStatuses.SUCCESS;
const { onDeleteMessageClick } = useMessage(); const isError = message.status == MessageStatuses.ERROR;
const {
onDeleteMessageClick,
onEditMessageClick,
onRepeatSendingClick,
} = useMessage();
const { showContextMenu } = useContextMenu(); const { showContextMenu } = useContextMenu();
const contextMenu = () => showContextMenu([ const contextMenuSuccessMsg = () => showContextMenu([
{ {
key: "delete", key: "delete",
onClick: () => onDeleteMessageClick(message), onClick: () => onDeleteMessageClick(message),
title: "Удалить", title: "Удалить",
icon: <IconTrash />, icon: <IconTrash />,
}, },
{
key: "edit",
onClick: () => onEditMessageClick(message),
title: "Редактировать",
icon: <IconEdit />,
},
]); ]);
const contextMenuErrorMsg = () => showContextMenu([
{
key: "repeatSending",
onClick: () => onRepeatSendingClick(message),
title: "Повторить отправку",
icon: <IconBrandTelegram />,
},
])
const getContext = () => {
if (!isMine) return;
if (isSuccess) return contextMenuSuccessMsg();
if (isError) return contextMenuErrorMsg();
}
const getStatusIcon = () => { const getStatusIcon = () => {
const size = em(18); const size = em(18);
if (message.status == MessageStatuses.ERROR) { if (message.status == MessageStatuses.ERROR) {
@@ -49,11 +79,15 @@ const Message = ({ message }: Props) => {
py={rem(5)} py={rem(5)}
px={rem(15)} px={rem(15)}
className={classNames(isMine ? styles["message"] : styles["other-message"])} className={classNames(isMine ? styles["message"] : styles["other-message"])}
onContextMenu={isMine && isSuccess ? contextMenu() : undefined} onContextMenu={getContext()}
maw={em(600)}
> >
{!isMine && ( {!isMine && (
<div>{message.tgSender!.lastName} {message.tgSender!.firstName}</div> <div>{message.tgSender!.lastName} {message.tgSender!.firstName}</div>
)} )}
{message.file && (
<ChatFile file={message.file} />
)}
<div>{message.text}</div> <div>{message.text}</div>
<Group <Group
gap={em(5)} gap={em(5)}
@@ -61,6 +95,7 @@ const Message = ({ message }: Props) => {
align={"center"} align={"center"}
wrap={"nowrap"} wrap={"nowrap"}
> >
{message.isEdited && "ред."}
<Center> <Center>
{formatDateTime(message.createdAt).substring(11, 16)} {formatDateTime(message.createdAt).substring(11, 16)}
</Center> </Center>

View File

@@ -1,7 +1,9 @@
import { ChatService, MessageSchema } from "../../../../../client"; import { ChatService, MessageSchema } from "../../../../../client";
import { notifications } from "../../../../../shared/lib/notifications.ts"; import { notifications } from "../../../../../shared/lib/notifications.ts";
import { useChatContext } from "../../../../../pages/ClientsPage/contexts/ChatContext.tsx";
const useMessage = () => { const useMessage = () => {
const { form } = useChatContext();
const onDeleteMessageClick = (message: MessageSchema) => { const onDeleteMessageClick = (message: MessageSchema) => {
ChatService.deleteMessage({ ChatService.deleteMessage({
@@ -15,8 +17,31 @@ const useMessage = () => {
.catch(err => console.log(err)); .catch(err => console.log(err));
}; };
const onEditMessageClick = (message: MessageSchema) => {
form.setValues({
messageId: message.id,
message: message.text,
});
};
const onRepeatSendingClick = (message: MessageSchema) => {
ChatService.repeatSendingTextMessage({
requestBody: {
message,
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
})
.catch(err => console.log(err));
};
return { return {
onDeleteMessageClick, onDeleteMessageClick,
onEditMessageClick,
onRepeatSendingClick,
}; };
}; };

View File

@@ -1,43 +1,71 @@
import { Button, Group, TextInput, Tooltip } from "@mantine/core"; import { ActionIcon, Button, Divider, Group, Stack, TextInput, Tooltip } from "@mantine/core";
import { IconSend2 } from "@tabler/icons-react"; import { IconCheck, IconPaperclip, IconSend2, IconX } from "@tabler/icons-react";
import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx"; import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx";
import { useForm } from "@mantine/form";
import ActionIconCopy from "../../../ActionIconCopy/ActionIconCopy.tsx"; import ActionIconCopy from "../../../ActionIconCopy/ActionIconCopy.tsx";
import SelectedFile from "../SelectedFile/SelectedFile.tsx";
import { useMemo } from "react";
export type MessageForm = {
message: string;
}
const MessageInput = () => { const MessageInput = () => {
const { sendMessage, chat } = useChatContext(); const {
const form = useForm<MessageForm>({ submitMessage,
initialValues: { chat,
message: "", form,
}, files,
}); fileDialog,
} = useChatContext();
const getFiles = useMemo(() => {
return files.map(file => (
<SelectedFile key={file.name} file={file} />
));
}, [files]);
return ( return (
<form onSubmit={form.onSubmit(values => sendMessage(values, form))}> <form onSubmit={form.onSubmit(values => submitMessage(values))}>
<Stack gap={"xs"}>
<Divider />
{getFiles}
<Group wrap={"nowrap"} align={"center"}> <Group wrap={"nowrap"} align={"center"}>
{chat?.tgGroup?.tgInviteLink && ( {chat?.tgGroup?.tgInviteLink && (
<Tooltip label={"Ссылка-приглашение"}>
<ActionIconCopy <ActionIconCopy
onCopiedLabel={"Ссылка скопирована в буфер обмена"} onCopiedLabel={"Ссылка на чат скопирована в буфер обмена"}
value={chat.tgGroup.tgInviteLink} value={chat.tgGroup.tgInviteLink}
/> />
</Tooltip>
)} )}
<Tooltip label={"Прикрепить файлы"}>
<ActionIcon
variant={"default"}
onClick={fileDialog.open}
size="lg"
>
<IconPaperclip />
</ActionIcon>
</Tooltip>
<TextInput <TextInput
{...form.getInputProps("message")} {...form.getInputProps("message")}
w={"100%"} w={"100%"}
/> />
{form.values.messageId && (
<Button
variant={"default"}
onClick={form.reset}
>
<IconX />
</Button>
)}
<Button <Button
variant={"default"} variant={"default"}
type="submit" type="submit"
> >
{form.values.messageId ? (
<IconCheck />
) : (
<IconSend2 /> <IconSend2 />
)}
</Button> </Button>
</Group> </Group>
</Stack>
</form> </form>
); );
}; };

View File

@@ -0,0 +1,38 @@
import { Box, Group, Text } from "@mantine/core";
import { IconX } from "@tabler/icons-react";
import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx";
type Props = {
file: File;
}
const SelectedFile = ({ file }: Props) => {
const { files, filesHandlers } = useChatContext();
const onCancelFileClick = () => {
const idx = files.findIndex(f => f.name === file.name && f.type === file.type);
if (idx < 0) return;
filesHandlers.remove(idx);
};
return (
<Box>
<Group justify={"space-between"}>
<Text>
{file.name}
</Text>
<Box
onClick={onCancelFileClick}
style={{
cursor: "pointer",
}}
>
<IconX />
</Box>
</Group>
</Box>
);
};
export default SelectedFile;

View File

@@ -3,13 +3,16 @@ import CreateServiceCategoryModal from "../pages/ServicesPage/modals/CreateServi
import CreateServiceModal from "../pages/ServicesPage/modals/CreateServiceModal.tsx"; import CreateServiceModal from "../pages/ServicesPage/modals/CreateServiceModal.tsx";
import createProductModal from "../pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx"; import createProductModal from "../pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx";
import ProductFormModal from "../pages/ClientsPage/modals/ClientFormModal/ClientFormModal.tsx"; import ProductFormModal from "../pages/ClientsPage/modals/ClientFormModal/ClientFormModal.tsx";
import AddCardServiceModal from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardServiceModal.tsx"; import AddCardServiceModal
import AddCardProductModal from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardProductModal.tsx"; from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardServiceModal.tsx";
import AddCardProductModal
from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardProductModal.tsx";
import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx"; import PrintBarcodeModal from "./PrintBarcodeModal/PrintBarcodeModal.tsx";
import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx"; import AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx";
import BarcodeTemplateFormModal import BarcodeTemplateFormModal
from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx"; from "../pages/BarcodePage/modals/BarcodeTemplateFormModal/BarcodeTemplateFormModal.tsx";
import ProductServiceFormModal from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/ProductServiceFormModal.tsx"; import ProductServiceFormModal
from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/ProductServiceFormModal.tsx";
import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx"; import UserFormModal from "../pages/AdminPage/modals/UserFormModal/UserFormModal.tsx";
import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx"; import EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx";
import EmployeeTableModal from "./EmployeeTableModal/EmployeeTableModal.tsx"; import EmployeeTableModal from "./EmployeeTableModal/EmployeeTableModal.tsx";

View File

@@ -64,7 +64,7 @@ const generateRows = (modules: Module[]) => {
}; };
const tsxContent = template(data); const tsxContent = template(data);
fs.writeFileSync(OUTPUT_PATH, tsxContent); fs.writeFileSync(OUTPUT_PATH, tsxContent);
console.log("File successfully generated."); console.log("ChatFile successfully generated.");
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@@ -1,17 +1,31 @@
import React, { createContext, FC, MutableRefObject, useContext, useEffect, useRef, useState } from "react"; import React, { createContext, FC, MutableRefObject, useContext, useEffect, useRef, useState } from "react";
import { ChatSchema, ChatService, MessageSchema } from "../../../client"; import { ChatSchema, ChatService, MessageSchema } from "../../../client";
import { notifications } from "../../../shared/lib/notifications.ts"; import { notifications } from "../../../shared/lib/notifications.ts";
import { MessageForm } from "../../../components/Chat/components/MessageInput/MessageInput.tsx"; import { useForm, UseFormReturnType } from "@mantine/form";
import { UseFormReturnType } from "@mantine/form"; import { useDebouncedState, useFileDialog, useListState, UseListStateHandlers } from "@mantine/hooks";
import { useDebouncedState } from "@mantine/hooks";
export type MessageForm = {
message: string;
messageId?: number;
}
type FileDialog = {
files: FileList | null;
open: () => void;
reset: () => void;
}
type ChatContextState = { type ChatContextState = {
chat: ChatSchema | null; chat: ChatSchema | null;
setChat: (chat: ChatSchema | null) => void; setChat: (chat: ChatSchema | null) => void;
messages: MessageSchema[]; messages: MessageSchema[];
form: UseFormReturnType<MessageForm>;
onScrollPositionChange: (values: { x: number, y: number }) => void; onScrollPositionChange: (values: { x: number, y: number }) => void;
scrollRef: MutableRefObject<HTMLDivElement | null>; scrollRef: MutableRefObject<HTMLDivElement | null>;
sendMessage: (values: MessageForm, form: UseFormReturnType<MessageForm>) => void; submitMessage: (values: MessageForm) => void;
files: Array<File>;
filesHandlers: UseListStateHandlers<File>;
fileDialog: FileDialog;
}; };
const ChatContext = createContext<ChatContextState | undefined>(undefined); const ChatContext = createContext<ChatContextState | undefined>(undefined);
@@ -26,17 +40,30 @@ const useChatContextState = () => {
const [isScrollToBottom, setIsScrollToBottom] = useState(true); const [isScrollToBottom, setIsScrollToBottom] = useState(true);
const [scrollPosition, setScrollPosition] = useDebouncedState<{ x: number, y: number }>({ x: 0, y: 0 }, 400); const [scrollPosition, setScrollPosition] = useDebouncedState<{ x: number, y: number }>({ x: 0, y: 0 }, 400);
const [files, filesHandlers] = useListState<File>([]);
const fileDialog = useFileDialog();
useEffect(() => {
filesHandlers.setState(Array.from(fileDialog.files ?? []));
}, [fileDialog.files]);
const form = useForm<MessageForm>({
initialValues: {
message: "",
},
});
const setChatValue = (chat: ChatSchema | null) => { const setChatValue = (chat: ChatSchema | null) => {
if (chat) { if (chat) {
setOffset(0); setOffset(0);
setHasMore(true); setHasMore(true);
setScrollPosition({ x: 0, y: 0 }); setScrollPosition({ x: 0, y: 0 });
filesHandlers.setState([]);
form.reset();
} }
setChat(chat); setChat(chat);
}; };
console.log(`has more = ${hasMore}`);
const fetchMessages = () => { const fetchMessages = () => {
if (!chat) return; if (!chat) return;
@@ -71,9 +98,7 @@ const useChatContextState = () => {
setMessages((prev) => [...prev, ...newMessages]); setMessages((prev) => [...prev, ...newMessages]);
setOffset((prev) => prev + limit); setOffset((prev) => prev + limit);
if (scrollRef.current) { if (scrollRef.current) {
console.log("FETCH ON SCROLL")
const prevPosition = limit / offset * scrollRef.current.scrollHeight; const prevPosition = limit / offset * scrollRef.current.scrollHeight;
console.log(`PREV POSITION = ${prevPosition}`)
scrollRef.current.scrollTo({ top: prevPosition, behavior: "instant" }); scrollRef.current.scrollTo({ top: prevPosition, behavior: "instant" });
} }
}) })
@@ -91,25 +116,42 @@ const useChatContextState = () => {
useEffect(() => { useEffect(() => {
if (scrollRef.current && isScrollToBottom && chat) { if (scrollRef.current && isScrollToBottom && chat) {
setIsScrollToBottom(false); setIsScrollToBottom(false);
console.log("Scroll to the bottom");
console.log(`scrollHeight = ${scrollRef.current.scrollHeight}`);
scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" }); scrollRef.current.scrollTo({ top: scrollRef.current.scrollHeight, behavior: "smooth" });
} }
}, [messages, isScrollToBottom]); }, [messages, isScrollToBottom]);
useEffect(() => { useEffect(() => {
if (!chat) return; if (!chat) return;
console.log(`handleScroll ${Math.abs(scrollPosition.y - 200)}`);
if (Math.abs(scrollPosition.y - 200) <= 200 && hasMore) { if (Math.abs(scrollPosition.y - 200) <= 200 && hasMore) {
console.log("handleScroll WORK");
fetchMessagesOnScroll(); fetchMessagesOnScroll();
} }
}, [scrollPosition]); }, [scrollPosition]);
const sendMessage = (values: MessageForm, form: UseFormReturnType<MessageForm>) => { const sendMessageWithFiles = (values: MessageForm) => {
if (!chat) return; if (!chat) return;
ChatService.sendMessage({ ChatService.sendMessagesWithFiles({
formData: {
files,
},
chatId: chat.id,
caption: values.message,
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
return;
}
filesHandlers.setState([]);
form.reset();
})
.catch(err => console.log(err));
};
const sendTextMessage = (values: MessageForm) => {
if (!chat) return;
ChatService.sendTextMessage({
requestBody: { requestBody: {
message: { message: {
text: values.message, text: values.message,
@@ -127,13 +169,49 @@ const useChatContextState = () => {
.catch(err => console.log(err)); .catch(err => console.log(err));
}; };
const editMessage = (values: MessageForm) => {
if (!chat) return;
ChatService.editMessage({
requestBody: {
message: {
id: values.messageId!,
text: values.message,
chatId: chat.id,
},
},
})
.then(({ ok, message }) => {
if (!ok) {
notifications.error({ message });
}
form.reset();
setIsScrollToBottom(true);
})
.catch(err => console.log(err));
};
const submitMessage = (values: MessageForm) => {
if (values.messageId) {
editMessage(values);
} else if (files.length === 0) {
sendTextMessage(values);
} else {
sendMessageWithFiles(values);
}
};
return { return {
chat, chat,
setChat: setChatValue, setChat: setChatValue,
messages, messages,
form,
onScrollPositionChange: setScrollPosition, onScrollPositionChange: setScrollPosition,
scrollRef, scrollRef,
sendMessage, submitMessage,
files,
filesHandlers,
fileDialog,
}; };
}; };