Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
6.5 kB · 197 lines
TSX
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198import {useRef} from 'react'import {View} from 'react-native'import {type AnimatedRef} from 'react-native-reanimated'import {Image} from 'expo-image'
import {resolveEmbedImageUris} from '#/lib/media/embed-image-formats'import {userStyle} from '#/lib/userstyles'import {useFullsizeFormat} from '#/state/preferences/fullsize-format'import { applyImageTransforms, useImageCdnHost,} from '#/state/preferences/image-cdn-host'import {useLoadAsPngs} from '#/state/preferences/load-small-pngs'import {useThumbnailFormat} from '#/state/preferences/thumbnail-format'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 {app} from '#/lexicons'import * as bsky from '#/types/bsky'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 fullsizeFormat = useFullsizeFormat() const thumbnailFormat = useThumbnailFormat() const loadAsPngs = useLoadAsPngs() const imageCdnHost = useImageCdnHost() const images: app.bsky.embed.images.ViewImage[] = embed.type === 'gallery' ? embed.view.items .filter(item => bsky.isType(app.bsky.embed.gallery.viewImage, item)) .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<AnimatedRef | null>(null) const singleDimsRef = useRef<Dimensions | null>(null)
if (images.length > 0) { const items = images.map(img => { const {fullsize, thumb} = resolveEmbedImageUris(img, { thumbnailFormat: thumbnailFormat ?? 'webp', fullsizeFormat: fullsizeFormat ?? 'webp', loadAsPngs: loadAsPngs ?? true, }) return { uri: applyImageTransforms(fullsize, {imageCdnHost}), thumbUri: applyImageTransforms(thumb, {imageCdnHost}), 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 = (index: number) => { // Start the selected full-size image immediately; other gallery images // must not compete with it before the lightbox opens. void Image.prefetch(items[index].uri, 'memory').catch(() => {}) }
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 ( <View style={[a.mt_sm, rest.style, userStyle('wsky-post__media')]}> <ImageContextMenu fullsizeUri={image.fullsize} thumbUri={image.thumb} aspectRatio={aspect} borderRadius={tokens.borderRadius.md} onPreviewPress={openFromSingle}> <AutoSizedImage crop={ rest.viewContext === PostEmbedViewContext.ThreadHighlighted ? 'none' : 'constrained' } image={image} onContainerRef={ref => { singleContainerRef.current = ref }} onDimsChange={dims => { singleDimsRef.current = dims }} onPress={(containerRef, dims) => onPress(0, [containerRef], [dims]) } onPressIn={() => onPressIn(0)} /> </ImageContextMenu> </View> ) }
if (useExpandedLayout) { return ( <View style={[a.mt_sm, rest.style, userStyle('wsky-post__media')]}> <Gallery images={images} onPress={onPress} onPressIn={onPressIn} viewContext={rest.viewContext} isWithinQuote={rest.isWithinQuote} metricsPostContext={postContext} /> </View> ) }
return ( <View style={[a.mt_sm, rest.style, userStyle('wsky-post__media')]}> <ImageLayoutGrid images={images} onPress={onPress} onPressIn={onPressIn} viewContext={rest.viewContext} isWithinQuote={rest.isWithinQuote} /> </View> ) }}