mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at remove-hackfix 235 lines 6.7 kB view raw
1import React, {useRef} from 'react' 2import {type DimensionValue, Pressable, View} from 'react-native' 3import Animated, { 4 type AnimatedRef, 5 useAnimatedRef, 6} from 'react-native-reanimated' 7import {Image} from 'expo-image' 8import {type AppBskyEmbedImages} from '@atproto/api' 9import {msg} from '@lingui/macro' 10import {useLingui} from '@lingui/react' 11 12import {type Dimensions} from '#/lib/media/types' 13import {isNative} from '#/platform/detection' 14import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 15import {atoms as a, useBreakpoints, useTheme} from '#/alf' 16import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' 17import {MediaInsetBorder} from '#/components/MediaInsetBorder' 18import {Text} from '#/components/Typography' 19 20export function ConstrainedImage({ 21 aspectRatio, 22 fullBleed, 23 children, 24 minMobileAspectRatio, 25}: { 26 aspectRatio: number 27 fullBleed?: boolean 28 minMobileAspectRatio?: number 29 children: React.ReactNode 30}) { 31 const t = useTheme() 32 const {gtMobile} = useBreakpoints() 33 /** 34 * Computed as a % value to apply as `paddingTop`, this basically controls 35 * the height of the image. 36 */ 37 const outerAspectRatio = React.useMemo<DimensionValue>(() => { 38 const ratio = 39 isNative || !gtMobile 40 ? Math.min(1 / aspectRatio, minMobileAspectRatio ?? 16 / 9) // 9:16 bounding box 41 : Math.min(1 / aspectRatio, 1) // 1:1 bounding box 42 return `${ratio * 100}%` 43 }, [aspectRatio, gtMobile, minMobileAspectRatio]) 44 45 return ( 46 <View style={[a.w_full]}> 47 <View style={[a.overflow_hidden, {paddingTop: outerAspectRatio}]}> 48 <View style={[a.absolute, a.inset_0, a.flex_row]}> 49 <View 50 style={[ 51 a.h_full, 52 a.rounded_md, 53 a.overflow_hidden, 54 t.atoms.bg_contrast_25, 55 fullBleed ? a.w_full : {aspectRatio}, 56 ]}> 57 {children} 58 </View> 59 </View> 60 </View> 61 </View> 62 ) 63} 64 65export function AutoSizedImage({ 66 image, 67 crop = 'constrained', 68 hideBadge, 69 onPress, 70 onLongPress, 71 onPressIn, 72}: { 73 image: AppBskyEmbedImages.ViewImage 74 crop?: 'none' | 'square' | 'constrained' 75 hideBadge?: boolean 76 onPress?: ( 77 containerRef: AnimatedRef<any>, 78 fetchedDims: Dimensions | null, 79 ) => void 80 onLongPress?: () => void 81 onPressIn?: () => void 82}) { 83 const t = useTheme() 84 const {_} = useLingui() 85 const largeAlt = useLargeAltBadgeEnabled() 86 const containerRef = useAnimatedRef() 87 const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) 88 89 let aspectRatio: number | undefined 90 const dims = image.aspectRatio 91 if (dims) { 92 aspectRatio = dims.width / dims.height 93 if (Number.isNaN(aspectRatio)) { 94 aspectRatio = undefined 95 } 96 } 97 98 let constrained: number | undefined 99 let max: number | undefined 100 let rawIsCropped: boolean | undefined 101 if (aspectRatio !== undefined) { 102 const ratio = 1 / 2 // max of 1:2 ratio in feeds 103 constrained = Math.max(aspectRatio, ratio) 104 max = Math.max(aspectRatio, 0.25) // max of 1:4 in thread 105 rawIsCropped = aspectRatio < constrained 106 } 107 108 const cropDisabled = crop === 'none' 109 const isCropped = rawIsCropped && !cropDisabled 110 const isContain = aspectRatio === undefined 111 const hasAlt = !!image.alt 112 113 const contents = ( 114 <Animated.View ref={containerRef} collapsable={false} style={{flex: 1}}> 115 <Image 116 contentFit={isContain ? 'contain' : 'cover'} 117 style={[a.w_full, a.h_full]} 118 source={image.thumb} 119 accessible={true} // Must set for `accessibilityLabel` to work 120 accessibilityIgnoresInvertColors 121 accessibilityLabel={image.alt} 122 accessibilityHint="" 123 onLoad={e => { 124 if (!isContain) { 125 fetchedDimsRef.current = { 126 width: e.source.width, 127 height: e.source.height, 128 } 129 } 130 }} 131 /> 132 <MediaInsetBorder /> 133 134 {(hasAlt || isCropped) && !hideBadge ? ( 135 <View 136 accessible={false} 137 style={[ 138 a.absolute, 139 a.flex_row, 140 { 141 bottom: a.p_xs.padding, 142 right: a.p_xs.padding, 143 gap: 3, 144 }, 145 largeAlt && [ 146 { 147 gap: 4, 148 }, 149 ], 150 ]}> 151 {isCropped && ( 152 <View 153 style={[ 154 a.rounded_xs, 155 t.atoms.bg_contrast_25, 156 { 157 padding: 3, 158 opacity: 0.8, 159 }, 160 largeAlt && [ 161 { 162 padding: 5, 163 }, 164 ], 165 ]}> 166 <Fullscreen 167 fill={t.atoms.text_contrast_high.color} 168 width={largeAlt ? 18 : 12} 169 /> 170 </View> 171 )} 172 {hasAlt && ( 173 <View 174 style={[ 175 a.justify_center, 176 a.rounded_xs, 177 t.atoms.bg_contrast_25, 178 { 179 padding: 3, 180 opacity: 0.8, 181 }, 182 largeAlt && [ 183 { 184 padding: 5, 185 }, 186 ], 187 ]}> 188 <Text style={[a.font_bold, largeAlt ? a.text_xs : {fontSize: 8}]}> 189 ALT 190 </Text> 191 </View> 192 )} 193 </View> 194 ) : null} 195 </Animated.View> 196 ) 197 198 if (cropDisabled) { 199 return ( 200 <Pressable 201 onPress={() => onPress?.(containerRef, fetchedDimsRef.current)} 202 onLongPress={onLongPress} 203 onPressIn={onPressIn} 204 // alt here is what screen readers actually use 205 accessibilityLabel={image.alt} 206 accessibilityHint={_(msg`Views full image`)} 207 style={[ 208 a.w_full, 209 a.rounded_md, 210 a.overflow_hidden, 211 t.atoms.bg_contrast_25, 212 {aspectRatio: max ?? 1}, 213 ]}> 214 {contents} 215 </Pressable> 216 ) 217 } else { 218 return ( 219 <ConstrainedImage 220 fullBleed={crop === 'square'} 221 aspectRatio={constrained ?? 1}> 222 <Pressable 223 onPress={() => onPress?.(containerRef, fetchedDimsRef.current)} 224 onLongPress={onLongPress} 225 onPressIn={onPressIn} 226 // alt here is what screen readers actually use 227 accessibilityLabel={image.alt} 228 accessibilityHint={_(msg`Views full image`)} 229 style={[a.h_full]}> 230 {contents} 231 </Pressable> 232 </ConstrainedImage> 233 ) 234 } 235}