about summary refs log tree commit diff
diff options
context:
space:
mode:
authorjim <310223+jimmylee@users.noreply.github.com>2025-07-29 17:42:30 -0700
committerGitHub <noreply@github.com>2025-07-29 17:42:30 -0700
commitc31b0338a15b07e1b9d6a54002e35b7e3eab2f71 (patch)
tree5156053fb4be1f5327be1ba471c0549f49989aff
parent332dbc1bb4632e9fd1ffed6ea8566b8df8984f64 (diff)
parent940e00c8cb979576ae15076fa268aecda81b0e6d (diff)
downloadvoidsky-c31b0338a15b07e1b9d6a54002e35b7e3eab2f71.tar.zst
Merge pull request #8733 from internet-development/caidan/app-1330-new-design-for-suggested-for-you-interstitial
feat: new design for "Suggested for you" interstitial
-rw-r--r--src/components/FeedInterstitials.tsx201
-rw-r--r--src/components/ProfileCard.tsx45
2 files changed, 161 insertions, 85 deletions
diff --git a/src/components/FeedInterstitials.tsx b/src/components/FeedInterstitials.tsx
index a92e7be7f..2a3a00ba7 100644
--- a/src/components/FeedInterstitials.tsx
+++ b/src/components/FeedInterstitials.tsx
@@ -25,18 +25,17 @@ import {
   type ViewStyleProp,
   web,
 } from '#/alf'
-import {Button} from '#/components/Button'
+import {Button, ButtonText} from '#/components/Button'
 import * as FeedCard from '#/components/FeedCard'
 import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
 import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
