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() {