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:
@@ -25,18 +25,17 @@ import {
|
|||||||
type ViewStyleProp,
|
type ViewStyleProp,
|
||||||
web,
|
web,
|
||||||
} from '#/alf'
|
} from '#/alf'
|
||||||
import {Button} from '#/components/Button'
|
import {Button, ButtonText} from '#/components/Button'
|
||||||
import * as FeedCard from '#/components/FeedCard'
|
import * as FeedCard from '#/components/FeedCard'
|
||||||
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow'
|
||||||
import {Hashtag_Stroke2_Corner0_Rounded as Hashtag} from '#/components/icons/Hashtag'
|
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 {InlineLinkText} from '#/components/Link'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import type * as bsky from '#/types/bsky'
|
import type * as bsky from '#/types/bsky'
|
||||||
import {ProgressGuideList} from './ProgressGuide/List'
|
import {ProgressGuideList} from './ProgressGuide/List'
|
||||||
|
|
||||||
const MOBILE_CARD_WIDTH = 300
|
const MOBILE_CARD_WIDTH = 165
|
||||||
|
|
||||||
function CardOuter({
|
function CardOuter({
|
||||||
children,
|
children,
|
||||||
@@ -48,8 +47,8 @@ function CardOuter({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.w_full,
|
a.w_full,
|
||||||
a.p_lg,
|
a.p_md,
|
||||||
a.rounded_md,
|
a.rounded_lg,
|
||||||
a.border,
|
a.border,
|
||||||
t.atoms.bg,
|
t.atoms.bg,
|
||||||
t.atoms.border_contrast_low,
|
t.atoms.border_contrast_low,
|
||||||
@@ -65,14 +64,30 @@ function CardOuter({
|
|||||||
|
|
||||||
export function SuggestedFollowPlaceholder() {
|
export function SuggestedFollowPlaceholder() {
|
||||||
const t = useTheme()
|
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>
|
</CardOuter>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -243,10 +258,9 @@ export function ProfileGrid({
|
|||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const moderationOpts = useModerationOpts()
|
const moderationOpts = useModerationOpts()
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
const {gtMobile} = useBreakpoints()
|
const {gtMobile} = useBreakpoints()
|
||||||
const isLoading = isSuggestionsLoading || !moderationOpts
|
const isLoading = isSuggestionsLoading || !moderationOpts
|
||||||
const maxLength = gtMobile ? 4 : 6
|
const maxLength = gtMobile ? 3 : 6
|
||||||
|
|
||||||
const content = isLoading ? (
|
const content = isLoading ? (
|
||||||
Array(maxLength)
|
Array(maxLength)
|
||||||
@@ -254,7 +268,14 @@ export function ProfileGrid({
|
|||||||
.map((_, i) => (
|
.map((_, i) => (
|
||||||
<View
|
<View
|
||||||
key={i}
|
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 />
|
<SuggestedFollowPlaceholder />
|
||||||
</View>
|
</View>
|
||||||
))
|
))
|
||||||
@@ -276,44 +297,69 @@ export function ProfileGrid({
|
|||||||
}}
|
}}
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
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}) => (
|
{({hovered, pressed}) => (
|
||||||
<CardOuter
|
<CardOuter
|
||||||
style={[
|
style={[
|
||||||
a.flex_1,
|
a.flex_1,
|
||||||
|
t.atoms.shadow_sm,
|
||||||
(hovered || pressed) && t.atoms.border_contrast_high,
|
(hovered || pressed) && t.atoms.border_contrast_high,
|
||||||
]}>
|
]}>
|
||||||
<ProfileCard.Outer>
|
<ProfileCard.Outer>
|
||||||
<ProfileCard.Header>
|
<View
|
||||||
|
style={[
|
||||||
|
a.flex_col,
|
||||||
|
a.align_center,
|
||||||
|
a.gap_sm,
|
||||||
|
a.pb_sm,
|
||||||
|
a.mb_auto,
|
||||||
|
]}>
|
||||||
<ProfileCard.Avatar
|
<ProfileCard.Avatar
|
||||||
profile={profile}
|
profile={profile}
|
||||||
moderationOpts={moderationOpts}
|
moderationOpts={moderationOpts}
|
||||||
|
size={88}
|
||||||
/>
|
/>
|
||||||
<ProfileCard.NameAndHandle
|
<View style={[a.flex_col, a.align_center, a.max_w_full]}>
|
||||||
profile={profile}
|
<ProfileCard.Name
|
||||||
moderationOpts={moderationOpts}
|
profile={profile}
|
||||||
/>
|
moderationOpts={moderationOpts}
|
||||||
<ProfileCard.FollowButton
|
/>
|
||||||
profile={profile}
|
<ProfileCard.Description
|
||||||
moderationOpts={moderationOpts}
|
profile={profile}
|
||||||
logContext="FeedInterstitial"
|
numberOfLines={2}
|
||||||
shape="round"
|
style={[
|
||||||
colorInverted
|
t.atoms.text_contrast_medium,
|
||||||
onFollow={() => {
|
a.text_center,
|
||||||
logEvent('suggestedUser:follow', {
|
a.text_xs,
|
||||||
logContext:
|
]}
|
||||||
viewContext === 'feed'
|
/>
|
||||||
? 'InterstitialDiscover'
|
</View>
|
||||||
: 'InterstitialProfile',
|
</View>
|
||||||
location: 'Card',
|
|
||||||
recId,
|
<ProfileCard.FollowButton
|
||||||
position: index,
|
profile={profile}
|
||||||
})
|
moderationOpts={moderationOpts}
|
||||||
}}
|
logContext="FeedInterstitial"
|
||||||
/>
|
withIcon={false}
|
||||||
</ProfileCard.Header>
|
style={[a.rounded_sm]}
|
||||||
<ProfileCard.Description profile={profile} numberOfLines={2} />
|
onFollow={() => {
|
||||||
|
logEvent('suggestedUser:follow', {
|
||||||
|
logContext:
|
||||||
|
viewContext === 'feed'
|
||||||
|
? 'InterstitialDiscover'
|
||||||
|
: 'InterstitialProfile',
|
||||||
|
location: 'Card',
|
||||||
|
recId,
|
||||||
|
position: index,
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</ProfileCard.Outer>
|
</ProfileCard.Outer>
|
||||||
</CardOuter>
|
</CardOuter>
|
||||||
)}
|
)}
|
||||||
@@ -333,36 +379,30 @@ export function ProfileGrid({
|
|||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
a.p_lg,
|
a.p_lg,
|
||||||
a.pb_xs,
|
a.py_md,
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
a.align_center,
|
a.align_center,
|
||||||
a.justify_between,
|
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' ? (
|
{viewContext === 'profile' ? (
|
||||||
<Trans>Similar accounts</Trans>
|
<Trans>Similar accounts</Trans>
|
||||||
) : (
|
) : (
|
||||||
<Trans>Suggested for you</Trans>
|
<Trans>Suggested for you</Trans>
|
||||||
)}
|
)}
|
||||||
</Text>
|
</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>
|
</View>
|
||||||
|
|
||||||
{gtMobile ? (
|
{gtMobile ? (
|
||||||
<View style={[a.flex_1, a.px_lg, a.pt_sm, a.pb_lg, a.gap_md]}>
|
<View style={[a.px_lg, a.pb_lg]}>
|
||||||
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_sm]}>
|
<View style={[a.flex_1, a.flex_row, a.flex_wrap, a.gap_md]}>
|
||||||
{content}
|
{content}
|
||||||
</View>
|
</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>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<BlockDrawerGesture>
|
<BlockDrawerGesture>
|
||||||
@@ -371,29 +411,12 @@ export function ProfileGrid({
|
|||||||
horizontal
|
horizontal
|
||||||
showsHorizontalScrollIndicator={false}
|
showsHorizontalScrollIndicator={false}
|
||||||
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
snapToInterval={MOBILE_CARD_WIDTH + a.gap_md.gap}
|
||||||
decelerationRate="fast">
|
decelerationRate="fast"
|
||||||
<View style={[a.px_lg, a.pt_sm, a.pb_lg, a.flex_row, a.gap_md]}>
|
style={[a.overflow_visible]}>
|
||||||
|
<View style={[a.px_lg, a.pb_lg, a.flex_row, a.gap_md]}>
|
||||||
{content}
|
{content}
|
||||||
|
|
||||||
<Button
|
<SeeMoreSuggestedProfilesCard />
|
||||||
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>
|
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</View>
|
</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() {
|
export function SuggestedFeeds() {
|
||||||
const numFeedsToDisplay = 3
|
const numFeedsToDisplay = 3
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ import {useProfileFollowMutationQueue} from '#/state/queries/profile'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {PreviewableUserAvatar, UserAvatar} from '#/view/com/util/UserAvatar'
|
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 {
|
import {
|
||||||
Button,
|
Button,
|
||||||
ButtonIcon,
|
ButtonIcon,
|
||||||
@@ -136,12 +142,14 @@ export function Avatar({
|
|||||||
onPress,
|
onPress,
|
||||||
disabledPreview,
|
disabledPreview,
|
||||||
liveOverride,
|
liveOverride,
|
||||||
|
size = 40,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
onPress?: () => void
|
onPress?: () => void
|
||||||
disabledPreview?: boolean
|
disabledPreview?: boolean
|
||||||
liveOverride?: boolean
|
liveOverride?: boolean
|
||||||
|
size?: number
|
||||||
}) {
|
}) {
|
||||||
const moderation = moderateProfile(profile, moderationOpts)
|
const moderation = moderateProfile(profile, moderationOpts)
|
||||||
|
|
||||||
@@ -149,7 +157,7 @@ export function Avatar({
|
|||||||
|
|
||||||
return disabledPreview ? (
|
return disabledPreview ? (
|
||||||
<UserAvatar
|
<UserAvatar
|
||||||
size={40}
|
size={size}
|
||||||
avatar={profile.avatar}
|
avatar={profile.avatar}
|
||||||
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
type={profile.associated?.labeler ? 'labeler' : 'user'}
|
||||||
moderation={moderation.ui('avatar')}
|
moderation={moderation.ui('avatar')}
|
||||||
@@ -157,7 +165,7 @@ export function Avatar({
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<PreviewableUserAvatar
|
<PreviewableUserAvatar
|
||||||
size={40}
|
size={size}
|
||||||
profile={profile}
|
profile={profile}
|
||||||
moderation={moderation.ui('avatar')}
|
moderation={moderation.ui('avatar')}
|
||||||
onBeforePress={onPress}
|
onBeforePress={onPress}
|
||||||
@@ -166,7 +174,7 @@ export function Avatar({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function AvatarPlaceholder() {
|
export function AvatarPlaceholder({size = 40}: {size?: number}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
@@ -174,8 +182,8 @@ export function AvatarPlaceholder() {
|
|||||||
a.rounded_full,
|
a.rounded_full,
|
||||||
t.atoms.bg_contrast_25,
|
t.atoms.bg_contrast_25,
|
||||||
{
|
{
|
||||||
width: 40,
|
width: size,
|
||||||
height: 40,
|
height: size,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -274,7 +282,7 @@ export function Name({
|
|||||||
)
|
)
|
||||||
const verification = useSimpleVerificationState({profile})
|
const verification = useSimpleVerificationState({profile})
|
||||||
return (
|
return (
|
||||||
<View style={[a.flex_row, a.align_center]}>
|
<View style={[a.flex_row, a.align_center, a.max_w_full]}>
|
||||||
<Text
|
<Text
|
||||||
emoji
|
emoji
|
||||||
style={[
|
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({
|
export function Description({
|
||||||
profile: profileUnshadowed,
|
profile: profileUnshadowed,
|
||||||
numberOfLines = 3,
|
numberOfLines = 3,
|
||||||
|
style,
|
||||||
}: {
|
}: {
|
||||||
profile: bsky.profile.AnyProfileView
|
profile: bsky.profile.AnyProfileView
|
||||||
numberOfLines?: number
|
numberOfLines?: number
|
||||||
}) {
|
} & TextStyleProp) {
|
||||||
const profile = useProfileShadow(profileUnshadowed)
|
const profile = useProfileShadow(profileUnshadowed)
|
||||||
const rt = useMemo(() => {
|
const rt = useMemo(() => {
|
||||||
if (!('description' in profile)) return
|
if (!('description' in profile)) return
|
||||||
@@ -369,7 +396,7 @@ export function Description({
|
|||||||
<View style={[a.pt_xs]}>
|
<View style={[a.pt_xs]}>
|
||||||
<RichText
|
<RichText
|
||||||
value={rt}
|
value={rt}
|
||||||
style={[a.leading_snug]}
|
style={[a.leading_snug, style]}
|
||||||
numberOfLines={numberOfLines}
|
numberOfLines={numberOfLines}
|
||||||
disableLinks
|
disableLinks
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user