about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2024-07-09 17:10:50 +0100
committerGitHub <noreply@github.com>2024-07-09 11:10:50 -0500
commit2d0eefebc338eee0d5d7e3e4c02bd6bba7f6baa0 (patch)
treef4a6b16a96c1ba0910e94359105857c9baadd51b /src
parent4360087ced80a5633781cf6f1c307606a1b0a210 (diff)
downloadvoidsky-2d0eefebc338eee0d5d7e3e4c02bd6bba7f6baa0.tar.zst
Add social proof to suggested follows (#4602)
* replace unused `followers` prop with social proof

* Introduce 'minimal' version

* Gate social proof one explore page, fix space if no desc

* Use smaller avis for minimal

---------

Co-authored-by: Eric Bailey <git@esb.lol>
Diffstat (limited to 'src')
-rw-r--r--src/components/KnownFollowers.tsx20
-rw-r--r--src/lib/statsig/gates.ts1
-rw-r--r--src/view/com/profile/ProfileCard.tsx103
-rw-r--r--src/view/screens/Search/Explore.tsx15
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 (