diff options
author | Eric Bailey <git@esb.lol> | 2024-06-11 17:42:28 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-06-11 15:42:28 -0700 |
commit | bb0a6a4b6c4e86c62d599c424dae35c9ee9d200d (patch) | |
tree | dc4732574140d62efe2bb205193c93996b05a98e /src/components/KnownFollowers.tsx | |
parent | 7011ac8f72ed18153ea485b6cce2e18040de2dc9 (diff) | |
download | voidsky-bb0a6a4b6c4e86c62d599c424dae35c9ee9d200d.tar.zst |
Add KnownFollowers component to standard profile header (#4420)
* Add KnownFollowers component to standard profile header * Prep for known followers screen * Add known followers screen * Tighten space * Add pressed state * Edit title * Vertically center * Don't show if no known followers * Bump sdk * Use actual followers.length to show * Updates to show logic, space * Prevent fresh data from applying to cached screens * Tighten space * Better label * Oxford comma * Fix count logic * Add bskyweb route * Useless ternary * Minor spacing tweak --------- Co-authored-by: Paul Frazee <pfrazee@gmail.com>
Diffstat (limited to 'src/components/KnownFollowers.tsx')
-rw-r--r-- | src/components/KnownFollowers.tsx | 200 |
1 files changed, 200 insertions, 0 deletions
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx new file mode 100644 index 000000000..b99fe3398 --- /dev/null +++ b/src/components/KnownFollowers.tsx @@ -0,0 +1,200 @@ +import React from 'react' +import {View} from 'react-native' +import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' +import {msg, plural, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {makeProfileLink} from '#/lib/routes/links' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {UserAvatar} from '#/view/com/util/UserAvatar' +import {atoms as a, useTheme} from '#/alf' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' + +const AVI_SIZE = 30 +const AVI_BORDER = 1 + +/** + * Shared logic to determine if `KnownFollowers` should be shown. + * + * Checks the # of actual returned users instead of the `count` value, because + * `count` includes blocked users and `followers` does not. + */ +export function shouldShowKnownFollowers( + knownFollowers?: AppBskyActorDefs.KnownFollowers, +) { + return knownFollowers && knownFollowers.followers.length > 0 +} + +export function KnownFollowers({ + profile, + moderationOpts, +}: { + profile: AppBskyActorDefs.ProfileViewDetailed + moderationOpts: ModerationOpts +}) { + const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>( + new Map(), + ) + + /* + * Results for `knownFollowers` are not sorted consistently, so when + * revalidating we can see a flash of this data updating. This cache prevents + * this happening for screens that remain in memory. When pushing a new + * screen, or once this one is popped, this cache is empty, so new data is + * displayed. + */ + if (profile.viewer?.knownFollowers && !cache.current.has(profile.did)) { + cache.current.set(profile.did, profile.viewer.knownFollowers) + } + + const cachedKnownFollowers = cache.current.get(profile.did) + + if (cachedKnownFollowers && shouldShowKnownFollowers(cachedKnownFollowers)) { + return ( + <KnownFollowersInner + profile={profile} + cachedKnownFollowers={cachedKnownFollowers} + moderationOpts={moderationOpts} + /> + ) + } + + return null +} + +function KnownFollowersInner({ + profile, + moderationOpts, + cachedKnownFollowers, +}: { + profile: AppBskyActorDefs.ProfileViewDetailed + moderationOpts: ModerationOpts + cachedKnownFollowers: AppBskyActorDefs.KnownFollowers +}) { + const t = useTheme() + const {_} = useLingui() + + const textStyle = [ + a.flex_1, + a.text_sm, + a.leading_snug, + t.atoms.text_contrast_medium, + ] + + // list of users, minus blocks + const returnedCount = cachedKnownFollowers.followers.length + // db count, includes blocks + const fullCount = cachedKnownFollowers.count + // knownFollowers can return up to 5 users, but will exclude blocks + // therefore, if we have less 5 users, use whichever count is lower + const count = + returnedCount < 5 ? Math.min(fullCount, returnedCount) : fullCount + + const slice = cachedKnownFollowers.followers.slice(0, 3).map(f => { + const moderation = moderateProfile(f, moderationOpts) + return { + profile: { + ...f, + displayName: sanitizeDisplayName( + f.displayName || f.handle, + moderation.ui('displayName'), + ), + }, + moderation, + } + }) + + return ( + <Link + label={_( + msg`Press to view followers of this account that you also follow`, + )} + to={makeProfileLink(profile, 'known-followers')} + style={[ + a.flex_1, + a.flex_row, + a.gap_md, + a.align_center, + {marginLeft: -AVI_BORDER}, + ]}> + {({hovered, pressed}) => ( + <> + <View + style={[ + { + height: AVI_SIZE, + width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap, + }, + pressed && { + opacity: 0.5, + }, + ]}> + {slice.map(({profile: prof, moderation}, i) => ( + <View + key={prof.did} + style={[ + a.absolute, + a.rounded_full, + { + borderWidth: AVI_BORDER, + borderColor: t.atoms.bg.backgroundColor, + width: AVI_SIZE + AVI_BORDER * 2, + height: AVI_SIZE + AVI_BORDER * 2, + left: i * a.gap_md.gap, + zIndex: AVI_BORDER - i, + }, + ]}> + <UserAvatar + size={AVI_SIZE} + avatar={prof.avatar} + moderation={moderation.ui('avatar')} + /> + </View> + ))} + </View> + + <Text + style={[ + textStyle, + hovered && { + textDecorationLine: 'underline', + textDecorationColor: t.atoms.text_contrast_medium.color, + }, + pressed && { + opacity: 0.5, + }, + ]} + numberOfLines={2}> + <Trans>Followed by</Trans>{' '} + {count > 2 ? ( + <> + {slice.slice(0, 2).map(({profile: prof}, i) => ( + <Text key={prof.did} style={textStyle}> + {prof.displayName} + {i === 0 && ', '} + </Text> + ))} + {', '} + {plural(count - 2, { + one: 'and # other', + other: 'and # others', + })} + </> + ) : count === 2 ? ( + slice.map(({profile: prof}, i) => ( + <Text key={prof.did} style={textStyle}> + {prof.displayName} {i === 0 ? _(msg`and`) + ' ' : ''} + </Text> + )) + ) : ( + <Text key={slice[0].profile.did} style={textStyle}> + {slice[0].profile.displayName} + </Text> + )} + </Text> + </> + )} + </Link> + ) +} |