From 806e52705d53e879a347a63ea8cc451ec953aa8d Mon Sep 17 00:00:00 2001 From: Paul Frazee Date: Fri, 5 May 2023 11:40:56 -0500 Subject: [PATCH] Add empty states to the lists screen --- src/state/models/lists/lists-list.ts | 2 +- src/view/com/lists/ListsList.tsx | 297 +++++++++--------- src/view/com/lists/MyListsEmptyState.tsx | 83 ++--- .../lists/SubscribedBlocklistsEmptyState.tsx | 78 ++--- src/view/screens/Lists.tsx | 2 +- 5 files changed, 236 insertions(+), 226 deletions(-) diff --git a/src/state/models/lists/lists-list.ts b/src/state/models/lists/lists-list.ts index 598652b122..8130a383be 100644 --- a/src/state/models/lists/lists-list.ts +++ b/src/state/models/lists/lists-list.ts @@ -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, diff --git a/src/view/com/lists/ListsList.tsx b/src/view/com/lists/ListsList.tsx index c936839aca..e50361be64 100644 --- a/src/view/com/lists/ListsList.tsx +++ b/src/view/com/lists/ListsList.tsx @@ -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 - showPostFollowBtn?: boolean - scrollElRef?: MutableRefObject | 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 + showPostFollowBtn?: boolean + scrollElRef?: MutableRefObject | 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 - } else if (item === ERROR_ITEM) { - return ( - - ) - } else if (item === LOAD_MORE_ERROR_ITEM) { - return ( - - ) - } else if (item === LOADING_ITEM) { - return + 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 - }, - [listsList, onPressTryAgain, onPressRetryLoadMore, showPostFollowBtn], - ) + return items + }, [ + listsList.hasError, + listsList.hasLoaded, + listsList.isLoading, + listsList.isEmpty, + listsList.lists, + listsList.loadMoreError, + ]) - const Footer = React.useCallback( - () => - listsList.isLoading ? ( - - - - ) : ( - - ), - [listsList], - ) + // events + // = - return ( - - {data.length > 0 && ( - item._reactKey} - renderItem={renderItem} - ListFooterComponent={Footer} - refreshControl={ - + 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 - /> - )} - - ) -} + return + } else if (item === ERROR_ITEM) { + return ( + + ) + } else if (item === LOAD_MORE_ERROR_ITEM) { + return ( + + ) + } else if (item === LOADING_ITEM) { + return + } + return + }, + [listsList, onPressTryAgain, onPressRetryLoadMore, showPostFollowBtn], + ) + + const Footer = React.useCallback( + () => + listsList.isLoading ? ( + + + + ) : ( + + ), + [listsList], + ) + + return ( + + {data.length > 0 && ( + item._reactKey} + renderItem={renderItem} + ListFooterComponent={Footer} + refreshControl={ + + } + 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 + /> + )} + + ) + }, +) const styles = StyleSheet.create({ feedFooter: {paddingTop: 20}, diff --git a/src/view/com/lists/MyListsEmptyState.tsx b/src/view/com/lists/MyListsEmptyState.tsx index 2751e10808..6afb84111d 100644 --- a/src/view/com/lists/MyListsEmptyState.tsx +++ b/src/view/com/lists/MyListsEmptyState.tsx @@ -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() - const onPressFindAccounts = React.useCallback(() => { - navigation.navigate('SearchTab') - navigation.popToTop() - }, [navigation]) + const onPressCreateList = React.useCallback(() => { + // TODO + }, []) return ( - - - + + + - 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. - + + + + Currently only "blocklists" are available, which is kind of weird but + we wanted to prioritize user safety. Feedlists will be implemented + soon! - - + ) } 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, }, }) diff --git a/src/view/com/lists/SubscribedBlocklistsEmptyState.tsx b/src/view/com/lists/SubscribedBlocklistsEmptyState.tsx index 977fc10765..1b38663ccd 100644 --- a/src/view/com/lists/SubscribedBlocklistsEmptyState.tsx +++ b/src/view/com/lists/SubscribedBlocklistsEmptyState.tsx @@ -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() - const onPressFindAccounts = React.useCallback(() => { - navigation.navigate('SearchTab') - navigation.popToTop() - }, [navigation]) + const onPressCreateList = React.useCallback(() => { + // TODO + }, []) return ( - - - + + + - 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. - + + + ) } 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, }, }) diff --git a/src/view/screens/Lists.tsx b/src/view/screens/Lists.tsx index 6380c38d52..954efcb38f 100644 --- a/src/view/screens/Lists.tsx +++ b/src/view/screens/Lists.tsx @@ -46,7 +46,7 @@ export const ListsScreen = withAuthRequired(({route}: Props) => { return (