Implement lists screen and lists-list in profiles

This commit is contained in:
Paul Frazee
2023-05-05 00:17:43 -05:00
parent 8f408d57e9
commit 5e723b0821
11 changed files with 810 additions and 48 deletions
+133
View File
@@ -0,0 +1,133 @@
import {makeAutoObservable} from 'mobx'
import {
AppBskyGraphGetLists as GetLists,
AppBskyGraphGetListBlocks as GetListBlocks,
AppBskyGraphDefs as GraphDefs,
} from '@atproto/api'
import {RootStoreModel} from '../root-store'
import {cleanError} from 'lib/strings/errors'
import {bundleAsync} from 'lib/async/bundle'
const PAGE_SIZE = 30
export class ListsListModel {
// state
isLoading = false
isRefreshing = false
hasLoaded = false
error = ''
loadMoreError = ''
hasMore = true
loadMoreCursor?: string
// data
lists: GraphDefs.ListView[] = []
constructor(
public rootStore: RootStoreModel,
public source: 'blocklists' | string,
) {
makeAutoObservable(
this,
{
rootStore: false,
},
{autoBind: true},
)
}
get hasContent() {
return this.lists.length > 0
}
get hasError() {
return this.error !== ''
}
get isEmpty() {
return this.hasLoaded && !this.hasContent
}
// public api
// =
async refresh() {
return this.loadMore(true)
}
loadMore = bundleAsync(async (replace: boolean = false) => {
if (!replace && !this.hasMore) {
return
}
this._xLoading(replace)
try {
let res
if (this.source === 'mine') {
res = await this.rootStore.agent.app.bsky.graph.getListBlocks({
limit: PAGE_SIZE,
cursor: replace ? undefined : this.loadMoreCursor,
})
} else {
res = await this.rootStore.agent.app.bsky.graph.getLists({
actor: this.source,
limit: PAGE_SIZE,
cursor: replace ? undefined : this.loadMoreCursor,
})
}
if (replace) {
this._replaceAll(res)
} else {
this._appendAll(res)
}
this._xIdle()
} catch (e: any) {
this._xIdle(replace ?? e, !replace ?? e)
}
})
/**
* Attempt to load more again after a failure
*/
async retryLoadMore() {
this.loadMoreError = ''
this.hasMore = true
return this.loadMore()
}
// state transitions
// =
_xLoading(isRefreshing = false) {
this.isLoading = true
this.isRefreshing = isRefreshing
this.error = ''
}
_xIdle(err?: any, loadMoreErr?: any) {
this.isLoading = false
this.isRefreshing = false
this.hasLoaded = true
this.error = cleanError(err)
this.loadMoreError = cleanError(loadMoreErr)
if (err) {
this.rootStore.log.error('Failed to fetch user lists', err)
}
if (loadMoreErr) {
this.rootStore.log.error('Failed to fetch user lists', loadMoreErr)
}
}
// helper functions
// =
_replaceAll(res: GetLists.Response | GetListBlocks.Response) {
this.lists = []
this._appendAll(res)
}
_appendAll(res: GetLists.Response | GetListBlocks.Response) {
this.loadMoreCursor = res.data.cursor
this.hasMore = !!this.loadMoreCursor
this.lists = this.lists.concat(res.data.lists)
}
}
+23 -2
View File
@@ -2,13 +2,19 @@ import {makeAutoObservable} from 'mobx'
import {RootStoreModel} from '../root-store'
import {ProfileModel} from '../content/profile'
import {PostsFeedModel} from '../feeds/posts'
import {ListsListModel} from '../lists/lists-list'
export enum Sections {
Posts = 'Posts',
PostsWithReplies = 'Posts & replies',
Lists = 'Lists',
}
const USER_SELECTOR_ITEMS = [Sections.Posts, Sections.PostsWithReplies]
const USER_SELECTOR_ITEMS = [
Sections.Posts,
Sections.PostsWithReplies,
Sections.Lists,
]
export interface ProfileUiParams {
user: string
@@ -22,6 +28,7 @@ export class ProfileUiModel {
// data
profile: ProfileModel
feed: PostsFeedModel
lists: ListsListModel
// ui state
selectedViewIndex = 0
@@ -43,14 +50,17 @@ export class ProfileUiModel {
actor: params.user,
limit: 10,
})
this.lists = new ListsListModel(rootStore, params.user)
}
get currentView(): PostsFeedModel {
get currentView(): PostsFeedModel | ListsListModel {
if (
this.selectedView === Sections.Posts ||
this.selectedView === Sections.PostsWithReplies
) {
return this.feed
} else if (this.selectedView === Sections.Lists) {
return this.lists
}
throw new Error(`Invalid selector value: ${this.selectedViewIndex}`)
}
@@ -100,6 +110,12 @@ export class ProfileUiModel {
} else if (this.feed.isEmpty) {
arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
}
} else if (this.selectedView === Sections.Lists) {
if (this.lists.hasContent) {
arr = this.lists.lists
} else if (this.lists.isEmpty) {
arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
}
} else {
arr = arr.concat([ProfileUiModel.EMPTY_ITEM])
}
@@ -113,6 +129,8 @@ export class ProfileUiModel {
this.selectedView === Sections.PostsWithReplies
) {
return this.feed.hasContent && this.feed.hasMore && this.feed.isLoading
} else if (this.selectedView === Sections.Lists) {
return this.lists.hasContent && this.lists.hasMore && this.lists.isLoading
}
return false
}
@@ -133,6 +151,9 @@ export class ProfileUiModel {
.setup()
.catch(err => this.rootStore.log.error('Failed to fetch feed', err)),
])
this.lists
.loadMore()
.catch(err => this.rootStore.log.error('Failed to fetch lists', err))
}
async update() {
+170
View File
@@ -0,0 +1,170 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {AppBskyGraphDefs, RichText} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {RichText as RichTextCom} from '../util/text/RichText'
import {UserAvatar} from '../util/UserAvatar'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {
getProfileViewBasicLabelInfo,
getProfileModeration,
} from 'lib/labeling/helpers'
import {ModerationBehaviorCode} from 'lib/labeling/types'
export const ListCard = ({
testID,
list,
noBg,
noBorder,
renderButton,
}: {
testID?: string
list: AppBskyGraphDefs.ListView
noBg?: boolean
noBorder?: boolean
renderButton?: () => JSX.Element
}) => {
const pal = usePalette('default')
const descriptionRichText = React.useMemo(() => {
if (list.description) {
return new RichText({
text: list.description,
facets: list.descriptionFacets,
})
}
return undefined
}, [list])
return (
<Link
testID={testID}
style={[
styles.outer,
pal.border,
noBorder && styles.outerNoBorder,
!noBg && pal.view,
]}
href={`/profile/${'list.author'}/lists/${'list.rkey'}`}
title={list.name}
asAnchor
anchorNoUnderline>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<UserAvatar size={40} avatar={list.avatar} />
</View>
<View style={styles.layoutContent}>
<Text
type="lg"
style={[s.bold, pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{sanitizeDisplayName(list.name)}
</Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
{list.purpose === 'app.bsky.graph.defs#blocklist' && 'Block list'}{' '}
by @TODO
</Text>
{!!list.viewer?.blocked && (
<View style={s.flexRow}>
<View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}>
Subscribed
</Text>
</View>
</View>
)}
</View>
{renderButton ? (
<View style={styles.layoutButton}>{renderButton()}</View>
) : undefined}
</View>
{descriptionRichText ? (
<View style={styles.details}>
<RichTextCom
style={pal.text}
numberOfLines={20}
richText={descriptionRichText}
/>
</View>
) : undefined}
</Link>
)
}
const styles = StyleSheet.create({
outer: {
borderTopWidth: 1,
paddingHorizontal: 6,
},
outerNoBorder: {
borderTopWidth: 0,
},
layout: {
flexDirection: 'row',
alignItems: 'center',
},
layoutAvi: {
width: 54,
paddingLeft: 4,
paddingTop: 8,
paddingBottom: 10,
},
avi: {
width: 40,
height: 40,
borderRadius: 20,
resizeMode: 'cover',
},
layoutContent: {
flex: 1,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
},
layoutButton: {
paddingRight: 10,
},
details: {
paddingLeft: 54,
paddingRight: 10,
paddingBottom: 10,
},
pill: {
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
},
btn: {
paddingVertical: 7,
borderRadius: 50,
marginLeft: 6,
paddingHorizontal: 14,
},
followedBy: {
flexDirection: 'row',
alignItems: 'center',
paddingLeft: 54,
paddingRight: 20,
marginBottom: 10,
marginTop: -6,
},
followedByAviContainer: {
width: 24,
height: 36,
},
followedByAvi: {
width: 36,
height: 36,
borderRadius: 18,
padding: 2,
},
followsByDesc: {
flex: 1,
paddingRight: 10,
},
})
View File
+181
View File
@@ -0,0 +1,181 @@
import React, {MutableRefObject} from 'react'
import {
ActivityIndicator,
RefreshControl,
StyleProp,
StyleSheet,
View,
ViewStyle,
} from 'react-native'
import {FlatList} from '../util/Views'
import {ListCard} from './ListCard'
import {ProfileCardFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {ListsListModel} from 'state/models/lists/lists-list'
import {useAnalytics} from 'lib/analytics'
import {usePalette} from 'lib/hooks/usePalette'
import {s} from 'lib/styles'
const LOADING_ITEM = {_reactKey: '__loading__'}
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)
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()
}
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},
})
+80
View File
@@ -0,0 +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 MyListsEmptyState() {
const pal = usePalette('default')
const palInverted = usePalette('inverted')
const navigation = useNavigation<NavigationProp>()
const onPressFindAccounts = React.useCallback(() => {
navigation.navigate('SearchTab')
navigation.popToTop()
}, [navigation])
return (
<View style={styles.emptyContainer}>
<View style={styles.emptyIconContainer}>
<MagnifyingGlassIcon style={[styles.emptyIcon, 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.
</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>
)
}
const styles = StyleSheet.create({
emptyContainer: {
height: '100%',
paddingVertical: 40,
paddingHorizontal: 30,
},
emptyIconContainer: {
marginBottom: 16,
},
emptyIcon: {
marginLeft: 'auto',
marginRight: 'auto',
},
emptyBtn: {
marginVertical: 20,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 18,
paddingHorizontal: 24,
borderRadius: 30,
},
feedsTip: {
position: 'absolute',
left: 22,
},
feedsTipArrow: {
marginLeft: 32,
marginTop: 8,
},
})
@@ -0,0 +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])
return (
<View style={styles.emptyContainer}>
<View style={styles.emptyIconContainer}>
<MagnifyingGlassIcon style={[styles.emptyIcon, 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.
</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>
)
}
const styles = StyleSheet.create({
emptyContainer: {
height: '100%',
paddingVertical: 40,
paddingHorizontal: 30,
},
emptyIconContainer: {
marginBottom: 16,
},
emptyIcon: {
marginLeft: 'auto',
marginRight: 'auto',
},
emptyBtn: {
marginVertical: 20,
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingVertical: 18,
paddingHorizontal: 24,
borderRadius: 30,
},
feedsTip: {
position: 'absolute',
left: 22,
},
feedsTipArrow: {
marginLeft: 32,
marginTop: 8,
},
})
+12 -8
View File
@@ -4,6 +4,7 @@ import {Text} from '../util/text/Text'
import {PressableWithHover} from '../util/PressableWithHover'
import {usePalette} from 'lib/hooks/usePalette'
import {isDesktopWeb} from 'platform/detection'
import {CenteredView} from '../util/Views'
interface Layout {
x: number
@@ -65,7 +66,7 @@ export function TabBar({
],
}
const onLayout = () => {
const onLayout = React.useCallback(() => {
const promises = []
for (let i = 0; i < items.length; i++) {
promises.push(
@@ -86,14 +87,17 @@ export function TabBar({
Promise.all(promises).then((layouts: Layout[]) => {
setItemLayouts(layouts)
})
}
}, [containerRef, itemRefs, setItemLayouts])
const onPressItem = (index: number) => {
onSelect?.(index)
if (index === selectedPage) {
onPressSelected?.()
}
}
const onPressItem = React.useCallback(
(index: number) => {
onSelect?.(index)
if (index === selectedPage) {
onPressSelected?.()
}
},
[onSelect, onPressSelected, selectedPage],
)
return (
<View
+1 -1
View File
@@ -62,7 +62,7 @@ export const HomeScreen = withAuthRequired(
setSelectedPage(index)
store.shell.setIsDrawerSwipeDisabled(index > 0)
},
[store],
[store, setSelectedPage],
)
const onPressSelected = React.useCallback(() => {
+67 -6
View File
@@ -1,24 +1,85 @@
import React from 'react'
import {View} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewHeader} from '../com/util/ViewHeader'
import {TabBar} from '../com/pager/TabBar'
import {Pager, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {MyListsEmptyState} from 'view/com/lists/MyListsEmptyState'
import {SubscribedBlocklistsEmptyState} from 'view/com/lists/SubscribedBlocklistsEmptyState'
import {useStores} from 'state/index'
import {ListsListModel} from 'state/models/lists/lists-list'
import {ListsList} from 'view/com/lists/ListsList'
import {usePalette} from 'lib/hooks/usePalette'
type Props = NativeStackScreenProps<CommonNavigatorParams, 'Lists'>
export const ListsScreen = withAuthRequired(({route}: Props) => {
const pal = usePalette('default')
const store = useStores()
const [selectedPage, setSelectedPage] = React.useState(0)
const mine: ListsListModel = React.useMemo(() => {
const list = new ListsListModel(store, store.me.did)
list.loadMore()
return list
}, [store])
const blocklists: ListsListModel = React.useMemo(() => {
const list = new ListsListModel(store, 'blocklists')
list.loadMore()
return list
}, [store])
useFocusEffect(
React.useCallback(() => {
store.shell.setMinimalShellMode(false)
}, [store]),
}, [store, selectedPage]),
)
const onPageSelected = React.useCallback(
(index: number) => {
setSelectedPage(index)
},
[store, setSelectedPage],
)
const renderTabBar = React.useCallback((props: RenderTabBarFnProps) => {
return (
<TabBar
{...props}
items={['Mine', 'Subscribed Blocklists']}
indicatorColor={pal.colors.link}
indicatorPosition="bottom"
testID="tabs"
/>
)
}, [])
const renderMineEmptyState = React.useCallback(() => {
return <MyListsEmptyState />
}, [])
const renderBlockliststEmptyState = React.useCallback(() => {
return <SubscribedBlocklistsEmptyState />
}, [])
return (
<View>
<ViewHeader title="Lists" />
</View>
<Pager
testID="listsScreen"
onPageSelected={onPageSelected}
renderTabBar={renderTabBar}
tabBarPosition="top">
<ListsList
key="1"
testID="minePage"
listsList={mine}
renderEmptyState={renderMineEmptyState}
/>
<ListsList
key="2"
testID="blocklistsPage"
listsList={blocklists}
renderEmptyState={renderBlockliststEmptyState}
/>
</Pager>
)
})
+63 -31
View File
@@ -7,12 +7,16 @@ import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {ViewSelector} from '../com/util/ViewSelector'
import {CenteredView} from '../com/util/Views'
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
import {ProfileUiModel} from 'state/models/ui/profile'
import {ProfileUiModel, Sections} from 'state/models/ui/profile'
import {useStores} from 'state/index'
import {PostsFeedSliceModel} from 'state/models/feeds/posts'
import {ProfileHeader} from '../com/profile/ProfileHeader'
import {FeedSlice} from '../com/posts/FeedSlice'
import {PostFeedLoadingPlaceholder} from '../com/util/LoadingPlaceholder'
import {ListCard} from 'view/com/lists/ListCard'
import {
PostFeedLoadingPlaceholder,
ProfileCardFeedLoadingPlaceholder,
} from '../com/util/LoadingPlaceholder'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {ErrorMessage} from '../com/util/error/ErrorMessage'
import {EmptyState} from '../com/util/EmptyState'
@@ -111,52 +115,80 @@ export const ProfileScreen = withAuthRequired(
}, [uiState.showLoadingMoreFooter])
const renderItem = React.useCallback(
(item: any) => {
if (item === ProfileUiModel.END_ITEM) {
return <Text style={styles.endItem}>- end of feed -</Text>
} else if (item === ProfileUiModel.LOADING_ITEM) {
return <PostFeedLoadingPlaceholder />
} else if (item._reactKey === '__error__') {
if (uiState.feed.isBlocking) {
if (uiState.selectedView === Sections.Lists) {
if (item === ProfileUiModel.LOADING_ITEM) {
return <ProfileCardFeedLoadingPlaceholder />
} else if (item._reactKey === '__error__') {
return (
<View style={s.p5}>
<ErrorMessage
message={item.error}
onPressTryAgain={onPressTryAgain}
/>
</View>
)
} else if (item === ProfileUiModel.EMPTY_ITEM) {
return (
<EmptyState
icon="ban"
message="Posts hidden"
icon="list-ul"
message="No lists yet!"
style={styles.emptyState}
/>
)
} else {
return <ListCard list={item} />
}
if (uiState.feed.isBlockedBy) {
} else {
if (item === ProfileUiModel.END_ITEM) {
return <Text style={styles.endItem}>- end of feed -</Text>
} else if (item === ProfileUiModel.LOADING_ITEM) {
return <PostFeedLoadingPlaceholder />
} else if (item._reactKey === '__error__') {
if (uiState.feed.isBlocking) {
return (
<EmptyState
icon="ban"
message="Posts hidden"
style={styles.emptyState}
/>
)
}
if (uiState.feed.isBlockedBy) {
return (
<EmptyState
icon="ban"
message="Posts hidden"
style={styles.emptyState}
/>
)
}
return (
<View style={s.p5}>
<ErrorMessage
message={item.error}
onPressTryAgain={onPressTryAgain}
/>
</View>
)
} else if (item === ProfileUiModel.EMPTY_ITEM) {
return (
<EmptyState
icon="ban"
message="Posts hidden"
icon={['far', 'message']}
message="No posts yet!"
style={styles.emptyState}
/>
)
} else if (item instanceof PostsFeedSliceModel) {
return (
<FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
)
}
return (
<View style={s.p5}>
<ErrorMessage
message={item.error}
onPressTryAgain={onPressTryAgain}
/>
</View>
)
} else if (item === ProfileUiModel.EMPTY_ITEM) {
return (
<EmptyState
icon={['far', 'message']}
message="No posts yet!"
style={styles.emptyState}
/>
)
} else if (item instanceof PostsFeedSliceModel) {
return <FeedSlice slice={item} ignoreMuteFor={uiState.profile.did} />
}
return <View />
},
[
onPressTryAgain,
uiState.selectedView,
uiState.profile.did,
uiState.feed.isBlocking,
uiState.feed.isBlockedBy,