mirror of
https://github.com/hexastack/hexabot
synced 2025-01-22 10:35:37 +00:00
feat: enhance attachment display in inbox
This commit is contained in:
parent
f7363563ad
commit
8d1bb47b2a
@ -7,20 +7,22 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
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 { FC } from "react";
|
||||||
|
|
||||||
import { DialogTitle } from "@/app-components/dialogs";
|
import { DialogTitle } from "@/app-components/dialogs";
|
||||||
import { useDialog } from "@/hooks/useDialog";
|
import { useDialog } from "@/hooks/useDialog";
|
||||||
import { useGetAttachmentUrl } from "@/hooks/useGetAttachmentUrl";
|
import { useGetAttachmentMetadata } from "@/hooks/useGetAttachmentMetadata";
|
||||||
import { useTranslate } from "@/hooks/useTranslate";
|
import { useTranslate } from "@/hooks/useTranslate";
|
||||||
import {
|
import {
|
||||||
FileType,
|
FileType,
|
||||||
|
IAttachmentPayload,
|
||||||
StdIncomingAttachmentMessage,
|
StdIncomingAttachmentMessage,
|
||||||
StdOutgoingAttachmentMessage,
|
StdOutgoingAttachmentMessage,
|
||||||
} from "@/types/message.types";
|
} from "@/types/message.types";
|
||||||
|
|
||||||
interface AttachmentInterface {
|
interface AttachmentInterface {
|
||||||
|
name?: string;
|
||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,17 +72,23 @@ const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
|
|||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<Box>
|
||||||
<span style={{ fontWeight: "bold" }}>{t("label.attachment")}: </span>
|
<Typography
|
||||||
|
component="span"
|
||||||
|
className="cs-message__text-content"
|
||||||
|
mr={2}
|
||||||
|
>
|
||||||
|
{props.name}
|
||||||
|
</Typography>
|
||||||
<Button
|
<Button
|
||||||
href={props.url}
|
href={props.url}
|
||||||
endIcon={<DownloadIcon />}
|
endIcon={<DownloadIcon />}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
variant="text"
|
variant="contained"
|
||||||
>
|
>
|
||||||
{t("button.download")}
|
{t("button.download")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[FileType.video]: ({ url }: AttachmentInterface) => (
|
[FileType.video]: ({ url }: AttachmentInterface) => (
|
||||||
@ -91,23 +99,43 @@ const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
|
|||||||
[FileType.unknown]: ({ url }: AttachmentInterface) => <>Unknown Type:{url}</>,
|
[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;
|
message: StdIncomingAttachmentMessage | StdOutgoingAttachmentMessage;
|
||||||
}) => {
|
}) => {
|
||||||
const message = props.message;
|
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
|
// 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
|
// Remark: Messenger doesn't send multiple attachments when user sends multiple at once, it only relays the first one to Hexabot
|
||||||
// TODO: Implenent this
|
// TODO: Implenent this
|
||||||
|
|
||||||
if (!message.attachment) {
|
if (!message.attachment) {
|
||||||
return <>No attachment to display</>;
|
return <>No attachment to display</>;
|
||||||
} else if (Array.isArray(message.attachment)) {
|
|
||||||
return <>Not yet Implemented</>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const AttachmentViewerForType = componentMap[message.attachment.type];
|
const attachments = Array.isArray(message.attachment)
|
||||||
const url = getUrl(message.attachment?.payload);
|
? message.attachment
|
||||||
|
: [message.attachment];
|
||||||
|
|
||||||
return <AttachmentViewerForType url={url} />;
|
return attachments.map((attachment, idx) => {
|
||||||
|
return (
|
||||||
|
<MessageAttachmentViewer
|
||||||
|
key={`${attachment.payload.id}-${idx}`}
|
||||||
|
attachment={attachment}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
@ -21,7 +21,7 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
|
import { forwardRef, useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
import { useGetAttachmentUrl } from "@/hooks/useGetAttachmentUrl";
|
import { useGetAttachmentMetadata } from "@/hooks/useGetAttachmentMetadata";
|
||||||
import {
|
import {
|
||||||
AnyButton as ButtonType,
|
AnyButton as ButtonType,
|
||||||
OutgoingPopulatedListMessage,
|
OutgoingPopulatedListMessage,
|
||||||
@ -190,7 +190,7 @@ const ListCard = forwardRef<
|
|||||||
buttons: ButtonType[];
|
buttons: ButtonType[];
|
||||||
}
|
}
|
||||||
>(function ListCardRef(props, ref) {
|
>(function ListCardRef(props, ref) {
|
||||||
const getUrl = useGetAttachmentUrl();
|
const metadata = useGetAttachmentMetadata(props.content.image_url?.payload);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card
|
<Card
|
||||||
@ -205,9 +205,9 @@ const ListCard = forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
id={"A" + props.id}
|
id={"A" + props.id}
|
||||||
>
|
>
|
||||||
{props.content.image_url ? (
|
{metadata ? (
|
||||||
<CardMedia
|
<CardMedia
|
||||||
image={getUrl(props.content.image_url.payload)}
|
image={metadata.url}
|
||||||
sx={{ height: "185px" }}
|
sx={{ height: "185px" }}
|
||||||
title={props.content.title}
|
title={props.content.title}
|
||||||
/>
|
/>
|
||||||
|
@ -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:
|
* 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.
|
* 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).
|
* 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 { Message, MessageModel } from "@chatscope/chat-ui-kit-react";
|
||||||
import MenuRoundedIcon from "@mui/icons-material/MenuRounded";
|
import MenuRoundedIcon from "@mui/icons-material/MenuRounded";
|
||||||
import ReplyIcon from "@mui/icons-material/Reply";
|
import ReplyIcon from "@mui/icons-material/Reply";
|
||||||
@ -17,7 +18,7 @@ import { EntityType } from "@/services/types";
|
|||||||
import { IMessage, IMessageFull } from "@/types/message.types";
|
import { IMessage, IMessageFull } from "@/types/message.types";
|
||||||
import { buildURL } from "@/utils/URL";
|
import { buildURL } from "@/utils/URL";
|
||||||
|
|
||||||
import { AttachmentViewer } from "../components/AttachmentViewer";
|
import { MessageAttachmentsViewer } from "../components/AttachmentViewer";
|
||||||
import { Carousel } from "../components/Carousel";
|
import { Carousel } from "../components/Carousel";
|
||||||
|
|
||||||
function hasSameSender(
|
function hasSameSender(
|
||||||
@ -110,7 +111,7 @@ export function getMessageContent(
|
|||||||
if ("attachment" in message) {
|
if ("attachment" in message) {
|
||||||
content.push(
|
content.push(
|
||||||
<Message.CustomContent>
|
<Message.CustomContent>
|
||||||
<AttachmentViewer message={message} />
|
<MessageAttachmentsViewer message={message} />
|
||||||
</Message.CustomContent>,
|
</Message.CustomContent>,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
52
frontend/src/hooks/useGetAttachmentMetadata.ts
Normal file
52
frontend/src/hooks/useGetAttachmentMetadata.ts
Normal 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,
|
||||||
|
};
|
||||||
|
};
|
@ -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);
|
|
||||||
};
|
|
||||||
};
|
|
@ -42,7 +42,7 @@ export enum FileType {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Attachments
|
// Attachments
|
||||||
export interface AttachmentAttrs {
|
export interface IAttachmentAttrs {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
size: number;
|
size: number;
|
||||||
@ -51,15 +51,15 @@ export interface AttachmentAttrs {
|
|||||||
url?: string;
|
url?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AttachmentForeignKey = {
|
export type TAttachmentForeignKey = {
|
||||||
id: string | null;
|
id: string | null;
|
||||||
/** @deprecated use id instead */
|
/** @deprecated use id instead */
|
||||||
url?: string;
|
url?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface AttachmentPayload {
|
export interface IAttachmentPayload {
|
||||||
type: FileType;
|
type: FileType;
|
||||||
payload: AttachmentForeignKey;
|
payload: TAttachmentForeignKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Content
|
// Content
|
||||||
@ -95,7 +95,7 @@ export type Payload =
|
|||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
type: PayloadType.attachments;
|
type: PayloadType.attachments;
|
||||||
attachments: AttachmentPayload;
|
attachments: IAttachmentPayload;
|
||||||
};
|
};
|
||||||
|
|
||||||
export enum QuickReplyType {
|
export enum QuickReplyType {
|
||||||
@ -164,7 +164,7 @@ export type StdOutgoingListMessage = {
|
|||||||
};
|
};
|
||||||
export type StdOutgoingAttachmentMessage = {
|
export type StdOutgoingAttachmentMessage = {
|
||||||
// Stored in DB as `AttachmentPayload`, `Attachment` when populated for channels relaying
|
// Stored in DB as `AttachmentPayload`, `Attachment` when populated for channels relaying
|
||||||
attachment: AttachmentPayload;
|
attachment: IAttachmentPayload;
|
||||||
quickReplies?: StdQuickReply[];
|
quickReplies?: StdQuickReply[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -187,7 +187,7 @@ export type StdIncomingLocationMessage = {
|
|||||||
export type StdIncomingAttachmentMessage = {
|
export type StdIncomingAttachmentMessage = {
|
||||||
type: PayloadType.attachments;
|
type: PayloadType.attachments;
|
||||||
serialized_text: string;
|
serialized_text: string;
|
||||||
attachment: AttachmentPayload | AttachmentPayload[];
|
attachment: IAttachmentPayload | IAttachmentPayload[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type StdPluginMessage = {
|
export type StdPluginMessage = {
|
||||||
|
@ -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";
|
import { buildURL } from "./URL";
|
||||||
|
|
||||||
@ -40,9 +41,31 @@ export function getFileType(mimeType: string): FileType {
|
|||||||
|
|
||||||
export function getAttachmentDownloadUrl(
|
export function getAttachmentDownloadUrl(
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
attachment: AttachmentForeignKey,
|
attachment: TAttachmentForeignKey | IAttachment,
|
||||||
) {
|
) {
|
||||||
return "id" in attachment && attachment.id
|
return "id" in attachment && attachment.id
|
||||||
? buildURL(baseUrl, `/attachment/download/${attachment.id}`)
|
? buildURL(baseUrl, `/attachment/download/${attachment.id}`)
|
||||||
: attachment.url;
|
: 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;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user