Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

at 82f42e734c50b34de31e8aff1e7ced248ab6e96f 245 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/macro' 11import {useLingui} from '@lingui/react' 12 13import {type Dimensions} from '#/lib/media/types' 14import {useLargeAltBadgeEnabled} from '#/state/preferences/large-alt-badge' 15import {atoms as a, useTheme} from '#/alf' 16import {ArrowsDiagonalOut_Stroke2_Corner0_Rounded as Fullscreen} from '#/components/icons/ArrowsDiagonal' 17import {MediaInsetBorder} from '#/components/MediaInsetBorder' 18import {Text} from '#/components/Typography' 19import {IS_NATIVE} from '#/env' 20 21export function ConstrainedImage({ 22 aspectRatio, 23 fullBleed, 24 children, 25 minMobileAspectRatio, 26}: { 27 aspectRatio: number 28 fullBleed?: boolean 29 minMobileAspectRatio?: number 30 children: React.ReactNode 31}) { 32 const t = useTheme() 33 /** 34 * Computed as a % value to apply as `paddingTop`, this basically controls 35 * the height of the image. 36 */ 37 const outerAspectRatio = useMemo<DimensionValue>(() => { 38 const ratio = IS_NATIVE 39 ? Math.min(1 / aspectRatio, minMobileAspectRatio ?? 16 / 9) // 9:16 bounding box 40 : Math.min(1 / aspectRatio, 1) // 1:1 bounding box 41 return `${ratio * 100}%` 42 }, [aspectRatio, minMobileAspectRatio]) 43 44 return ( 45 <View style={[a.w_full]}> 46 <View style={[a.overflow_hidden, {paddingTop: outerAspectRatio}]}> 47 <View style={[a.absolute, a.inset_0, a.flex_row]}> 48 <View 49 style={[ 50 a.h_full, 51 a.rounded_md, 52 a.overflow_hidden, 53 t.atoms.bg_contrast_25, 54 fullBleed ? a.w_full : {aspectRatio}, 55 ]}> 56 {children} 57 </View> 58 </View> 59 </View> 60 </View> 61 ) 62} 63 64export function AutoSizedImage({ 65 image, 66 crop = 'constrained', 67 hideBadge, 68 onPress, 69 onLongPress, 70 onPressIn, 71}: { 72 image: AppBskyEmbedImages.ViewImage 73 crop?: 'none' | 'square' | 'constrained' 74 hideBadge?: boolean 75 onPress?: ( 76 containerRef: AnimatedRef<any>, 77 fetchedDims: Dimensions | null, 78 ) => void 79 onLongPress?: () => void 80 onPressIn?: () => void 81}) { 82 const t = useTheme() 83 const {_} = useLingui() 84 const largeAlt = useLargeAltBadgeEnabled() 85 const containerRef = useAnimatedRef() 86 const fetchedDimsRef = useRef<{width: number; height: number} | null>(null) 87 88 let aspectRatio: number | undefined 89 const dims = image.aspectRatio 90 if (dims) { 91 aspectRatio = dims.width / dims.height 92 if (Number.isNaN(aspectRatio)) { 93 aspectRatio = undefined 94 } 95 } 96 97 let constrained: number | undefined 98 let max: number | undefined 99 let rawIsCropped: boolean | undefined 100 if (aspectRatio !== undefined) { 101 const ratio = 1 / 2 // max of 1:2 ratio in feeds 102 constrained = Math.max(aspectRatio, ratio) 103 max = Math.max(aspectRatio, 0.25) // max of 1:4 in thread 104 rawIsCropped = aspectRatio < constrained 105 } 106 107 const cropDisabled = crop === 'none' 108 const isCropped = rawIsCropped && !cropDisabled 109 const isContain = aspectRatio === undefined 110 const hasAlt = !!image.alt 111 112 const contents = ( 113 <Animated.View ref={containerRef} collapsable={false} style={{flex: 1}}> 114 <Image 115 contentFit={isContain ? 'contain' : 'cover'} 116 style={[a.w_full, a.h_full]} 117 source={image.thumb} 118 accessible={true} // Must set for `accessibilityLabel` to work 119 accessibilityIgnoresInvertColors 120 accessibilityLabel={image.alt} 121 accessibilityHint="" 122 onLoad={e => { 123 if (!isContain) { 124 fetchedDimsRef.current = { 125 width: e.source.width, 126 height: e.source.height, 127 } 128 } 129 }} 130 loading="lazy" 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 accessibilityRole="button" 208 android_ripple={{ 209 color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), 210 foreground: true, 211 }} 212 style={[ 213 a.w_full, 214 a.rounded_md, 215 a.overflow_hidden, 216 t.atoms.bg_contrast_25, 217 {aspectRatio: max ?? 1}, 218 ]}> 219 {contents} 220 </Pressable> 221 ) 222 } else { 223 return ( 224 <ConstrainedImage 225 fullBleed={crop === 'square'} 226 aspectRatio={constrained ?? 1}> 227 <Pressable 228 onPress={() => onPress?.(containerRef, fetchedDimsRef.current)} 229 onLongPress={onLongPress} 230 onPressIn={onPressIn} 231 // alt here is what screen readers actually use 232 accessibilityLabel={image.alt} 233 accessibilityHint={_(msg`Views full image`)} 234 accessibilityRole="button" 235 android_ripple={{ 236 color: utils.alpha(t.atoms.bg.backgroundColor, 0.2), 237 foreground: true, 238 }} 239 style={[a.h_full]}> 240 {contents} 241 </Pressable> 242 </ConstrainedImage> 243 ) 244 } 245}