diff --git a/assets/icons/arrowRight_stroke2_corner0_rounded.svg b/assets/icons/arrowRight_stroke2_corner0_rounded.svg new file mode 100644 index 0000000000..dbbbbc2c12 --- /dev/null +++ b/assets/icons/arrowRight_stroke2_corner0_rounded.svg @@ -0,0 +1 @@ + diff --git a/src/components/FeedSuggestedFollows/index.tsx b/src/components/FeedSuggestedFollows/index.tsx index 1973cbd721..a32369e87b 100644 --- a/src/components/FeedSuggestedFollows/index.tsx +++ b/src/components/FeedSuggestedFollows/index.tsx @@ -1,160 +1,60 @@ import React from 'react' import {View} from 'react-native' import {ScrollView} from 'react-native-gesture-handler' -import {AppBskyActorDefs, moderateProfile} from '@atproto/api' -import {msg} from '@lingui/macro' +import {AppBskyActorDefs} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' -import {useProfileShadow} from '#/state/cache/profile-shadow' +import {NavigationProp} from '#/lib/routes/types' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows' -import {isJustAMute} from 'lib/moderation' -import {sanitizeDisplayName} from 'lib/strings/display-names' -import {sanitizeHandle} from 'lib/strings/handles' -import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' -import {atoms as a, useTheme} from '#/alf' +import {atoms as a, useBreakpoints, useTheme, ViewStyleProp} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' -import {useFollowMethods} from '#/components/hooks/useFollowMethods' -import {useRichText} from '#/components/hooks/useRichText' +import {ArrowRight_Stroke2_Corner0_Rounded as Arrow} from '#/components/icons/Arrow' import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Refresh} from '#/components/icons/ArrowRotateCounterClockwise' -import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {CircleInfo_Stroke2_Corner0_Rounded as CircleInfo} from '#/components/icons/CircleInfo' -import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' -import {RichText} from '#/components/RichText' +import * as ProfileCard from '#/components/ProfileCard' import {Text} from '#/components/Typography' -export function SuggestedFollowCardSkeleton() { +function CardOuter({ + children, + style, +}: {children: React.ReactNode | React.ReactNode[]} & ViewStyleProp) { const t = useTheme() + const {gtMobile} = useBreakpoints() return ( - - - - - - - - - - - - - - - - + {children} ) } -export function SuggestedFollowCard({ - profile: profileUnshadowed, -}: { - profile: AppBskyActorDefs.ProfileViewBasic -}) { +export function SuggestedFollowCardSkeleton() { const t = useTheme() - const {_} = useLingui() - const profile = useProfileShadow(profileUnshadowed) - const moderationOpts = useModerationOpts() - const {follow, unfollow} = useFollowMethods({ - profile, - logContext: 'FeedSuggestedFollowCard', - }) - // @ts-ignore TODO why isn't this type correct - const [descriptionRT] = useRichText(profileUnshadowed?.description ?? '') - - if (!moderationOpts) return null - const moderation = moderateProfile(profile, moderationOpts) - const modui = moderation.ui('profileList') - if (modui.filter && !isJustAMute(modui)) return null - return ( - - - + + + + + - - - - {sanitizeDisplayName( - profile.displayName || sanitizeHandle(profile.handle), - moderation.ui('displayName'), - )} - - - {sanitizeHandle(profile.handle, '@')} - - - - - - - - - - - + + ) } @@ -162,17 +62,7 @@ export function ErrorState({retry}: {retry: () => void}) { const t = useTheme() return ( <> - + Whoops, something went wrong :( - + @@ -193,12 +83,19 @@ export function ErrorState({retry}: {retry: () => void}) { } export function FeedSuggestedFollowsCards() { + const t = useTheme() + const {_} = useLingui() const { - isLoading, + isLoading: isSuggestionsLoading, data: suggestions, error, refetch, - } = useSuggestedFollowsQuery() + } = useSuggestedFollowsQuery({limit: 6}) + const moderationOpts = useModerationOpts() + const navigation = useNavigation() + const {gtMobile} = useBreakpoints() + const isLoading = isSuggestionsLoading || !moderationOpts + const maxLength = gtMobile ? 3 : 6 const profiles: AppBskyActorDefs.ProfileViewBasic[] = [] if (suggestions) { @@ -215,21 +112,69 @@ export function FeedSuggestedFollowsCards() { } } - return ( + const content = isLoading ? ( + Array(3) + .fill(0) + .map((_, i) => ) + ) : error || !profiles.length ? ( + + ) : ( + <> + {profiles.slice(0, maxLength).map(profile => ( + + + + + + + + + + + + + ))} + + ) + + return gtMobile ? ( + + {content} + + ) : ( - {isLoading ? ( - Array(3) - .fill(0) - .map((_, i) => ) - ) : error || !profiles.length ? ( - - ) : ( - profiles.map((profile, i) => ( - - )) - )} + {content} + + ) } diff --git a/src/components/ProfileCard.tsx b/src/components/ProfileCard.tsx index a6ca7627b2..ce6e6ffe8b 100644 --- a/src/components/ProfileCard.tsx +++ b/src/components/ProfileCard.tsx @@ -9,6 +9,7 @@ import { import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {LogEvents} from '#/lib/statsig/statsig' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {sanitizeHandle} from 'lib/strings/handles' @@ -87,7 +88,7 @@ export function Header({ }: { children: React.ReactElement | React.ReactElement[] }) { - return {children} + return {children} } export function Link({did, children}: {did: string} & Omit) { @@ -96,7 +97,8 @@ export function Link({did, children}: {did: string} & Omit) { to={{ screen: 'Profile', params: {name: did}, - }}> + }} + style={[a.flex_col]}> {children} ) @@ -121,6 +123,22 @@ export function Avatar({ ) } +export function AvatarPlaceholder() { + const t = useTheme() + return ( + + ) +} + export function NameAndHandle({ profile, moderationOpts, @@ -150,6 +168,36 @@ export function NameAndHandle({ ) } +export function NameAndHandlePlaceholder() { + const t = useTheme() + + return ( + + + + + + ) +} + export function Description({ profile: profileUnshadowed, }: { @@ -183,9 +231,32 @@ export function Description({ ) } +export function DescriptionPlaceholder() { + const t = useTheme() + return ( + + + + + + ) +} + export type FollowButtonProps = { profile: AppBskyActorDefs.ProfileViewBasic - logContext: 'ProfileCard' | 'StarterPackProfilesList' + logContext: LogEvents['profile:follow']['logContext'] & + LogEvents['profile:unfollow']['logContext'] } & Partial export function FollowButton(props: FollowButtonProps) { diff --git a/src/components/icons/Arrow.tsx b/src/components/icons/Arrow.tsx index d6fb635e96..0d4bc9479e 100644 --- a/src/components/icons/Arrow.tsx +++ b/src/components/icons/Arrow.tsx @@ -8,6 +8,10 @@ export const ArrowLeft_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M3 12a1 1 0 0 1 .293-.707l6-6a1 1 0 0 1 1.414 1.414L6.414 11H20a1 1 0 1 1 0 2H6.414l4.293 4.293a1 1 0 0 1-1.414 1.414l-6-6A1 1 0 0 1 3 12Z', }) +export const ArrowRight_Stroke2_Corner0_Rounded = createSinglePathSVG({ + path: 'M21 12a1 1 0 0 1-.293.707l-6 6a1 1 0 0 1-1.414-1.414L17.586 13H4a1 1 0 1 1 0-2h13.586l-4.293-4.293a1 1 0 0 1 1.414-1.414l6 6A1 1 0 0 1 21 12Z', +}) + export const ArrowBottom_Stroke2_Corner0_Rounded = createSinglePathSVG({ path: 'M12 21a1 1 0 0 1-.707-.293l-6-6a1 1 0 1 1 1.414-1.414L11 17.586V4a1 1 0 1 1 2 0v13.586l4.293-4.293a1 1 0 0 1 1.414 1.414l-6 6A1 1 0 0 1 12 21Z', }) diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts index 3efc11a51f..2eb921b094 100644 --- a/src/lib/statsig/events.ts +++ b/src/lib/statsig/events.ts @@ -147,6 +147,7 @@ export type LogEvents = { | 'ProfileHoverCard' | 'AvatarButton' | 'StarterPackProfilesList' + | 'FeedSuggestedFollowsCard' } 'profile:unfollow': { logContext: @@ -160,6 +161,7 @@ export type LogEvents = { | 'Chat' | 'AvatarButton' | 'StarterPackProfilesList' + | 'FeedSuggestedFollowsCard' } 'chat:create': { logContext: 'ProfileHeader' | 'NewChatDialog' | 'SendViaChatDialog' diff --git a/src/screens/Home/EmptyTimeline.tsx b/src/screens/Home/EmptyTimeline.tsx index 4fc3d2f743..4c70193a72 100644 --- a/src/screens/Home/EmptyTimeline.tsx +++ b/src/screens/Home/EmptyTimeline.tsx @@ -3,14 +3,13 @@ import {View} from 'react-native' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' +import {emitSoftReset} from '#/state/events' import {CenteredView} from '#/view/com/util/Views' import {atoms as a} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {Divider} from '#/components/Divider' import {FeedSuggestedFollowsCards} from '#/components/FeedSuggestedFollows' -import {IconCircle} from '#/components/IconCircle' import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as Refresh} from '#/components/icons/ArrowRotateCounterClockwise' -import {FilterTimeline_Stroke2_Corner0_Rounded as FilterTimeline} from '#/components/icons/FilterTimeline' import {Text} from '#/components/Typography' export function EmptyTimeline() { @@ -19,22 +18,17 @@ export function EmptyTimeline() { return ( - - - - Welcome to your timeline + + Your Following feed is empty :( - + - Your timeline is where you can see posts from people you follow. - It's always chronologically sorted, so you'll never miss a post. + Follow some users to see what's happening. Their content will appear + here in chronological order. - - - Follow some people to get started! - + @@ -54,12 +48,13 @@ export function EmptyTimeline() {