import {useRef} from 'react' import {InteractionManager, View} from 'react-native' import {type AnimatedRef} from 'react-native-reanimated' import {Image} from 'expo-image' import {AppBskyEmbedGallery, type AppBskyEmbedImages} from '@atproto/api' import {atoms as a, tokens} from '#/alf' import {AutoSizedImage} from '#/components/images/AutoSizedImage' import {Gallery} from '#/components/images/Gallery' import {ImageLayoutGrid} from '#/components/images/ImageLayoutGrid' import { type LightboxMetricsContext, useLightboxControls, } from '#/components/Lightbox/state' import {type Dimensions} from '#/components/Lightbox/types' import {ImageContextMenu} from '#/components/Post/Embed/ImageContextMenu' import {PostEmbedViewContext} from '#/components/Post/Embed/types' import {useAnalytics} from '#/analytics' import {type EmbedType} from '#/types/bsky/post' import {type CommonProps} from './types' const MAX_GRID_IMAGES = 4 export function ImageEmbed({ embed, ...rest }: CommonProps & { embed: EmbedType<'images'> | EmbedType<'gallery'> }) { const ax = useAnalytics() const {openLightbox} = useLightboxControls() const images: AppBskyEmbedImages.ViewImage[] = embed.type === 'gallery' ? embed.view.items.filter(AppBskyEmbedGallery.isViewImage).map(item => ({ thumb: item.thumbnail, fullsize: item.fullsize, alt: item.alt, aspectRatio: item.aspectRatio, })) : embed.view.images const useExpandedLayout = embed.type === 'gallery' ? images.length > MAX_GRID_IMAGES : ax.features.enabled(ax.features.PostGalleryEmbedEnable) const layout: 'single' | 'grid' | 'carousel' = images.length === 1 ? 'single' : useExpandedLayout ? 'carousel' : 'grid' const postContext = rest.post ? { postUri: rest.post.uri, postAuthorDid: rest.post.author.did, feedDescriptor: rest.feedDescriptor, } : undefined const metricsContext: LightboxMetricsContext | undefined = postContext ? {layout, ...postContext} : undefined // 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 | null>(null) const singleDimsRef = useRef(null) if (images.length > 0) { const items = images.map(img => ({ uri: img.fullsize, thumbUri: img.thumb, alt: img.alt, dimensions: img.aspectRatio ?? null, })) const onPress = ( index: number, refs: AnimatedRef[], fetchedDims: (Dimensions | null)[], ) => { if (postContext) { ax.metric('post:photoEmbed:open', { layout, fromImage: index + 1, totalImages: images.length, ...postContext, }) } openLightbox({ images: items.map((item, i) => ({ ...item, thumbRect: null, thumbRef: refs[i] ?? null, thumbDimensions: fetchedDims[i] ?? null, thumbBorderRadius: tokens.borderRadius.md, type: 'image', })), index, metricsContext, }) } const onPressIn = (_: number) => { InteractionManager.runAfterInteractions(() => { Image.prefetch( items.map(i => i.uri), 'memory', ) }) } if (images.length === 1) { const image = images[0] const aspect = image.aspectRatio && image.aspectRatio.height > 0 ? image.aspectRatio.width / image.aspectRatio.height : undefined const openFromSingle = () => { if (singleContainerRef.current) { onPress(0, [singleContainerRef.current], [singleDimsRef.current]) } } return ( { singleContainerRef.current = ref }} onDimsChange={dims => { singleDimsRef.current = dims }} onPress={(containerRef, dims) => onPress(0, [containerRef], [dims]) } onPressIn={() => onPressIn(0)} /> ) } if (useExpandedLayout) { return ( ) } return ( ) } }