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

Configure Feed

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

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