import React, { ReactNode, useCallback, useState } from 'react'; import { Box, Button, Icon, Icons, Modal, Overlay, OverlayBackdrop, OverlayCenter, Spinner, Text, Tooltip, TooltipProvider, as, } from 'folds'; import FileSaver from 'file-saver'; import { EncryptedAttachmentInfo } from 'browser-encrypt-attachment'; import FocusTrap from 'focus-trap-react'; import { IFileInfo } from '../../../../types/matrix/common'; import { AsyncStatus, useAsyncCallback } from '../../../hooks/useAsyncCallback'; import { useMatrixClient } from '../../../hooks/useMatrixClient'; import { bytesToSize } from '../../../utils/common'; import { READABLE_EXT_TO_MIME_TYPE, READABLE_TEXT_MIME_TYPES, getFileNameExt, mimeTypeToExt, } from '../../../utils/mimeTypes'; import * as css from './style.css'; import { stopPropagation } from '../../../utils/keyboard'; import { decryptFile, downloadEncryptedMedia, downloadMedia, mxcUrlToHttp, } from '../../../utils/matrix'; import { useMediaAuthentication } from '../../../hooks/useMediaAuthentication'; const renderErrorButton = (retry: () => void, text: string) => ( Failed to load file! } position="Top" align="Center" > {(triggerRef) => ( )} ); type RenderTextViewerProps = { name: string; text: string; langName: string; requestClose: () => void; }; type ReadTextFileProps = { body: string; mimeType: string; url: string; encInfo?: EncryptedAttachmentInfo; renderViewer: (props: RenderTextViewerProps) => ReactNode; }; export function ReadTextFile({ body, mimeType, url, encInfo, renderViewer }: ReadTextFileProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const [textViewer, setTextViewer] = useState(false); const [textState, loadText] = 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 text = fileContent.text(); setTextViewer(true); return text; }, [mx, useAuthentication, mimeType, encInfo, url]) ); return ( <> {textState.status === AsyncStatus.Success && ( }> setTextViewer(false), clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} > evt.stopPropagation()} > {renderViewer({ name: body, text: textState.data, langName: READABLE_TEXT_MIME_TYPES.includes(mimeType) ? mimeTypeToExt(mimeType) : mimeTypeToExt(READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)] ?? mimeType), requestClose: () => setTextViewer(false), })} )} {textState.status === AsyncStatus.Error ? ( renderErrorButton(loadText, 'Open File') ) : ( )} ); } type RenderPdfViewerProps = { name: string; src: string; requestClose: () => void; }; export type ReadPdfFileProps = { body: string; mimeType: string; url: string; encInfo?: EncryptedAttachmentInfo; renderViewer: (props: RenderPdfViewerProps) => ReactNode; }; export function ReadPdfFile({ body, mimeType, url, encInfo, renderViewer }: ReadPdfFileProps) { const mx = useMatrixClient(); const useAuthentication = useMediaAuthentication(); const [pdfViewer, setPdfViewer] = useState(false); const [pdfState, loadPdf] = useAsyncCallback( useCallback(async () => { const mediaUrl = mxcUrlToHttp(mx, url, useAuthentication) ?? url; const fileContent = encInfo ? await downloadEncryptedMedia(mediaUrl, (encBuf) => decryptFile(encBuf, mimeType, encInfo)) : await downloadMedia(mediaUrl); setPdfViewer(true); return URL.createObjectURL(fileContent); }, [mx, url, useAuthentication, mimeType, encInfo]) ); return ( <> {pdfState.status === AsyncStatus.Success && ( }> setPdfViewer(false), clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} > evt.stopPropagation()} > {renderViewer({ name: body, src: pdfState.data, requestClose: () => setPdfViewer(false), })} )} {pdfState.status === AsyncStatus.Error ? ( renderErrorButton(loadPdf, 'Open PDF') ) : ( )} ); } export type DownloadFileProps = { body: string; mimeType: string; url: string; info: IFileInfo; encInfo?: EncryptedAttachmentInfo; }; export function DownloadFile({ body, mimeType, url, info, encInfo }: DownloadFileProps) { 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, body); return fileURL; }, [mx, url, useAuthentication, mimeType, encInfo, body]) ); return downloadState.status === AsyncStatus.Error ? ( renderErrorButton(download, `Retry Download (${bytesToSize(info.size ?? 0)})`) ) : ( ); } type FileContentProps = { body: string; mimeType: string; renderAsTextFile: () => ReactNode; renderAsPdfFile: () => ReactNode; }; export const FileContent = as<'div', FileContentProps>( ({ body, mimeType, renderAsTextFile, renderAsPdfFile, children, ...props }, ref) => ( {(READABLE_TEXT_MIME_TYPES.includes(mimeType) || READABLE_EXT_TO_MIME_TYPE[getFileNameExt(body)]) && renderAsTextFile()} {mimeType === 'application/pdf' && renderAsPdfFile()} {children} ) );