diff options
author | Caidan <caidan@internet.dev> | 2025-08-21 11:56:17 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-08-21 21:56:17 +0300 |
commit | eabcd9150d3513988f5b3c47b95a601d5f1bf738 (patch) | |
tree | 1a07a27f9d6c4fb9d675f75e9559071a408077f5 /src/screens | |
parent | d900d0b7a79f2edfbd3865c2484694a0de61a35c (diff) | |
download | voidsky-eabcd9150d3513988f5b3c47b95a601d5f1bf738.tar.zst |
[APP-1357] profile header follow recommendations (#8784)
Diffstat (limited to 'src/screens')
-rw-r--r-- | src/screens/Profile/Header/ProfileHeaderStandard.tsx | 329 | ||||
-rw-r--r-- | src/screens/Profile/Header/Shell.tsx | 2 | ||||
-rw-r--r-- | src/screens/Profile/Header/SuggestedFollows.tsx | 45 |
3 files changed, 218 insertions, 158 deletions
diff --git a/src/screens/Profile/Header/ProfileHeaderStandard.tsx b/src/screens/Profile/Header/ProfileHeaderStandard.tsx index 2f61ba4df..1df35d5e0 100644 --- a/src/screens/Profile/Header/ProfileHeaderStandard.tsx +++ b/src/screens/Profile/Header/ProfileHeaderStandard.tsx @@ -1,4 +1,4 @@ -import React, {memo, useMemo} from 'react' +import {memo, useCallback, useMemo, useState} from 'react' import {View} from 'react-native' import { type AppBskyActorDefs, @@ -40,6 +40,7 @@ import {EditProfileDialog} from './EditProfileDialog' import {ProfileHeaderHandle} from './Handle' import {ProfileHeaderMetrics} from './Metrics' import {ProfileHeaderShell} from './Shell' +import {AnimatedProfileHeaderSuggestedFollows} from './SuggestedFollows' interface Props { profile: AppBskyActorDefs.ProfileViewDetailed @@ -73,6 +74,7 @@ let ProfileHeaderStandard = ({ const [_queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) const unblockPromptControl = Prompt.usePromptControl() const requireAuth = useRequireAuth() + const [showSuggestedFollows, setShowSuggestedFollows] = useState(false) const isBlockedUser = profile.viewer?.blocking || profile.viewer?.blockedBy || @@ -81,6 +83,7 @@ let ProfileHeaderStandard = ({ const editProfileControl = useDialogControl() const onPressFollow = () => { + setShowSuggestedFollows(true) requireAuth(async () => { try { await queueFollow() @@ -102,6 +105,7 @@ let ProfileHeaderStandard = ({ } const onPressUnfollow = () => { + setShowSuggestedFollows(false) requireAuth(async () => { try { await queueUnfollow() @@ -122,7 +126,7 @@ let ProfileHeaderStandard = ({ }) } - const unblockAccount = React.useCallback(async () => { + const unblockAccount = useCallback(async () => { try { await queueUnblock() Toast.show(_(msg({message: 'Account unblocked', context: 'toast'}))) @@ -155,174 +159,185 @@ let ProfileHeaderStandard = ({ }, [profile]) return ( - <ProfileHeaderShell - profile={profile} - moderation={moderation} - hideBackButton={hideBackButton} - isPlaceholderProfile={isPlaceholderProfile}> - <View - style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]} - pointerEvents={isIOS ? 'auto' : 'box-none'}> + <> + <ProfileHeaderShell + profile={profile} + moderation={moderation} + hideBackButton={hideBackButton} + isPlaceholderProfile={isPlaceholderProfile}> <View - style={[ - {paddingLeft: 90}, - a.flex_row, - a.align_center, - a.justify_end, - a.gap_xs, - a.pb_sm, - a.flex_wrap, - ]} + style={[a.px_lg, a.pt_md, a.pb_sm, a.overflow_hidden]} pointerEvents={isIOS ? 'auto' : 'box-none'}> - {isMe ? ( - <> - <Button - testID="profileHeaderEditProfileButton" - size="small" - color="secondary" - variant="solid" - onPress={editProfileControl.open} - label={_(msg`Edit profile`)} - style={[a.rounded_full]}> - <ButtonText> - <Trans>Edit Profile</Trans> - </ButtonText> - </Button> - <EditProfileDialog - profile={profile} - control={editProfileControl} - /> - </> - ) : profile.viewer?.blocking ? ( - profile.viewer?.blockingByList ? null : ( - <Button - testID="unblockBtn" - size="small" - color="secondary" - variant="solid" - label={_(msg`Unblock`)} - disabled={!hasSession} - onPress={() => unblockPromptControl.open()} - style={[a.rounded_full]}> - <ButtonText> - <Trans context="action">Unblock</Trans> - </ButtonText> - </Button> - ) - ) : !profile.viewer?.blockedBy ? ( - <> - {hasSession && subscriptionsAllowed && ( - <SubscribeProfileButton + <View + style={[ + {paddingLeft: 90}, + a.flex_row, + a.align_center, + a.justify_end, + a.gap_xs, + a.pb_sm, + a.flex_wrap, + ]} + pointerEvents={isIOS ? 'auto' : 'box-none'}> + {isMe ? ( + <> + <Button + testID="profileHeaderEditProfileButton" + size="small" + color="secondary" + variant="solid" + onPress={editProfileControl.open} + label={_(msg`Edit profile`)} + style={[a.rounded_full]}> + <ButtonText> + <Trans>Edit Profile</Trans> + </ButtonText> + </Button> + <EditProfileDialog profile={profile} - moderationOpts={moderationOpts} + control={editProfileControl} /> - )} - {hasSession && <MessageProfileButton profile={profile} />} - - <Button - testID={profile.viewer?.following ? 'unfollowBtn' : 'followBtn'} - size="small" - color={profile.viewer?.following ? 'secondary' : 'primary'} - variant="solid" - label={ - profile.viewer?.following - ? _(msg`Unfollow ${profile.handle}`) - : _(msg`Follow ${profile.handle}`) - } - onPress={ - profile.viewer?.following ? onPressUnfollow : onPressFollow - } - style={[a.rounded_full]}> - {!profile.viewer?.following && ( - <ButtonIcon position="left" icon={Plus} /> + </> + ) : profile.viewer?.blocking ? ( + profile.viewer?.blockingByList ? null : ( + <Button + testID="unblockBtn" + size="small" + color="secondary" + variant="solid" + label={_(msg`Unblock`)} + disabled={!hasSession} + onPress={() => unblockPromptControl.open()} + style={[a.rounded_full]}> + <ButtonText> + <Trans context="action">Unblock</Trans> + </ButtonText> + </Button> + ) + ) : !profile.viewer?.blockedBy ? ( + <> + {hasSession && subscriptionsAllowed && ( + <SubscribeProfileButton + profile={profile} + moderationOpts={moderationOpts} + /> )} - <ButtonText> - {profile.viewer?.following ? ( - <Trans>Following</Trans> - ) : profile.viewer?.followedBy ? ( - <Trans>Follow Back</Trans> - ) : ( - <Trans>Follow</Trans> + {hasSession && <MessageProfileButton profile={profile} />} + + <Button + testID={ + profile.viewer?.following ? 'unfollowBtn' : 'followBtn' + } + size="small" + color={profile.viewer?.following ? 'secondary' : 'primary'} + variant="solid" + label={ + profile.viewer?.following + ? _(msg`Unfollow ${profile.handle}`) + : _(msg`Follow ${profile.handle}`) + } + onPress={ + profile.viewer?.following ? onPressUnfollow : onPressFollow + } + style={[a.rounded_full]}> + {!profile.viewer?.following && ( + <ButtonIcon position="left" icon={Plus} /> )} - </ButtonText> - </Button> - </> - ) : null} - <ProfileMenu profile={profile} /> - </View> - <View - style={[a.flex_col, a.gap_xs, a.pb_sm, live ? a.pt_sm : a.pt_2xs]}> - <View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}> - <Text - emoji - testID="profileHeaderDisplayName" - style={[ - t.atoms.text, - gtMobile ? a.text_4xl : a.text_3xl, - a.self_start, - a.font_heavy, - a.leading_tight, - ]}> - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} - <View + <ButtonText> + {profile.viewer?.following ? ( + <Trans>Following</Trans> + ) : profile.viewer?.followedBy ? ( + <Trans>Follow Back</Trans> + ) : ( + <Trans>Follow</Trans> + )} + </ButtonText> + </Button> + </> + ) : null} + <ProfileMenu profile={profile} /> + </View> + <View + style={[a.flex_col, a.gap_xs, a.pb_sm, live ? a.pt_sm : a.pt_2xs]}> + <View style={[a.flex_row, a.align_center, a.gap_xs, a.flex_1]}> + <Text + emoji + testID="profileHeaderDisplayName" style={[ - a.pl_xs, - { - marginTop: platform({ios: 2}), - }, + t.atoms.text, + gtMobile ? a.text_4xl : a.text_3xl, + a.self_start, + a.font_heavy, + a.leading_tight, ]}> - <VerificationCheckButton profile={profile} size="lg" /> - </View> - </Text> + {sanitizeDisplayName( + profile.displayName || sanitizeHandle(profile.handle), + moderation.ui('displayName'), + )} + <View + style={[ + a.pl_xs, + { + marginTop: platform({ios: 2}), + }, + ]}> + <VerificationCheckButton profile={profile} size="lg" /> + </View> + </Text> + </View> + <ProfileHeaderHandle profile={profile} /> </View> - <ProfileHeaderHandle profile={profile} /> - </View> - {!isPlaceholderProfile && !isBlockedUser && ( - <View style={a.gap_md}> - <ProfileHeaderMetrics profile={profile} /> - {descriptionRT && !moderation.ui('profileView').blur ? ( - <View pointerEvents="auto"> - <RichText - testID="profileHeaderDescription" - style={[a.text_md]} - numberOfLines={15} - value={descriptionRT} - enableTags - authorHandle={profile.handle} - /> - </View> - ) : undefined} - - {!isMe && - !isBlockedUser && - shouldShowKnownFollowers(profile.viewer?.knownFollowers) && ( - <View style={[a.flex_row, a.align_center, a.gap_sm]}> - <KnownFollowers - profile={profile} - moderationOpts={moderationOpts} + {!isPlaceholderProfile && !isBlockedUser && ( + <View style={a.gap_md}> + <ProfileHeaderMetrics profile={profile} /> + {descriptionRT && !moderation.ui('profileView').blur ? ( + <View pointerEvents="auto"> + <RichText + testID="profileHeaderDescription" + style={[a.text_md]} + numberOfLines={15} + value={descriptionRT} + enableTags + authorHandle={profile.handle} /> </View> - )} - </View> - )} - </View> - <Prompt.Basic - control={unblockPromptControl} - title={_(msg`Unblock Account?`)} - description={_( - msg`The account will be able to interact with you after unblocking.`, - )} - onConfirm={unblockAccount} - confirmButtonCta={ - profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) - } - confirmButtonColor="negative" + ) : undefined} + + {!isMe && + !isBlockedUser && + shouldShowKnownFollowers(profile.viewer?.knownFollowers) && ( + <View style={[a.flex_row, a.align_center, a.gap_sm]}> + <KnownFollowers + profile={profile} + moderationOpts={moderationOpts} + /> + </View> + )} + </View> + )} + </View> + + <Prompt.Basic + control={unblockPromptControl} + title={_(msg`Unblock Account?`)} + description={_( + msg`The account will be able to interact with you after unblocking.`, + )} + onConfirm={unblockAccount} + confirmButtonCta={ + profile.viewer?.blocking ? _(msg`Unblock`) : _(msg`Block`) + } + confirmButtonColor="negative" + /> + </ProfileHeaderShell> + + <AnimatedProfileHeaderSuggestedFollows + isExpanded={showSuggestedFollows} + actorDid={profile.did} /> - </ProfileHeaderShell> + </> ) } + ProfileHeaderStandard = memo(ProfileHeaderStandard) export {ProfileHeaderStandard} diff --git a/src/screens/Profile/Header/Shell.tsx b/src/screens/Profile/Header/Shell.tsx index 167be0aa8..cff0a707c 100644 --- a/src/screens/Profile/Header/Shell.tsx +++ b/src/screens/Profile/Header/Shell.tsx @@ -211,7 +211,7 @@ let ProfileHeaderShell = ({ {!isPlaceholderProfile && ( <View - style={[a.px_lg, a.py_xs]} + style={[a.px_lg, a.pt_xs, a.pb_sm]} pointerEvents={isIOS ? 'auto' : 'box-none'}> {isMe ? ( <LabelsOnMe type="account" labels={profile.labels} /> diff --git a/src/screens/Profile/Header/SuggestedFollows.tsx b/src/screens/Profile/Header/SuggestedFollows.tsx new file mode 100644 index 000000000..d005d888e --- /dev/null +++ b/src/screens/Profile/Header/SuggestedFollows.tsx @@ -0,0 +1,45 @@ +import {AccordionAnimation} from '#/lib/custom-animations/AccordionAnimation' +import {useGate} from '#/lib/statsig/statsig' +import {isAndroid} from '#/platform/detection' +import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' +import {ProfileGrid} from '#/components/FeedInterstitials' + +export function ProfileHeaderSuggestedFollows({actorDid}: {actorDid: string}) { + const {isLoading, data, error} = useSuggestedFollowsByActorQuery({ + did: actorDid, + }) + + return ( + <ProfileGrid + isSuggestionsLoading={isLoading} + profiles={data?.suggestions ?? []} + recId={data?.recId} + error={error} + viewContext="profileHeader" + /> + ) +} + +export function AnimatedProfileHeaderSuggestedFollows({ + isExpanded, + actorDid, +}: { + isExpanded: boolean + actorDid: string +}) { + const gate = useGate() + if (!gate('post_follow_profile_suggested_accounts')) return null + + /* NOTE (caidanw): + * Android does not work well with this feature yet. + * This issue stems from Android not allowing dragging on clickable elements in the profile header. + * Blocking the ability to scroll on Android is too much of a trade-off for now. + **/ + if (isAndroid) return null + + return ( + <AccordionAnimation isExpanded={isExpanded}> + <ProfileHeaderSuggestedFollows actorDid={actorDid} /> + </AccordionAnimation> + ) +} |