diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 925066d72e..e3116351f8 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -59,6 +59,7 @@ let ProfileHeaderShell = ({ { uri: profile.avatar, thumbUri: profile.avatar, + thumbRect: null, dimensions: { // It's fine if it's actually smaller but we know it's 1:1. height: 1000, @@ -67,7 +68,6 @@ let ProfileHeaderShell = ({ }, ], index: 0, - thumbDims: null, }) } }, [openLightbox, profile, moderation]) diff --git a/src/state/lightbox.tsx b/src/state/lightbox.tsx index b02bc3ba54..b383b952aa 100644 --- a/src/state/lightbox.tsx +++ b/src/state/lightbox.tsx @@ -1,5 +1,4 @@ import React from 'react' -import type {MeasuredDimensions} from 'react-native-reanimated' import {nanoid} from 'nanoid/non-secure' import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback' @@ -8,7 +7,6 @@ import {ImageSource} from '#/view/com/lightbox/ImageViewing/@types' export type Lightbox = { id: string images: ImageSource[] - thumbDims: MeasuredDimensions | null index: number } diff --git a/src/view/com/lightbox/ImageViewing/@types/index.ts b/src/view/com/lightbox/ImageViewing/@types/index.ts index f5ab8bba9a..3476368d31 100644 --- a/src/view/com/lightbox/ImageViewing/@types/index.ts +++ b/src/view/com/lightbox/ImageViewing/@types/index.ts @@ -6,6 +6,8 @@ * */ +import {MeasuredDimensions} from 'react-native-reanimated' + export type Dimensions = { width: number height: number @@ -19,6 +21,7 @@ export type Position = { export type ImageSource = { uri: string thumbUri: string + thumbRect: MeasuredDimensions | null alt?: string dimensions: Dimensions | null } diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index 38e4ad898d..71afb579f1 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -54,7 +54,6 @@ function ImageViewing({ lightbox, openProgress, onFlyAway, - onChangeIndex, onRequestClose, onPressSave, onPressShare, @@ -62,12 +61,11 @@ function ImageViewing({ lightbox: Lightbox openProgress: SharedValue onFlyAway: () => void - onChangeIndex: (index: number) => void onRequestClose: () => void onPressSave: (uri: string) => void onPressShare: (uri: string) => void }) { - const {images, index: initialImageIndex, thumbDims} = lightbox + const {images, index: initialImageIndex} = lightbox const [isScaled, setIsScaled] = useState(false) const [isDragging, setIsDragging] = useState(false) const [imageIndex, setImageIndex] = useState(initialImageIndex) @@ -106,12 +104,13 @@ function ImageViewing({ transform: [{translateY: dismissSwipeTranslateY.value}], } } - if (thumbDims && images[imageIndex].dimensions) { + const image = images[imageIndex] + if (image.thumbRect && image.dimensions) { const interpolatedTransform = interpolateTransform( openProgress.value, - thumbDims, + image.thumbRect, SCREEN, - images[imageIndex].dimensions, + image.dimensions, ) return { pointerEvents: 'none', @@ -209,7 +208,6 @@ function ImageViewing({ onPageSelected={e => { setImageIndex(e.nativeEvent.position) setIsScaled(false) - onChangeIndex(e.nativeEvent.position) }} onPageScrollStateChanged={e => { setIsDragging(e.nativeEvent.pageScrollState !== 'idle') @@ -427,35 +425,30 @@ function ImageViewingRoot({ }) { const [activeLightbox, setActiveLightbox] = useState(nextLightbox) const openProgress = useSharedValue(0) - const isAnimatable = useSharedValue(false) - const isInitialIndex = useSharedValue(true) if (!activeLightbox && nextLightbox) { setActiveLightbox(nextLightbox) } React.useEffect(() => { - if (nextLightbox) { - if ( - nextLightbox.images[nextLightbox.index].dimensions && - nextLightbox.thumbDims - ) { - openProgress.value = withClampedSpring(1) - isAnimatable.value = true - } else { - openProgress.value = 1 - isAnimatable.value = false - } - isInitialIndex.value = true - } else { - // TODO: Support animation to non-initial index. - if (isAnimatable.value && isInitialIndex.value) { + if (!nextLightbox) { + return + } + const canAnimate = nextLightbox.images.every( + img => img.dimensions && img.thumbRect, + ) + if (canAnimate) { + openProgress.value = withClampedSpring(1) + return () => { openProgress.value = withClampedSpring(0) - } else { + } + } else { + openProgress.value = 1 + return () => { openProgress.value = 0 } } - }, [nextLightbox, openProgress, isAnimatable, isInitialIndex]) + }, [nextLightbox, openProgress]) useAnimatedReaction( () => openProgress.value === 0, @@ -476,9 +469,6 @@ function ImageViewingRoot({ lightbox={activeLightbox} openProgress={openProgress} onRequestClose={onRequestClose} - onChangeIndex={index => { - isInitialIndex.value = index === activeLightbox.index - }} onFlyAway={() => { 'worklet' openProgress.value = 0 diff --git a/src/view/com/profile/ProfileSubpageHeader.tsx b/src/view/com/profile/ProfileSubpageHeader.tsx index b712b346b5..fbe87aca91 100644 --- a/src/view/com/profile/ProfileSubpageHeader.tsx +++ b/src/view/com/profile/ProfileSubpageHeader.tsx @@ -75,6 +75,7 @@ export function ProfileSubpageHeader({ { uri: avatar, thumbUri: avatar, + thumbRect: null, dimensions: { // It's fine if it's actually smaller but we know it's 1:1. height: 1000, @@ -83,7 +84,6 @@ export function ProfileSubpageHeader({ }, ], index: 0, - thumbDims: null, }) } }, [openLightbox, avatar]) diff --git a/src/view/com/util/images/Gallery.tsx b/src/view/com/util/images/Gallery.tsx index d4d7d223d5..2e8730d784 100644 --- a/src/view/com/util/images/Gallery.tsx +++ b/src/view/com/util/images/Gallery.tsx @@ -1,6 +1,6 @@ import React from 'react' import {Pressable, StyleProp, View, ViewStyle} from 'react-native' -import Animated, {AnimatedRef, useAnimatedRef} from 'react-native-reanimated' +import Animated, {AnimatedRef} from 'react-native-reanimated' import {Image, ImageStyle} from 'expo-image' import {AppBskyEmbedImages} from '@atproto/api' import {msg} from '@lingui/macro' @@ -19,13 +19,14 @@ interface Props { index: number onPress?: ( index: number, - containerRef: AnimatedRef>, + containerRefs: AnimatedRef>[], ) => void onLongPress?: EventFunction onPressIn?: EventFunction imageStyle?: StyleProp viewContext?: PostEmbedViewContext insetBorderStyle?: StyleProp + containerRefs: AnimatedRef>[] } export function GalleryItem({ @@ -37,6 +38,7 @@ export function GalleryItem({ onLongPress, viewContext, insetBorderStyle, + containerRefs, }: Props) { const t = useTheme() const {_} = useLingui() @@ -45,11 +47,10 @@ export function GalleryItem({ const hasAlt = !!image.alt const hideBadges = viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia - const containerRef = useAnimatedRef() return ( - + onPress(index, containerRef) : undefined} + onPress={onPress ? () => onPress(index, containerRefs) : undefined} onPressIn={onPressIn ? () => onPressIn(index) : undefined} onLongPress={onLongPress ? () => onLongPress(index) : undefined} style={[ diff --git a/src/view/com/util/images/ImageLayoutGrid.tsx b/src/view/com/util/images/ImageLayoutGrid.tsx index 830040ba6c..3638302df7 100644 --- a/src/view/com/util/images/ImageLayoutGrid.tsx +++ b/src/view/com/util/images/ImageLayoutGrid.tsx @@ -1,6 +1,6 @@ import React from 'react' import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' -import {AnimatedRef} from 'react-native-reanimated' +import {AnimatedRef, useAnimatedRef} from 'react-native-reanimated' import {AppBskyEmbedImages} from '@atproto/api' import {PostEmbedViewContext} from '#/view/com/util/post-embeds/types' @@ -11,7 +11,7 @@ interface ImageLayoutGridProps { images: AppBskyEmbedImages.ViewImage[] onPress?: ( index: number, - containerRef: AnimatedRef>, + containerRefs: AnimatedRef>[], ) => void onLongPress?: (index: number) => void onPressIn?: (index: number) => void @@ -42,7 +42,7 @@ interface ImageLayoutGridInnerProps { images: AppBskyEmbedImages.ViewImage[] onPress?: ( index: number, - containerRef: AnimatedRef>, + containerRefs: AnimatedRef>[], ) => void onLongPress?: (index: number) => void onPressIn?: (index: number) => void @@ -54,8 +54,14 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { const gap = props.gap const count = props.images.length + const containerRef1 = useAnimatedRef() + const containerRef2 = useAnimatedRef() + const containerRef3 = useAnimatedRef() + const containerRef4 = useAnimatedRef() + switch (count) { - case 2: + case 2: { + const containerRefs = [containerRef1, containerRef2] return ( @@ -63,6 +69,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { {...props} index={0} insetBorderStyle={noCorners(['topRight', 'bottomRight'])} + containerRefs={containerRefs} /> @@ -70,12 +77,15 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { {...props} index={1} insetBorderStyle={noCorners(['topLeft', 'bottomLeft'])} + containerRefs={containerRefs} /> ) + } - case 3: + case 3: { + const containerRefs = [containerRef1, containerRef2, containerRef3] return ( @@ -83,6 +93,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { {...props} index={0} insetBorderStyle={noCorners(['topRight', 'bottomRight'])} + containerRefs={containerRefs} /> @@ -95,6 +106,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { 'bottomLeft', 'bottomRight', ])} + containerRefs={containerRefs} /> @@ -106,13 +118,21 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { 'bottomLeft', 'topRight', ])} + containerRefs={containerRefs} /> ) + } - case 4: + case 4: { + const containerRefs = [ + containerRef1, + containerRef2, + containerRef3, + containerRef4, + ] return ( <> @@ -125,6 +145,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { 'topRight', 'bottomRight', ])} + containerRefs={containerRefs} /> @@ -136,6 +157,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { 'bottomLeft', 'bottomRight', ])} + containerRefs={containerRefs} /> @@ -149,6 +171,7 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { 'topRight', 'bottomRight', ])} + containerRefs={containerRefs} /> @@ -160,11 +183,13 @@ function ImageLayoutGridInner(props: ImageLayoutGridInnerProps) { 'bottomLeft', 'topRight', ])} + containerRefs={containerRefs} /> ) + } default: return null diff --git a/src/view/com/util/post-embeds/index.tsx b/src/view/com/util/post-embeds/index.tsx index d686d2bd32..7ffb575660 100644 --- a/src/view/com/util/post-embeds/index.tsx +++ b/src/view/com/util/post-embeds/index.tsx @@ -149,22 +149,24 @@ export function PostEmbeds({ })) const _openLightbox = ( index: number, - thumbDims: MeasuredDimensions | null, + thumbRects: (MeasuredDimensions | null)[], ) => { openLightbox({ - images: items, + images: items.map((item, i) => ({ + ...item, + thumbRect: thumbRects[i] ?? null, + })), index, - thumbDims, }) } const onPress = ( index: number, - ref: AnimatedRef>, + refs: AnimatedRef>[], ) => { runOnUI(() => { 'worklet' - const dims = measure(ref) - runOnJS(_openLightbox)(index, dims) + const rects = refs.map(ref => (ref ? measure(ref) : null)) + runOnJS(_openLightbox)(index, rects) })() } const onPressIn = (_: number) => { @@ -188,7 +190,7 @@ export function PostEmbeds({ : 'constrained' } image={image} - onPress={() => onPress(0, containerRef)} + onPress={() => onPress(0, [containerRef])} onPressIn={() => onPressIn(0)} hideBadge={ viewContext === PostEmbedViewContext.FeedEmbedRecordWithMedia