this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at e28f6d2f370b4e882ed6f23d08ca0f8d94dbac5f 246 lines 7.0 kB view raw
1import {useMemo, 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 {utils} from '@bsky.app/alf' 10import {msg} from '@lingui/core/macro' 11import {useLingui} from '@lingui/react' 12import {Trans} from '@lingui/react/macro' 13 14import {type Dimensions} from '#/lib/media/types' 15import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 16import {atoms as a, useTheme} from '#/alf' 17import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' 18import {MediaInsetBorder} from '#/components/MediaInsetBorder' 19import {Text} from '#/components/Typography' 20import {IS_NATIVE} from '#/env' 21 22export function ConstrainedImage({ 23 aspectRatio, 24 fullBleed, 25 children, 26 minMobileAspectRatio, 27}: { 28 aspectRatio: number 29 fullBleed?: boolean 30 minMobileAspectRatio?: number 31 children: React.ReactNode 32}) { 33 const t = useTheme() 34 /** 35 * Computed as a % value to apply as `paddingTop`, this basically controls 36 * the height of the image. 37 */ 38 const outerAspectRatio = useMemo<DimensionValue>(() => { 39 const ratio = IS_NATIVE 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, 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 loading="lazy" 132 /> 133 <MediaInsetBorder /> 134 135 {(hasAlt || isCropped) && !hideBadge ? ( 136 <View 137 accessible={false} 138 style={[ 139 a.absolute, 140 a.flex_row, 141 { 142 bottom: a.p_xs.padding, 143 right: a.p_xs.padding, 144 gap: 3, 145 }, 146 largeAlt && [ 147 { 148 gap: 4, 149 }, 150 ], 151 ]}> 152 {isCropped && ( 153 <View 154 style={[ 155 a.rounded_xs, 156 t.atoms.bg_contrast_25, 157 { 158 padding: 3, 159 opacity: 0.8, 160 }, 161 largeAlt && [ 162 { 163 padding: 5, 164 }, 165 ], 166 ]}> 167 <Fullscreen 168 fill={t.atoms.text_contrast_high.color} 169 width={largeAlt ? 18 : 12} 170 /> 171 </View> 172 )} 173 {hasAlt && ( 174 <View 175 style={[ 176 a.justify_center, 177 a.rounded_xs, 178 t.atoms.bg_contrast_25, 179 { 180 padding: 3, 181 opacity: 0.8, 182 }, 183 largeAlt && [ 184 { 185 padding: 5, 186 }, 187 ], 188 ]}> 189 <Text style={[a.font_bold, largeAlt ? a.text_xs : {fontSize: 8}]}> 190 <Trans>ALT</Trans> 191 </Text> 192 </View> 193 )} 194 </View> 195 ) : null} 196 </Animated.View> 197 ) 198 199 if (cropDisabled) { 200 return ( 201 <Pressable 202 onPress={() => onPress?.(containerRef, fetchedDimsRef.current)} 203 onLongPress={onLongPress} 204 onPressIn={onPressIn} 205 // alt here is what screen readers actually use 206 accessibilityLabel={image.alt} 207 accessibilityHint={_(msg`Views full image`)} 208 accessibilityRole="button" 209 android_ripple={{ 210 color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), 211 foreground: true, 212 }} 213 style={[ 214 a.w_full, 215 a.rounded_md, 216 a.overflow_hidden, 217 t.atoms.bg_contrast_25, 218 {aspectRatio: max ?? 1}, 219 ]}> 220 {contents} 221 </Pressable> 222 ) 223 } else { 224 return ( 225 <ConstrainedImage 226 fullBleed={crop === 'square'} 227 aspectRatio={constrained ?? 1}> 228 <Pressable 229 onPress={() => onPress?.(containerRef, fetchedDimsRef.current)} 230 onLongPress={onLongPress} 231 onPressIn={onPressIn} 232 // alt here is what screen readers actually use 233 accessibilityLabel={image.alt} 234 accessibilityHint={_(msg`Views full image`)} 235 accessibilityRole="button" 236 android_ripple={{ 237 color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), 238 foreground: true, 239 }} 240 style={[a.h_full]}> 241 {contents} 242 </Pressable> 243 </ConstrainedImage> 244 ) 245 } 246}