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:
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user