diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 997a366a..9a427ad4 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -159,6 +159,7 @@ export type LogEvents = { | 'AvatarButton' | 'StarterPackProfilesList' | 'FeedInterstitial' + | 'ProfileHeaderSuggestedFollows' } 'profile:unfollow': { logContext: @@ -173,6 +174,7 @@ export type LogEvents = { | 'AvatarButton' | 'StarterPackProfilesList' | 'FeedInterstitial' + | 'ProfileHeaderSuggestedFollows' } 'chat:create': { logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog' @@ -211,6 +213,8 @@ export type LogEvents = { 'feed:interstitial:profileCard:press': {} 'feed:interstitial:feedCard:press': {} + 'profile:header:suggestedFollowsCard:press': {} + 'debug:followingPrefs': { followingShowRepliesFromPref: 'all' | 'following' | 'off' followingRepliesMinLikePref: number diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts index a1244721..f5d51a97 100644 --- a/src/state/queries/suggested-follows.ts +++ b/src/state/queries/suggested-follows.ts @@ -106,6 +106,7 @@ export function useSuggestedFollowsQuery(options?: SuggestedFollowsOptions) { export function useSuggestedFollowsByActorQuery({did}: {did: string}) { const agent = useAgent() return useQuery({ + gcTime: 0, queryKey: suggestedFollowsByActorQueryKey(did), queryFn: async () => { const res = await agent.app.bsky.graph.getSuggestedFollowsByActor({ diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx index c7df4d75..356b3f09 100644 --- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx +++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx @@ -1,32 +1,60 @@ import React from 'react' -import {Pressable, ScrollView, StyleSheet, View} from 'react-native' -import {AppBskyActorDefs, moderateProfile} from '@atproto/api' -import { - FontAwesomeIcon, - FontAwesomeIconStyle, -} from '@fortawesome/react-native-fontawesome' +import {ScrollView, View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' -import {useProfileShadow} from '#/state/cache/profile-shadow' +import {logEvent} from '#/lib/statsig/statsig' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows' -import {useAnalytics} from 'lib/analytics/analytics' -import {usePalette} from 'lib/hooks/usePalette' -import {makeProfileLink} from 'lib/routes/links' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' import {isWeb} from 'platform/detection' -import {Button} from 'view/com/util/forms/Button' -import {Link} from 'view/com/util/Link' -import {Text} from 'view/com/util/text/Text' -import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' -import * as Toast from '../util/Toast' +import {atoms as a, useTheme, ViewStyleProp} from '#/alf' +import {Button, ButtonIcon} from '#/components/Button' +import {TimesLarge_Stroke2_Corner0_Rounded as X} from '#/components/icons/Times' +import * as ProfileCard from '#/components/ProfileCard' +import {Text} from '#/components/Typography' -const OUTER_PADDING = 10 -const INNER_PADDING = 14 -const TOTAL_HEIGHT = 250 +const OUTER_PADDING = a.p_md.padding +const INNER_PADDING = a.p_lg.padding +const TOTAL_HEIGHT = 232 +const MOBILE_CARD_WIDTH = 300 + +function CardOuter({ + children, + style, +}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) { + const t = useTheme() + return ( + + {children} + + ) +} + +export function SuggestedFollowPlaceholder() { + const t = useTheme() + return ( + + + + + + + + + ) +} export function ProfileHeaderSuggestedFollows({ actorDid, @@ -35,47 +63,55 @@ export function ProfileHeaderSuggestedFollows({ actorDid: string requestDismiss: () => void }) { - const pal = usePalette('default') - const {isLoading, data} = useSuggestedFollowsByActorQuery({ - did: actorDid, - }) + const t = useTheme() + const {_} = useLingui() + const {isLoading: isSuggestionsLoading, data} = + useSuggestedFollowsByActorQuery({ + did: actorDid, + }) + const moderationOpts = useModerationOpts() + const isLoading = isSuggestionsLoading || !moderationOpts + return ( + style={[ + t.atoms.bg_contrast_25, + { + height: '100%', + paddingTop: INNER_PADDING / 2, + }, + ]}> - - Suggested for you + style={[ + a.flex_row, + a.justify_between, + a.align_center, + a.pt_xs, + { + paddingBottom: INNER_PADDING / 2, + paddingLeft: INNER_PADDING, + paddingRight: INNER_PADDING / 2, + }, + ]}> + + Similar accounts - - - + label={_(msg`Dismiss`)} + size="xsmall" + variant="ghost" + color="secondary" + shape="round"> + + - {isLoading ? ( - <> - - - - - - - - ) : data ? ( - data.suggestions - .filter(s => (s.associated?.labeler ? false : true)) - .map(profile => ( - - )) - ) : ( - - )} + snapToInterval={MOBILE_CARD_WIDTH + a.gap_sm.gap} + decelerationRate="fast"> + + {isLoading ? ( + <> + + + + + + + ) : data ? ( + data.suggestions + .filter(s => (s.associated?.labeler ? false : true)) + .map(profile => ( + { + logEvent('profile:header:suggestedFollowsCard:press', {}) + }} + style={[a.flex_1]}> + {({hovered, pressed}) => ( + + + + + + + + + + + )} + + )) + ) : ( + + )} + ) } - -function SuggestedFollowSkeleton() { - const pal = usePalette('default') - return ( - - - - - - - ) -} - -function SuggestedFollow({ - profile: profileUnshadowed, -}: { - profile: AppBskyActorDefs.ProfileView -}) { - const {track} = useAnalytics() - const pal = usePalette('default') - const {_} = useLingui() - const moderationOpts = useModerationOpts() - const profile = useProfileShadow(profileUnshadowed) - const [queueFollow, queueUnfollow] = useProfileFollowMutationQueue( - profile, - 'ProfileHeaderSuggestedFollows', - ) - - const onPressFollow = React.useCallback(async () => { - try { - track('ProfileHeader:SuggestedFollowFollowed') - await queueFollow() - } catch (e: any) { - if (e?.name !== 'AbortError') { - Toast.show(_(msg`An issue occurred, please try again.`), 'xmark') - } - } - }, [queueFollow, track, _]) - - const onPressUnfollow = React.useCallback(async () => { - try { - await queueUnfollow() - } catch (e: any) { - if (e?.name !== 'AbortError') { - Toast.show(_(msg`An issue occurred, please try again.`), 'xmark') - } - } - }, [queueUnfollow, _]) - - if (!moderationOpts) { - return null - } - const moderation = moderateProfile(profile, moderationOpts) - const following = profile.viewer?.following - return ( - - - - - - - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} - - - {sanitizeHandle(profile.handle, '@')} - - - -