diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx index bbc75a456..ec3f17918 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesButton.tsx @@ -60,23 +60,29 @@ export const AttachedFilesButton: FunctionComponent = observer( const containerRef = useRef(null) const [closeOnBlur, keepMenuOpen] = useCloseOnBlur(containerRef, setOpen) - const [attachedFilesCount, setAttachedFilesCount] = useState( - note ? application.items.getFilesForNote(note).length : 0, - ) - - const reloadAttachedFilesCount = useCallback(() => { - setAttachedFilesCount(note ? application.items.getFilesForNote(note).length : 0) - }, [application.items, note]) + const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles) + const [allFiles, setAllFiles] = useState([]) + const [attachedFiles, setAttachedFiles] = useState([]) + const attachedFilesCount = attachedFiles.length useEffect(() => { const unregisterFileStream = application.streamItems(ContentType.File, () => { - reloadAttachedFilesCount() + setAllFiles( + application.items + .getItems(ContentType.File) + .sort((a, b) => (a.created_at < b.created_at ? 1 : -1)), + ) + setAttachedFiles( + application.items + .getFilesForNote(note) + .sort((a, b) => (a.created_at < b.created_at ? 1 : -1)), + ) }) return () => { unregisterFileStream() } - }, [application, reloadAttachedFilesCount]) + }, [application, note]) const toggleAttachedFilesMenu = useCallback(async () => { if (!appState.features.isEntitledToFiles) { @@ -213,7 +219,10 @@ export const AttachedFilesButton: FunctionComponent = observer( await renameFile(file, action.payload.name) break case PopoverFileItemActionType.PreviewFile: - filePreviewModal.activate(file) + filePreviewModal.activate( + file, + currentTab === PopoverTabs.AllFiles ? allFiles : attachedFiles, + ) break } @@ -228,7 +237,6 @@ export const AttachedFilesButton: FunctionComponent = observer( } const [isDraggingFiles, setIsDraggingFiles] = useState(false) - const [currentTab, setCurrentTab] = useState(PopoverTabs.AttachedFiles) const dragCounter = useRef(0) const handleDrag = (event: DragEvent) => { @@ -373,12 +381,13 @@ export const AttachedFilesButton: FunctionComponent = observer( )} diff --git a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx index f363a8865..d5e9d9bed 100644 --- a/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx +++ b/app/assets/javascripts/Components/AttachedFilesPopover/AttachedFilesPopover.tsx @@ -1,11 +1,11 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' import { WebApplication } from '@/UIModels/Application' import { AppState } from '@/UIModels/AppState' -import { ContentType, SNFile, SNNote } from '@standardnotes/snjs' +import { SNFile } from '@standardnotes/snjs' import { FilesIllustration } from '@standardnotes/stylekit' import { observer } from 'mobx-react-lite' import { FunctionComponent } from 'preact' -import { StateUpdater, useEffect, useRef, useState } from 'preact/hooks' +import { StateUpdater, useRef, useState } from 'preact/hooks' import { Button } from '@/Components/Button/Button' import { Icon } from '@/Components/Icon' import { PopoverFileItem } from './PopoverFileItem' @@ -19,11 +19,12 @@ export enum PopoverTabs { type Props = { application: WebApplication appState: AppState - currentTab: PopoverTabs + allFiles: SNFile[] + attachedFiles: SNFile[] closeOnBlur: (event: { relatedTarget: EventTarget | null }) => void + currentTab: PopoverTabs handleFileAction: (action: PopoverFileItemAction) => Promise isDraggingFiles: boolean - note: SNNote setCurrentTab: StateUpdater } @@ -31,15 +32,14 @@ export const AttachedFilesPopover: FunctionComponent = observer( ({ application, appState, - currentTab, + allFiles, + attachedFiles, closeOnBlur, + currentTab, handleFileAction, isDraggingFiles, - note, setCurrentTab, }) => { - const [attachedFiles, setAttachedFiles] = useState([]) - const [allFiles, setAllFiles] = useState([]) const [searchQuery, setSearchQuery] = useState('') const searchInputRef = useRef(null) @@ -52,26 +52,6 @@ export const AttachedFilesPopover: FunctionComponent = observer( ) : filesList - useEffect(() => { - const unregisterFileStream = application.streamItems(ContentType.File, () => { - setAttachedFiles( - application.items - .getFilesForNote(note) - .sort((a, b) => (a.created_at < b.created_at ? 1 : -1)), - ) - - setAllFiles( - application.items - .getItems(ContentType.File) - .sort((a, b) => (a.created_at < b.created_at ? 1 : -1)) as SNFile[], - ) - }) - - return () => { - unregisterFileStream() - } - }, [application, note]) - const handleAttachFilesClick = async () => { const uploadedFiles = await appState.files.uploadNewFile() if (!uploadedFiles) { diff --git a/app/assets/javascripts/Components/Files/FilePreviewModal.tsx b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx index 3bc028301..13ee22c4c 100644 --- a/app/assets/javascripts/Components/Files/FilePreviewModal.tsx +++ b/app/assets/javascripts/Components/Files/FilePreviewModal.tsx @@ -11,18 +11,30 @@ import { Icon } from '@/Components/Icon' import { FilePreviewInfoPanel } from './FilePreviewInfoPanel' import { isFileTypePreviewable } from './isFilePreviewable' import { PreviewComponent } from './PreviewComponent' +import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants' +import { useFilePreviewModal } from './FilePreviewModalProvider' +import { KeyboardKey } from '@/Services/IOService' type Props = { application: WebApplication + files: SNFile[] file: SNFile onDismiss: () => void } -export const FilePreviewModal: FunctionComponent = ({ application, file, onDismiss }) => { +export const FilePreviewModal: FunctionComponent = ({ + application, + files, + file, + onDismiss, +}) => { + const context = useFilePreviewModal() + const [objectUrl, setObjectUrl] = useState() const [isFilePreviewable, setIsFilePreviewable] = useState(false) const [isLoadingFile, setIsLoadingFile] = useState(true) const [showFileInfoPanel, setShowFileInfoPanel] = useState(false) + const currentFileIdRef = useRef() const closeButtonRef = useRef(null) const getObjectUrl = useCallback(async () => { @@ -46,6 +58,10 @@ export const FilePreviewModal: FunctionComponent = ({ application, file, } }, [application.files, file]) + useEffect(() => { + setIsLoadingFile(true) + }, [file.uuid]) + useEffect(() => { const isPreviewable = isFileTypePreviewable(file.mimeType) setIsFilePreviewable(isPreviewable) @@ -54,16 +70,48 @@ export const FilePreviewModal: FunctionComponent = ({ application, file, setIsLoadingFile(false) } - if (!objectUrl && isPreviewable) { + if (currentFileIdRef.current !== file.uuid && isPreviewable) { getObjectUrl().catch(console.error) } + currentFileIdRef.current = file.uuid + return () => { if (objectUrl) { URL.revokeObjectURL(objectUrl) } } - }, [file.mimeType, getObjectUrl, objectUrl]) + }, [file, getObjectUrl, objectUrl]) + + const keyDownHandler = (event: KeyboardEvent) => { + if (event.key !== KeyboardKey.Left && event.key !== KeyboardKey.Right) { + return + } + + event.preventDefault() + + const currentFileIndex = files.findIndex((fileFromArray) => fileFromArray.uuid === file.uuid) + + switch (event.key) { + case KeyboardKey.Left: { + const previousFileIndex = + currentFileIndex - 1 >= 0 ? currentFileIndex - 1 : files.length - 1 + const previousFile = files[previousFileIndex] + if (previousFile) { + context.setCurrentFile(previousFile) + } + break + } + case KeyboardKey.Right: { + const nextFileIndex = currentFileIndex + 1 < files.length ? currentFileIndex + 1 : 0 + const nextFile = files[nextFileIndex] + if (nextFile) { + context.setCurrentFile(nextFile) + } + break + } + } + } return ( = ({ application, file, background: 'var(--sn-stylekit-background-color)', }} > -
+
{getFileIconComponent( @@ -121,10 +173,10 @@ export const FilePreviewModal: FunctionComponent = ({ application, file,
- {objectUrl ? ( - - ) : isLoadingFile ? ( + {isLoadingFile ? (
+ ) : objectUrl ? ( + ) : (
diff --git a/app/assets/javascripts/Components/Files/FilePreviewModalProvider.tsx b/app/assets/javascripts/Components/Files/FilePreviewModalProvider.tsx index 38d588b99..758c2b629 100644 --- a/app/assets/javascripts/Components/Files/FilePreviewModalProvider.tsx +++ b/app/assets/javascripts/Components/Files/FilePreviewModalProvider.tsx @@ -4,8 +4,11 @@ import { createContext, FunctionComponent } from 'preact' import { useContext, useState } from 'preact/hooks' import { FilePreviewModal } from './FilePreviewModal' +type FilePreviewActivateFunction = (file: SNFile, files: SNFile[]) => void + type FilePreviewModalContextData = { - activate: (file: SNFile) => void + activate: FilePreviewActivateFunction + setCurrentFile: (file: SNFile) => void } const FilePreviewModalContext = createContext(null) @@ -24,10 +27,12 @@ export const FilePreviewModalProvider: FunctionComponent<{ application: WebApplication }> = ({ application, children }) => { const [isOpen, setIsOpen] = useState(false) - const [file, setFile] = useState() + const [currentFile, setCurrentFile] = useState() + const [files, setFiles] = useState([]) - const activate = (file: SNFile) => { - setFile(file) + const activate: FilePreviewActivateFunction = (file, files) => { + setCurrentFile(file) + setFiles(files) setIsOpen(true) } @@ -37,10 +42,15 @@ export const FilePreviewModalProvider: FunctionComponent<{ return ( <> - {isOpen && file && ( - - )} - + + {isOpen && currentFile && ( + + )} {children} diff --git a/app/assets/javascripts/Services/IOService.ts b/app/assets/javascripts/Services/IOService.ts index f5fadee2f..f976f672b 100644 --- a/app/assets/javascripts/Services/IOService.ts +++ b/app/assets/javascripts/Services/IOService.ts @@ -5,6 +5,8 @@ export enum KeyboardKey { Backspace = 'Backspace', Up = 'ArrowUp', Down = 'ArrowDown', + Left = 'ArrowLeft', + Right = 'ArrowRight', Enter = 'Enter', Escape = 'Escape', Home = 'Home',