From d8009978e5f931b76ff23f3176fc5df09462132b Mon Sep 17 00:00:00 2001
From: Ajay Bura <32841439+ajbura@users.noreply.github.com>
Date: Thu, 6 Mar 2025 14:29:23 +1100
Subject: [PATCH] add option to download audio/video file (#2253)
* add option to download audio file
* add button to download video
---
src/app/components/message/FileHeader.tsx | 79 ++++++++++++++++---
.../components/message/MsgTypeRenderers.tsx | 32 +++++++-
2 files changed, 99 insertions(+), 12 deletions(-)
diff --git a/src/app/components/message/FileHeader.tsx b/src/app/components/message/FileHeader.tsx
index 947be90e..0248862d 100644
--- a/src/app/components/message/FileHeader.tsx
+++ b/src/app/components/message/FileHeader.tsx
@@ -1,22 +1,81 @@
-import { Badge, Box, Text, as, toRem } from 'folds';
-import React from 'react';
+import { Badge, Box, Icon, IconButton, Icons, Spinner, Text, as, toRem } from 'folds';
+import React, { ReactNode, useCallback } from 'react';
+import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment';
+import FileSaver from 'file-saver';
import { mimeTypeToExt } from '../../utils/mimeTypes';
+import { useMatrixClient } from '../../hooks/useMatrixClient';
+import { useMediaAuthentication } from '../../hooks/useMediaAuthentication';
+import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
+import {
+ decryptFile,
+ downloadEncryptedMedia,
+ downloadMedia,
+ mxcUrlToHttp,
+} from '../../utils/matrix';
const badgeStyles = { maxWidth: toRem(100) };
+type FileDownloadButtonProps = {
+ filename: string;
+ url: string;
+ mimeType: string;
+ encInfo?: EncryptedAttachmentInfo;
+};
+export function FileDownloadButton({ filename, url, mimeType, encInfo }: FileDownloadButtonProps) {
+ const mx = useMatrixClient();
+ const useAuthentication = useMediaAuthentication();
+
+ const [downloadState, download] = useAsyncCallback(
+ useCallback(async () => {
+ const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url;
+ const fileContent = encInfo
+ ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo))
+ : await downloadMedia(mediaUrl);
+
+ const fileURL = URL.createObjectURL(fileContent);
+ FileSaver.saveAs(fileURL, filename);
+ return fileURL;
+ }, [mx, url, useAuthentication, mimeType, encInfo, filename])
+ );
+
+ const downloading = downloadState.status === AsyncStatus.Loading;
+ const hasError = downloadState.status === AsyncStatus.Error;
+ return (
+
+ {downloading ? (
+
+ ) : (
+
+ )}
+
+ );
+}
+
export type FileHeaderProps = {
body: string;
mimeType: string;
+ after?: ReactNode;
};
-export const FileHeader = as<'div', FileHeaderProps>(({ body, mimeType, ...props }, ref) => (
+export const FileHeader = as<'div', FileHeaderProps>(({ body, mimeType, after, ...props }, ref) => (
-
-
- {mimeTypeToExt(mimeType)}
+
+
+
+ {mimeTypeToExt(mimeType)}
+
+
+
+
+
+ {body}
-
-
- {body}
-
+
+ {after}
));
diff --git a/src/app/components/message/MsgTypeRenderers.tsx b/src/app/components/message/MsgTypeRenderers.tsx
index 287a5ca4..cea5220b 100644
--- a/src/app/components/message/MsgTypeRenderers.tsx
+++ b/src/app/components/message/MsgTypeRenderers.tsx
@@ -28,7 +28,7 @@ import {
import { FALLBACK_MIMETYPE, getBlobSafeMimeType } from '../../utils/mimeTypes';
import { parseGeoUri, scaleYDimension } from '../../utils/common';
import { Attachment, AttachmentBox, AttachmentContent, AttachmentHeader } from './attachment';
-import { FileHeader } from './FileHeader';
+import { FileHeader, FileDownloadButton } from './FileHeader';
export function MBadEncrypted() {
return (
@@ -243,8 +243,24 @@ export function MVideo({ content, renderAsFile, renderVideoContent, outlined }:
const height = scaleYDimension(videoInfo.w || 400, 400, videoInfo.h || 400);
+ const filename = content.filename ?? content.body ?? 'Video';
+
return (
+
+
+ }
+ />
+
;
}
+ const filename = content.filename ?? content.body ?? 'Audio';
return (
-
+
+ }
+ />