[APP-1403] Profile empty states (#8969)

* update: Empty state component

* update type error fixes

* add empty state icon

* update translations on labels

* update: lint error for empty state

* remove unused prompt

* update type errors

* fix lint errors and update profile followers and profile follows

* updated starterpack

* fix lint errors

* address feedback

* update icons to be a react element

* update icons

* update viewbox for icons

* optimize: icons and update profile lists

* optimize: icons and update profile lists

* lint error fix

* update iconSize from the rebase
This commit is contained in:
Anastasiya Uraleva
2025-11-27 03:22:03 -08:00
committed by GitHub
parent 2d056e5be6
commit d0649e9a9e
44 changed files with 551 additions and 124 deletions
+30 -4
View File
@@ -7,7 +7,11 @@ import {
} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {
type NavigationProp,
useFocusEffect,
useNavigation,
} from '@react-navigation/native'
import {useCleanError} from '#/lib/hooks/useCleanError'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
@@ -21,12 +25,12 @@ import {useBookmarkMutation} from '#/state/queries/bookmarks/useBookmarkMutation
import {useBookmarksQuery} from '#/state/queries/bookmarks/useBookmarksQuery'
import {useSetMinimalShellMode} from '#/state/shell'
import {Post} from '#/view/com/post/Post'
import {EmptyState} from '#/view/com/util/EmptyState'
import {List} from '#/view/com/util/List'
import {PostFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {EmptyState} from '#/screens/Bookmarks/components/EmptyState'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {BookmarkFilled} from '#/components/icons/Bookmark'
import {BookmarkDeleteLarge, BookmarkFilled} from '#/components/icons/Bookmark'
import {CircleQuestion_Stroke2_Corner2_Rounded as QuestionIcon} from '#/components/icons/CircleQuestion'
import * as Layout from '#/components/Layout'
import {ListFooter} from '#/components/Lists'
@@ -259,13 +263,35 @@ function BookmarkNotFound({
)
}
function BookmarksEmpty() {
const t = useTheme()
const {_} = useLingui()
const navigation = useNavigation<NavigationProp<CommonNavigatorParams>>()
return (
<EmptyState
icon={BookmarkDeleteLarge}
message={_(msg`Nothing saved yet`)}
textStyle={[t.atoms.text_contrast_medium, a.font_medium]}
button={{
label: _(msg`Button to go back to the home timeline`),
text: _(msg`Go home`),
onPress: () => navigation.navigate('Home' as never),
size: 'small',
color: 'secondary',
}}
style={[a.pt_3xl]}
/>
)
}
function renderItem({item, index}: {item: ListItem; index: number}) {
switch (item.type) {
case 'loading': {
return <PostFeedLoadingPlaceholder />
}
case 'empty': {
return <EmptyState />
return <BookmarksEmpty />
}
case 'bookmark': {
return (
+6 -1
View File
@@ -5,6 +5,7 @@ import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {type AllNavigatorParams} from '#/lib/routes/types'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
import * as Layout from '#/components/Layout'
import {ListFooter} from '#/components/Lists'
@@ -35,7 +36,11 @@ export function NotificationsActivityListScreen({
feed={`posts|${uris}`}
disablePoll
renderEmptyState={() => (
<EmptyState icon="growth" message={_(msg`No posts here`)} />
<EmptyState
icon={EditIcon}
iconSize="2xl"
message={_(msg`No posts here`)}
/>
)}
renderEndOfFeed={() => <ListFooter />}
/>
+8 -1
View File
@@ -45,6 +45,7 @@ import {
ProfileFeedHeader,
ProfileFeedHeaderSkeleton,
} from '#/screens/Profile/components/ProfileFeedHeader'
import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
import * as Layout from '#/components/Layout'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'ProfileFeed'>
@@ -189,7 +190,13 @@ export function ProfileFeedScreenInner({
}, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
return (
<EmptyState
icon={HashtagWideIcon}
iconSize="2xl"
message={_(msg`This feed is empty.`)}
/>
)
}, [_])
const isVideoFeed = React.useMemo(() => {
+28 -6
View File
@@ -6,14 +6,20 @@ import {useQueryClient} from '@tanstack/react-query'
import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender'
import {isIOS, isNative} from '#/platform/detection'
import {type FeedDescriptor} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {
type FeedDescriptor,
RQKEY as FEED_RQKEY,
} from '#/state/queries/post-feed'
import {truncateAndInvalidate} from '#/state/queries/util'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {
EmptyState,
type EmptyStateButtonProps,
} from '#/view/com/util/EmptyState'
import {type ListRef} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {atoms as a, ios, useTheme} from '#/alf'
import {EditBig_Stroke1_Corner0_Rounded as EditIcon} from '#/components/icons/EditBig'
import {Text} from '#/components/Typography'
import {type SectionRef} from './types'
@@ -25,7 +31,11 @@ interface FeedSectionProps {
scrollElRef: ListRef
ignoreFilterFor?: string
setScrollViewTag: (tag: number | null) => void
emptyStateMessage?: string
emptyStateButton?: EmptyStateButtonProps
emptyStateIcon?: React.ComponentType<any> | React.ReactElement
}
export function ProfileFeedSection({
ref,
feed,
@@ -34,6 +44,9 @@ export function ProfileFeedSection({
scrollElRef,
ignoreFilterFor,
setScrollViewTag,
emptyStateMessage,
emptyStateButton,
emptyStateIcon,
}: FeedSectionProps) {
const {_} = useLingui()
const queryClient = useQueryClient()
@@ -44,7 +57,6 @@ export function ProfileFeedSection({
const adjustedInitialNumToRender = useInitialNumToRender({
screenHeightOffset: headerHeight,
})
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({
animated: isNative,
@@ -59,8 +71,18 @@ export function ProfileFeedSection({
}))
const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="growth" message={_(msg`No posts yet.`)} />
}, [_])
return (
<View style={[a.flex_1, a.justify_center, a.align_center]}>
<EmptyState
style={{width: '100%'}}
icon={emptyStateIcon || EditIcon}
iconSize="3xl"
message={emptyStateMessage || _(msg`No posts yet`)}
button={emptyStateButton}
/>
</View>
)
}, [_, emptyStateButton, emptyStateIcon, emptyStateMessage])
useEffect(() => {
if (isIOS && isFocused && scrollElRef.current) {
+3 -2
View File
@@ -12,6 +12,7 @@ import {type ListRef} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {atoms as a, useBreakpoints} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {BulletList_Stroke1_Corner0_Rounded as ListIcon} from '#/components/icons/BulletList'
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
interface SectionRef {
@@ -95,7 +96,7 @@ export function AboutSection({
const renderEmptyState = useCallback(() => {
return (
<View style={[a.gap_xl, a.align_center]}>
<EmptyState icon="users-slash" message={_(msg`This list is empty.`)} />
<EmptyState icon={ListIcon} message={_(msg`This list is empty.`)} />
{isOwner && (
<Button
testID="emptyStateAddUserBtn"
@@ -111,7 +112,7 @@ export function AboutSection({
)}
</View>
)
}, [_, onPressAddUser, isOwner])
}, [_, isOwner, onPressAddUser])
return (
<View>
+10 -3
View File
@@ -7,14 +7,17 @@ import {useQueryClient} from '@tanstack/react-query'
import {isNative} from '#/platform/detection'
import {listenSoftReset} from '#/state/events'
import {type FeedDescriptor} from '#/state/queries/post-feed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {
type FeedDescriptor,
RQKEY as FEED_RQKEY,
} from '#/state/queries/post-feed'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {EmptyState} from '#/view/com/util/EmptyState'
import {type ListRef} from '#/view/com/util/List'
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
import {atoms as a} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {HashtagWide_Stroke1_Corner0_Rounded as HashtagWideIcon} from '#/components/icons/Hashtag'
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
interface SectionRef {
@@ -68,7 +71,11 @@ export function FeedSection({
const renderPostsEmpty = useCallback(() => {
return (
<View style={[a.gap_xl, a.align_center]}>
<EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
<EmptyState
icon={HashtagWideIcon}
iconSize="2xl"
message={_(msg`This feed is empty.`)}
/>
{isOwner && (
<Button
label={_(msg`Start adding people`)}
+2 -1
View File
@@ -24,6 +24,7 @@ import {atoms as a, useTheme} from '#/alf'
import {Admonition, colors} from '#/components/Admonition'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog'
import {Growth_Stroke2_Corner0_Rounded as Growth} from '#/components/icons/Growth'
import {PlusLarge_Stroke2_Corner0_Rounded as PlusIcon} from '#/components/icons/Plus'
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning'
@@ -102,7 +103,7 @@ export function AppPasswordsScreen({}: Props) {
</View>
) : (
<EmptyState
icon="growth"
icon={Growth}
message={_(msg`No app passwords yet`)}
/>
)
@@ -201,6 +201,7 @@ export function ItemIcon({
lg: 24,
xl: 28,
'2xl': 32,
'3xl': 40,
}[size]
const color =