From 369530b0193eb344e0bf83776ff1e330113ebb88 Mon Sep 17 00:00:00 2001 From: Aman Harwara Date: Fri, 3 Jun 2022 16:18:55 +0530 Subject: [PATCH 01/24] feat: add file view --- .../Components/FilePreview/FilePreview.tsx | 81 ++++++++++++++ .../FilePreview/FilePreviewError.tsx | 62 +++++++++++ .../FilePreview/PreviewComponent.tsx | 50 +++++++++ .../Components/FilePreview/TextPreview.tsx | 26 +++++ .../FilePreview/isFilePreviewable.ts | 15 +++ .../Components/FileView/FileView.tsx | 101 ++++++++++++++++++ .../NoteGroupView/NoteGroupView.tsx | 40 +++++-- 7 files changed, 367 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/Components/FilePreview/FilePreview.tsx create mode 100644 app/assets/javascripts/Components/FilePreview/FilePreviewError.tsx create mode 100644 app/assets/javascripts/Components/FilePreview/PreviewComponent.tsx create mode 100644 app/assets/javascripts/Components/FilePreview/TextPreview.tsx create mode 100644 app/assets/javascripts/Components/FilePreview/isFilePreviewable.ts create mode 100644 app/assets/javascripts/Components/FileView/FileView.tsx diff --git a/app/assets/javascripts/Components/FilePreview/FilePreview.tsx b/app/assets/javascripts/Components/FilePreview/FilePreview.tsx new file mode 100644 index 00000000000..bbea2383bd7 --- /dev/null +++ b/app/assets/javascripts/Components/FilePreview/FilePreview.tsx @@ -0,0 +1,81 @@ +import { WebApplication } from '@/Application/Application' +import { concatenateUint8Arrays } from '@/Utils' +import { FileItem } from '@standardnotes/snjs' +import { useEffect, useMemo, useState } from 'react' +import FilePreviewError from './FilePreviewError' +import { isFileTypePreviewable } from './isFilePreviewable' +import PreviewComponent from './PreviewComponent' + +type Props = { + application: WebApplication + file: FileItem +} + +const FilePreview = ({ file, application }: Props) => { + const isFilePreviewable = useMemo(() => { + return isFileTypePreviewable(file.mimeType) + }, [file.mimeType]) + + const [isDownloading, setIsDownloading] = useState(false) + const [downloadProgress, setDownloadProgress] = useState(0) + const [downloadedBytes, setDownloadedBytes] = useState() + + useEffect(() => { + if (!isFilePreviewable) { + setIsDownloading(false) + setDownloadProgress(0) + setDownloadedBytes(undefined) + return + } + + const downloadFileForPreview = async () => { + if (downloadedBytes) { + return + } + + setIsDownloading(true) + + try { + const chunks: Uint8Array[] = [] + setDownloadProgress(0) + await application.files.downloadFile(file, async (decryptedChunk, progress) => { + chunks.push(decryptedChunk) + if (progress) { + setDownloadProgress(Math.round(progress.percentComplete)) + } + }) + const finalDecryptedBytes = concatenateUint8Arrays(chunks) + setDownloadedBytes(finalDecryptedBytes) + } catch (error) { + console.error(error) + } finally { + setIsDownloading(false) + } + } + + void downloadFileForPreview() + }, [application.files, downloadedBytes, file, isFilePreviewable]) + + return isDownloading ? ( +
+
+
+
{downloadProgress}%
+
+ Loading file... +
+ ) : downloadedBytes ? ( + + ) : ( + { + setDownloadedBytes(undefined) + }} + isFilePreviewable={isFilePreviewable} + /> + ) +} + +export default FilePreview diff --git a/app/assets/javascripts/Components/FilePreview/FilePreviewError.tsx b/app/assets/javascripts/Components/FilePreview/FilePreviewError.tsx new file mode 100644 index 00000000000..ca0013af6d5 --- /dev/null +++ b/app/assets/javascripts/Components/FilePreview/FilePreviewError.tsx @@ -0,0 +1,62 @@ +import { WebApplication } from '@/Application/Application' +import { NoPreviewIllustration } from '@standardnotes/icons' +import { FileItem } from '@standardnotes/snjs' +import Button from '../Button/Button' + +type Props = { + application: WebApplication + file: FileItem + isFilePreviewable: boolean + tryAgainCallback: () => void +} + +const FilePreviewError = ({ application, file, isFilePreviewable, tryAgainCallback }: Props) => { + return ( +
+ +
This file can't be previewed.
+ {isFilePreviewable ? ( + <> +
+ There was an error loading the file. Try again, or download the file and open it using another application. +
+
+ + +
+ + ) : ( + <> +
+ To view this file, download it and open it using another application. +
+ + + )} +
+ ) +} + +export default FilePreviewError diff --git a/app/assets/javascripts/Components/FilePreview/PreviewComponent.tsx b/app/assets/javascripts/Components/FilePreview/PreviewComponent.tsx new file mode 100644 index 00000000000..ad8526df81d --- /dev/null +++ b/app/assets/javascripts/Components/FilePreview/PreviewComponent.tsx @@ -0,0 +1,50 @@ +import { FileItem } from '@standardnotes/snjs' +import { FunctionComponent, useEffect, useState } from 'react' +import ImagePreview from './ImagePreview' +import { PreviewableTextFileTypes } from './isFilePreviewable' +import TextPreview from './TextPreview' + +type Props = { + file: FileItem + bytes: Uint8Array +} + +const PreviewComponent: FunctionComponent = ({ file, bytes }) => { + const [objectUrl, setObjectUrl] = useState('') + + useEffect(() => { + if (!objectUrl) { + setObjectUrl( + URL.createObjectURL( + new Blob([bytes], { + type: file.mimeType, + }), + ), + ) + } + + return () => { + URL.revokeObjectURL(objectUrl) + } + }, [bytes, file.mimeType, objectUrl]) + + if (file.mimeType.startsWith('image/')) { + return + } + + if (file.mimeType.startsWith('video/')) { + return