From 23f0cdc0819bf23ca50d7211bbceff0b83125801 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Wed, 2 Apr 2025 15:27:57 +0400 Subject: [PATCH] feat: sending and receiving messages with files, editing text messages --- src/client/index.ts | 13 ++- .../models/Body_send_messages_with_files.ts | 8 ++ src/client/models/EditMessageRequest.ts | 9 ++ ...sageResponse.ts => EditMessageResponse.ts} | 2 +- src/client/models/EditMessageSchema.ts | 10 ++ src/client/models/LoadMessagesResponse.ts | 9 ++ src/client/models/MessageFileSchema.ts | 12 ++ src/client/models/MessageSchema.ts | 3 + .../models/RepeatSendingMessageSchema.ts | 10 ++ .../models/RepeatSendingTextMessageRequest.ts | 9 ++ .../RepeatSendingTextMessageResponse.ts | 9 ++ ...geRequest.ts => SendTextMessageRequest.ts} | 2 +- src/client/models/SendTextMessageResponse.ts | 9 ++ src/client/services/ChatService.ts | 109 ++++++++++++++++-- .../ActionIconCopy/ActionIconCopy.tsx | 6 +- src/components/Chat/Chat.tsx | 9 +- .../components/ChatFile/ChatFile.module.css | 15 +++ .../Chat/components/ChatFile/ChatFile.tsx | 21 ++++ .../ChatFile/components/ChatDocument.tsx | 48 ++++++++ .../ChatFile/components/ChatPhoto.tsx | 44 +++++++ .../components/ChatFile/utils/downloadFile.ts | 14 +++ .../components/ChatFile/utils/getDocLink.ts | 5 + .../Chat/components/Message/Message.tsx | 43 ++++++- .../components/Message/hooks/useMessage.tsx | 25 ++++ .../components/MessageInput/MessageInput.tsx | 86 +++++++++----- .../components/SelectedFile/SelectedFile.tsx | 38 ++++++ src/modals/modals.ts | 9 +- src/modules/modulesFileGen/modulesFileGen.ts | 2 +- .../ClientsPage/contexts/ChatContext.tsx | 108 ++++++++++++++--- 29 files changed, 615 insertions(+), 72 deletions(-) create mode 100644 src/client/models/Body_send_messages_with_files.ts create mode 100644 src/client/models/EditMessageRequest.ts rename src/client/models/{SendMessageResponse.ts => EditMessageResponse.ts} (82%) create mode 100644 src/client/models/EditMessageSchema.ts create mode 100644 src/client/models/LoadMessagesResponse.ts create mode 100644 src/client/models/MessageFileSchema.ts create mode 100644 src/client/models/RepeatSendingMessageSchema.ts create mode 100644 src/client/models/RepeatSendingTextMessageRequest.ts create mode 100644 src/client/models/RepeatSendingTextMessageResponse.ts rename src/client/models/{SendMessageRequest.ts => SendTextMessageRequest.ts} (85%) create mode 100644 src/client/models/SendTextMessageResponse.ts create mode 100644 src/components/Chat/components/ChatFile/ChatFile.module.css create mode 100644 src/components/Chat/components/ChatFile/ChatFile.tsx create mode 100644 src/components/Chat/components/ChatFile/components/ChatDocument.tsx create mode 100644 src/components/Chat/components/ChatFile/components/ChatPhoto.tsx create mode 100644 src/components/Chat/components/ChatFile/utils/downloadFile.ts create mode 100644 src/components/Chat/components/ChatFile/utils/getDocLink.ts create mode 100644 src/components/Chat/components/SelectedFile/SelectedFile.tsx diff --git a/src/client/index.ts b/src/client/index.ts index 2c07e16..f10d6ba 100644 --- a/src/client/index.ts +++ b/src/client/index.ts @@ -40,6 +40,7 @@ export type { BillPaymentStatus } from './models/BillPaymentStatus'; export type { BillStatusUpdateRequest } from './models/BillStatusUpdateRequest'; export type { BoardSchema } from './models/BoardSchema'; 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_product_barcode_image } from './models/Body_upload_product_barcode_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 { DepartmentSectionBriefSchema } from './models/DepartmentSectionBriefSchema'; 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 { FinishPauseByUserIdResponse } from './models/FinishPauseByUserIdResponse'; export type { FinishShiftByIdResponse } from './models/FinishShiftByIdResponse'; @@ -279,12 +283,14 @@ export type { GetWorkShiftsPlanningDataRequest } from './models/GetWorkShiftsPla export type { GetWorkShiftsResponse } from './models/GetWorkShiftsResponse'; export type { GroupBillRequestSchema } from './models/GroupBillRequestSchema'; export type { HTTPValidationError } from './models/HTTPValidationError'; +export type { LoadMessagesResponse } from './models/LoadMessagesResponse'; export type { LoadReceiptRequest } from './models/LoadReceiptRequest'; export type { LoadReceiptResponse } from './models/LoadReceiptResponse'; export type { ManageEmployeeRequest } from './models/ManageEmployeeRequest'; export type { ManageEmployeeResponse } from './models/ManageEmployeeResponse'; export type { MarketplaceCreateSchema } from './models/MarketplaceCreateSchema'; export type { MarketplaceSchema } from './models/MarketplaceSchema'; +export type { MessageFileSchema } from './models/MessageFileSchema'; export type { MessageSchema } from './models/MessageSchema'; export type { ModuleSchema } from './models/ModuleSchema'; export type { NotificationChannel } from './models/NotificationChannel'; @@ -334,12 +340,15 @@ export type { ProjectGeneralInfoSchema } from './models/ProjectGeneralInfoSchema export type { ProjectSchema } from './models/ProjectSchema'; export type { ReceiptBoxSchema } from './models/ReceiptBoxSchema'; 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 { ResidualPalletSchema } from './models/ResidualPalletSchema'; export type { ResidualProductSchema } from './models/ResidualProductSchema'; export type { RoleSchema } from './models/RoleSchema'; -export type { SendMessageRequest } from './models/SendMessageRequest'; -export type { SendMessageResponse } from './models/SendMessageResponse'; +export type { SendTextMessageRequest } from './models/SendTextMessageRequest'; +export type { SendTextMessageResponse } from './models/SendTextMessageResponse'; export type { ServiceCategoryReorderRequest } from './models/ServiceCategoryReorderRequest'; export type { ServiceCategoryReorderResponse } from './models/ServiceCategoryReorderResponse'; export type { ServiceCategorySchema } from './models/ServiceCategorySchema'; diff --git a/src/client/models/Body_send_messages_with_files.ts b/src/client/models/Body_send_messages_with_files.ts new file mode 100644 index 0000000..7ae84d3 --- /dev/null +++ b/src/client/models/Body_send_messages_with_files.ts @@ -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; +}; + diff --git a/src/client/models/EditMessageRequest.ts b/src/client/models/EditMessageRequest.ts new file mode 100644 index 0000000..83116f2 --- /dev/null +++ b/src/client/models/EditMessageRequest.ts @@ -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; +}; + diff --git a/src/client/models/SendMessageResponse.ts b/src/client/models/EditMessageResponse.ts similarity index 82% rename from src/client/models/SendMessageResponse.ts rename to src/client/models/EditMessageResponse.ts index f9ebde1..8ad1d1a 100644 --- a/src/client/models/SendMessageResponse.ts +++ b/src/client/models/EditMessageResponse.ts @@ -2,7 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ -export type SendMessageResponse = { +export type EditMessageResponse = { ok: boolean; message: string; }; diff --git a/src/client/models/EditMessageSchema.ts b/src/client/models/EditMessageSchema.ts new file mode 100644 index 0000000..044ce39 --- /dev/null +++ b/src/client/models/EditMessageSchema.ts @@ -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; +}; + diff --git a/src/client/models/LoadMessagesResponse.ts b/src/client/models/LoadMessagesResponse.ts new file mode 100644 index 0000000..e5a0d02 --- /dev/null +++ b/src/client/models/LoadMessagesResponse.ts @@ -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; +}; + diff --git a/src/client/models/MessageFileSchema.ts b/src/client/models/MessageFileSchema.ts new file mode 100644 index 0000000..81b54ea --- /dev/null +++ b/src/client/models/MessageFileSchema.ts @@ -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; +}; + diff --git a/src/client/models/MessageSchema.ts b/src/client/models/MessageSchema.ts index e452a3e..f465e78 100644 --- a/src/client/models/MessageSchema.ts +++ b/src/client/models/MessageSchema.ts @@ -2,6 +2,7 @@ /* istanbul ignore file */ /* tslint:disable */ /* eslint-disable */ +import type { MessageFileSchema } from './MessageFileSchema'; import type { TgUserSchema } from './TgUserSchema'; export type MessageSchema = { text: string; @@ -10,5 +11,7 @@ export type MessageSchema = { createdAt: string; tgSender: (TgUserSchema | null); status: string; + isEdited: boolean; + file?: (MessageFileSchema | null); }; diff --git a/src/client/models/RepeatSendingMessageSchema.ts b/src/client/models/RepeatSendingMessageSchema.ts new file mode 100644 index 0000000..e1e68da --- /dev/null +++ b/src/client/models/RepeatSendingMessageSchema.ts @@ -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; +}; + diff --git a/src/client/models/RepeatSendingTextMessageRequest.ts b/src/client/models/RepeatSendingTextMessageRequest.ts new file mode 100644 index 0000000..0aedc81 --- /dev/null +++ b/src/client/models/RepeatSendingTextMessageRequest.ts @@ -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; +}; + diff --git a/src/client/models/RepeatSendingTextMessageResponse.ts b/src/client/models/RepeatSendingTextMessageResponse.ts new file mode 100644 index 0000000..8cbd44d --- /dev/null +++ b/src/client/models/RepeatSendingTextMessageResponse.ts @@ -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; +}; + diff --git a/src/client/models/SendMessageRequest.ts b/src/client/models/SendTextMessageRequest.ts similarity index 85% rename from src/client/models/SendMessageRequest.ts rename to src/client/models/SendTextMessageRequest.ts index c1db847..c6d0925 100644 --- a/src/client/models/SendMessageRequest.ts +++ b/src/client/models/SendTextMessageRequest.ts @@ -3,7 +3,7 @@ /* tslint:disable */ /* eslint-disable */ import type { BaseMessageSchema } from './BaseMessageSchema'; -export type SendMessageRequest = { +export type SendTextMessageRequest = { message: BaseMessageSchema; }; diff --git a/src/client/models/SendTextMessageResponse.ts b/src/client/models/SendTextMessageResponse.ts new file mode 100644 index 0000000..b611d07 --- /dev/null +++ b/src/client/models/SendTextMessageResponse.ts @@ -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; +}; + diff --git a/src/client/services/ChatService.ts b/src/client/services/ChatService.ts index 457c374..80da7aa 100644 --- a/src/client/services/ChatService.ts +++ b/src/client/services/ChatService.ts @@ -2,29 +2,35 @@ /* istanbul ignore file */ /* tslint: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 { CreateChatResponse } from '../models/CreateChatResponse'; 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 { GetChatResponse } from '../models/GetChatResponse'; import type { GetMessagesRequest } from '../models/GetMessagesRequest'; import type { GetMessagesResponse } from '../models/GetMessagesResponse'; -import type { SendMessageRequest } from '../models/SendMessageRequest'; -import type { SendMessageResponse } from '../models/SendMessageResponse'; +import type { LoadMessagesResponse } from '../models/LoadMessagesResponse'; +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 { OpenAPI } from '../core/OpenAPI'; import { request as __request } from '../core/request'; export class ChatService { /** - * Send Message - * @returns SendMessageResponse Successful Response + * Send Text Message + * @returns SendTextMessageResponse Successful Response * @throws ApiError */ - public static sendMessage({ + public static sendTextMessage({ requestBody, }: { - requestBody: SendMessageRequest, - }): CancelablePromise { + requestBody: SendTextMessageRequest, + }): CancelablePromise { return __request(OpenAPI, { method: 'POST', 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 { + 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 { + 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 { + 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 * @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 { + return __request(OpenAPI, { + method: 'GET', + url: '/chat/tg-file/{file_id}', + path: { + 'file_id': fileId, + }, + errors: { + 422: `Validation Error`, + }, + }); + } } diff --git a/src/components/ActionIconCopy/ActionIconCopy.tsx b/src/components/ActionIconCopy/ActionIconCopy.tsx index aecab78..1181dc5 100644 --- a/src/components/ActionIconCopy/ActionIconCopy.tsx +++ b/src/components/ActionIconCopy/ActionIconCopy.tsx @@ -15,17 +15,13 @@ export const ActionIconCopy: FC = ({ onCopiedLabel, value }) => { { - console.log("AKLSKLSKSM"); - return clipboard.copy(value); - }} + onClick={() => clipboard.copy(value)} > {clipboard.copied ? ( { /> )); } - elements.push(( + elements.push( - )); + ); prevMessage = currMessage; } @@ -38,12 +38,13 @@ const Chat = () => { }; return ( - + - + {getChatElements()} diff --git a/src/components/Chat/components/ChatFile/ChatFile.module.css b/src/components/Chat/components/ChatFile/ChatFile.module.css new file mode 100644 index 0000000..468ff12 --- /dev/null +++ b/src/components/Chat/components/ChatFile/ChatFile.module.css @@ -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; +} diff --git a/src/components/Chat/components/ChatFile/ChatFile.tsx b/src/components/Chat/components/ChatFile/ChatFile.tsx new file mode 100644 index 0000000..f2fcabb --- /dev/null +++ b/src/components/Chat/components/ChatFile/ChatFile.tsx @@ -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 ( + + ); + } + return ( + + ); +}; + +export default ChatFile; diff --git a/src/components/Chat/components/ChatFile/components/ChatDocument.tsx b/src/components/Chat/components/ChatFile/components/ChatDocument.tsx new file mode 100644 index 0000000..957bebc --- /dev/null +++ b/src/components/Chat/components/ChatFile/components/ChatDocument.tsx @@ -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 ( + +
downloadFile(file)} + > + +
+ + {file.fileName} + {getFileSize()} + +
+ ); +}; + +export default ChatDocument; diff --git a/src/components/Chat/components/ChatFile/components/ChatPhoto.tsx b/src/components/Chat/components/ChatFile/components/ChatPhoto.tsx new file mode 100644 index 0000000..47f5b76 --- /dev/null +++ b/src/components/Chat/components/ChatFile/components/ChatPhoto.tsx @@ -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 = ( + + ); + + return ( + <> + + + {image} + + + + {image} + + + ); +}; + +export default ChatPhoto; diff --git a/src/components/Chat/components/ChatFile/utils/downloadFile.ts b/src/components/Chat/components/ChatFile/utils/downloadFile.ts new file mode 100644 index 0000000..ccf5641 --- /dev/null +++ b/src/components/Chat/components/ChatFile/utils/downloadFile.ts @@ -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; diff --git a/src/components/Chat/components/ChatFile/utils/getDocLink.ts b/src/components/Chat/components/ChatFile/utils/getDocLink.ts new file mode 100644 index 0000000..75bd530 --- /dev/null +++ b/src/components/Chat/components/ChatFile/utils/getDocLink.ts @@ -0,0 +1,5 @@ +const getDocLink = (fileId: number) => { + return `${import.meta.env.VITE_API_URL}/chat/tg-file/${fileId}`; +}; + +export default getDocLink; diff --git a/src/components/Chat/components/Message/Message.tsx b/src/components/Chat/components/Message/Message.tsx index 52eef53..a7e634e 100644 --- a/src/components/Chat/components/Message/Message.tsx +++ b/src/components/Chat/components/Message/Message.tsx @@ -1,11 +1,12 @@ 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 { IconAlertCircle, IconBrandTelegram, IconCheck, IconClock, IconEdit, 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"; +import ChatFile from "../ChatFile/ChatFile.tsx"; type Props = { message: MessageSchema; @@ -20,18 +21,47 @@ enum MessageStatuses { const Message = ({ message }: Props) => { const isMine = !message.tgSender; const isSuccess = message.status == MessageStatuses.SUCCESS; - const { onDeleteMessageClick } = useMessage(); + const isError = message.status == MessageStatuses.ERROR; + + const { + onDeleteMessageClick, + onEditMessageClick, + onRepeatSendingClick, + } = useMessage(); + const { showContextMenu } = useContextMenu(); - const contextMenu = () => showContextMenu([ + const contextMenuSuccessMsg = () => showContextMenu([ { key: "delete", onClick: () => onDeleteMessageClick(message), title: "Удалить", icon: , }, + { + key: "edit", + onClick: () => onEditMessageClick(message), + title: "Редактировать", + icon: , + }, ]); + const contextMenuErrorMsg = () => showContextMenu([ + { + key: "repeatSending", + onClick: () => onRepeatSendingClick(message), + title: "Повторить отправку", + icon: , + }, + ]) + + const getContext = () => { + if (!isMine) return; + + if (isSuccess) return contextMenuSuccessMsg(); + if (isError) return contextMenuErrorMsg(); + } + const getStatusIcon = () => { const size = em(18); if (message.status == MessageStatuses.ERROR) { @@ -49,11 +79,15 @@ const Message = ({ message }: Props) => { py={rem(5)} px={rem(15)} className={classNames(isMine ? styles["message"] : styles["other-message"])} - onContextMenu={isMine && isSuccess ? contextMenu() : undefined} + onContextMenu={getContext()} + maw={em(600)} > {!isMine && (
{message.tgSender!.lastName} {message.tgSender!.firstName}
)} + {message.file && ( + + )}
{message.text}
{ align={"center"} wrap={"nowrap"} > + {message.isEdited && "ред."}
{formatDateTime(message.createdAt).substring(11, 16)}
diff --git a/src/components/Chat/components/Message/hooks/useMessage.tsx b/src/components/Chat/components/Message/hooks/useMessage.tsx index db0e6a8..4f8d695 100644 --- a/src/components/Chat/components/Message/hooks/useMessage.tsx +++ b/src/components/Chat/components/Message/hooks/useMessage.tsx @@ -1,7 +1,9 @@ import { ChatService, MessageSchema } from "../../../../../client"; import { notifications } from "../../../../../shared/lib/notifications.ts"; +import { useChatContext } from "../../../../../pages/ClientsPage/contexts/ChatContext.tsx"; const useMessage = () => { + const { form } = useChatContext(); const onDeleteMessageClick = (message: MessageSchema) => { ChatService.deleteMessage({ @@ -15,8 +17,31 @@ const useMessage = () => { .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 { onDeleteMessageClick, + onEditMessageClick, + onRepeatSendingClick, }; }; diff --git a/src/components/Chat/components/MessageInput/MessageInput.tsx b/src/components/Chat/components/MessageInput/MessageInput.tsx index 3e5538e..64fce61 100644 --- a/src/components/Chat/components/MessageInput/MessageInput.tsx +++ b/src/components/Chat/components/MessageInput/MessageInput.tsx @@ -1,43 +1,71 @@ -import { Button, Group, TextInput, Tooltip } from "@mantine/core"; -import { IconSend2 } from "@tabler/icons-react"; +import { ActionIcon, Button, Divider, Group, Stack, TextInput, Tooltip } from "@mantine/core"; +import { IconCheck, IconPaperclip, IconSend2, IconX } from "@tabler/icons-react"; import { useChatContext } from "../../../../pages/ClientsPage/contexts/ChatContext.tsx"; -import { useForm } from "@mantine/form"; import ActionIconCopy from "../../../ActionIconCopy/ActionIconCopy.tsx"; +import SelectedFile from "../SelectedFile/SelectedFile.tsx"; +import { useMemo } from "react"; -export type MessageForm = { - message: string; -} const MessageInput = () => { - const { sendMessage, chat } = useChatContext(); - const form = useForm({ - initialValues: { - message: "", - }, - }); + const { + submitMessage, + chat, + form, + files, + fileDialog, + } = useChatContext(); + + const getFiles = useMemo(() => { + return files.map(file => ( + + )); + }, [files]); return ( -
sendMessage(values, form))}> - - {chat?.tgGroup?.tgInviteLink && ( - + submitMessage(values))}> + + + {getFiles} + + {chat?.tgGroup?.tgInviteLink && ( + )} + + + + - )} - - - + + {form.values.messageId && ( + + )} + + +
); }; diff --git a/src/components/Chat/components/SelectedFile/SelectedFile.tsx b/src/components/Chat/components/SelectedFile/SelectedFile.tsx new file mode 100644 index 0000000..805f201 --- /dev/null +++ b/src/components/Chat/components/SelectedFile/SelectedFile.tsx @@ -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 ( + + + + {file.name} + + + + + + + ); +}; + +export default SelectedFile; \ No newline at end of file diff --git a/src/modals/modals.ts b/src/modals/modals.ts index cfd5cfd..2de725b 100644 --- a/src/modals/modals.ts +++ b/src/modals/modals.ts @@ -3,13 +3,16 @@ import CreateServiceCategoryModal from "../pages/ServicesPage/modals/CreateServi import CreateServiceModal from "../pages/ServicesPage/modals/CreateServiceModal.tsx"; import createProductModal from "../pages/ProductsPage/modals/CreateProductModal/CreateProductModal.tsx"; import ProductFormModal from "../pages/ClientsPage/modals/ClientFormModal/ClientFormModal.tsx"; -import AddCardServiceModal from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardServiceModal.tsx"; -import AddCardProductModal from "../modules/cardModules/cardEditorTabs/ProductAndServiceTab/modals/AddCardProductModal.tsx"; +import AddCardServiceModal + 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 AddBarcodeModal from "./AddBarcodeModal/AddBarcodeModal.tsx"; import BarcodeTemplateFormModal 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 EmployeeSelectModal from "./EmployeeSelectModal/EmployeeSelectModal.tsx"; import EmployeeTableModal from "./EmployeeTableModal/EmployeeTableModal.tsx"; diff --git a/src/modules/modulesFileGen/modulesFileGen.ts b/src/modules/modulesFileGen/modulesFileGen.ts index 1bbd604..bd02f09 100644 --- a/src/modules/modulesFileGen/modulesFileGen.ts +++ b/src/modules/modulesFileGen/modulesFileGen.ts @@ -64,7 +64,7 @@ const generateRows = (modules: Module[]) => { }; const tsxContent = template(data); fs.writeFileSync(OUTPUT_PATH, tsxContent); - console.log("File successfully generated."); + console.log("ChatFile successfully generated."); } catch (error) { console.error(error); } diff --git a/src/pages/ClientsPage/contexts/ChatContext.tsx b/src/pages/ClientsPage/contexts/ChatContext.tsx index 23a3a46..6732af5 100644 --- a/src/pages/ClientsPage/contexts/ChatContext.tsx +++ b/src/pages/ClientsPage/contexts/ChatContext.tsx @@ -1,17 +1,31 @@ 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"; +import { useForm, UseFormReturnType } from "@mantine/form"; +import { useDebouncedState, useFileDialog, useListState, UseListStateHandlers } from "@mantine/hooks"; + +export type MessageForm = { + message: string; + messageId?: number; +} + +type FileDialog = { + files: FileList | null; + open: () => void; + reset: () => void; +} type ChatContextState = { chat: ChatSchema | null; setChat: (chat: ChatSchema | null) => void; messages: MessageSchema[]; + form: UseFormReturnType; onScrollPositionChange: (values: { x: number, y: number }) => void; scrollRef: MutableRefObject; - sendMessage: (values: MessageForm, form: UseFormReturnType) => void; + submitMessage: (values: MessageForm) => void; + files: Array; + filesHandlers: UseListStateHandlers; + fileDialog: FileDialog; }; const ChatContext = createContext(undefined); @@ -26,17 +40,30 @@ const useChatContextState = () => { const [isScrollToBottom, setIsScrollToBottom] = useState(true); const [scrollPosition, setScrollPosition] = useDebouncedState<{ x: number, y: number }>({ x: 0, y: 0 }, 400); + const [files, filesHandlers] = useListState([]); + const fileDialog = useFileDialog(); + + useEffect(() => { + filesHandlers.setState(Array.from(fileDialog.files ?? [])); + }, [fileDialog.files]); + + const form = useForm({ + initialValues: { + message: "", + }, + }); + const setChatValue = (chat: ChatSchema | null) => { if (chat) { setOffset(0); setHasMore(true); setScrollPosition({ x: 0, y: 0 }); + filesHandlers.setState([]); + form.reset(); } setChat(chat); }; - console.log(`has more = ${hasMore}`); - const fetchMessages = () => { if (!chat) return; @@ -71,9 +98,7 @@ const useChatContextState = () => { 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" }); } }) @@ -91,25 +116,42 @@ const useChatContextState = () => { 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) => { + const sendMessageWithFiles = (values: MessageForm) => { 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: { message: { text: values.message, @@ -127,13 +169,49 @@ const useChatContextState = () => { .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 { chat, setChat: setChatValue, messages, + form, onScrollPositionChange: setScrollPosition, scrollRef, - sendMessage, + submitMessage, + files, + filesHandlers, + fileDialog, }; };