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
This commit is contained in:
jim
2025-07-29 17:42:30 -07:00
committed by GitHub
2 changed files with 162 additions and 86 deletions
+126 -77
View File
@@ -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>
<ProfileCard.DescriptionPlaceholder numberOfLines={2} />
return (
<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>
<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()
+36 -9
View File
@@ -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
/>