Pass thumbnail ref through peek commit so lightbox return animates
The lightbox's close animation needs the original thumbnail ref/dims to know where to animate back to. The peek-commit path was passing null, so the lightbox dropped to a fade instead of the usual return-to-thumb animation. Rework each embed path so the tap handler and the peek-commit handler invoke the same openLightbox call with the same ref + dims: - Grid and scrollable Gallery: derive `openLightboxAtIndex` once per cell and hand it to both Pressable.onPress and ContextMenu.onPreviewPress. - Single image: add onContainerRef / onDimsChange callbacks to AutoSizedImage, capture them in ImageEmbed, and use them for the peek commit. Drops the now-redundant onPreviewPress prop chain. https://claude.ai/code/session_015REmux3R9uuEMMJUHxTyQT
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
|
import {useRef} from 'react'
|
||||||
import {InteractionManager, View} from 'react-native'
|
import {InteractionManager, View} from 'react-native'
|
||||||
import {type AnimatedRef} from 'react-native-reanimated'
|
import {type AnimatedRef} from 'react-native-reanimated'
|
||||||
import {Image} from 'expo-image'
|
import {Image} from 'expo-image'
|
||||||
@@ -25,6 +26,11 @@ export function ImageEmbed({
|
|||||||
const {images} = embed.view
|
const {images} = embed.view
|
||||||
const galleryEnabled = ax.features.enabled(ax.features.PostGalleryEmbedEnable)
|
const galleryEnabled = ax.features.enabled(ax.features.PostGalleryEmbedEnable)
|
||||||
|
|
||||||
|
// Captured from AutoSizedImage so the peek-commit handler can reuse the same
|
||||||
|
// ref + dims that a tap would — keeps the lightbox's return animation intact.
|
||||||
|
const singleContainerRef = useRef<AnimatedRef<any> | null>(null)
|
||||||
|
const singleDimsRef = useRef<Dimensions | null>(null)
|
||||||
|
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
const items = images.map(img => ({
|
const items = images.map(img => ({
|
||||||
uri: img.fullsize,
|
uri: img.fullsize,
|
||||||
@@ -57,18 +63,6 @@ export function ImageEmbed({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const onPreviewPress = (index: number) =>
|
|
||||||
openLightbox({
|
|
||||||
images: items.map(item => ({
|
|
||||||
...item,
|
|
||||||
thumbRect: null,
|
|
||||||
thumbRef: null,
|
|
||||||
thumbDimensions: null,
|
|
||||||
thumbBorderRadius: tokens.borderRadius.md,
|
|
||||||
type: 'image',
|
|
||||||
})),
|
|
||||||
index,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (images.length === 1) {
|
if (images.length === 1) {
|
||||||
const image = images[0]
|
const image = images[0]
|
||||||
@@ -76,6 +70,11 @@ export function ImageEmbed({
|
|||||||
image.aspectRatio && image.aspectRatio.height > 0
|
image.aspectRatio && image.aspectRatio.height > 0
|
||||||
? image.aspectRatio.width / image.aspectRatio.height
|
? image.aspectRatio.width / image.aspectRatio.height
|
||||||
: undefined
|
: undefined
|
||||||
|
const openFromSingle = () => {
|
||||||
|
if (singleContainerRef.current) {
|
||||||
|
onPress(0, [singleContainerRef.current], [singleDimsRef.current])
|
||||||
|
}
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<View style={[a.mt_sm, rest.style]}>
|
<View style={[a.mt_sm, rest.style]}>
|
||||||
<ImageContextMenu
|
<ImageContextMenu
|
||||||
@@ -83,7 +82,7 @@ export function ImageEmbed({
|
|||||||
thumbUri={image.thumb}
|
thumbUri={image.thumb}
|
||||||
aspectRatio={aspect}
|
aspectRatio={aspect}
|
||||||
borderRadius={tokens.borderRadius.md}
|
borderRadius={tokens.borderRadius.md}
|
||||||
onPreviewPress={() => onPreviewPress(0)}>
|
onPreviewPress={openFromSingle}>
|
||||||
<AutoSizedImage
|
<AutoSizedImage
|
||||||
crop={
|
crop={
|
||||||
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
|
rest.viewContext === PostEmbedViewContext.ThreadHighlighted
|
||||||
@@ -94,6 +93,12 @@ export function ImageEmbed({
|
|||||||
: 'constrained'
|
: 'constrained'
|
||||||
}
|
}
|
||||||
image={image}
|
image={image}
|
||||||
|
onContainerRef={ref => {
|
||||||
|
singleContainerRef.current = ref
|
||||||
|
}}
|
||||||
|
onDimsChange={dims => {
|
||||||
|
singleDimsRef.current = dims
|
||||||
|
}}
|
||||||
onPress={(containerRef, dims) =>
|
onPress={(containerRef, dims) =>
|
||||||
onPress(0, [containerRef], [dims])
|
onPress(0, [containerRef], [dims])
|
||||||
}
|
}
|
||||||
@@ -115,7 +120,6 @@ export function ImageEmbed({
|
|||||||
images={images}
|
images={images}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
onPreviewPress={onPreviewPress}
|
|
||||||
viewContext={rest.viewContext}
|
viewContext={rest.viewContext}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
@@ -128,7 +132,6 @@ export function ImageEmbed({
|
|||||||
images={images}
|
images={images}
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
onPressIn={onPressIn}
|
onPressIn={onPressIn}
|
||||||
onPreviewPress={onPreviewPress}
|
|
||||||
viewContext={rest.viewContext}
|
viewContext={rest.viewContext}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useMemo, useRef} from 'react'
|
import {useEffect, useMemo, useRef} from 'react'
|
||||||
import {type DimensionValue, Pressable, View} from 'react-native'
|
import {type DimensionValue, Pressable, View} from 'react-native'
|
||||||
import Animated, {
|
import Animated, {
|
||||||
type AnimatedRef,
|
type AnimatedRef,
|
||||||
@@ -69,6 +69,8 @@ export function AutoSizedImage({
|
|||||||
onPress,
|
onPress,
|
||||||
onLongPress,
|
onLongPress,
|
||||||
onPressIn,
|
onPressIn,
|
||||||
|
onContainerRef,
|
||||||
|
onDimsChange,
|
||||||
}: {
|
}: {
|
||||||
image: AppBskyEmbedImages.ViewImage
|
image: AppBskyEmbedImages.ViewImage
|
||||||
crop?: 'none' | 'square' | 'constrained'
|
crop?: 'none' | 'square' | 'constrained'
|
||||||
@@ -79,6 +81,11 @@ export function AutoSizedImage({
|
|||||||
) => void
|
) => void
|
||||||
onLongPress?: () => void
|
onLongPress?: () => void
|
||||||
onPressIn?: () => void
|
onPressIn?: () => void
|
||||||
|
/** Fires once with the internal container ref so a parent can drive its
|
||||||
|
* own lightbox-return animation without waiting for an `onPress`. */
|
||||||
|
onContainerRef?: (ref: AnimatedRef<any>) => void
|
||||||
|
/** Fires when the underlying image reports its natural dimensions. */
|
||||||
|
onDimsChange?: (dims: Dimensions) => void
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
@@ -86,6 +93,10 @@ export function AutoSizedImage({
|
|||||||
const containerRef = useAnimatedRef()
|
const containerRef = useAnimatedRef()
|
||||||
const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
|
const fetchedDimsRef = useRef<{width: number; height: number} | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
onContainerRef?.(containerRef)
|
||||||
|
}, [containerRef, onContainerRef])
|
||||||
|
|
||||||
let aspectRatio: number | undefined
|
let aspectRatio: number | undefined
|
||||||
const dims = image.aspectRatio
|
const dims = image.aspectRatio
|
||||||
if (dims) {
|
if (dims) {
|
||||||
@@ -122,10 +133,12 @@ export function AutoSizedImage({
|
|||||||
accessibilityHint=""
|
accessibilityHint=""
|
||||||
onLoad={e => {
|
onLoad={e => {
|
||||||
if (!isContain) {
|
if (!isContain) {
|
||||||
fetchedDimsRef.current = {
|
const dims = {
|
||||||
width: e.source.width,
|
width: e.source.width,
|
||||||
height: e.source.height,
|
height: e.source.height,
|
||||||
}
|
}
|
||||||
|
fetchedDimsRef.current = dims
|
||||||
|
onDimsChange?.(dims)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ interface GalleryProps {
|
|||||||
fetchedDims: (Dimensions | null)[],
|
fetchedDims: (Dimensions | null)[],
|
||||||
) => void
|
) => void
|
||||||
onPressIn?: (index: number) => void
|
onPressIn?: (index: number) => void
|
||||||
onPreviewPress?: (index: number) => void
|
|
||||||
viewContext?: PostEmbedViewContext
|
viewContext?: PostEmbedViewContext
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,7 +96,6 @@ export function Gallery({
|
|||||||
images,
|
images,
|
||||||
onPress,
|
onPress,
|
||||||
onPressIn,
|
onPressIn,
|
||||||
onPreviewPress,
|
|
||||||
viewContext,
|
viewContext,
|
||||||
}: GalleryProps) {
|
}: GalleryProps) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
@@ -267,6 +265,21 @@ export function Gallery({
|
|||||||
data={images}
|
data={images}
|
||||||
keyExtractor={(item, index) => item.thumb + index}
|
keyExtractor={(item, index) => item.thumb + index}
|
||||||
renderItem={({item, index}) => {
|
renderItem={({item, index}) => {
|
||||||
|
const openLightboxAtIndex = onPress
|
||||||
|
? () => {
|
||||||
|
ax.metric('post:gallery:openLightbox', {
|
||||||
|
fromImage: index + 1, // convert to 1-based index for easier analysis
|
||||||
|
totalImages: images.length,
|
||||||
|
})
|
||||||
|
const refs: AnimatedRef<any>[] = []
|
||||||
|
const dims: (Dimensions | null)[] = []
|
||||||
|
for (let i = 0; i < images.length; i++) {
|
||||||
|
refs.push(containerRefsRef.current.get(i)!)
|
||||||
|
dims.push(thumbDimsRef.current.get(i) ?? null)
|
||||||
|
}
|
||||||
|
onPress(index, refs, dims)
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
return (
|
return (
|
||||||
<GalleryImage
|
<GalleryImage
|
||||||
hideBadges={hideBadges}
|
hideBadges={hideBadges}
|
||||||
@@ -291,27 +304,9 @@ export function Gallery({
|
|||||||
onThumbDims={(i, dims) => {
|
onThumbDims={(i, dims) => {
|
||||||
thumbDimsRef.current.set(i, dims)
|
thumbDimsRef.current.set(i, dims)
|
||||||
}}
|
}}
|
||||||
onPress={
|
onPress={openLightboxAtIndex}
|
||||||
onPress
|
|
||||||
? () => {
|
|
||||||
ax.metric('post:gallery:openLightbox', {
|
|
||||||
fromImage: index + 1, // convert to 1-based index for easier analysis
|
|
||||||
totalImages: images.length,
|
|
||||||
})
|
|
||||||
const refs: AnimatedRef<any>[] = []
|
|
||||||
const dims: (Dimensions | null)[] = []
|
|
||||||
for (let i = 0; i < images.length; i++) {
|
|
||||||
refs.push(containerRefsRef.current.get(i)!)
|
|
||||||
dims.push(thumbDimsRef.current.get(i) ?? null)
|
|
||||||
}
|
|
||||||
onPress(index, refs, dims)
|
|
||||||
}
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
|
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
|
||||||
onPreviewPress={
|
onPreviewPress={openLightboxAtIndex}
|
||||||
onPreviewPress ? () => onPreviewPress(index) : undefined
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ interface ImageLayoutGridProps {
|
|||||||
) => void
|
) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
onPressIn?: (index: number) => void
|
onPressIn?: (index: number) => void
|
||||||
onPreviewPress?: (index: number) => void
|
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
viewContext?: PostEmbedViewContext
|
viewContext?: PostEmbedViewContext
|
||||||
}
|
}
|
||||||
@@ -49,7 +48,6 @@ interface ImageLayoutGridInnerProps {
|
|||||||
) => void
|
) => void
|
||||||
onLongPress?: (index: number) => void
|
onLongPress?: (index: number) => void
|
||||||
onPressIn?: (index: number) => void
|
onPressIn?: (index: number) => void
|
||||||
onPreviewPress?: (index: number) => void
|
|
||||||
viewContext?: PostEmbedViewContext
|
viewContext?: PostEmbedViewContext
|
||||||
gap: {gap: number}
|
gap: {gap: number}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,6 @@ interface Props {
|
|||||||
) => void
|
) => void
|
||||||
onLongPress?: EventFunction
|
onLongPress?: EventFunction
|
||||||
onPressIn?: EventFunction
|
onPressIn?: EventFunction
|
||||||
/** Fired from the native iOS peek preview tap. */
|
|
||||||
onPreviewPress?: EventFunction
|
|
||||||
imageStyle?: StyleProp<ImageStyle>
|
imageStyle?: StyleProp<ImageStyle>
|
||||||
viewContext?: PostEmbedViewContext
|
viewContext?: PostEmbedViewContext
|
||||||
insetBorderStyle?: StyleProp<ViewStyle>
|
insetBorderStyle?: StyleProp<ViewStyle>
|
||||||
@@ -43,7 +41,6 @@ export function GalleryItem({
|
|||||||
onPress,
|
onPress,
|
||||||
onPressIn,
|
onPressIn,
|
||||||
onLongPress,
|
onLongPress,
|
||||||
onPreviewPress,
|
|
||||||
viewContext,
|
viewContext,
|
||||||
insetBorderStyle,
|
insetBorderStyle,
|
||||||
containerRefs,
|
containerRefs,
|
||||||
@@ -62,22 +59,23 @@ export function GalleryItem({
|
|||||||
? image.aspectRatio.width / image.aspectRatio.height
|
? image.aspectRatio.width / image.aspectRatio.height
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
// The tap handler and the peek-commit handler do the same thing: open the
|
||||||
|
// lightbox with this cell's ref + dims so the lightbox's return animation
|
||||||
|
// can target the original thumbnail.
|
||||||
|
const openLightboxAtIndex = onPress
|
||||||
|
? () => onPress(index, containerRefs, thumbDimsRef.current.slice())
|
||||||
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={a.flex_1} ref={containerRefs[index]} collapsable={false}>
|
<View style={a.flex_1} ref={containerRefs[index]} collapsable={false}>
|
||||||
<ImageContextMenu
|
<ImageContextMenu
|
||||||
fullsizeUri={image.fullsize}
|
fullsizeUri={image.fullsize}
|
||||||
thumbUri={image.thumb}
|
thumbUri={image.thumb}
|
||||||
aspectRatio={aspect}
|
aspectRatio={aspect}
|
||||||
onPreviewPress={
|
onPreviewPress={openLightboxAtIndex}
|
||||||
onPreviewPress ? () => onPreviewPress(index) : undefined
|
|
||||||
}
|
|
||||||
style={a.flex_1}>
|
style={a.flex_1}>
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={
|
onPress={openLightboxAtIndex}
|
||||||
onPress
|
|
||||||
? () => onPress(index, containerRefs, thumbDimsRef.current.slice())
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
|
onPressIn={onPressIn ? () => onPressIn(index) : undefined}
|
||||||
onLongPress={onLongPress ? () => onLongPress(index) : undefined}
|
onLongPress={onLongPress ? () => onLongPress(index) : undefined}
|
||||||
android_ripple={{
|
android_ripple={{
|
||||||
|
|||||||
Reference in New Issue
Block a user