feat: enhance attachment display in inbox

This commit is contained in:
Mohamed Marrouchi 2025-01-17 08:00:45 +01:00
parent f7363563ad
commit 8d1bb47b2a
7 changed files with 134 additions and 50 deletions

View File

@ -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<AttachmentInterface> } = {
const { t } = useTranslate();
return (
<div>
<span style={{ fontWeight: "bold" }}>{t("label.attachment")}: </span>
<Box>
<Typography
component="span"
className="cs-message__text-content"
mr={2}
>
{props.name}
</Typography>
<Button
href={props.url}
endIcon={<DownloadIcon />}
color="inherit"
variant="text"
variant="contained"
>
{t("button.download")}
</Button>
</div>
</Box>
);
},
[FileType.video]: ({ url }: AttachmentInterface) => (
@ -91,23 +99,43 @@ const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
[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 <AttachmentViewerForType url={metadata.url} name={metadata.name} />;
};
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 <AttachmentViewerForType url={url} />;
return attachments.map((attachment, idx) => {
return (
<MessageAttachmentViewer
key={`${attachment.payload.id}-${idx}`}
attachment={attachment}
/>
);
});
};

View File

@ -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 (
<Card
@ -205,9 +205,9 @@ const ListCard = forwardRef<
ref={ref}
id={"A" + props.id}
>
{props.content.image_url ? (
{metadata ? (
<CardMedia
image={getUrl(props.content.image_url.payload)}
image={metadata.url}
sx={{ height: "185px" }}
title={props.content.title}
/>

View File

@ -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(
<Message.CustomContent>
<AttachmentViewer message={message} />
<MessageAttachmentsViewer message={message} />
</Message.CustomContent>,
);
}

View File

@ -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,
};
};

View File

@ -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);
};
};

View File

@ -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 = {

View File

@ -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;
}
}