Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add social proof to suggested follows #4602

Merged
merged 5 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions src/components/KnownFollowers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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

/**
Expand All @@ -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(),
Expand All @@ -59,6 +62,7 @@ export function KnownFollowers({
cachedKnownFollowers={cachedKnownFollowers}
moderationOpts={moderationOpts}
onLinkPress={onLinkPress}
minimal={minimal}
/>
)
}
Expand All @@ -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()
Expand Down Expand Up @@ -110,6 +116,8 @@ function KnownFollowersInner({
*/
if (slice.length === 0) return null

const SIZE = minimal ? AVI_SIZE_SMALL : AVI_SIZE

return (
<Link
label={_(
Expand All @@ -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},
]}>
Expand All @@ -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,
Expand All @@ -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')}
/>
Expand Down
1 change: 1 addition & 0 deletions src/lib/statsig/gates.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down
103 changes: 37 additions & 66 deletions src/view/com/profile/ProfileCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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({
Expand All @@ -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')
Expand All @@ -70,6 +73,11 @@ export function ProfileCard({
return null
}

const knownFollowersVisible =
showKnownFollowers &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) &&
moderationOpts

return (
<Link
testID={testID}
Expand Down Expand Up @@ -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>
)
}
Expand Down Expand Up @@ -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
Expand All @@ -231,7 +202,6 @@ export function ProfileCardWithFollowBtn({
profile={profile}
noBg={noBg}
noBorder={noBorder}
followers={followers}
renderButton={
isMe
? undefined
Expand All @@ -240,6 +210,7 @@ export function ProfileCardWithFollowBtn({
)
}
onPress={onPress}
showKnownFollowers={!isMe && showKnownFollowers}
/>
)
}
Expand Down
15 changes: 12 additions & 3 deletions src/view/screens/Search/Explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -241,7 +242,7 @@ type ExploreScreenItems =
| {
type: 'profile'
key: string
profile: AppBskyActorDefs.ProfileViewBasic
profile: AppBskyActorDefs.ProfileView
}
| {
type: 'feed'
Expand Down Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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>
)
}
Expand Down Expand Up @@ -555,7 +564,7 @@ export function Explore() {
}
}
},
[t, moderationOpts],
[t, moderationOpts, gate],
)

return (
Expand Down
Loading