diff --git a/web/packages/gallery/components/viewer/FileViewer.tsx b/web/packages/gallery/components/viewer/FileViewer.tsx index 0414ed582d..115ca4cbd2 100644 --- a/web/packages/gallery/components/viewer/FileViewer.tsx +++ b/web/packages/gallery/components/viewer/FileViewer.tsx @@ -120,11 +120,12 @@ export interface FileViewerFileAnnotation { } /** - * A file and its annotation, in a nice cosy box. + * A file, its annotation, and its item data, in a nice cosy box. */ export interface FileViewerAnnotatedFile { file: EnteFile; annotation: FileViewerFileAnnotation; + itemData: ItemData; } export type FileViewerProps = ModalVisibilityProps & { @@ -309,12 +310,6 @@ const FileViewer: React.FC = ({ FileInfoExif | undefined >(undefined); - // With semantics similar to `activeAnnotatedFile`, this is the imageURL - // associated with the `activeAnnotatedFile`, if any. - const [activeImageURL, setActiveImageURL] = useState( - undefined, - ); - const [openFileInfo, setOpenFileInfo] = useState(false); const [moreMenuAnchorEl, setMoreMenuAnchorEl] = useState(null); @@ -448,20 +443,21 @@ const FileViewer: React.FC = ({ }); }; - // Not memoized since it uses the frequently changing `activeImageURL`. + // Not memoized since it uses the frequently changing `activeAnnotatedFile`. const handleCopyImage = useCallback(() => { handleMoreMenuCloseIfNeeded(); + const imageURL = activeAnnotatedFile?.itemData.imageURL; // Safari does not copy if we do not call `navigator.clipboard.write` // synchronously within the click event handler, but it does supports // passing a promise in lieu of the blob. void window.navigator.clipboard .write([ new ClipboardItem({ - "image/png": createImagePNGBlob(activeImageURL!), + "image/png": createImagePNGBlob(imageURL!), }), ]) .catch(onGenericError); - }, [onGenericError, handleMoreMenuCloseIfNeeded, activeImageURL]); + }, [onGenericError, handleMoreMenuCloseIfNeeded, activeAnnotatedFile]); const handleEditImage = useMemo(() => { return onSaveEditedImageCopy @@ -486,7 +482,7 @@ const FileViewer: React.FC = ({ ); const handleAnnotate = useCallback( - (file: EnteFile, itemData: ItemData): FileViewerFileAnnotation => { + (file: EnteFile, itemData: ItemData): FileViewerAnnotatedFile => { const fileID = file.id; const isOwnFile = file.ownerID == user?.id; @@ -537,10 +533,9 @@ const FileViewer: React.FC = ({ showEditImage, }; - setActiveAnnotatedFile({ file, annotation }); - setActiveImageURL(itemData.imageURL); - - return annotation; + const annotatedFile = { file, annotation, itemData }; + setActiveAnnotatedFile(annotatedFile); + return annotatedFile; }, [ user, @@ -663,6 +658,7 @@ const FileViewer: React.FC = ({ }, [ handleConfirmDelete, + activeAnnotatedFile, activeImageURL, handleCopyImage, handleToggleFullscreen, diff --git a/web/packages/gallery/components/viewer/photoswipe.ts b/web/packages/gallery/components/viewer/photoswipe.ts index 97ab1801cc..9bb35a6380 100644 --- a/web/packages/gallery/components/viewer/photoswipe.ts +++ b/web/packages/gallery/components/viewer/photoswipe.ts @@ -15,10 +15,7 @@ import { updateFileInfoExifIfNeeded, type ItemData, } from "./data-source"; -import { - type FileViewerAnnotatedFile, - type FileViewerFileAnnotation, -} from "./FileViewer"; +import { type FileViewerAnnotatedFile } from "./FileViewer"; import { createPSRegisterElementIconHTML } from "./icons"; // TODO(PS): WIP gallery using upstream photoswipe @@ -132,10 +129,7 @@ type FileViewerPhotoSwipeOptions = Pick< * @param itemData This is the best currently available {@link ItemData} * corresponding to the current file. */ - onAnnotate: ( - file: EnteFile, - itemData: ItemData, - ) => FileViewerFileAnnotation; + onAnnotate: (file: EnteFile, itemData: ItemData) => FileViewerAnnotatedFile; /** * Called when the user activates the info action on a file. */ @@ -216,17 +210,6 @@ export class FileViewerPhotoSwipe { * - "auto-hidden" if controls were hidden by us because of inactivity. */ private lastActivityDate: Date | "auto-hidden" | "already-hidden"; - /** - * Derived data about the currently displayed file. - * - * This is recomputed on-demand (by using the {@link onAnnotate} callback) - * each time the slide changes, and cached until the next slide change. - * - * Instead of accessing this property directly, code should funnel through - * the `currentAnnotatedFile` helper function defined in the constructor - * scope. - */ - private activeFileAnnotation: FileViewerFileAnnotation | undefined; /** * IDs of files for which a there is a favorite update in progress. */ @@ -305,24 +288,28 @@ export class FileViewerPhotoSwipe { // Various helper routines to obtain the file at `currIndex`. + /** + * Derived data about the currently displayed file. + * + * This is recomputed on-demand (by using the {@link onAnnotate} + * callback) each time the slide changes, and cached until the next + * slide change. + * + * Instead of accessing this property directly, code should funnel + * through the `currentAnnotatedFile` helper function. + */ + let _currentAnnotatedFile: FileViewerAnnotatedFile | undefined; + const currentFile = () => delegate.getFiles()[pswp.currIndex]!; const currentAnnotatedFile = () => { const file = currentFile(); - let annotation = this.activeFileAnnotation; - if (annotation?.fileID != file.id) { - annotation = onAnnotate(file, pswp.currSlide.content.data); - this.activeFileAnnotation = annotation; + const annotatedFile = _currentAnnotatedFile; + if (!annotatedFile || annotatedFile.file.fileID != file.id) { + annotatedFile = onAnnotate(file, pswp.currSlide.content.data); + _currentAnnotatedFile = annotatedFile; } - return { - file, - // The above condition implies that annotation can never be - // undefined, but it doesn't seem to be enough to convince - // TypeScript. Writing the condition in a more unnatural way - // `(!(annotation && annotation?.fileID == file.id))` works, but - // instead we use a non-null assertion here. - annotation: annotation!, - }; + return annotatedFile; }; const currentFileAnnotation = () => currentAnnotatedFile().annotation;