Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
119
fork

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 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}