mirror of
https://github.com/hexastack/hexabot
synced 2025-06-26 18:27:28 +00:00
fix: attachment issues wip
This commit is contained in:
parent
208ddd411f
commit
33cc3f713d
BIN
api/assets/hexavatar.png
Normal file
BIN
api/assets/hexavatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -133,6 +133,7 @@ export class AttachmentController extends BaseController<Attachment> {
|
|||||||
access = AttachmentAccess.Public,
|
access = AttachmentAccess.Public,
|
||||||
}: AttachmentContextParamDto,
|
}: AttachmentContextParamDto,
|
||||||
): Promise<Attachment[]> {
|
): Promise<Attachment[]> {
|
||||||
|
debugger;
|
||||||
if (!files || !Array.isArray(files?.file) || files.file.length === 0) {
|
if (!files || !Array.isArray(files?.file) || files.file.length === 0) {
|
||||||
throw new BadRequestException('No file was selected');
|
throw new BadRequestException('No file was selected');
|
||||||
}
|
}
|
||||||
|
@ -6,27 +6,74 @@
|
|||||||
* 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 fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import { Readable, Stream } from 'stream';
|
import { Readable, Stream } from 'stream';
|
||||||
|
|
||||||
import { Injectable, Optional, StreamableFile } from '@nestjs/common';
|
import {
|
||||||
|
Injectable,
|
||||||
|
OnApplicationBootstrap,
|
||||||
|
Optional,
|
||||||
|
StreamableFile,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
|
||||||
import { HelperService } from '@/helper/helper.service';
|
import { HelperService } from '@/helper/helper.service';
|
||||||
import { HelperType } from '@/helper/types';
|
import { HelperType } from '@/helper/types';
|
||||||
|
import { SettingService } from '@/setting/services/setting.service';
|
||||||
import { BaseService } from '@/utils/generics/base-service';
|
import { BaseService } from '@/utils/generics/base-service';
|
||||||
|
|
||||||
import { AttachmentMetadataDto } from '../dto/attachment.dto';
|
import { AttachmentMetadataDto } from '../dto/attachment.dto';
|
||||||
import { AttachmentRepository } from '../repositories/attachment.repository';
|
import { AttachmentRepository } from '../repositories/attachment.repository';
|
||||||
import { Attachment } from '../schemas/attachment.schema';
|
import { Attachment } from '../schemas/attachment.schema';
|
||||||
|
import {
|
||||||
|
AttachmentAccess,
|
||||||
|
AttachmentCreatedByRef,
|
||||||
|
AttachmentResourceRef,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AttachmentService extends BaseService<Attachment> {
|
export class AttachmentService
|
||||||
|
extends BaseService<Attachment>
|
||||||
|
implements OnApplicationBootstrap
|
||||||
|
{
|
||||||
constructor(
|
constructor(
|
||||||
readonly repository: AttachmentRepository,
|
readonly repository: AttachmentRepository,
|
||||||
@Optional() private readonly helperService: HelperService,
|
@Optional() private readonly helperService: HelperService,
|
||||||
|
private settingService: SettingService,
|
||||||
) {
|
) {
|
||||||
super(repository);
|
super(repository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async onApplicationBootstrap() {
|
||||||
|
debugger;
|
||||||
|
const defaultAttachment = await this.repository.findOne({
|
||||||
|
createdByRef: AttachmentCreatedByRef.System,
|
||||||
|
});
|
||||||
|
if (!defaultAttachment) {
|
||||||
|
const imagePath = path.join(process.cwd(), 'assets', 'hexavatar.png');
|
||||||
|
// console.log({ imagePath });
|
||||||
|
const imageBuffer = fs.readFileSync(imagePath);
|
||||||
|
const result = await this.store(imageBuffer, {
|
||||||
|
access: AttachmentAccess.Public,
|
||||||
|
name: 'hexavatar.png',
|
||||||
|
createdBy: 'system',
|
||||||
|
size: imageBuffer.length,
|
||||||
|
type: 'png',
|
||||||
|
createdByRef: AttachmentCreatedByRef.System,
|
||||||
|
resourceRef: AttachmentResourceRef.SettingAttachment,
|
||||||
|
channel: {
|
||||||
|
'web-channel': '',
|
||||||
|
'console-channel': '',
|
||||||
|
'discord-channel': '',
|
||||||
|
'whatsapp-channel': '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
console.log({ result });
|
||||||
|
console.log({ result });
|
||||||
|
}
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stores a file using the default storage helper and creates an attachment record.
|
* Stores a file using the default storage helper and creates an attachment record.
|
||||||
*
|
*
|
||||||
|
@ -15,6 +15,7 @@ import { Readable, Stream } from 'stream';
|
|||||||
export enum AttachmentCreatedByRef {
|
export enum AttachmentCreatedByRef {
|
||||||
User = 'User',
|
User = 'User',
|
||||||
Subscriber = 'Subscriber',
|
Subscriber = 'Subscriber',
|
||||||
|
System = 'System',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -308,21 +308,20 @@ export default abstract class ChannelHandler<
|
|||||||
* @return A signed URL string for downloading the specified attachment.
|
* @return A signed URL string for downloading the specified attachment.
|
||||||
*/
|
*/
|
||||||
public async getPublicUrl(attachment: AttachmentRef | Attachment) {
|
public async getPublicUrl(attachment: AttachmentRef | Attachment) {
|
||||||
|
const [name, _suffix] = this.getName().split('-');
|
||||||
if ('id' in attachment) {
|
if ('id' in attachment) {
|
||||||
if (!attachment.id) {
|
if (!attachment || !attachment.id) {
|
||||||
throw new TypeError(
|
return buildURL(config.apiBaseUrl, `/webhook/${name}/not-found`);
|
||||||
'Attachment ID is empty, unable to generate public URL.',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const resource = await this.attachmentService.findOne(attachment.id);
|
const resource = await this.attachmentService.findOne(attachment.id);
|
||||||
|
|
||||||
if (!resource) {
|
if (!resource) {
|
||||||
throw new NotFoundException('Unable to find attachment');
|
this.logger.warn('Unable to find attachment sending fallback image');
|
||||||
|
return buildURL(config.apiBaseUrl, `/webhook/${name}/not-found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = this.jwtService.sign({ ...resource }, this.jwtSignOptions);
|
const token = this.jwtService.sign({ ...resource }, this.jwtSignOptions);
|
||||||
const [name, _suffix] = this.getName().split('-');
|
|
||||||
return buildURL(
|
return buildURL(
|
||||||
config.apiBaseUrl,
|
config.apiBaseUrl,
|
||||||
`/webhook/${name}/download/${resource.name}?t=${encodeURIComponent(token)}`,
|
`/webhook/${name}/download/${resource.name}?t=${encodeURIComponent(token)}`,
|
||||||
@ -331,7 +330,9 @@ export default abstract class ChannelHandler<
|
|||||||
// In case the url is external
|
// In case the url is external
|
||||||
return attachment.url;
|
return attachment.url;
|
||||||
} else {
|
} else {
|
||||||
throw new TypeError('Unable to resolve the attachment public URL.');
|
return buildURL(config.apiBaseUrl, `/webhook/${name}/not-found`);
|
||||||
|
|
||||||
|
// throw new TypeError('Unable to resolve the attachment public URL.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,4 +88,10 @@ export class WebhookController {
|
|||||||
this.logger.log('Channel notification : ', req.method, channel);
|
this.logger.log('Channel notification : ', req.method, channel);
|
||||||
return await this.channelService.handle(channel, req, res);
|
return await this.channelService.handle(channel, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Roles('public')
|
||||||
|
@Get(':channel/not-found')
|
||||||
|
async handleNotFound(@Res() res: Response) {
|
||||||
|
return res.status(404).send({ error: 'Not found!' });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,25 @@ export class SettingService extends BaseService<Setting> {
|
|||||||
if (count === 0) {
|
if (count === 0) {
|
||||||
await this.seeder.seed(data);
|
await this.seeder.seed(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// const imagePath = path.join(process.cwd(), 'assets', 'hexavatar.png');
|
||||||
|
|
||||||
|
// const imageBuffer = fs.readFileSync(imagePath);
|
||||||
|
// await this.attachmentService.store(imageBuffer, {
|
||||||
|
// access: AttachmentAccess.Public,
|
||||||
|
// name: 'hexavatar.png',
|
||||||
|
// createdBy: 'system',
|
||||||
|
// size: imageBuffer.length,
|
||||||
|
// type: 'png',
|
||||||
|
// createdByRef: AttachmentCreatedByRef.System,
|
||||||
|
// resourceRef: AttachmentResourceRef.SettingAttachment,
|
||||||
|
// channel: {
|
||||||
|
// 'web-channel': '',
|
||||||
|
// 'console-channel': '',
|
||||||
|
// 'discord-channel': '',
|
||||||
|
// 'whatsapp-channel': '',
|
||||||
|
// },
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -116,7 +116,11 @@
|
|||||||
"select_category": "Select a flow",
|
"select_category": "Select a flow",
|
||||||
"logout_failed": "Something went wrong during logout",
|
"logout_failed": "Something went wrong during logout",
|
||||||
"duplicate_labels_not_allowed": "Duplicate labels are not allowed",
|
"duplicate_labels_not_allowed": "Duplicate labels are not allowed",
|
||||||
"duplicate_block_error": "Something went wrong while duplicating block"
|
"duplicate_block_error": "Something went wrong while duplicating block",
|
||||||
|
"image_error": "Image not found",
|
||||||
|
"file_error": "File not found",
|
||||||
|
"audio_error": "Audio not found",
|
||||||
|
"video_error": "Video not found"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"terms": "Terms of Use",
|
"terms": "Terms of Use",
|
||||||
|
@ -116,7 +116,11 @@
|
|||||||
"select_category": "Sélectionner une catégorie",
|
"select_category": "Sélectionner une catégorie",
|
||||||
"logout_failed": "Une erreur s'est produite lors de la déconnexion",
|
"logout_failed": "Une erreur s'est produite lors de la déconnexion",
|
||||||
"duplicate_labels_not_allowed": "Les étiquettes en double ne sont pas autorisées",
|
"duplicate_labels_not_allowed": "Les étiquettes en double ne sont pas autorisées",
|
||||||
"duplicate_block_error": "Une erreur est survenue lors de la duplication du bloc"
|
"duplicate_block_error": "Une erreur est survenue lors de la duplication du bloc",
|
||||||
|
"image_error": "Image introuvable",
|
||||||
|
"file_error": "Fichier introuvable",
|
||||||
|
"audio_error": "Audio introuvable",
|
||||||
|
"video_error": "Vidéo introuvable"
|
||||||
},
|
},
|
||||||
"menu": {
|
"menu": {
|
||||||
"terms": "Conditions d'utilisation",
|
"terms": "Conditions d'utilisation",
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
import DownloadIcon from "@mui/icons-material/Download";
|
import DownloadIcon from "@mui/icons-material/Download";
|
||||||
import { Box, Button, Typography } from "@mui/material";
|
import { Box, Button, Typography } from "@mui/material";
|
||||||
import { FC } from "react";
|
import { FC, useState } from "react";
|
||||||
|
|
||||||
import { useDialogs } from "@/hooks/useDialogs";
|
import { useDialogs } from "@/hooks/useDialogs";
|
||||||
import { useGetAttachmentMetadata } from "@/hooks/useGetAttachmentMetadata";
|
import { useGetAttachmentMetadata } from "@/hooks/useGetAttachmentMetadata";
|
||||||
@ -30,11 +30,17 @@ interface AttachmentInterface {
|
|||||||
const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
|
const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
|
||||||
[FileType.image]: ({ url }: AttachmentInterface) => {
|
[FileType.image]: ({ url }: AttachmentInterface) => {
|
||||||
const dialogs = useDialogs();
|
const dialogs = useDialogs();
|
||||||
|
const [imageErrored, setImageErrored] = useState(false);
|
||||||
|
const { t } = useTranslate();
|
||||||
|
|
||||||
|
if (imageErrored) {
|
||||||
|
return <p>{t("message.image_error")}</p>;
|
||||||
|
}
|
||||||
if (url)
|
if (url)
|
||||||
return (
|
return (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
|
onError={() => setImageErrored(true)}
|
||||||
width="auto"
|
width="auto"
|
||||||
height={200}
|
height={200}
|
||||||
style={{ objectFit: "contain", cursor: "pointer" }}
|
style={{ objectFit: "contain", cursor: "pointer" }}
|
||||||
@ -59,11 +65,24 @@ const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
[FileType.audio]: (props: AttachmentInterface) => {
|
[FileType.audio]: (props: AttachmentInterface) => {
|
||||||
return <audio controls src={props.url} />;
|
const [audioErrored, setAudioErrored] = useState(false);
|
||||||
|
const { t } = useTranslate();
|
||||||
|
|
||||||
|
if (audioErrored) {
|
||||||
|
return <p>{t("message.video_error")}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<audio controls src={props.url} onError={() => setAudioErrored(true)} />
|
||||||
|
);
|
||||||
},
|
},
|
||||||
[FileType.file]: (props: AttachmentInterface) => {
|
[FileType.file]: (props: AttachmentInterface) => {
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
|
if (!props.url) {
|
||||||
|
return <p>{t("message.file_error")}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Typography
|
<Typography
|
||||||
@ -84,11 +103,20 @@ const componentMap: { [key in FileType]: FC<AttachmentInterface> } = {
|
|||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[FileType.video]: ({ url }: AttachmentInterface) => (
|
[FileType.video]: ({ url }: AttachmentInterface) => {
|
||||||
<video controls width="250">
|
const [videoErrored, setVideoErrored] = useState(false);
|
||||||
<source src={url} />
|
const { t } = useTranslate();
|
||||||
</video>
|
|
||||||
),
|
if (videoErrored) {
|
||||||
|
return <p>{t("message.video_error")}</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<video controls width="250">
|
||||||
|
<source src={url} onError={() => setVideoErrored(true)} />
|
||||||
|
</video>
|
||||||
|
);
|
||||||
|
},
|
||||||
[FileType.unknown]: ({ url }: AttachmentInterface) => <>Unknown Type:{url}</>,
|
[FileType.unknown]: ({ url }: AttachmentInterface) => <>Unknown Type:{url}</>,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -10,6 +10,15 @@
|
|||||||
video {
|
video {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
.error-message {
|
||||||
|
margin: 0;
|
||||||
|
// padding: 10px 0 10px 10px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: #666;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sc-message--content.sent .sc-message--file {
|
.sc-message--content.sent .sc-message--file {
|
||||||
@ -64,3 +73,13 @@
|
|||||||
.sc-message--content.received .sc-message--file a:hover {
|
.sc-message--content.received .sc-message--file a:hover {
|
||||||
color: #0c0c0c;
|
color: #0c0c0c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// .error-message {
|
||||||
|
// margin: 0;
|
||||||
|
// padding: 10px 0;
|
||||||
|
// text-align: center;
|
||||||
|
// color: #666;
|
||||||
|
// font-style: italic;
|
||||||
|
// width: 100%;
|
||||||
|
// display: block;
|
||||||
|
// }
|
||||||
|
@ -6,13 +6,12 @@
|
|||||||
* 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 React from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import { useTranslation } from "../../hooks/useTranslation";
|
import { useTranslation } from "../../hooks/useTranslation";
|
||||||
import { useColors } from "../../providers/ColorProvider";
|
import { useColors } from "../../providers/ColorProvider";
|
||||||
import { Direction, TMessage } from "../../types/message.types";
|
import { Direction, TMessage } from "../../types/message.types";
|
||||||
import FileIcon from "../icons/FileIcon";
|
import FileIcon from "../icons/FileIcon";
|
||||||
|
|
||||||
import "./FileMessage.scss";
|
import "./FileMessage.scss";
|
||||||
|
|
||||||
interface FileMessageProps {
|
interface FileMessageProps {
|
||||||
@ -23,11 +22,17 @@ const FileMessage: React.FC<FileMessageProps> = ({ message }) => {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { colors: allColors } = useColors();
|
const { colors: allColors } = useColors();
|
||||||
const colors = allColors[message.direction || Direction.received];
|
const colors = allColors[message.direction || Direction.received];
|
||||||
|
const [videoErrored, setVideoErrored] = useState(false);
|
||||||
|
const [audioErrored, setAudioErrored] = useState(false);
|
||||||
|
const [fileErrored, setFileErrored] = useState(false);
|
||||||
|
const [imageErrored, setImageErrored] = useState(false);
|
||||||
|
|
||||||
if (!("type" in message.data)) {
|
if (!("type" in message.data)) {
|
||||||
throw new Error("Unable to detect type for file message");
|
throw new Error("Unable to detect type for file message");
|
||||||
}
|
}
|
||||||
|
if (message.data.type === "unknown") {
|
||||||
|
return <p className="error-message">unknown file type</p>;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
message.data &&
|
message.data &&
|
||||||
message.data.type !== "image" &&
|
message.data.type !== "image" &&
|
||||||
@ -35,7 +40,7 @@ const FileMessage: React.FC<FileMessageProps> = ({ message }) => {
|
|||||||
message.data.type !== "video" &&
|
message.data.type !== "video" &&
|
||||||
message.data.type !== "file"
|
message.data.type !== "file"
|
||||||
) {
|
) {
|
||||||
throw new Error("Uknown type for file message");
|
throw new Error("Unknown type for file message");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -48,23 +53,60 @@ const FileMessage: React.FC<FileMessageProps> = ({ message }) => {
|
|||||||
>
|
>
|
||||||
{message.data.type === "image" && (
|
{message.data.type === "image" && (
|
||||||
<div className="sc-message--file-icon">
|
<div className="sc-message--file-icon">
|
||||||
<img src={message.data.url || ""} className="sc-image" alt="File" />
|
{imageErrored ? (
|
||||||
|
<p
|
||||||
|
className="error-message"
|
||||||
|
style={{
|
||||||
|
backgroundColor: colors.bg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("messages.file_message.image_error")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
onError={() => setImageErrored(true)}
|
||||||
|
src={message?.data?.url}
|
||||||
|
className="sc-image"
|
||||||
|
alt="File"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message.data.type === "audio" && (
|
{message.data.type === "audio" && (
|
||||||
<div className="sc-message--file-audio">
|
<div className="sc-message--file-audio">
|
||||||
<audio controls>
|
{audioErrored ? (
|
||||||
<source src={message.data.url} />
|
<p
|
||||||
{t("messages.file_message.browser_audio_unsupport")}
|
className="error-message"
|
||||||
</audio>
|
style={{
|
||||||
|
backgroundColor: colors.bg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("messages.file_message.audio_error")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<audio controls onError={() => setAudioErrored(true)}>
|
||||||
|
<source src={message.data.url} />
|
||||||
|
</audio>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message.data.type === "video" && (
|
{message.data.type === "video" && (
|
||||||
<div className="sc-message--file-video">
|
<div className="sc-message--file-video">
|
||||||
<video controls width="100%">
|
{videoErrored ? (
|
||||||
<source src={message.data.url} />
|
<p
|
||||||
{t("messages.file_message.browser_video_unsupport")}
|
className="error-message"
|
||||||
</video>
|
style={{
|
||||||
|
backgroundColor: colors.bg,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t("messages.file_message.video_error")}
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<video controls width="100%" onError={() => setVideoErrored(true)}>
|
||||||
|
<source src={message.data.url} />
|
||||||
|
{t("messages.file_message.browser_video_unsupport")}
|
||||||
|
</video>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{message.data.type === "file" && (
|
{message.data.type === "file" && (
|
||||||
@ -75,15 +117,22 @@ const FileMessage: React.FC<FileMessageProps> = ({ message }) => {
|
|||||||
backgroundColor: colors.bg,
|
backgroundColor: colors.bg,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<a
|
{!message?.data?.url ||
|
||||||
href={message.data.url ? message.data.url : "#"}
|
message?.data?.url?.includes("webhook/download/not-found") ? (
|
||||||
target="_blank"
|
<p className="error-message" style={{ padding: 0 }}>
|
||||||
rel="noopener noreferrer"
|
{t("messages.file_message.file_error")}
|
||||||
download
|
</p>
|
||||||
>
|
) : (
|
||||||
<FileIcon />
|
<a
|
||||||
{t("messages.file_message.download")}
|
href={message.data.url ? message.data.url : "#"}
|
||||||
</a>
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
download
|
||||||
|
>
|
||||||
|
<FileIcon />
|
||||||
|
{t("messages.file_message.download")}
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -17,7 +17,11 @@
|
|||||||
"browser_video_unsupport": "Browser does not support the video element.",
|
"browser_video_unsupport": "Browser does not support the video element.",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"unsupported_file_type": "This file type is not supported.",
|
"unsupported_file_type": "This file type is not supported.",
|
||||||
"unsupported_file_size": "This file size is not supported."
|
"unsupported_file_size": "This file size is not supported.",
|
||||||
|
"image_error": "Image not found",
|
||||||
|
"file_error": "File not found",
|
||||||
|
"audio_error": "Audio not found",
|
||||||
|
"video_error": "Video not found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,7 +17,11 @@
|
|||||||
"browser_video_unsupport": "Le navigateur ne prend pas en charge l'élément vidéo.",
|
"browser_video_unsupport": "Le navigateur ne prend pas en charge l'élément vidéo.",
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
"unsupported_file_type": "Ce type de fichier n'est pas pris en charge.",
|
"unsupported_file_type": "Ce type de fichier n'est pas pris en charge.",
|
||||||
"unsupported_file_size": "Cette taille de fichier n'est pas prise en charge."
|
"unsupported_file_size": "Cette taille de fichier n'est pas prise en charge.",
|
||||||
|
"image_error": "Image introuvable",
|
||||||
|
"file_error": "Fichier introuvable",
|
||||||
|
"audio_error": "Audio introuvable",
|
||||||
|
"video_error": "Vidéo introuvable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user