diff options
-rw-r--r-- | src/components/KnownFollowers.tsx | 20 | ||||
-rw-r--r-- | src/lib/statsig/gates.ts | 1 | ||||
-rw-r--r-- | src/view/com/profile/ProfileCard.tsx | 103 | ||||
-rw-r--r-- | src/view/screens/Search/Explore.tsx | 15 |
4 files changed, 64 insertions, 75 deletions
diff --git a/src/components/KnownFollowers.tsx b/src/components/KnownFollowers.tsx index 7b861dc66..4017a7b0b 100644 --- a/src/components/KnownFollowers.tsx +++ b/src/components/KnownFollowers.tsx @@ -12,6 +12,7 @@ import {Link, LinkProps} from '#/components/Link' import {Text} from '#/components/Typography' const AVI_SIZE = 30 +const AVI_SIZE_SMALL = 20 const AVI_BORDER = 1 /** @@ -30,10 +31,12 @@ export function KnownFollowers({ profile, moderationOpts, onLinkPress, + minimal, }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts onLinkPress?: LinkProps['onPress'] + minimal?: boolean }) { const cache = React.useRef<Map<string, AppBskyActorDefs.KnownFollowers>>( new Map(), @@ -59,6 +62,7 @@ export function KnownFollowers({ cachedKnownFollowers={cachedKnownFollowers} moderationOpts={moderationOpts} onLinkPress={onLinkPress} + minimal={minimal} /> ) } @@ -71,11 +75,13 @@ function KnownFollowersInner({ moderationOpts, cachedKnownFollowers, onLinkPress, + minimal, }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts cachedKnownFollowers: AppBskyActorDefs.KnownFollowers onLinkPress?: LinkProps['onPress'] + minimal?: boolean }) { const t = useTheme() const {_} = useLingui() @@ -110,6 +116,8 @@ function KnownFollowersInner({ */ if (slice.length === 0) return null + const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE + return ( <Link label={_( @@ -120,7 +128,7 @@ function KnownFollowersInner({ style={[ a.flex_1, a.flex_row, - a.gap_md, + minimal ? a.gap_sm : a.gap_md, a.align_center, {marginLeft: -AVI_BORDER}, ]}> @@ -129,8 +137,8 @@ function KnownFollowersInner({ <View style={[ { - height: AVI_SIZE, - width: AVI_SIZE + (slice.length - 1) * a.gap_md.gap, + height: SIZE, + width: SIZE + (slice.length - 1) * a.gap_md.gap, }, pressed && { opacity: 0.5, @@ -145,14 +153,14 @@ function KnownFollowersInner({ { borderWidth: AVI_BORDER, borderColor: t.atoms.bg.backgroundColor, - width: AVI_SIZE + AVI_BORDER * 2, - height: AVI_SIZE + AVI_BORDER * 2, + width: SIZE + AVI_BORDER * 2, + height: SIZE + AVI_BORDER * 2, left: i * a.gap_md.gap, zIndex: AVI_BORDER - i, }, ]}> <UserAvatar - size={AVI_SIZE} + size={SIZE} avatar={prof.avatar} moderation={moderation.ui('avatar')} /> diff --git a/src/lib/statsig/gates.ts b/src/lib/statsig/gates.ts index 378b27349..1c86d01da 100644 --- a/src/lib/statsig/gates.ts +++ b/src/lib/statsig/gates.ts @@ -1,6 +1,7 @@ export type Gate = // Keep this alphabetic please. | 'debug_show_feedcontext' + | 'explore_page_profile_card_social_proof' | 'native_pwi_disabled' | 'new_user_guided_tour' | 'new_user_progress_guide' diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx index d18103f30..9458fbaf8 100644 --- a/src/view/com/profile/ProfileCard.tsx +++ b/src/view/com/profile/ProfileCard.tsx @@ -5,7 +5,6 @@ import { moderateProfile, ModerationDecision, } from '@atproto/api' -import {Trans} from '@lingui/macro' import {useQueryClient} from '@tanstack/react-query' import {useProfileShadow} from '#/state/cache/profile-shadow' @@ -19,12 +18,16 @@ import {sanitizeDisplayName} from 'lib/strings/display-names' import {sanitizeHandle} from 'lib/strings/handles' import {s} from 'lib/styles' import {precacheProfile} from 'state/queries/profile' +import {atoms as a} from '#/alf' +import { + KnownFollowers, + shouldShowKnownFollowers, +} from '#/components/KnownFollowers' import {Link} from '../util/Link' import {Text} from '../util/text/Text' import {PreviewableUserAvatar} from '../util/UserAvatar' import {FollowButton} from './FollowButton' import hairlineWidth = StyleSheet.hairlineWidth -import {atoms as a} from '#/alf' import * as Pills from '#/components/Pills' export function ProfileCard({ @@ -33,22 +36,22 @@ export function ProfileCard({ noModFilter, noBg, noBorder, - followers, renderButton, onPress, style, + showKnownFollowers, }: { testID?: string profile: AppBskyActorDefs.ProfileViewBasic noModFilter?: boolean noBg?: boolean noBorder?: boolean - followers?: AppBskyActorDefs.ProfileView[] | undefined renderButton?: ( profile: Shadow<AppBskyActorDefs.ProfileViewBasic>, ) => React.ReactNode onPress?: () => void style?: StyleProp<ViewStyle> + showKnownFollowers?: boolean }) { const queryClient = useQueryClient() const pal = usePalette('default') @@ -70,6 +73,11 @@ export function ProfileCard({ return null } + const knownFollowersVisible = + showKnownFollowers && + shouldShowKnownFollowers(profile.viewer?.knownFollowers) && + moderationOpts + return ( <Link testID={testID} @@ -118,14 +126,30 @@ export function ProfileCard({ <View style={styles.layoutButton}>{renderButton(profile)}</View> ) : undefined} </View> - {profile.description ? ( + {profile.description || knownFollowersVisible ? ( <View style={styles.details}> - <Text style={pal.text} numberOfLines={4}> - {profile.description as string} - </Text> + {profile.description ? ( + <Text style={pal.text} numberOfLines={4}> + {profile.description as string} + </Text> + ) : null} + {knownFollowersVisible ? ( + <View + style={[ + a.flex_row, + a.align_center, + a.gap_sm, + !!profile.description && a.mt_md, + ]}> + <KnownFollowers + minimal + profile={profile} + moderationOpts={moderationOpts} + /> + </View> + ) : null} </View> ) : null} - <FollowersList followers={followers} /> </Link> ) } @@ -155,73 +179,20 @@ export function ProfileCardPills({ ) } -function FollowersList({ - followers, -}: { - followers?: AppBskyActorDefs.ProfileView[] | undefined -}) { - const pal = usePalette('default') - const moderationOpts = useModerationOpts() - - const followersWithMods = React.useMemo(() => { - if (!followers || !moderationOpts) { - return [] - } - - return followers - .map(f => ({ - f, - mod: moderateProfile(f, moderationOpts), - })) - .filter(({mod}) => !mod.ui('profileList').filter) - }, [followers, moderationOpts]) - - if (!followersWithMods?.length) { - return null - } - - return ( - <View style={styles.followedBy}> - <Text - type="sm" - style={[styles.followsByDesc, pal.textLight]} - numberOfLines={2} - lineHeight={1.2}> - <Trans> - Followed by{' '} - {followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')} - </Trans> - </Text> - {followersWithMods.slice(0, 3).map(({f, mod}) => ( - <View key={f.did} style={styles.followedByAviContainer}> - <View style={[styles.followedByAvi, pal.view]}> - <PreviewableUserAvatar - size={32} - profile={f} - moderation={mod.ui('avatar')} - type={f.associated?.labeler ? 'labeler' : 'user'} - /> - </View> - </View> - ))} - </View> - ) -} - export function ProfileCardWithFollowBtn({ profile, noBg, noBorder, - followers, onPress, logContext = 'ProfileCard', + showKnownFollowers, }: { - profile: AppBskyActorDefs.ProfileViewBasic + profile: AppBskyActorDefs.ProfileView noBg?: boolean noBorder?: boolean - followers?: AppBskyActorDefs.ProfileView[] | undefined onPress?: () => void logContext?: 'ProfileCard' | 'StarterPackProfilesList' + showKnownFollowers?: boolean }) { const {currentAccount} = useSession() const isMe = profile.did === currentAccount?.did @@ -231,7 +202,6 @@ export function ProfileCardWithFollowBtn({ profile={profile} noBg={noBg} noBorder={noBorder} - followers={followers} renderButton={ isMe ? undefined @@ -240,6 +210,7 @@ export function ProfileCardWithFollowBtn({ ) } onPress={onPress} + showKnownFollowers={!isMe && showKnownFollowers} /> ) } diff --git a/src/view/screens/Search/Explore.tsx b/src/view/screens/Search/Explore.tsx index 85e8ffa4e..05fd85eff 100644 --- a/src/view/screens/Search/Explore.tsx +++ b/src/view/screens/Search/Explore.tsx @@ -10,6 +10,7 @@ import { import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useGate} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {isWeb} from '#/platform/detection' import {useModerationOpts} from '#/state/preferences/moderation-opts' @@ -241,7 +242,7 @@ type ExploreScreenItems = | { type: 'profile' key: string - profile: AppBskyActorDefs.ProfileViewBasic + profile: AppBskyActorDefs.ProfileView } | { type: 'feed' @@ -291,6 +292,7 @@ export function Explore() { error: feedsError, fetchNextPage: fetchNextFeedsPage, } = useGetPopularFeedsQuery({limit: 10}) + const gate = useGate() const isLoadingMoreProfiles = isFetchingNextProfilesPage && !isLoadingProfiles const onLoadMoreProfiles = React.useCallback(async () => { @@ -492,7 +494,14 @@ export function Explore() { case 'profile': { return ( <View style={[a.border_b, t.atoms.border_contrast_low]}> - <ProfileCardWithFollowBtn profile={item.profile} noBg noBorder /> + <ProfileCardWithFollowBtn + profile={item.profile} + noBg + noBorder + showKnownFollowers={gate( + 'explore_page_profile_card_social_proof', + )} + /> </View> ) } @@ -555,7 +564,7 @@ export function Explore() { } } }, - [t, moderationOpts], + [t, moderationOpts, gate], ) return ( |