import {useLayoutEffect, useRef, useState} from 'react' import {View} from 'react-native' import { type AppBskyActorDefs, moderateProfile, type ModerationOpts, } from '@atproto/api' import {Plural, Trans, useLingui} from '@lingui/react/macro' import { type FollowedByMetricsDisplay, shouldShowFollowedByOverflowCount, shouldShowFollowedByOverflowPlus, shouldShowFollowedByText, } from '#/lib/metrics-display' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useTheme} from '#/alf' import {PlusSmall_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {Link, type LinkProps} from '#/components/Link' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' const AVI_SIZE = 30 const AVI_SIZE_SMALL = 20 const AVI_BORDER = 1 function avatarBorderRadius(size: number, square: boolean) { if (square) { return size > 32 ? 8 : 3 } return (size + AVI_BORDER * 2) / 2 } /** * 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, onLinkPress, minimal, showIfEmpty, followedByDisplay = 'names', }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts onLinkPress?: LinkProps['onPress'] minimal?: boolean showIfEmpty?: boolean followedByDisplay?: FollowedByMetricsDisplay }) { const cacheRef = useRef>( new Map(), ) const [cachedKnownFollowers, setCachedKnownFollowers] = useState< AppBskyActorDefs.KnownFollowers | undefined >() const knownFollowers = profile.viewer?.knownFollowers /* * 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. */ useLayoutEffect(() => { if (knownFollowers && !cacheRef.current.has(profile.did)) { cacheRef.current.set(profile.did, knownFollowers) } setCachedKnownFollowers(cacheRef.current.get(profile.did)) }, [profile.did, knownFollowers]) if (cachedKnownFollowers && shouldShowKnownFollowers(cachedKnownFollowers)) { return ( ) } return } function KnownFollowersInner({ profile, moderationOpts, cachedKnownFollowers, onLinkPress, minimal, showIfEmpty, followedByDisplay, }: { profile: bsky.profile.AnyProfileView moderationOpts: ModerationOpts cachedKnownFollowers: AppBskyActorDefs.KnownFollowers onLinkPress?: LinkProps['onPress'] minimal?: boolean showIfEmpty?: boolean followedByDisplay: FollowedByMetricsDisplay }) { const t = useTheme() const {t: l} = useLingui() const enableSquareAvatars = useEnableSquareAvatars() const textStyle = [a.text_sm, a.leading_snug, t.atoms.text_contrast_medium] const showText = shouldShowFollowedByText(followedByDisplay) 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, } }) // Does not have blocks applied. Always >= slices.length const serverCount = cachedKnownFollowers.count /* * We check above too, but here for clarity and a reminder to _check for * valid indices_ */ if (slice.length === 0) return const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE const dim = SIZE + AVI_BORDER * 2 const radius = avatarBorderRadius(SIZE, enableSquareAvatars) const overflowCount = serverCount - slice.length const showOverflowCount = shouldShowFollowedByOverflowCount( followedByDisplay, serverCount, slice.length, ) const showOverflowPlus = shouldShowFollowedByOverflowPlus( followedByDisplay, serverCount, slice.length, ) const showEndCap = showOverflowCount || showOverflowPlus return ( {({hovered, pressed}) => ( <> {slice.map(({profile: prof, moderation}, i) => ( 0 ? -8 : 0, overflow: 'hidden', }, ]}> ))} {showEndCap ? ( {showOverflowCount && overflowCount > 0 ? ( +{overflowCount} ) : ( )} ) : null} {showText ? ( {slice.length >= 2 ? ( // 2-n followers, including blocks // only 2 serverCount > 2 ? ( Followed by{' '} {slice[0].profile.displayName} ,{' '} {slice[1].profile.displayName} , and{' '} ) : ( Followed by{' '} {slice[0].profile.displayName} {' '} and{' '} {slice[1].profile.displayName} ) ) : serverCount > 1 ? ( // 1-n followers, including blocks Followed by{' '} {slice[0].profile.displayName} {' '} and{' '} ) : ( // only 1 Followed by{' '} {slice[0].profile.displayName} )} ) : null} )} ) } function EmptyFallback({show}: {show?: boolean}) { const t = useTheme() if (!show) return null return ( Not followed by anyone you’re following ) }