Better list empty state (#7157)

* render header conditionally, use modern footer

* move add button to empty state if empty

* center align empty state

* Tweak add people btn

---------

Co-authored-by: Paul Frazee <pfrazee@gmail.com>
This commit is contained in:
Samuel Newman
2024-12-18 19:03:53 +00:00
committed by GitHub
parent 5c4f870898
commit 05c43ed998
3 changed files with 98 additions and 64 deletions
+17 -18
View File
@@ -1,11 +1,5 @@
import React from 'react' import React, {useCallback} from 'react'
import { import {Dimensions, StyleProp, View, ViewStyle} from 'react-native'
ActivityIndicator,
Dimensions,
StyleProp,
View,
ViewStyle,
} from 'react-native'
import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api' import {AppBskyActorDefs, AppBskyGraphDefs} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
@@ -16,6 +10,7 @@ import {logger} from '#/logger'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useListMembersQuery} from '#/state/queries/list-members' import {useListMembersQuery} from '#/state/queries/list-members'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {ListFooter} from '#/components/Lists'
import {ProfileCard} from '../profile/ProfileCard' import {ProfileCard} from '../profile/ProfileCard'
import {ErrorMessage} from '../util/error/ErrorMessage' import {ErrorMessage} from '../util/error/ErrorMessage'
import {Button} from '../util/forms/Button' import {Button} from '../util/forms/Button'
@@ -66,6 +61,7 @@ export function ListMembers({
refetch, refetch,
fetchNextPage, fetchNextPage,
hasNextPage, hasNextPage,
isFetchingNextPage,
} = useListMembersQuery(list) } = useListMembersQuery(list)
const isEmpty = !isFetching && !data?.pages[0].items.length const isEmpty = !isFetching && !data?.pages[0].items.length
const isOwner = const isOwner =
@@ -197,14 +193,17 @@ export function ListMembers({
], ],
) )
const Footer = React.useCallback( const renderFooter = useCallback(() => {
() => ( if (isEmpty) return null
<View style={{paddingTop: 20, paddingBottom: 400}}> return (
{isFetching && <ActivityIndicator />} <ListFooter
</View> hasNextPage={hasNextPage}
), error={cleanError(error)}
[isFetching], isFetchingNextPage={isFetchingNextPage}
) onRetry={fetchNextPage}
/>
)
}, [hasNextPage, error, isFetchingNextPage, fetchNextPage, isEmpty])
return ( return (
<View testID={testID} style={style}> <View testID={testID} style={style}>
@@ -214,8 +213,8 @@ export function ListMembers({
data={items} data={items}
keyExtractor={(item: any) => item.subject?.did || item._reactKey} keyExtractor={(item: any) => item.subject?.did || item._reactKey}
renderItem={renderItem} renderItem={renderItem}
ListHeaderComponent={renderHeader} ListHeaderComponent={!isEmpty ? renderHeader : undefined}
ListFooterComponent={Footer} ListFooterComponent={renderFooter}
refreshing={isRefreshing} refreshing={isRefreshing}
onRefresh={onRefresh} onRefresh={onRefresh}
headerOffset={headerOffset} headerOffset={headerOffset}
+1 -3
View File
@@ -26,9 +26,7 @@ export function EmptyState({
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const iconSize = isTabletOrDesktop ? 64 : 48 const iconSize = isTabletOrDesktop ? 64 : 48
return ( return (
<View <View testID={testID} style={style}>
testID={testID}
style={[isTabletOrDesktop && {paddingRight: 20}, style]}>
<View <View
style={[ style={[
styles.iconContainer, styles.iconContainer,
+80 -43
View File
@@ -1,5 +1,5 @@
import React, {useCallback, useMemo} from 'react' import React, {useCallback, useMemo} from 'react'
import {Pressable, StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {useAnimatedRef} from 'react-native-reanimated' import {useAnimatedRef} from 'react-native-reanimated'
import { import {
AppBskyGraphDefs, AppBskyGraphDefs,
@@ -70,7 +70,9 @@ import {Text} from '#/view/com/util/text/Text'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen' import {ListHiddenScreen} from '#/screens/List/ListHiddenScreen'
import {atoms as a} from '#/alf' import {atoms as a} from '#/alf'
import {Button as NewButton, ButtonIcon, ButtonText} from '#/components/Button'
import {useDialogControl} from '#/components/Dialog' import {useDialogControl} from '#/components/Dialog'
import {PersonPlus_Stroke2_Corner0_Rounded as PersonPlusIcon} from '#/components/icons/Person'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import * as Hider from '#/components/moderation/Hider' import * as Hider from '#/components/moderation/Hider'
import * as Prompt from '#/components/Prompt' import * as Prompt from '#/components/Prompt'
@@ -220,6 +222,7 @@ function ProfileListScreenLoaded({
scrollElRef={scrollElRef as ListRef} scrollElRef={scrollElRef as ListRef}
headerHeight={headerHeight} headerHeight={headerHeight}
isFocused={isScreenFocused && isFocused} isFocused={isScreenFocused && isFocused}
onPressAddUser={onPressAddUser}
/> />
)} )}
{({headerHeight, scrollElRef}) => ( {({headerHeight, scrollElRef}) => (
@@ -771,9 +774,13 @@ interface FeedSectionProps {
headerHeight: number headerHeight: number
scrollElRef: ListRef scrollElRef: ListRef
isFocused: boolean isFocused: boolean
onPressAddUser: () => void
} }
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>( const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl({feed, scrollElRef, headerHeight, isFocused}, ref) { function FeedSectionImpl(
{feed, scrollElRef, headerHeight, isFocused, onPressAddUser},
ref,
) {
const queryClient = useQueryClient() const queryClient = useQueryClient()
const [hasNew, setHasNew] = React.useState(false) const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
@@ -800,8 +807,23 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
}, [onScrollToTop, isScreenFocused]) }, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => { const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} /> return (
}, [_]) <View style={[a.gap_xl, a.align_center]}>
<EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
<NewButton
label={_(msg`Start adding people`)}
onPress={onPressAddUser}
color="primary"
size="small"
variant="solid">
<ButtonIcon icon={PersonPlusIcon} />
<ButtonText>
<Trans>Start adding people!</Trans>
</ButtonText>
</NewButton>
</View>
)
}, [_, onPressAddUser])
return ( return (
<View> <View>
@@ -840,10 +862,9 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
{list, onPressAddUser, headerHeight, scrollElRef}, {list, onPressAddUser, headerHeight, scrollElRef},
ref, ref,
) { ) {
const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const {isMobile} = useWebMediaQueries()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const {isMobile} = useWebMediaQueries()
const [isScrolledDown, setIsScrolledDown] = React.useState(false) const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const isOwner = list.creator.did === currentAccount?.did const isOwner = list.creator.did === currentAccount?.did
@@ -862,50 +883,66 @@ const AboutSection = React.forwardRef<SectionRef, AboutSectionProps>(
if (!isOwner) { if (!isOwner) {
return <View /> return <View />
} }
return ( if (isMobile) {
<View style={a.pt_lg}> return (
<View <View style={[a.px_sm, a.py_sm]}>
style={[ <NewButton
{ testID="addUserBtn"
flexDirection: 'row', label={_(msg`Add a user to this list`)}
alignItems: 'center', onPress={onPressAddUser}
justifyContent: 'space-between', color="primary"
paddingHorizontal: isMobile ? 14 : 20, size="small"
paddingBottom: isMobile ? 14 : 18, variant="outline"
}, style={[a.py_md]}>
]}> <ButtonIcon icon={PersonPlusIcon} />
{isOwner && ( <ButtonText>
<Pressable <Trans>Add people</Trans>
testID="addUserBtn" </ButtonText>
accessibilityRole="button" </NewButton>
accessibilityLabel={_(msg`Add a user to this list`)}
accessibilityHint=""
onPress={onPressAddUser}
style={{flexDirection: 'row', alignItems: 'center', gap: 6}}>
<FontAwesomeIcon
icon="user-plus"
color={pal.colors.link}
size={16}
/>
<Text style={pal.link}>
<Trans>Add</Trans>
</Text>
</Pressable>
)}
</View> </View>
)
}
return (
<View style={[a.px_lg, a.py_md, a.flex_row_reverse]}>
<NewButton
testID="addUserBtn"
label={_(msg`Add a user to this list`)}
onPress={onPressAddUser}
color="primary"
size="small"
variant="ghost"
style={[a.py_sm]}>
<ButtonIcon icon={PersonPlusIcon} />
<ButtonText>
<Trans>Add people</Trans>
</ButtonText>
</NewButton>
</View> </View>
) )
}, [isMobile, pal.colors.link, pal.link, isOwner, _, onPressAddUser]) }, [isOwner, _, onPressAddUser, isMobile])
const renderEmptyState = useCallback(() => { const renderEmptyState = useCallback(() => {
return ( return (
<EmptyState <View style={[a.gap_xl, a.align_center]}>
icon="users-slash" <EmptyState
message={_(msg`This list is empty!`)} icon="users-slash"
style={{paddingTop: 40}} message={_(msg`This list is empty.`)}
/> />
<NewButton
testID="emptyStateAddUserBtn"
label={_(msg`Start adding people`)}
onPress={onPressAddUser}
color="primary"
size="small"
variant="solid">
<ButtonIcon icon={PersonPlusIcon} />
<ButtonText>
<Trans>Start adding people!</Trans>
</ButtonText>
</NewButton>
</View>
) )
}, [_]) }, [_, onPressAddUser])
return ( return (
<View> <View>