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 ( - + + } + />