diff --git a/frontend/src/components/gallery/PhotoLightbox.tsx b/frontend/src/components/gallery/PhotoLightbox.tsx index 2b5b9a4e..efdd606d 100644 --- a/frontend/src/components/gallery/PhotoLightbox.tsx +++ b/frontend/src/components/gallery/PhotoLightbox.tsx @@ -696,20 +696,38 @@ export const PhotoLightbox: React.FC = ({ {(() => { const isVideoCurrent = currentPhoto.media_type === 'video'; - const renderSlide = (photo: Photo | null, isCurrent: boolean) => { + // Stable per-slide keys so React's reconciler can MOVE existing + // DOM nodes across slot positions on commit rather than + // re-fetching the AuthenticatedImage at the new position (#505 — + // that re-fetch is what caused the black blink during swipe). + // Edge case: 2-photo galleries assign the same photo to both + // `prev` and `next`; fall back to slot-prefixed keys to keep + // siblings unique. >2-photo galleries (the common case) get + // plain photo.id keys so a "next becomes current" commit + // preserves the loaded image instance. + const slideKey = (photo: Photo | null, slot: 'prev' | 'current' | 'next') => { + if (!photo) return `empty-${slot}`; + if (photos.length === 2) return `${slot}-${photo.id}`; + return `photo-${photo.id}`; + }; + + const renderSlide = (photo: Photo | null, isCurrent: boolean, slot: 'prev' | 'current' | 'next') => { // Reserve the slot even when there's no neighbour (single-photo // gallery) so the flex layout keeps slides aligned. if (!photo) { - return