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