-import {PersonPlus_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Person'
 import {InlineLinkText} from '#/components/Link'
 import * as ProfileCard from '#/components/ProfileCard'
 import {Text} from '#/components/Typography'
 import type * as bsky from '#/types/bsky'
 import {ProgressGuideList} from './ProgressGuide/List'
 
-const MOBILE_CARD_WIDTH = 300
+const MOBILE_CARD_WIDTH = 165
 
 function CardOuter({
   children,
@@ -48,8 +47,8 @@ function CardOuter({
     <View
       style={[
         a.w_full,
-        a.p_lg,
-        a.rounded_md,
+        a.p_md,
+        a.rounded_lg,
         a.border,
         t.atoms.bg,
         t.atoms.border_contrast_low,
@@ -65,14 +64,30 @@ function CardOuter({
 
 export function SuggestedFollowPlaceholder() {
   const t = useTheme()
+
   return (
-    <CardOuter style={[a.gap_md, t.atoms.border_contrast_low]}>
-      <ProfileCard.Header>
-        <ProfileCard.AvatarPlaceholder />
-        <ProfileCard.NameAndHandlePlaceholder />
-      </ProfileCard.Header>
+    <CardOuter
+      style={[a.gap_md, t.atoms.border_contrast_low, t.atoms.shadow_sm]}>
+      <ProfileCard.Outer>
+        <View
+          style={[a.flex_col, a.align_center, a.gap_sm, a.pb_sm, a.mb_auto]}>
+          <ProfileCard.AvatarPlaceholder size={88} />
+          <ProfileCard.NamePlaceholder />
+          <View style={[a.w_full]}>
+            <ProfileCard.DescriptionPlaceholder numberOfLines={2} />
+          </View>
+        </View>
 
-      <ProfileCard.DescriptionPlaceholder numberOfLines={2} />
+        <Button
+          label=""
+          size="small"
+          variant="solid"
+          color="secondary"
+          disabled
+          style={[a.w_full, a.rounded_sm]}>
+          <ButtonText>Follow</ButtonText>
+        </Button>
+      </ProfileCard.Outer>
     </CardOuter>
   )
 }
@@ -243,10 +258,9 @@ export function ProfileGrid({
   const t = useTheme()
   const {_} = useLingui()
   const moderationOpts = useModerationOpts()
-  const navigation = useNavigation<NavigationProp>()
   const {gtMobile} = useBreakpoints()
   const isLoading = isSuggestionsLoading || !moderationOpts
-  const maxLength = gtMobile ? 4 : 6
+  const maxLength = gtMobile ? 3 : 6
 
   const content = isLoading ? (
     Array(maxLength)
@@ -254,7 +268,14 @@ export function ProfileGrid({
       .map((_, i) => (
         <View
           key={i}
-          style={[gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}])]}>
+          style={[
+            gtMobile &&
+              web([
+                a.flex_0,
+                a.flex_grow,
+                {width: `calc(30% - ${a.gap_md.gap / 2}px)`},
+              ]),
+          ]}>
           <SuggestedFollowPlaceholder />
         </View>
       ))
@@ -276,44 +297,69 @@ export function ProfileGrid({
           }}
           style={[
             a.flex_1,
-            gtMobile && web([a.flex_0, {width: 'calc(50% - 6px)'}]),
+            gtMobile &&
+              web([
+                a.flex_0,
+                a.flex_grow,
+                {width: `calc(30% - ${a.gap_md.gap / 2}px)`},
+              ]),
           ]}>
           {({hovered, pressed}) => (
             <CardOuter
               style={[
                 a.flex_1,
+                t.atoms.shadow_sm,
                 (hovered || pressed) && t.atoms.border_contrast_high,
               ]}>
               <ProfileCard.Outer>
-                <ProfileCard.Header>
+                <View
+                  style={[
+                    a.flex_col,
+                    a.align_center,
+                    a.gap_sm,
+                    a.pb_sm,
+                    a.mb_auto,
+                  ]}>
                   <ProfileCard.Avatar
                     profile={profile}
                     moderationOpts={moderationOpts}
+                    size={88}
                   />
-                  <ProfileCard.NameAndHandle
-                    profile={profile}
-                    moderationOpts={moderationOpts}
-                  />
-                  <ProfileCard.FollowButton
-                    profile={profile}
-                    moderationOpts={moderationOpts}
-                    logContext="FeedInterstitial"
-                    shape="round"
-                    colorInverted
-                    onFollow={() => {
-                      logEvent('suggestedUser:follow', {
-                        logContext:
-                          viewContext === 'feed'
-                            ? 'InterstitialDiscover'
-                            : 'InterstitialProfile',
-                        location: 'Card',
-                        recId,
-                        position: index,
-                      })
-                    }}
-                  />
-                </ProfileCard.Header>
-                <ProfileCard.Description profile={profile} numberOfLines={2} />
+                  <View style={[a.flex_col, a.align_center, a.max_w_full]}>
+                    <ProfileCard.Name
+                      profile={profile}
+                      moderationOpts={moderationOpts}
+                    />
+                    <ProfileCard.Description
+                      profile={profile}
+                      numberOfLines={2}
+                      style={[
+                        t.atoms.text_contrast_medium,
+                        a.text_center,
+                        a.text_xs,
+                      ]}
+                    />
+                  </View>
+                </View>
+
+                <ProfileCard.FollowButton
+                  profile={profile}
+                  moderationOpts={moderationOpts}
+                  logContext="FeedInterstitial"
+                  withIcon={false}
+                  style={[a.rounded_sm]}
+                  onFollow={() => {
+                    logEvent('suggestedUser:follow', {
+                      logContext:
+                        viewContext === 'feed'
+                          ? 'InterstitialDiscover'
+                          : 'InterstitialProfile',
+                      location: 'Card',
+                      recId,
+                      position: index,
+                    })
+                  }}
+                />
               </ProfileCard.Outer>
             </CardOuter>
           )}
@@ -333,36 +379,30 @@ export function ProfileGrid({
       <View
         style={[
           a.p_lg,
-          a.pb_xs,
+          a.py_md,
           a.flex_row,
           a.align_center,
           a.justify_between,
         ]}>
-        <Text style={[a.text_sm, a.font_bold, t.atoms.text_contrast_medium]}>
+        <Text style={[a.text_sm, a.font_bold, t.atoms.text]}>
           {viewContext === 'profile' ? (
             <Trans>Similar accounts</Trans>
           ) : (
             <Trans>Suggested for you</Trans>
           )}
         </Text>
-        <Person fill={t.atoms.text_contrast_low.color} size="sm" />
+        <InlineLinkText
+          label={_(msg`See more suggested profiles on the Explore page`)}
+          to="/search">
+          <Trans>See more</Trans>
+        </InlineLinkText>
       </View>
 
       {gtMobile ? (
-        <View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}>
-          <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}>
+        <View style={[a.px_lg, a.pb_lg]}>
+          <View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
             {content}
           </View>
-
-          <View style={[a.flex_row, a.justify_end, a.align_center, a.gap_md]}>
-            <InlineLinkText
-              label={_(msg`Browse more suggestions`)}
-              to="/search"
-              style={[t.atoms.text_contrast_medium]}>
-              <Trans>Browse more suggestions</Trans>
-            </InlineLinkText>
-            <Arrow size="sm" fill={t.atoms.text_contrast_medium.color} />
-          </View>
         </View>
       ) : (
         <BlockDrawerGesture>
@@ -371,29 +411,12 @@ export function ProfileGrid({
               horizontal
               showsHorizontalScrollIndicator={false}
               snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
-              decelerationRate="fast">
-              <View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
+              decelerationRate="fast"
+              style={[a.overflow_visible]}>
+              <View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}>
                 {content}
 
-                <Button
-                  label={_(msg`Browse more accounts on the Explore page`)}
-                  onPress={() => {
-                    navigation.navigate('SearchTab')
-                  }}>
-                  <CardOuter style={[a.flex_1, {borderWidth: 0}]}>
-                    <View style={[a.flex_1, a.justify_center]}>
-                      <View style={[a.flex_row, a.px_lg]}>
-                        <Text style={[a.pr_xl, a.flex_1, a.leading_snug]}>
-                          <Trans>
-                            Browse more suggestions on the Explore page
-                          </Trans>
-                        </Text>
-
-                        <Arrow size="xl" />
-                      </View>
-                    </View>
-                  </CardOuter>
-                </Button>
+                <SeeMoreSuggestedProfilesCard />
               </View>
             </ScrollView>
           </View>
@@ -403,6 +426,32 @@ export function ProfileGrid({
   )
 }
 
+function SeeMoreSuggestedProfilesCard() {
+  const navigation = useNavigation<NavigationProp>()
+  const t = useTheme()
+  const {_} = useLingui()
+
+  return (
+    <Button
+      label={_(msg`Browse more accounts on the Explore page`)}
+      onPress={() => {
+        navigation.navigate('SearchTab')
+      }}>
+      <CardOuter style={[a.flex_1, t.atoms.shadow_sm]}>
+        <View style={[a.flex_1, a.justify_center]}>
+          <View style={[a.flex_col, a.align_center, a.gap_md]}>
+            <Text style={[a.leading_snug, a.text_center]}>
+              <Trans>See more accounts you might like</Trans>
+            </Text>
+
+            <Arrow size="xl" />
+          </View>
+        </View>
+      </CardOuter>
+    </Button>
+  )
+}
+
 export function SuggestedFeeds() {
   const numFeedsToDisplay = 3
   const t = useTheme()
diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx
index e01c27655..f12d922fd 100644
--- a/src/components/ProfileCard.tsx
+++ b/src/components/ProfileCard.tsx
@@ -20,7 +20,13 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
 import * as Toast from '#/view/com/util/Toast'
 import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
-import {atoms as a, platform, useTheme} from '#/alf'
+import {
+  atoms as a,
+  platform,
+  type TextStyleProp,
+  useTheme,
+  type ViewStyleProp,
+} from '#/alf'
 import {
   Button,
   ButtonIcon,
@@ -136,12 +142,14 @@ export function Avatar({
   onPress,
   disabledPreview,
   liveOverride,
+  size = 40,
 }: {
   profile: bsky.profile.AnyProfileView
   moderationOpts: ModerationOpts
   onPress?: () => void
   disabledPreview?: boolean
   liveOverride?: boolean
+  size?: number
 }) {
   const moderation = moderateProfile(profile, moderationOpts)
 
@@ -149,7 +157,7 @@ export function Avatar({
 
   return disabledPreview ? (
     <UserAvatar
-      size={40}
+      size={size}
       avatar={profile.avatar}
       type={profile.associated?.labeler ? 'labeler' : 'user'}
       moderation={moderation.ui('avatar')}
@@ -157,7 +165,7 @@ export function Avatar({
     />
   ) : (
     <PreviewableUserAvatar
-      size={40}
+      size={size}
       profile={profile}
       moderation={moderation.ui('avatar')}
       onBeforePress={onPress}
@@ -166,7 +174,7 @@ export function Avatar({
   )
 }
 
-export function AvatarPlaceholder() {
+export function AvatarPlaceholder({size = 40}: {size?: number}) {
   const t = useTheme()
   return (
     <View
@@ -174,8 +182,8 @@ export function AvatarPlaceholder() {
         a.rounded_full,
         t.atoms.bg_contrast_25,
         {
-          width: 40,
-          height: 40,
+          width: size,
+          height: size,
         },
       ]}
     />
@@ -274,7 +282,7 @@ export function Name({
   )
   const verification = useSimpleVerificationState({profile})
   return (
-    <View style={[a.flex_row, a.align_center]}>
+    <View style={[a.flex_row, a.align_center, a.max_w_full]}>
       <Text
         emoji
         style={[
@@ -343,13 +351,32 @@ export function NameAndHandlePlaceholder() {
   )
 }
 
+export function NamePlaceholder({style}: ViewStyleProp) {
+  const t = useTheme()
+
+  return (
+    <View
+      style={[
+        a.rounded_xs,
+        t.atoms.bg_contrast_25,
+        {
+          width: '60%',
+          height: 14,
+        },
+        style,
+      ]}
+    />
+  )
+}
+
 export function Description({
   profile: profileUnshadowed,
   numberOfLines = 3,
+  style,
 }: {
   profile: bsky.profile.AnyProfileView
   numberOfLines?: number
-}) {
+} & TextStyleProp) {
   const profile = useProfileShadow(profileUnshadowed)
   const rt = useMemo(() => {
     if (!('description' in profile)) return
@@ -369,7 +396,7 @@ export function Description({
     <View style={[a.pt_xs]}>
       <RichText
         value={rt}
-        style={[a.leading_snug]}
+        style={[a.leading_snug, style]}
         numberOfLines={numberOfLines}
         disableLinks
       />