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

Configure Feed

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

at main 250 lines 7.6 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/core/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 {ProfileBadges} from '#/components/ProfileBadges' 21import {Text} from '#/components/Typography' 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 return ( 74 <Pressable 75 style={[ 76 a.flex_row, 77 a.align_start, 78 a.pt_xs, 79 a.pb_lg, 80 a.mb_md, 81 a.mx_lg, 82 a.border_b, 83 t.atoms.border_contrast_medium, 84 web(a.user_select_text), 85 ]} 86 onPress={onPress} 87 accessibilityRole="button" 88 accessibilityLabel={_( 89 msg`Expand or collapse the full post you are replying to`, 90 )} 91 accessibilityHint=""> 92 <PreviewableUserAvatar 93 size={42} 94 profile={replyTo.author} 95 moderation={replyTo.moderation?.ui('avatar')} 96 type={replyTo.author.associated?.labeler ? 'labeler' : 'user'} 97 disableNavigation={true} 98 /> 99 <View style={[a.flex_1, a.pl_md, a.pr_sm, a.gap_2xs]}> 100 <View style={[a.flex_row, a.align_center, a.pr_xs]}> 101 <Text 102 style={[a.font_semi_bold, a.text_md, a.leading_snug, a.flex_shrink]} 103 numberOfLines={1} 104 emoji> 105 {sanitizeDisplayName( 106 replyTo.author.displayName || 107 sanitizeHandle(replyTo.author.handle), 108 )} 109 </Text> 110 <ProfileBadges profile={replyTo.author} size="sm" style={[a.pl_xs]} /> 111 {replyTo.author?.pronouns && ( 112 <Text 113 style={[ 114 t.atoms.text_contrast_low, 115 a.text_md, 116 a.leading_snug, 117 a.pl_sm, 118 ]} 119 numberOfLines={1} 120 emoji> 121 {sanitizePronouns(replyTo.author.pronouns, true)} 122 </Text> 123 )} 124 </View> 125 <View style={[a.flex_row, a.gap_md]}> 126 <View style={[a.flex_1, a.flex_grow]}> 127 <Text 128 style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]} 129 numberOfLines={!showFull ? 6 : undefined} 130 emoji> 131 {replyTo.text} 132 </Text> 133 </View> 134 {images && !replyTo.moderation?.ui('contentMedia').blur && ( 135 <ComposerReplyToImages images={images} showFull={showFull} /> 136 )} 137 </View> 138 {showFull && parsedQuoteEmbed && parsedQuoteEmbed.type === 'post' && ( 139 <QuoteEmbed 140 embed={parsedQuoteEmbed} 141 showPronouns={true} 142 linkDisabled 143 /> 144 )} 145 </View> 146 </Pressable> 147 ) 148} 149 150function ComposerReplyToImages({ 151 images, 152}: { 153 images: AppBskyEmbedImages.ViewImage[] 154 showFull: boolean 155}) { 156 return ( 157 <View 158 style={[ 159 a.rounded_xs, 160 a.overflow_hidden, 161 a.mt_2xs, 162 a.mx_xs, 163 { 164 height: 64, 165 width: 64, 166 }, 167 ]}> 168 {(images.length === 1 && ( 169 <Image 170 source={{uri: images[0].thumb}} 171 style={[a.flex_1]} 172 cachePolicy="memory-disk" 173 accessibilityIgnoresInvertColors 174 /> 175 )) || 176 (images.length === 2 && ( 177 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}> 178 <Image 179 source={{uri: images[0].thumb}} 180 style={[a.flex_1]} 181 cachePolicy="memory-disk" 182 accessibilityIgnoresInvertColors 183 /> 184 <Image 185 source={{uri: images[1].thumb}} 186 style={[a.flex_1]} 187 cachePolicy="memory-disk" 188 accessibilityIgnoresInvertColors 189 /> 190 </View> 191 )) || 192 (images.length === 3 && ( 193 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}> 194 <Image 195 source={{uri: images[0].thumb}} 196 style={[a.flex_1]} 197 cachePolicy="memory-disk" 198 accessibilityIgnoresInvertColors 199 /> 200 <View style={[a.flex_1, a.gap_2xs]}> 201 <Image 202 source={{uri: images[1].thumb}} 203 style={[a.flex_1]} 204 cachePolicy="memory-disk" 205 accessibilityIgnoresInvertColors 206 /> 207 <Image 208 source={{uri: images[2].thumb}} 209 style={[a.flex_1]} 210 cachePolicy="memory-disk" 211 accessibilityIgnoresInvertColors 212 /> 213 </View> 214 </View> 215 )) || 216 (images.length === 4 && ( 217 <View style={[a.flex_1, a.gap_2xs]}> 218 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}> 219 <Image 220 source={{uri: images[0].thumb}} 221 style={[a.flex_1]} 222 cachePolicy="memory-disk" 223 accessibilityIgnoresInvertColors 224 /> 225 <Image 226 source={{uri: images[1].thumb}} 227 style={[a.flex_1]} 228 cachePolicy="memory-disk" 229 accessibilityIgnoresInvertColors 230 /> 231 </View> 232 <View style={[a.flex_1, a.flex_row, a.gap_2xs]}> 233 <Image 234 source={{uri: images[2].thumb}} 235 style={[a.flex_1]} 236 cachePolicy="memory-disk" 237 accessibilityIgnoresInvertColors 238 /> 239 <Image 240 source={{uri: images[3].thumb}} 241 style={[a.flex_1]} 242 cachePolicy="memory-disk" 243 accessibilityIgnoresInvertColors 244 /> 245 </View> 246 </View> 247 ))} 248 </View> 249 ) 250}