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 260 lines 7.7 kB view raw
1import {useRef} from 'react' 2import {View} from 'react-native' 3import { 4 type AppBskyActorDefs, 5 moderateProfile, 6 type ModerationOpts, 7} from '@atproto/api' 8import {msg} from '@lingui/core/macro' 9import {useLingui} from '@lingui/react' 10import {Plural, Trans} from '@lingui/react/macro' 11 12import {makeProfileLink} from '#/lib/routes/links' 13import {sanitizeDisplayName} from '#/lib/strings/display-names' 14import {UserAvatar} from '#/view/com/util/UserAvatar' 15import {atoms as a, useTheme} from '#/alf' 16import {Link, type LinkProps} from '#/components/Link' 17import {Text} from '#/components/Typography' 18import type * as bsky from '#/types/bsky' 19 20const AVI_SIZE = 30 21const AVI_SIZE_SMALL = 20 22const AVI_BORDER = 1 23 24/** 25 * Shared logic to determine if `KnownFollowers` should be shown. 26 * 27 * Checks the # of actual returned users instead of the `count` value, because 28 * `count` includes blocked users and `followers` does not. 29 */ 30export function shouldShowKnownFollowers( 31 knownFollowers?: AppBskyActorDefs.KnownFollowers, 32) { 33 return knownFollowers && knownFollowers.followers.length > 0 34} 35 36export function KnownFollowers({ 37 profile, 38 moderationOpts, 39 onLinkPress, 40 minimal, 41 showIfEmpty, 42}: { 43 profile: bsky.profile.AnyProfileView 44 moderationOpts: ModerationOpts 45 onLinkPress?: LinkProps['onPress'] 46 minimal?: boolean 47 showIfEmpty?: boolean 48}) { 49 const cache = useRef<Map<string, AppBskyActorDefs.KnownFollowers>>(new Map()) 50 51 /* 52 * Results for `knownFollowers` are not sorted consistently, so when 53 * revalidating we can see a flash of this data updating. This cache prevents 54 * this happening for screens that remain in memory. When pushing a new 55 * screen, or once this one is popped, this cache is empty, so new data is 56 * displayed. 57 */ 58 if (profile.viewer?.knownFollowers && !cache.current.has(profile.did)) { 59 cache.current.set(profile.did, profile.viewer.knownFollowers) 60 } 61 62 const cachedKnownFollowers = cache.current.get(profile.did) 63 64 if (cachedKnownFollowers && shouldShowKnownFollowers(cachedKnownFollowers)) { 65 return ( 66 <KnownFollowersInner 67 profile={profile} 68 cachedKnownFollowers={cachedKnownFollowers} 69 moderationOpts={moderationOpts} 70 onLinkPress={onLinkPress} 71 minimal={minimal} 72 showIfEmpty={showIfEmpty} 73 /> 74 ) 75 } 76 77 return <EmptyFallback show={showIfEmpty} /> 78} 79 80function KnownFollowersInner({ 81 profile, 82 moderationOpts, 83 cachedKnownFollowers, 84 onLinkPress, 85 minimal, 86 showIfEmpty, 87}: { 88 profile: bsky.profile.AnyProfileView 89 moderationOpts: ModerationOpts 90 cachedKnownFollowers: AppBskyActorDefs.KnownFollowers 91 onLinkPress?: LinkProps['onPress'] 92 minimal?: boolean 93 showIfEmpty?: boolean 94}) { 95 const t = useTheme() 96 const {_} = useLingui() 97 98 const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium] 99 100 const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => { 101 const moderation = moderateProfile(f, moderationOpts) 102 return { 103 profile: { 104 ...f, 105 displayName: sanitizeDisplayName( 106 f.displayName || f.handle, 107 moderation.ui('displayName'), 108 ), 109 }, 110 moderation, 111 } 112 }) 113 114 // Does not have blocks applied. Always >= slices.length 115 const serverCount = cachedKnownFollowers.count 116 117 /* 118 * We check above too, but here for clarity and a reminder to _check for 119 * valid indices_ 120 */ 121 if (slice.length === 0) return <EmptyFallback show={showIfEmpty} /> 122 123 const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE 124 125 return ( 126 <Link 127 label={_( 128 msg`Press to view followers of this account that you also follow`, 129 )} 130 onPress={onLinkPress} 131 to={makeProfileLink(profile, 'known-followers')} 132 style={[ 133 a.max_w_full, 134 a.flex_row, 135 minimal ? a.gap_sm : a.gap_md, 136 a.align_center, 137 {marginLeft: -AVI_BORDER}, 138 ]}> 139 {({hovered, pressed}) => ( 140 <> 141 <View 142 style={[ 143 a.flex_row, 144 { 145 height: SIZE, 146 }, 147 pressed && { 148 opacity: 0.5, 149 }, 150 ]}> 151 {slice.map(({profile: prof, moderation}, i) => ( 152 <View 153 key={prof.did} 154 style={[ 155 a.rounded_full, 156 { 157 borderWidth: AVI_BORDER, 158 borderColor: t.atoms.bg.backgroundColor, 159 width: SIZE + AVI_BORDER * 2, 160 height: SIZE + AVI_BORDER * 2, 161 zIndex: AVI_BORDER - i, 162 marginLeft: i > 0 ? -8 : 0, 163 }, 164 ]}> 165 <UserAvatar 166 size={SIZE} 167 avatar={prof.avatar} 168 moderation={moderation.ui('avatar')} 169 type={prof.associated?.labeler ? 'labeler' : 'user'} 170 noBorder 171 /> 172 </View> 173 ))} 174 </View> 175 176 <Text 177 style={[ 178 a.flex_shrink, 179 textStyle, 180 hovered && { 181 textDecorationLine: 'underline', 182 textDecorationColor: t.atoms.text_contrast_medium.color, 183 }, 184 pressed && { 185 opacity: 0.5, 186 }, 187 ]} 188 numberOfLines={2}> 189 {slice.length >= 2 ? ( 190 // 2-n followers, including blocks 191 serverCount > 2 ? ( // only 2 192 <Trans> 193 Followed by{' '} 194 <Text emoji key={slice[0].profile.did} style={textStyle}> 195 {slice[0].profile.displayName} 196 </Text> 197 ,{' '} 198 <Text emoji key={slice[1].profile.did} style={textStyle}> 199 {slice[1].profile.displayName} 200 </Text> 201 , and{' '} 202 <Plural 203 value={serverCount - 2} 204 one="# other" 205 other="# others" 206 /> 207 </Trans> 208 ) : ( 209 <Trans> 210 Followed by{' '} 211 <Text emoji key={slice[0].profile.did} style={textStyle}> 212 {slice[0].profile.displayName} 213 </Text>{' '} 214 and{' '} 215 <Text emoji key={slice[1].profile.did} style={textStyle}> 216 {slice[1].profile.displayName} 217 </Text> 218 </Trans> 219 ) 220 ) : serverCount > 1 ? ( 221 // 1-n followers, including blocks 222 <Trans> 223 Followed by{' '} 224 <Text emoji key={slice[0].profile.did} style={textStyle}> 225 {slice[0].profile.displayName} 226 </Text>{' '} 227 and{' '} 228 <Plural 229 value={serverCount - 1} 230 one="# other" 231 other="# others" 232 /> 233 </Trans> 234 ) : ( 235 // only 1 236 <Trans> 237 Followed by{' '} 238 <Text emoji key={slice[0].profile.did} style={textStyle}> 239 {slice[0].profile.displayName} 240 </Text> 241 </Trans> 242 )} 243 </Text> 244 </> 245 )} 246 </Link> 247 ) 248} 249 250function EmptyFallback({show}: {show?: boolean}) { 251 const t = useTheme() 252 253 if (!show) return null 254 255 return ( 256 <Text style={[a.text_sm, a.leading_snug, t.atoms.text_contrast_medium]}> 257 <Trans>Not followed by anyone you're following</Trans> 258 </Text> 259 ) 260}