Bluesky app fork with some witchin' additions 馃挮
at main 263 lines 8.0 kB view raw
1import {useCallback, useMemo, useState} from 'react' 2import {LayoutAnimation, Pressable, View} from 'react-native' 3import {Image} from 'expo-image' 4import { 5 AppBskyEmbedImages, 6 AppBskyEmbedRecord, 7 AppBskyEmbedRecordWithMedia, 8 AppBskyFeedPost, 9} from '@atproto/api' 10import {msg} from '@lingui/macro' 11import {useLingui} from '@lingui/react' 12 13import {sanitizeDisplayName} from '#/lib/strings/display-names' 14import {sanitizeHandle} from '#/lib/strings/handles' 15import {sanitizePronouns} from '#/lib/strings/pronouns' 16import {type ComposerOptsPostRef} from '#/state/shell/composer' 17import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' 18import {atoms as a, useTheme, web} from '#/alf' 19import {QuoteEmbed} from '#/components/Post/Embed' 20import {Text} from '#/components/Typography' 21import {useSimpleVerificationState} from '#/components/verification' 22import {VerificationCheck} from '#/components/verification/VerificationCheck' 23import {parseEmbed} from '#/types/bsky/post' 24 25export function ComposerReplyTo({replyTo}: {replyTo: ComposerOptsPostRef}) { 26 const t = useTheme() 27 const {_} = useLingui() 28 const {embed} = replyTo 29 30 const [showFull, setShowFull] = useState(false) 31 32 const onPress = useCallback(() => { 33 setShowFull(prev => !prev) 34 LayoutAnimation.configureNext({ 35 duration: 350, 36 update: {type: 'spring', springDamping: 0.7}, 37 }) 38 }, []) 39 40 const quoteEmbed = useMemo(() => { 41 if ( 42 AppBskyEmbedRecord.isView(embed) && 43 AppBskyEmbedRecord.isViewRecord(embed.record) && 44 AppBskyFeedPost.isRecord(embed.record.value) 45 ) { 46 return embed 47 } else if ( 48 AppBskyEmbedRecordWithMedia.isView(embed) && 49 AppBskyEmbedRecord.isViewRecord(embed.record.record) && 50 AppBskyFeedPost.isRecord(embed.record.record.value) 51 ) { 52 return embed.record 53 } 54 return null 55 }, [embed]) 56 const parsedQuoteEmbed = quoteEmbed 57 ? parseEmbed({ 58 $type: 'app.bsky.embed.record#view', 59 ...quoteEmbed, 60 }) 61 : null 62 63 const images = useMemo(() => { 64 if (AppBskyEmbedImages.isView(embed)) { 65 return embed.images 66 } else if ( 67 AppBskyEmbedRecordWithMedia.isView(embed) && 68 AppBskyEmbedImages.isView(embed.media) 69 ) { 70 return embed.media.images 71 } 72 }, [embed]) 73 74 const verification = useSimpleVerificationState({profile: replyTo.author}) 75 76 return ( 77 <Pressable 78 style={[ 79 a.flex_row, 80 a.align_start, 81 a.pt_xs, 82 a.pb_lg, 83 a.mb_md, 84 a.mx_lg, 85 a.border_b, 86 t.atoms.border_contrast_medium, 87 web(a.user_select_text), 88 ]} 89 onPress={onPress} 90 accessibilityRole="button" 91 accessibilityLabel={_( 92 msg`Expand or collapse the full post you are replying to`, 93 )} 94 accessibilityHint=""> 95 <PreviewableUserAvatar 96 size={42} 97 profile={replyTo.author} 98 moderation={replyTo.moderation?.ui('avatar')} 99 type={replyTo.author.associated?.labeler ? 'labeler' : 'user'} 100 disableNavigation={true} 101 /> 102 <View style={[a.flex_1, a.pl_md, a.pr_sm, a.gap_2xs]}> 103 <View style={[a.flex_row, a.align_center, a.pr_xs]}> 104 <View style={[a.flex_row, a.align_center]}> 105 <Text 106 style={[ 107 a.font_semi_bold, 108 a.text_md, 109 a.leading_snug, 110 a.flex_shrink, 111 ]} 112 numberOfLines={1} 113 emoji> 114 {sanitizeDisplayName( 115 replyTo.author.displayName || 116 sanitizeHandle(replyTo.author.handle), 117 )} 118 </Text> 119 {verification.showBadge && ( 120 <View style={[a.pl_xs]}> 121 <VerificationCheck 122 width={14} 123 verifier={verification.role === 'verifier'} 124 /> 125 </View> 126 )} 127 </View> 128 {replyTo.author?.pronouns && ( 129 <Text 130 style={[ 131 t.atoms.text_contrast_low, 132 a.text_md, 133 a.leading_snug, 134 a.pl_sm, 135 ]} 136 numberOfLines={1} 137 emoji> 138 {sanitizePronouns(replyTo.author.pronouns, true)} 139 </Text> 140 )} 141 </View> 142 <View style={[a.flex_row, a.gap_md]}> 143 <View style={[a.flex_1, a.flex_grow]}> 144 <Text 145 style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]} 146 numberOfLines={!showFull ? 6 : undefined} 147 emoji> 148 {replyTo.text} 149 </Text> 150 </View> 151 {images && !replyTo.moderation?.ui('contentMedia').blur && ( 152 <ComposerReplyToImages images={images} showFull={showFull} /> 153 )} 154 </View> 155 {showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && ( 156 <QuoteEmbed embed={parsedQuoteEmbed} showPronouns={true} /> 157 )} 158 </View> 159 </Pressable> 160 ) 161} 162 163function ComposerReplyToImages({ 164 images, 165}: { 166 images: AppBskyEmbedImages.ViewImage[] 167 showFull: boolean 168}) { 169 return ( 170 <View 171 style={[ 172 a.rounded_xs, 173 a.overflow_hidden, 174 a.mt_2xs, 175 a.mx_xs, 176 { 177 height: 64, 178 width: 64, 179 }, 180 ]}> 181 {(images.length === 1 && ( 182 <Image 183 source={{uri: images[0].thumb}} 184 style={[a.flex_1]} 185 cachePolicy="memory-disk" 186 accessibilityIgnoresInvertColors 187 /> 188 )) || 189 (images.length === 2 && ( 190 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}> 191 <Image 192 source={{uri: images[0].thumb}} 193 style={[a.flex_1]} 194 cachePolicy="memory-disk" 195 accessibilityIgnoresInvertColors 196 /> 197 <Image 198 source={{uri: images[1].thumb}} 199 style={[a.flex_1]} 200 cachePolicy="memory-disk" 201 accessibilityIgnoresInvertColors 202 /> 203 </View> 204 )) || 205 (images.length === 3 && ( 206 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}> 207 <Image 208 source={{uri: images[0].thumb}} 209 style={[a.flex_1]} 210 cachePolicy="memory-disk" 211 accessibilityIgnoresInvertColors 212 /> 213 <View style={[a.flex_1, a.gap_2xs]}> 214 <Image 215 source={{uri: images[1].thumb}} 216 style={[a.flex_1]} 217 cachePolicy="memory-disk" 218 accessibilityIgnoresInvertColors 219 /> 220 <Image 221 source={{uri: images[2].thumb}} 222 style={[a.flex_1]} 223 cachePolicy="memory-disk" 224 accessibilityIgnoresInvertColors 225 /> 226 </View> 227 </View> 228 )) || 229 (images.length === 4 && ( 230 <View style={[a.flex_1, a.gap_2xs]}> 231 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}> 232 <Image 233 source={{uri: images[0].thumb}} 234 style={[a.flex_1]} 235 cachePolicy="memory-disk" 236 accessibilityIgnoresInvertColors 237 /> 238 <Image 239 source={{uri: images[1].thumb}} 240 style={[a.flex_1]} 241 cachePolicy="memory-disk" 242 accessibilityIgnoresInvertColors 243 /> 244 </View> 245 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}> 246 <Image 247 source={{uri: images[2].thumb}} 248 style={[a.flex_1]} 249 cachePolicy="memory-disk" 250 accessibilityIgnoresInvertColors 251 /> 252 <Image 253 source={{uri: images[3].thumb}} 254 style={[a.flex_1]} 255 cachePolicy="memory-disk" 256 accessibilityIgnoresInvertColors 257 /> 258 </View> 259 </View> 260 ))} 261 </View> 262 ) 263}