diff options
author | Eric Bailey <git@esb.lol> | 2024-08-08 09:19:51 -0500 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-08-08 09:19:51 -0500 |
commit | 1e3b2d6f42839501ce47f88a19ffd477f1e2f82d (patch) | |
tree | ad6d439aa5d61a8ebf69e72be6eed579caafcd3f | |
parent | af5262682eac63a54fb2f6351a5894b647251ab4 (diff) | |
download | voidsky-1e3b2d6f42839501ce47f88a19ffd477f1e2f82d.tar.zst |
ALF suggested follows in profile header (#4828)
* Refactor ProfileHeaderSuggestedFollows * Load fresh data every time * Oops, missed a file * Update ProfileCard.Link usage, tweak copy
-rw-r--r-- | src/lib/statsig/events.ts | 4 | ||||
-rw-r--r-- | src/state/queries/suggested-follows.ts | 1 | ||||
-rw-r--r-- | src/view/com/profile/ProfileHeaderSuggestedFollows.tsx | 379 |
3 files changed, 155 insertions, 229 deletions
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 997a366a4..9a427ad40 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 a1244721a..f5d51a974 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<AppBskyGraphGetSuggestedFollowsByActor.OutputSchema, Error>({ + 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 c7df4d75b..356b3f09c 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 ( + <View + style={[ + a.w_full, + a.p_lg, + a.rounded_md, + a.border, + t.atoms.bg, + t.atoms.border_contrast_low, + { + width: MOBILE_CARD_WIDTH, + }, + style, + ]}> + {children} + </View> + ) +} + +export function SuggestedFollowPlaceholder() { + const t = useTheme() + return ( + <CardOuter style={[a.gap_sm, t.atoms.border_contrast_low]}> + <ProfileCard.Header> + <ProfileCard.AvatarPlaceholder /> + <ProfileCard.NameAndHandlePlaceholder /> + </ProfileCard.Header> + + <ProfileCard.DescriptionPlaceholder /> + </CardOuter> + ) +} 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 ( <View style={{paddingVertical: OUTER_PADDING, height: TOTAL_HEIGHT}} pointerEvents="box-none"> <View pointerEvents="box-none" - style={{ - backgroundColor: pal.viewLight.backgroundColor, - height: '100%', - paddingTop: INNER_PADDING / 2, - }}> + style={[ + t.atoms.bg_contrast_25, + { + height: '100%', + paddingTop: INNER_PADDING / 2, + }, + ]}> <View pointerEvents="box-none" - style={{ - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingTop: 4, - paddingBottom: INNER_PADDING / 2, - paddingLeft: INNER_PADDING, - paddingRight: INNER_PADDING / 2, - }}> - <Text type="sm-bold" style={[pal.textLight]}> - <Trans>Suggested for you</Trans> + style={[ + a.flex_row, + a.justify_between, + a.align_center, + a.pt_xs, + { + paddingBottom: INNER_PADDING / 2, + paddingLeft: INNER_PADDING, + paddingRight: INNER_PADDING / 2, + }, + ]}> + <Text style={[a.text_md, a.font_bold, t.atoms.text_contrast_medium]}> + <Trans>Similar accounts</Trans> </Text> - <Pressable - accessibilityRole="button" + <Button onPress={requestDismiss} hitSlop={10} - style={{padding: INNER_PADDING / 2}}> - <FontAwesomeIcon - icon="x" - size={12} - style={pal.textLight as FontAwesomeIconStyle} - /> - </Pressable> + label={_(msg`Dismiss`)} + size="xsmall" + variant="ghost" + color="secondary" + shape="round"> + <ButtonIcon icon={X} size="sm" /> + </Button> </View> <ScrollView @@ -83,187 +119,72 @@ export function ProfileHeaderSuggestedFollows({ showsHorizontalScrollIndicator={isWeb} persistentScrollbar={true} scrollIndicatorInsets={{bottom: 0}} - scrollEnabled={true} - contentContainerStyle={{ - alignItems: 'flex-start', - paddingLeft: INNER_PADDING / 2, - paddingBottom: INNER_PADDING, - }}> - {isLoading ? ( - <> - <SuggestedFollowSkeleton /> - <SuggestedFollowSkeleton /> - <SuggestedFollowSkeleton /> - <SuggestedFollowSkeleton /> - <SuggestedFollowSkeleton /> - <SuggestedFollowSkeleton /> - </> - ) : data ? ( - data.suggestions - .filter(s => (s.associated?.labeler ? false : true)) - .map(profile => ( - <SuggestedFollow key={profile.did} profile={profile} /> - )) - ) : ( - <View /> - )} + snapToInterval={MOBILE_CARD_WIDTH + a.gap_sm.gap} + decelerationRate="fast"> + <View + style={[ + a.flex_row, + a.gap_sm, + { + paddingHorizontal: INNER_PADDING, + paddingBottom: INNER_PADDING, + }, + ]}> + {isLoading ? ( + <> + <SuggestedFollowPlaceholder /> + <SuggestedFollowPlaceholder /> + <SuggestedFollowPlaceholder /> + <SuggestedFollowPlaceholder /> + <SuggestedFollowPlaceholder /> + </> + ) : data ? ( + data.suggestions + .filter(s => (s.associated?.labeler ? false : true)) + .map(profile => ( + <ProfileCard.Link + key={profile.did} + profile={profile} + onPress={() => { + logEvent('profile:header:suggestedFollowsCard:press', {}) + }} + style={[a.flex_1]}> + {({hovered, pressed}) => ( + <CardOuter + style={[ + a.flex_1, + (hovered || pressed) && t.atoms.border_contrast_high, + ]}> + <ProfileCard.Outer> + <ProfileCard.Header> + <ProfileCard.Avatar + profile={profile} + moderationOpts={moderationOpts} + /> + <ProfileCard.NameAndHandle + profile={profile} + moderationOpts={moderationOpts} + /> + <ProfileCard.FollowButton + profile={profile} + moderationOpts={moderationOpts} + logContext="ProfileHeaderSuggestedFollows" + color="secondary_inverted" + shape="round" + /> + </ProfileCard.Header> + <ProfileCard.Description profile={profile} /> + </ProfileCard.Outer> + </CardOuter> + )} + </ProfileCard.Link> + )) + ) : ( + <View /> + )} + </View> </ScrollView> </View> </View> ) } - -function SuggestedFollowSkeleton() { - const pal = usePalette('default') - return ( - <View - style={[ - styles.suggestedFollowCardOuter, - { - backgroundColor: pal.view.backgroundColor, - }, - ]}> - <View - style={{ - height: 60, - width: 60, - borderRadius: 60, - backgroundColor: pal.viewLight.backgroundColor, - opacity: 0.6, - }} - /> - <View - style={{ - height: 17, - width: 70, - borderRadius: 4, - backgroundColor: pal.viewLight.backgroundColor, - marginTop: 12, - marginBottom: 4, - }} - /> - <View - style={{ - height: 12, - width: 70, - borderRadius: 4, - backgroundColor: pal.viewLight.backgroundColor, - marginBottom: 12, - opacity: 0.6, - }} - /> - <View - style={{ - height: 32, - borderRadius: 32, - width: '100%', - backgroundColor: pal.viewLight.backgroundColor, - }} - /> - </View> - ) -} - -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 ( - <Link - href={makeProfileLink(profile)} - title={profile.handle} - asAnchor - anchorNoUnderline> - <View - style={[ - styles.suggestedFollowCardOuter, - { - backgroundColor: pal.view.backgroundColor, - }, - ]}> - <PreviewableUserAvatar - size={60} - profile={profile} - avatar={profile.avatar} - moderation={moderation.ui('avatar')} - /> - - <View style={{width: '100%', paddingVertical: 12}}> - <Text - type="xs-medium" - style={[pal.text, {textAlign: 'center'}]} - numberOfLines={1}> - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} - </Text> - <Text - type="xs-medium" - style={[pal.textLight, {textAlign: 'center'}]} - numberOfLines={1}> - {sanitizeHandle(profile.handle, '@')} - </Text> - </View> - - <Button - label={following ? _(msg`Unfollow`) : _(msg`Follow`)} - type="inverted" - labelStyle={{textAlign: 'center'}} - onPress={following ? onPressUnfollow : onPressFollow} - /> - </View> - </Link> - ) -} - -const styles = StyleSheet.create({ - suggestedFollowCardOuter: { - marginHorizontal: INNER_PADDING / 2, - paddingTop: 10, - paddingBottom: 12, - paddingHorizontal: 10, - borderRadius: 8, - width: 130, - alignItems: 'center', - overflow: 'hidden', - flexShrink: 1, - }, -}) |