import React, {useRef} from 'react' import {type DimensionValue, Pressable, View} from 'react-native' import Animated, { type AnimatedRef, useAnimatedRef, } from 'react-native-reanimated' import {Image} from 'expo-image' import {type AppBskyEmbedImages} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {type Dimensions} from '#/lib/media/types' import {isNative} from '#/platform/detection' import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' import {MediaInsetBorder} from '#/components/MediaInsetBorder' import {Text} from '#/components/Typography' export function ConstrainedImage({ aspectRatio, fullBleed, children, }: { aspectRatio: number fullBleed?: boolean children: React.ReactNode }) { const t = useTheme() const {gtMobile} = useBreakpoints() /** * Computed as a % value to apply as `paddingTop`, this basically controls * the height of the image. */ const outerAspectRatio = React.useMemo(() => { const ratio = isNative || !gtMobile ? Math.min(1 / aspectRatio, 16 / 9) // 9:16 bounding box : Math.min(1 / aspectRatio, 1) // 1:1 bounding box return `${ratio * 100}%` }, [aspectRatio, gtMobile]) return ( {children} ) } export function AutoSizedImage({ image, crop = 'constrained', hideBadge, onPress, onLongPress, onPressIn, }: { image: AppBskyEmbedImages.ViewImage crop?: 'none' | 'square' | 'constrained' hideBadge?: boolean onPress?: ( containerRef: AnimatedRef, fetchedDims: Dimensions | null, ) => void onLongPress?: () => void onPressIn?: () => void }) { const t = useTheme() const {_} = useLingui() const largeAlt = useLargeAltBadgeEnabled() const containerRef = useAnimatedRef() const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) let aspectRatio: number | undefined const dims = image.aspectRatio if (dims) { aspectRatio = dims.width / dims.height if (Number.isNaN(aspectRatio)) { aspectRatio = undefined } } let constrained: number | undefined let max: number | undefined let rawIsCropped: boolean | undefined if (aspectRatio !== undefined) { const ratio = 1 / 2 // max of 1:2 ratio in feeds constrained = Math.max(aspectRatio, ratio) max = Math.max(aspectRatio, 0.25) // max of 1:4 in thread rawIsCropped = aspectRatio < constrained } const cropDisabled = crop === 'none' const isCropped = rawIsCropped && !cropDisabled const isContain = aspectRatio === undefined const hasAlt = !!image.alt const contents = ( { if (!isContain) { fetchedDimsRef.current = { width: e.source.width, height: e.source.height, } } }} /> {(hasAlt || isCropped) && !hideBadge ? ( {isCropped && ( )} {hasAlt && ( ALT )} ) : null} ) if (cropDisabled) { return ( onPress?.(containerRef, fetchedDimsRef.current)} onLongPress={onLongPress} onPressIn={onPressIn} // alt here is what screen readers actually use accessibilityLabel={image.alt} accessibilityHint={_(msg`Views full image`)} style={[ a.w_full, a.rounded_md, a.overflow_hidden, t.atoms.bg_contrast_25, {aspectRatio: max ?? 1}, ]}> {contents} ) } else { return ( onPress?.(containerRef, fetchedDimsRef.current)} onLongPress={onLongPress} onPressIn={onPressIn} // alt here is what screen readers actually use accessibilityLabel={image.alt} accessibilityHint={_(msg`Views full image`)} style={[a.h_full]}> {contents} ) } }