diff --git a/src/components/ImageGallery.tsx b/src/components/ImageGallery.tsx index 3fb24c8a..6de20f09 100644 --- a/src/components/ImageGallery.tsx +++ b/src/components/ImageGallery.tsx @@ -393,18 +393,19 @@ export function Lightbox({ images, currentIndex, onClose, onNext, onPrev, mediaT slotRefs.current.forEach((_, idx) => setSlotTransform(idx, offsetPx, 'none')); }, [setSlotTransform]); - /** Apply vertical drag offset + opacity to the lightbox container for swipe-to-dismiss. */ + /** Apply vertical drag offset + opacity to the lightbox for swipe-to-dismiss. + * Backdrop fades in-place; all visible content (top bar, nav, images, dots, + * bottom bar) translates together via [data-lightbox-content]. */ const applyVerticalDismiss = useCallback((offsetY: number, transition: string) => { const el = containerRef.current; if (!el) return; const progress = Math.min(Math.abs(offsetY) / (window.innerHeight * 0.4), 1); el.style.transition = transition ? `opacity ${transition.split(' ').slice(1).join(' ')}` : 'none'; el.style.opacity = String(1 - progress * 0.6); - // Apply translateY to the image strip container (the overflow div) - const strip = el.querySelector('[data-lightbox-strip]'); - if (strip) { - strip.style.transition = transition; - strip.style.transform = `translateY(${offsetY}px)`; + const content = el.querySelector('[data-lightbox-content]'); + if (content) { + content.style.transition = transition; + content.style.transform = `translateY(${offsetY}px)`; } }, []); @@ -414,7 +415,8 @@ export function Lightbox({ images, currentIndex, onClose, onNext, onPrev, mediaT snapAll(0); }, [currentIndex, snapAll]); - + // Safety: clear animating lock on unmount so stale refs can't block controls + useEffect(() => () => { animating.current = false; }, []); const onTouchStart = (e: React.TouchEvent) => { if (animating.current) return; @@ -475,9 +477,9 @@ export function Lightbox({ images, currentIndex, onClose, onNext, onPrev, mediaT const targetY = dy > 0 ? window.innerHeight : -window.innerHeight; applyVerticalDismiss(targetY, `transform ${DURATION}ms ${EASING}`); setTimeout(() => { - animating.current = false; verticalOffsetRef.current = 0; onClose(); + animating.current = false; }, DURATION); } else { // Spring back @@ -548,97 +550,98 @@ export function Lightbox({ images, currentIndex, onClose, onNext, onPrev, mediaT onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > - {/* Backdrop */} + {/* Backdrop — fades in-place, never translates */}
- {/* Top bar */} -
- {topBarLeft !== undefined ? topBarLeft : ( - <> - {hasMultiple && {currentIndex + 1} / {images.length}} - {!hasMultiple && } - - )} -
- {showDownload && ( - + {/* All interactive content — translated together during swipe-to-dismiss */} +
+ {/* Top bar */} +
+ {topBarLeft !== undefined ? topBarLeft : ( + <> + {hasMultiple && {currentIndex + 1} / {images.length}} + {!hasMultiple && } + )} - + )} + +
+
+ + {/* Prev/next buttons (desktop) */} + {canGoPrev && ( + + )} + {canGoNext && ( + + )} + + {/* Per-image slots — each absolutely positioned by index offset */} +
+ {visibleIndices.map((i) => { + const url = images[i]; + const isCurrent = i === currentIndex; + const initialX = (i - currentIndex) * window.innerWidth; + return ( +
{ + if (el) slotRefs.current.set(i, el); + else slotRefs.current.delete(i); + }} + className={cn( + 'absolute inset-0 flex items-center justify-center will-change-transform', + bottomBar ? 'pb-24 pt-14 px-4 sm:px-12' : 'py-6 pt-14 px-4 sm:px-12', + )} + style={{ transform: `translateX(${initialX}px)` }} + > + {isCurrent && !isLoaded && (mediaTypes?.[i] ?? 'image') === 'image' && ( +
+
+
+ )} + { dragX.current = null; axis.current = null; }} + onZoomChange={(zoomed) => { childZoomedRef.current = zoomed; }} + /> +
+ ); + })}
+ + {/* Dot indicators */} + {hasMultiple && images.length <= maxDotIndicators && ( +
+ {images.map((_, i) => ( +
+ ))} +
+ )} + + {/* Bottom bar — author info, reactions, captions, etc. */} + {bottomBar && ( +
e.stopPropagation()}> + {bottomBar} +
+ )}
- - {/* Prev/next buttons (desktop) */} - {canGoPrev && ( - - )} - {canGoNext && ( - - )} - - {/* Per-image slots — each absolutely positioned by index offset */} -
- {visibleIndices.map((i) => { - const url = images[i]; - const isCurrent = i === currentIndex; - const initialX = (i - currentIndex) * window.innerWidth; - return ( -
{ - if (el) slotRefs.current.set(i, el); - else slotRefs.current.delete(i); - }} - className={cn( - 'absolute inset-0 flex items-center justify-center will-change-transform', - bottomBar ? 'pb-24 pt-14 px-4 sm:px-12' : 'py-6 pt-14 px-4 sm:px-12', - )} - style={{ transform: `translateX(${initialX}px)` }} - > - {isCurrent && !isLoaded && (mediaTypes?.[i] ?? 'image') === 'image' && ( -
-
-
- )} - { dragX.current = null; axis.current = null; }} - onZoomChange={(zoomed) => { childZoomedRef.current = zoomed; }} - /> -
- ); - })} - - -
- - {/* Dot indicators */} - {hasMultiple && images.length <= maxDotIndicators && ( -
- {images.map((_, i) => ( -
- ))} -
- )} - - {/* Bottom bar — author info, reactions, captions, etc. */} - {bottomBar && ( -
e.stopPropagation()}> - {bottomBar} -
- )}
, document.body, ); diff --git a/src/pages/ProfilePage.tsx b/src/pages/ProfilePage.tsx index 5ade4211..5885b62e 100644 --- a/src/pages/ProfilePage.tsx +++ b/src/pages/ProfilePage.tsx @@ -840,6 +840,10 @@ function ProfileImageLightbox({ imageUrl, onClose }: { imageUrl: string; onClose }; }, []); + // Safety: clear animating lock on unmount so stale refs can't block controls + useEffect(() => () => { animatingRef.current = false; }, []); + + /** Backdrop fades in-place; all visible content translates together via contentRef. */ const applyVerticalDismiss = useCallback((offsetY: number, transition: string) => { const el = containerRef.current; const content = contentRef.current; @@ -885,9 +889,9 @@ function ProfileImageLightbox({ imageUrl, onClose }: { imageUrl: string; onClose const targetY = dy > 0 ? window.innerHeight : -window.innerHeight; applyVerticalDismiss(targetY, `transform ${DURATION}ms ${EASING}`); setTimeout(() => { - animatingRef.current = false; verticalOffset.current = 0; onClose(); + animatingRef.current = false; }, DURATION); } else { applyVerticalDismiss(0, `transform ${DURATION}ms ${EASING}`); @@ -917,44 +921,48 @@ function ProfileImageLightbox({ imageUrl, onClose }: { imageUrl: string; onClose onTouchStart={onTouchStart} onTouchEnd={onTouchEnd} > + {/* Backdrop — fades in-place, never translates */}
-
-
- - -
-
- -
- {!isLoaded && ( -
-
+ {/* All interactive content — translated together during swipe-to-dismiss */} +
+
+
+ +
- )} - + +
+ {!isLoaded && ( +
+
+
)} - onLoad={() => setIsLoaded(true)} - draggable={false} - /> + setIsLoaded(true)} + draggable={false} + /> +
, document.body,