Add empty states to the lists screen

This commit is contained in:
Paul Frazee
2023-05-05 11:40:56 -05:00
parent 5e723b0821
commit 806e52705d
5 changed files with 236 additions and 226 deletions
+1 -1
View File
@@ -62,7 +62,7 @@ export class ListsListModel {
this._xLoading(replace)
try {
let res
if (this.source === 'mine') {
if (this.source === 'blocklists') {
res = await this.rootStore.agent.app.bsky.graph.getListBlocks({
limit: PAGE_SIZE,
cursor: replace ? undefined : this.loadMoreCursor,
+150 -147
View File
@@ -7,6 +7,7 @@ import {
View,
ViewStyle,
} from 'react-native'
import {observer} from 'mobx-react-lite'
import {FlatList} from '../util/Views'
import {ListCard} from './ListCard'
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
@@ -22,159 +23,161 @@ const EMPTY_ITEM = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
export function ListsList({
listsList,
style,
showPostFollowBtn,
scrollElRef,
onPressTryAgain,
renderEmptyState,
testID,
headerOffset = 0,
}: {
listsList: ListsListModel
style?: StyleProp<ViewStyle>
showPostFollowBtn?: boolean
scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void
renderEmptyState?: () => JSX.Element
testID?: string
headerOffset?: number
}) {
const pal = usePalette('default')
const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false)
export const ListsList = observer(
({
listsList,
style,
showPostFollowBtn,
scrollElRef,
onPressTryAgain,
renderEmptyState,
testID,
headerOffset = 0,
}: {
listsList: ListsListModel
style?: StyleProp<ViewStyle>
showPostFollowBtn?: boolean
scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void
renderEmptyState?: () => JSX.Element
testID?: string
headerOffset?: number
}) => {
const pal = usePalette('default')
const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false)
const data = React.useMemo(() => {
let items: any[] = []
if (listsList.hasLoaded) {
if (listsList.hasError) {
items = items.concat([ERROR_ITEM])
}
if (listsList.isEmpty) {
items = items.concat([EMPTY_ITEM])
} else {
items = items.concat(listsList.lists)
}
if (listsList.loadMoreError) {
items = items.concat([LOAD_MORE_ERROR_ITEM])
}
} else if (listsList.isLoading) {
items = items.concat([LOADING_ITEM])
}
return items
}, [
listsList.hasError,
listsList.hasLoaded,
listsList.isLoading,
listsList.isEmpty,
listsList.lists,
listsList.loadMoreError,
])
// events
// =
const onRefresh = React.useCallback(async () => {
track('Lists:onRefresh')
setIsRefreshing(true)
try {
await listsList.refresh()
} catch (err) {
listsList.rootStore.log.error('Failed to refresh lists', err)
}
setIsRefreshing(false)
}, [listsList, track, setIsRefreshing])
const onEndReached = React.useCallback(async () => {
track('Lists:onEndReached')
try {
await listsList.loadMore()
} catch (err) {
listsList.rootStore.log.error('Failed to load more lists', err)
}
}, [listsList, track])
const onPressRetryLoadMore = React.useCallback(() => {
listsList.retryLoadMore()
}, [listsList])
// rendering
// =
const renderItem = React.useCallback(
({item}: {item: any}) => {
if (item === EMPTY_ITEM) {
if (renderEmptyState) {
return renderEmptyState()
const data = React.useMemo(() => {
let items: any[] = []
if (listsList.hasLoaded) {
if (listsList.hasError) {
items = items.concat([ERROR_ITEM])
}
return <View />
} else if (item === ERROR_ITEM) {
return (
<ErrorMessage
message={listsList.error}
onPressTryAgain={onPressTryAgain}
/>
)
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label="There was an issue fetching your lists. Tap here to try again."
onPress={onPressRetryLoadMore}
/>
)
} else if (item === LOADING_ITEM) {
return <ProfileCardFeedLoadingPlaceholder />
if (listsList.isEmpty) {
items = items.concat([EMPTY_ITEM])
} else {
items = items.concat(listsList.lists)
}
if (listsList.loadMoreError) {
items = items.concat([LOAD_MORE_ERROR_ITEM])
}
} else if (listsList.isLoading) {
items = items.concat([LOADING_ITEM])
}
return <ListCard list={item} />
},
[listsList, onPressTryAgain, onPressRetryLoadMore, showPostFollowBtn],
)
return items
}, [
listsList.hasError,
listsList.hasLoaded,
listsList.isLoading,
listsList.isEmpty,
listsList.lists,
listsList.loadMoreError,
])
const Footer = React.useCallback(
() =>
listsList.isLoading ? (
<View style={styles.feedFooter}>
<ActivityIndicator />
</View>
) : (
<View />
),
[listsList],
)
// events
// =
return (
<View testID={testID} style={style}>
{data.length > 0 && (
<FlatList
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={data}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListFooterComponent={Footer}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
const onRefresh = React.useCallback(async () => {
track('Lists:onRefresh')
setIsRefreshing(true)
try {
await listsList.refresh()
} catch (err) {
listsList.rootStore.log.error('Failed to refresh lists', err)
}
setIsRefreshing(false)
}, [listsList, track, setIsRefreshing])
const onEndReached = React.useCallback(async () => {
track('Lists:onEndReached')
try {
await listsList.loadMore()
} catch (err) {
listsList.rootStore.log.error('Failed to load more lists', err)
}
}, [listsList, track])
const onPressRetryLoadMore = React.useCallback(() => {
listsList.retryLoadMore()
}, [listsList])
// rendering
// =
const renderItem = React.useCallback(
({item}: {item: any}) => {
if (item === EMPTY_ITEM) {
if (renderEmptyState) {
return renderEmptyState()
}
contentContainerStyle={s.contentContainer}
style={{paddingTop: headerOffset}}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
)}
</View>
)
}
return <View />
} else if (item === ERROR_ITEM) {
return (
<ErrorMessage
message={listsList.error}
onPressTryAgain={onPressTryAgain}
/>
)
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
<LoadMoreRetryBtn
label="There was an issue fetching your lists. Tap here to try again."
onPress={onPressRetryLoadMore}
/>
)
} else if (item === LOADING_ITEM) {
return <ProfileCardFeedLoadingPlaceholder />
}
return <ListCard list={item} />
},
[listsList, onPressTryAgain, onPressRetryLoadMore, showPostFollowBtn],
)
const Footer = React.useCallback(
() =>
listsList.isLoading ? (
<View style={styles.feedFooter}>
<ActivityIndicator />
</View>
) : (
<View />
),
[listsList],
)
return (
<View testID={testID} style={style}>
{data.length > 0 && (
<FlatList
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={data}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListFooterComponent={Footer}
refreshControl={
<RefreshControl
refreshing={isRefreshing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
progressViewOffset={headerOffset}
/>
}
contentContainerStyle={s.contentContainer}
style={{paddingTop: headerOffset}}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
removeClippedSubviews={true}
contentOffset={{x: 0, y: headerOffset * -1}}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
)}
</View>
)
},
)
const styles = StyleSheet.create({
feedFooter: {paddingTop: 20},
+45 -38
View File
@@ -1,80 +1,87 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {MagnifyingGlassIcon} from 'lib/icons'
import {NavigationProp} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
export function MyListsEmptyState() {
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const navigation = useNavigation<NavigationProp>()
const onPressFindAccounts = React.useCallback(() => {
navigation.navigate('SearchTab')
navigation.popToTop()
}, [navigation])
const onPressCreateList = React.useCallback(() => {
// TODO
}, [])
return (
<View style={styles.emptyContainer}>
<View style={styles.emptyIconContainer}>
<MagnifyingGlassIcon style={[styles.emptyIcon, pal.text]} size={62} />
<View style={styles.container}>
<View style={styles.iconContainer}>
<FontAwesomeIcon
icon="list-ul"
style={[styles.icon, pal.text]}
size={62}
/>
</View>
<Text type="xl-medium" style={[s.textCenter, pal.text]}>
Your following feed is empty! Find some accounts to follow to fix this.
Lists are public collections of users. You can create a list or find
other users' lists on their profiles.
</Text>
<Button
type="inverted"
style={styles.emptyBtn}
onPress={onPressFindAccounts}>
<Text type="lg-medium" style={palInverted.text}>
Find accounts to follow
<View style={styles.btns}>
<Button type="inverted" style={styles.btn} onPress={onPressCreateList}>
<FontAwesomeIcon
icon="plus"
style={palInverted.text as FontAwesomeIconStyle}
size={14}
/>
<Text type="lg-medium" style={palInverted.text}>
New blocklist
</Text>
</Button>
</View>
<View style={[pal.viewLight, styles.notice]}>
<Text type="lg" style={[pal.textLight, s.textCenter]}>
Currently only "blocklists" are available, which is kind of weird but
we wanted to prioritize user safety. Feedlists will be implemented
soon!
</Text>
<FontAwesomeIcon
icon="angle-right"
style={palInverted.text as FontAwesomeIconStyle}
size={14}
/>
</Button>
</View>
</View>
)
}
const styles = StyleSheet.create({
emptyContainer: {
container: {
height: '100%',
paddingVertical: 40,
paddingHorizontal: 30,
},
emptyIconContainer: {
iconContainer: {
marginBottom: 16,
},
emptyIcon: {
icon: {
marginLeft: 'auto',
marginRight: 'auto',
},
emptyBtn: {
btns: {
flexDirection: 'row',
justifyContent: 'center',
},
btn: {
gap: 10,
marginVertical: 20,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 18,
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 30,
},
feedsTip: {
position: 'absolute',
left: 22,
},
feedsTipArrow: {
marginLeft: 32,
marginTop: 8,
notice: {
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 10,
marginHorizontal: 30,
},
})
@@ -1,80 +1,80 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {useNavigation} from '@react-navigation/native'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from '../util/text/Text'
import {Button} from '../util/forms/Button'
import {MagnifyingGlassIcon} from 'lib/icons'
import {NavigationProp} from 'lib/routes/types'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
export function SubscribedBlocklistsEmptyState() {
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const navigation = useNavigation<NavigationProp>()
const onPressFindAccounts = React.useCallback(() => {
navigation.navigate('SearchTab')
navigation.popToTop()
}, [navigation])
const onPressCreateList = React.useCallback(() => {
// TODO
}, [])
return (
<View style={styles.emptyContainer}>
<View style={styles.emptyIconContainer}>
<MagnifyingGlassIcon style={[styles.emptyIcon, pal.text]} size={62} />
<View style={styles.container}>
<View style={styles.iconContainer}>
<FontAwesomeIcon
icon="list-ul"
style={[styles.icon, pal.text]}
size={62}
/>
</View>
<Text type="xl-medium" style={[s.textCenter, pal.text]}>
Your following feed is empty! Find some accounts to follow to fix this.
You can subscribe to blocklists to automatically block all of the users
they include. Blocklists and your subscriptions are public.
</Text>
<Button
type="inverted"
style={styles.emptyBtn}
onPress={onPressFindAccounts}>
<Text type="lg-medium" style={palInverted.text}>
Find accounts to follow
</Text>
<FontAwesomeIcon
icon="angle-right"
style={palInverted.text as FontAwesomeIconStyle}
size={14}
/>
</Button>
<View style={styles.btns}>
<Button type="inverted" style={styles.btn} onPress={onPressCreateList}>
<FontAwesomeIcon
icon="plus"
style={palInverted.text as FontAwesomeIconStyle}
size={14}
/>
<Text type="lg-medium" style={palInverted.text}>
New blocklist
</Text>
</Button>
</View>
</View>
)
}
const styles = StyleSheet.create({
emptyContainer: {
container: {
height: '100%',
paddingVertical: 40,
paddingHorizontal: 30,
},
emptyIconContainer: {
iconContainer: {
marginBottom: 16,
},
emptyIcon: {
icon: {
marginLeft: 'auto',
marginRight: 'auto',
},
emptyBtn: {
btns: {
flexDirection: 'row',
justifyContent: 'center',
},
btn: {
gap: 10,
marginVertical: 20,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 18,
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 30,
},
feedsTip: {
position: 'absolute',
left: 22,
},
feedsTipArrow: {
marginLeft: 32,
marginTop: 8,
notice: {
borderRadius: 12,
paddingHorizontal: 12,
paddingVertical: 10,
marginHorizontal: 30,
},
})
+1 -1
View File
@@ -46,7 +46,7 @@ export const ListsScreen = withAuthRequired(({route}: Props) => {
return (
<TabBar
{...props}
items={['Mine', 'Subscribed Blocklists']}
items={['My Lists', 'Blocklists']}
indicatorColor={pal.colors.link}
indicatorPosition="bottom"
testID="tabs"