From 8d1bb47b2a3021dd4a9cb6eb799297dd2f57be59 Mon Sep 17 00:00:00 2001 From: Mohamed Marrouchi Date: Fri, 17 Jan 2025 08:00:45 +0100 Subject: [PATCH] feat: enhance attachment display in inbox --- .../inbox/components/AttachmentViewer.tsx | 56 ++++++++++++++----- .../components/inbox/components/Carousel.tsx | 8 +-- .../components/inbox/helpers/mapMessages.tsx | 7 ++- .../src/hooks/useGetAttachmentMetadata.ts | 52 +++++++++++++++++ frontend/src/hooks/useGetAttachmentUrl.ts | 20 ------- frontend/src/types/message.types.ts | 14 ++--- frontend/src/utils/attachment.ts | 27 ++++++++- 7 files changed, 134 insertions(+), 50 deletions(-) create mode 100644 frontend/src/hooks/useGetAttachmentMetadata.ts delete mode 100644 frontend/src/hooks/useGetAttachmentUrl.ts diff --git a/frontend/src/components/inbox/components/AttachmentViewer.tsx b/frontend/src/components/inbox/components/AttachmentViewer.tsx index 4784cf87..67fd3110 100644 --- a/frontend/src/components/inbox/components/AttachmentViewer.tsx +++ b/frontend/src/components/inbox/components/AttachmentViewer.tsx @@ -7,20 +7,22 @@ */ import DownloadIcon from "@mui/icons-material/Download"; -import { Button, Dialog, DialogContent } from "@mui/material"; +import { Box, Button, Dialog, DialogContent, Typography } from "@mui/material"; import { FC } from "react"; import { DialogTitle } from "@/app-components/dialogs"; import { useDialog } from "@/hooks/useDialog"; -import { useGetAttachmentUrl } from "@/hooks/useGetAttachmentUrl"; +import { useGetAttachmentMetadata } from "@/hooks/useGetAttachmentMetadata"; import { useTranslate } from "@/hooks/useTranslate"; import { FileType, + IAttachmentPayload, StdIncomingAttachmentMessage, StdOutgoingAttachmentMessage, } from "@/types/message.types"; interface AttachmentInterface { + name?: string; url?: string; } @@ -70,17 +72,23 @@ const componentMap: { [key in FileType]: FC } = { const { t } = useTranslate(); return ( -
- {t("label.attachment")}: + + + {props.name} + -
+ ); }, [FileType.video]: ({ url }: AttachmentInterface) => ( @@ -91,23 +99,43 @@ const componentMap: { [key in FileType]: FC } = { [FileType.unknown]: ({ url }: AttachmentInterface) => <>Unknown Type:{url}, }; -export const AttachmentViewer = (props: { +export const MessageAttachmentViewer = ({ + attachment, +}: { + attachment: IAttachmentPayload; +}) => { + const metadata = useGetAttachmentMetadata(attachment.payload); + const AttachmentViewerForType = componentMap[attachment.type]; + + if (!metadata) { + return <>No attachment to display; + } + + return ; +}; + +export const MessageAttachmentsViewer = (props: { message: StdIncomingAttachmentMessage | StdOutgoingAttachmentMessage; }) => { const message = props.message; - const getUrl = useGetAttachmentUrl(); - // if the attachment is an array show a 4x4 grid with a +{number of remaining attachment} and open a modal to show the list of attachments // Remark: Messenger doesn't send multiple attachments when user sends multiple at once, it only relays the first one to Hexabot // TODO: Implenent this + if (!message.attachment) { return <>No attachment to display; - } else if (Array.isArray(message.attachment)) { - return <>Not yet Implemented; } - const AttachmentViewerForType = componentMap[message.attachment.type]; - const url = getUrl(message.attachment?.payload); + const attachments = Array.isArray(message.attachment) + ? message.attachment + : [message.attachment]; - return ; + return attachments.map((attachment, idx) => { + return ( + + ); + }); }; diff --git a/frontend/src/components/inbox/components/Carousel.tsx b/frontend/src/components/inbox/components/Carousel.tsx index eb0c732f..d1e57c40 100644 --- a/frontend/src/components/inbox/components/Carousel.tsx +++ b/frontend/src/components/inbox/components/Carousel.tsx @@ -21,7 +21,7 @@ import { } from "@mui/material"; import { forwardRef, useCallback, useEffect, useRef, useState } from "react"; -import { useGetAttachmentUrl } from "@/hooks/useGetAttachmentUrl"; +import { useGetAttachmentMetadata } from "@/hooks/useGetAttachmentMetadata"; import { AnyButton as ButtonType, OutgoingPopulatedListMessage, @@ -190,7 +190,7 @@ const ListCard = forwardRef< buttons: ButtonType[]; } >(function ListCardRef(props, ref) { - const getUrl = useGetAttachmentUrl(); + const metadata = useGetAttachmentMetadata(props.content.image_url?.payload); return ( - {props.content.image_url ? ( + {metadata ? ( diff --git a/frontend/src/components/inbox/helpers/mapMessages.tsx b/frontend/src/components/inbox/helpers/mapMessages.tsx index c6d07c16..fd027e68 100644 --- a/frontend/src/components/inbox/helpers/mapMessages.tsx +++ b/frontend/src/components/inbox/helpers/mapMessages.tsx @@ -1,11 +1,12 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ + import { Message, MessageModel } from "@chatscope/chat-ui-kit-react"; import MenuRoundedIcon from "@mui/icons-material/MenuRounded"; import ReplyIcon from "@mui/icons-material/Reply"; @@ -17,7 +18,7 @@ import { EntityType } from "@/services/types"; import { IMessage, IMessageFull } from "@/types/message.types"; import { buildURL } from "@/utils/URL"; -import { AttachmentViewer } from "../components/AttachmentViewer"; +import { MessageAttachmentsViewer } from "../components/AttachmentViewer"; import { Carousel } from "../components/Carousel"; function hasSameSender( @@ -110,7 +111,7 @@ export function getMessageContent( if ("attachment" in message) { content.push( - + , ); } diff --git a/frontend/src/hooks/useGetAttachmentMetadata.ts b/frontend/src/hooks/useGetAttachmentMetadata.ts new file mode 100644 index 00000000..8f669d0e --- /dev/null +++ b/frontend/src/hooks/useGetAttachmentMetadata.ts @@ -0,0 +1,52 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { EntityType } from "@/services/types"; +import { TAttachmentForeignKey } from "@/types/message.types"; +import { + extractFilenameFromUrl, + getAttachmentDownloadUrl, +} from "@/utils/attachment"; + +import { useGet } from "./crud/useGet"; +import { useConfig } from "./useConfig"; + +export const useGetAttachmentMetadata = ( + attachmentPayload?: TAttachmentForeignKey, +) => { + const { apiUrl } = useConfig(); + const { data: attachment } = useGet( + attachmentPayload?.id || "", + { + entity: EntityType.ATTACHMENT, + }, + { + enabled: !!attachmentPayload?.id, + }, + ); + + if (!attachmentPayload) { + return null; + } + + if (attachment) { + return { + name: attachmentPayload.id + ? attachment.name + : extractFilenameFromUrl(attachment.url), + url: getAttachmentDownloadUrl(apiUrl, attachment), + }; + } + + const url = getAttachmentDownloadUrl(apiUrl, attachmentPayload); + + return { + name: extractFilenameFromUrl(url || "/#"), + url, + }; +}; diff --git a/frontend/src/hooks/useGetAttachmentUrl.ts b/frontend/src/hooks/useGetAttachmentUrl.ts deleted file mode 100644 index c3bfcfac..00000000 --- a/frontend/src/hooks/useGetAttachmentUrl.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright © 2025 Hexastack. All rights reserved. - * - * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: - * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. - * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). - */ - -import { AttachmentForeignKey } from "@/types/message.types"; -import { getAttachmentDownloadUrl } from "@/utils/attachment"; - -import { useConfig } from "./useConfig"; - -export const useGetAttachmentUrl = () => { - const { apiUrl } = useConfig(); - - return (attachment: AttachmentForeignKey) => { - return getAttachmentDownloadUrl(apiUrl, attachment); - }; -}; diff --git a/frontend/src/types/message.types.ts b/frontend/src/types/message.types.ts index 5e0d4cf0..f485db36 100644 --- a/frontend/src/types/message.types.ts +++ b/frontend/src/types/message.types.ts @@ -42,7 +42,7 @@ export enum FileType { } // Attachments -export interface AttachmentAttrs { +export interface IAttachmentAttrs { name: string; type: string; size: number; @@ -51,15 +51,15 @@ export interface AttachmentAttrs { url?: string; } -export type AttachmentForeignKey = { +export type TAttachmentForeignKey = { id: string | null; /** @deprecated use id instead */ url?: string; }; -export interface AttachmentPayload { +export interface IAttachmentPayload { type: FileType; - payload: AttachmentForeignKey; + payload: TAttachmentForeignKey; } // Content @@ -95,7 +95,7 @@ export type Payload = } | { type: PayloadType.attachments; - attachments: AttachmentPayload; + attachments: IAttachmentPayload; }; export enum QuickReplyType { @@ -164,7 +164,7 @@ export type StdOutgoingListMessage = { }; export type StdOutgoingAttachmentMessage = { // Stored in DB as `AttachmentPayload`, `Attachment` when populated for channels relaying - attachment: AttachmentPayload; + attachment: IAttachmentPayload; quickReplies?: StdQuickReply[]; }; @@ -187,7 +187,7 @@ export type StdIncomingLocationMessage = { export type StdIncomingAttachmentMessage = { type: PayloadType.attachments; serialized_text: string; - attachment: AttachmentPayload | AttachmentPayload[]; + attachment: IAttachmentPayload | IAttachmentPayload[]; }; export type StdPluginMessage = { diff --git a/frontend/src/utils/attachment.ts b/frontend/src/utils/attachment.ts index 59c2ba8b..93254491 100644 --- a/frontend/src/utils/attachment.ts +++ b/frontend/src/utils/attachment.ts @@ -7,7 +7,8 @@ */ -import { AttachmentForeignKey, FileType } from "@/types/message.types"; +import { IAttachment } from "@/types/attachment.types"; +import { FileType, TAttachmentForeignKey } from "@/types/message.types"; import { buildURL } from "./URL"; @@ -40,9 +41,31 @@ export function getFileType(mimeType: string): FileType { export function getAttachmentDownloadUrl( baseUrl: string, - attachment: AttachmentForeignKey, + attachment: TAttachmentForeignKey | IAttachment, ) { return "id" in attachment && attachment.id ? buildURL(baseUrl, `/attachment/download/${attachment.id}`) : attachment.url; } + +export function extractFilenameFromUrl(url: string) { + try { + // Parse the URL to ensure it is valid + const parsedUrl = new URL(url); + // Extract the pathname (part after the domain) + const pathname = parsedUrl.pathname; + // Extract the last segment of the pathname + const filename = pathname.substring(pathname.lastIndexOf("/") + 1); + + // Check if a valid filename exists + if (filename && filename.includes(".")) { + return filename; + } + + // If no valid filename, return the full URL + return url; + } catch (error) { + // If the URL is invalid, return the input as-is + return url; + } +} \ No newline at end of file