Simplify list logic further to prevent misuse (#3334)

* simplify list logic further

more simplification

simplify by removing `isEmpty`

use `isFetchingNextPage` everywhere for clarity

change `isFetching` to `isFetchingNextPage` for clarity

remove some useless `useMemo`s

move `renderItem` and `keyExtractor` out of component

* clean bundle size check

* update deploy

* adjust

* adjust

* one test

* try now

* test it

* done
This commit is contained in:
Hailey 2024-04-03 20:59:33 -07:00 committed by GitHub
parent b1bd7ab6e3
commit 8e393b16f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 241 additions and 258 deletions

View file

@ -1,9 +1,9 @@
import React from 'react'
import {View} from 'react-native'
import {useNavigation} from '@react-navigation/core'
import {StackActions} from '@react-navigation/native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useNavigation} from '@react-navigation/core'
import {StackActions} from '@react-navigation/native'
import {NavigationProp} from 'lib/routes/types'
import {CenteredView} from 'view/com/util/Views'

View file

@ -1,47 +1,54 @@
import React from 'react'
import {View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {Trans} from '@lingui/macro'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {logger} from '#/logger'
import {List} from '#/view/com/util/List'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {ListFooter} from '#/components/Lists'
import {cleanError} from 'lib/strings/errors'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {List} from '#/view/com/util/List'
import {ListFooter, ListMaybePlaceholder} from '#/components/Lists'
import {atoms as a, useTheme} from '#/alf'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
function renderItem({item}: {item: GetLikes.Like}) {
return <ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
}
function keyExtractor(item: GetLikes.Like) {
return item.actor.did
}
export function LikedByList({uri}: {uri: string}) {
const t = useTheme()
const {_} = useLingui()
const initialNumToRender = useInitialNumToRender()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
data: resolvedUri,
error: resolveError,
isFetching: isFetchingResolvedUri,
isLoading: isUriLoading,
} = useResolveUriQuery(uri)
const {
data,
isFetching,
isFetched,
isRefetching,
isLoading: isLikedByLoading,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
isError,
error: likedByError,
refetch,
} = useLikedByQuery(resolvedUri?.uri)
const error = resolveError || likedByError
const isError = !!resolveError || !!likedByError
const likes = React.useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.likes)
}
return []
}, [data])
const initialNumToRender = useInitialNumToRender()
const error = resolveError || likedByError
const onRefresh = React.useCallback(async () => {
setIsPTRing(true)
@ -54,56 +61,47 @@ export function LikedByList({uri}: {uri: string}) {
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
if (isFetchingNextPage || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more likes', {message: err})
}
}, [isFetching, hasNextPage, isError, fetchNextPage])
}, [isFetchingNextPage, hasNextPage, isError, fetchNextPage])
const renderItem = React.useCallback(({item}: {item: GetLikes.Like}) => {
if (likes.length < 1) {
return (
<ProfileCardWithFollowBtn key={item.actor.did} profile={item.actor} />
)
}, [])
if (isFetchingResolvedUri || !isFetched) {
return (
<View style={[a.w_full, a.align_center, a.p_lg]}>
<Loader size="xl" />
</View>
<ListMaybePlaceholder
isLoading={isUriLoading || isLikedByLoading}
isError={isError}
emptyType="results"
emptyMessage={_(
msg`Nobody has liked this yet. Maybe you should be the first!`,
)}
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
/>
)
}
return likes.length ? (
return (
<List
data={likes}
keyExtractor={item => item.actor.did}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={3}
renderItem={renderItem}
initialNumToRender={initialNumToRender}
ListFooterComponent={() => (
ListFooterComponent={
<ListFooter
isFetching={isFetching && !isRefetching}
isError={isError}
error={error ? error.toString() : undefined}
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
)}
}
onEndReachedThreshold={3}
initialNumToRender={initialNumToRender}
windowSize={11}
/>
) : (
<View style={[a.p_lg]}>
<View style={[a.p_lg, a.rounded_sm, t.atoms.bg_contrast_25]}>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
Nobody has liked this yet. Maybe you should be the first!
</Trans>
</Text>
</View>
</View>
)
}

View file

@ -1,25 +1,23 @@
import React from 'react'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {View} from 'react-native'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {Trans, msg} from '@lingui/macro'
import {CenteredView} from 'view/com/util/Views'
import {Loader} from '#/components/Loader'
import {cleanError} from 'lib/strings/errors'
import {CenteredView} from 'view/com/util/Views'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button} from '#/components/Button'
import {Text} from '#/components/Typography'
import {Error} from '#/components/Error'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
export function ListFooter({
isFetching,
isError,
isFetchingNextPage,
error,
onRetry,
height,
}: {
isFetching?: boolean
isError?: boolean
isFetchingNextPage?: boolean
error?: string
onRetry?: () => Promise<unknown>
height?: number
@ -36,32 +34,26 @@ export function ListFooter({
t.atoms.border_contrast_low,
{height: height ?? 180, paddingTop: 30},
]}>
{isFetching ? (
{isFetchingNextPage ? (
<Loader size="xl" />
) : (
<ListFooterMaybeError
isError={isError}
error={error}
onRetry={onRetry}
/>
<ListFooterMaybeError error={error} onRetry={onRetry} />
)}
</View>
)
}
function ListFooterMaybeError({
isError,
error,
onRetry,
}: {
isError?: boolean
error?: string
onRetry?: () => Promise<unknown>
}) {
const t = useTheme()
const {_} = useLingui()
if (!isError) return null
if (!error) return null
return (
<View style={[a.w_full, a.px_lg]}>
@ -128,7 +120,7 @@ export function ListHeaderDesktop({
export function ListMaybePlaceholder({
isLoading,
isEmpty,
noEmpty,
isError,
emptyTitle,
emptyMessage,
@ -138,7 +130,7 @@ export function ListMaybePlaceholder({
onRetry,
}: {
isLoading: boolean
isEmpty?: boolean
noEmpty?: boolean
isError?: boolean
emptyTitle?: string
emptyMessage?: string
@ -151,16 +143,6 @@ export function ListMaybePlaceholder({
const {_} = useLingui()
const {gtMobile, gtTablet} = useBreakpoints()
if (!isLoading && isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
if (isLoading) {
return (
<CenteredView
@ -180,7 +162,17 @@ export function ListMaybePlaceholder({
)
}
if (isEmpty) {
if (isError) {
return (
<Error
title={errorTitle ?? _(msg`Oops!`)}
message={errorMessage ?? _(`Something went wrong!`)}
onRetry={onRetry}
/>
)
}
if (!noEmpty) {
return (
<Error
title={
@ -197,4 +189,6 @@ export function ListMaybePlaceholder({
/>
)
}
return null
}

View file

@ -1,6 +1,6 @@
import React from 'react'
import {StyleProp, TextStyle, ViewStyle} from 'react-native'
import Svg, {Path, Rect, Line, Ellipse} from 'react-native-svg'
import Svg, {Ellipse, Line, Path, Rect} from 'react-native-svg'
export function GridIcon({
style,

View file

@ -1,28 +1,30 @@
import React from 'react'
import {ListRenderItemInfo, Pressable} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {useSetMinimalShellMode} from 'state/shell'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types'
import {useSearchPostsQuery} from 'state/queries/search-posts'
import {Post} from 'view/com/post/Post'
import {PostView} from '@atproto/api/dist/client/types/app/bsky/feed/defs'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {HITSLOP_10} from 'lib/constants'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {CommonNavigatorParams} from 'lib/routes/types'
import {shareUrl} from 'lib/sharing'
import {cleanError} from 'lib/strings/errors'
import {sanitizeHandle} from 'lib/strings/handles'
import {enforceLen} from 'lib/strings/helpers'
import {isNative} from 'platform/detection'
import {useSearchPostsQuery} from 'state/queries/search-posts'
import {useSetMinimalShellMode} from 'state/shell'
import {Post} from 'view/com/post/Post'
import {List} from 'view/com/util/List'
import {ViewHeader} from 'view/com/util/ViewHeader'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import {
ListFooter,
ListHeaderDesktop,
ListMaybePlaceholder,
} from '#/components/Lists'
import {List} from 'view/com/util/List'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {sanitizeHandle} from 'lib/strings/handles'
import {ArrowOutOfBox_Stroke2_Corner0_Rounded} from '#/components/icons/ArrowOutOfBox'
import {shareUrl} from 'lib/sharing'
import {HITSLOP_10} from 'lib/constants'
import {isNative} from 'platform/detection'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
const renderItem = ({item}: ListRenderItemInfo<PostView>) => {
return <Post post={item} />
@ -61,9 +63,8 @@ export default function HashtagScreen({
const {
data,
isFetching,
isFetchingNextPage,
isLoading,
isRefetching,
isError,
error,
refetch,
@ -97,9 +98,9 @@ export default function HashtagScreen({
}, [refetch])
const onEndReached = React.useCallback(() => {
if (isFetching || !hasNextPage || error) return
if (isFetchingNextPage || !hasNextPage || error) return
fetchNextPage()
}, [isFetching, hasNextPage, error, fetchNextPage])
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
return (
<>
@ -123,16 +124,16 @@ export default function HashtagScreen({
: undefined
}
/>
<ListMaybePlaceholder
isLoading={isLoading || isRefetching}
isError={isError}
isEmpty={posts.length < 1}
onRetry={refetch}
emptyTitle="results"
emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
/>
{!isLoading && posts.length > 0 && (
<List<PostView>
{posts.length < 1 ? (
<ListMaybePlaceholder
isLoading={isLoading}
isError={isError}
onRetry={refetch}
emptyType="results"
emptyMessage={_(msg`We couldn't find any results for that hashtag.`)}
/>
) : (
<List
data={posts}
renderItem={renderItem}
keyExtractor={keyExtractor}
@ -150,9 +151,8 @@ export default function HashtagScreen({
}
ListFooterComponent={
<ListFooter
isFetching={isFetching && !isRefetching}
isError={isError}
error={error?.name}
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
}

View file

@ -4,13 +4,11 @@ import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {NativeStackScreenProps, CommonNavigatorParams} from '#/lib/routes/types'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {useSetMinimalShellMode} from '#/state/shell'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {LikedByList} from '#/components/LikedByList'
import {useSetMinimalShellMode} from '#/state/shell'
import {makeRecordUri} from '#/lib/strings/url-helpers'
import {atoms as a, useBreakpoints} from '#/alf'
export function ProfileLabelerLikedByScreen({
route,
@ -19,7 +17,6 @@ export function ProfileLabelerLikedByScreen({
const {name: handleOrDid} = route.params
const uri = makeRecordUri(handleOrDid, 'app.bsky.labeler.service', 'self')
const {_} = useLingui()
const {gtMobile} = useBreakpoints()
useFocusEffect(
React.useCallback(() => {
@ -28,17 +25,7 @@ export function ProfileLabelerLikedByScreen({
)
return (
<View
style={[
a.mx_auto,
a.w_full,
a.h_full_vh,
gtMobile && [
{
maxWidth: 600,
},
],
]}>
<View style={{flex: 1}}>
<ViewHeader title={_(msg`Liked By`)} />
<LikedByList uri={uri} />
</View>

View file

@ -368,47 +368,52 @@ export function PostThread({
],
)
return (
<>
if (error || !thread) {
return (
<ListMaybePlaceholder
isLoading={(!preferences || !thread) && !error}
isError={!!error}
noEmpty
onRetry={refetch}
errorTitle={error?.title}
errorMessage={error?.message}
/>
{!error && thread && (
<List
ref={ref}
data={posts}
renderItem={renderItem}
keyExtractor={keyExtractor}
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
onStartReached={onStartReached}
onEndReached={onEndReached}
onEndReachedThreshold={2}
onMomentumScrollEnd={onMomentumScrollEnd}
onScrollToTop={onScrollToTop}
maintainVisibleContentPosition={
isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
}
// @ts-ignore our .web version only -prf
desktopFixedHeight
removeClippedSubviews={isAndroid ? false : undefined}
ListFooterComponent={
<ListFooter
isFetching={isFetching}
onRetry={refetch}
// 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to
// work without causing weird jumps on web or glitches on native
height={windowHeight - 200}
/>
}
initialNumToRender={initialNumToRender}
windowSize={11}
)
}
return (
<List
ref={ref}
data={posts}
renderItem={renderItem}
keyExtractor={keyExtractor}
onContentSizeChange={isNative ? undefined : onContentSizeChangeWeb}
onStartReached={onStartReached}
onEndReached={onEndReached}
onEndReachedThreshold={2}
onMomentumScrollEnd={onMomentumScrollEnd}
onScrollToTop={onScrollToTop}
maintainVisibleContentPosition={
isNative ? MAINTAIN_VISIBLE_CONTENT_POSITION : undefined
}
// @ts-ignore our .web version only -prf
desktopFixedHeight
removeClippedSubviews={isAndroid ? false : undefined}
ListFooterComponent={
<ListFooter
// Using `isFetching` over `isFetchingNextPage` is done on purpose here so we get the loader on
// initial render
isFetchingNextPage={isFetching}
error={cleanError(threadError)}
onRetry={refetch}
// 300 is based on the minimum height of a post. This is enough extra height for the `maintainVisPos` to
// work without causing weird jumps on web or glitches on native
height={windowHeight - 200}
/>
)}
</>
}
initialNumToRender={initialNumToRender}
windowSize={11}
/>
)
}

View file

@ -1,21 +1,21 @@
import React from 'react'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useProfileFollowersQuery} from '#/state/queries/profile-followers'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {useSession} from 'state/session'
import {
ListFooter,
ListHeaderDesktop,
ListMaybePlaceholder,
} from '#/components/Lists'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useSession} from 'state/session'
import {View} from 'react-native'
import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from './ProfileCard'
function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
return <ProfileCardWithFollowBtn key={item.did} profile={item} />
@ -39,7 +39,6 @@ export function ProfileFollowers({name}: {name: string}) {
const {
data,
isLoading: isFollowersLoading,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
@ -47,14 +46,8 @@ export function ProfileFollowers({name}: {name: string}) {
refetch,
} = useProfileFollowersQuery(resolvedDid)
const isError = React.useMemo(
() => !!resolveError || !!error,
[resolveError, error],
)
const isMe = React.useMemo(() => {
return resolvedDid === currentAccount?.did
}, [resolvedDid, currentAccount?.did])
const isError = !!resolveError || !!error
const isMe = resolvedDid === currentAccount?.did
const followers = React.useMemo(() => {
if (data?.pages) {
@ -73,20 +66,19 @@ export function ProfileFollowers({name}: {name: string}) {
setIsPTRing(false)
}, [refetch, setIsPTRing])
const onEndReached = async () => {
if (isFetching || !hasNextPage || !!error) return
const onEndReached = React.useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || !!error) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more followers', {message: err})
}
}
}, [isFetchingNextPage, hasNextPage, error, fetchNextPage])
return (
<View style={{flex: 1}}>
if (followers.length < 1) {
return (
<ListMaybePlaceholder
isLoading={isDidLoading || isFollowersLoading}
isEmpty={followers.length < 1}
isError={isError}
emptyType="results"
emptyMessage={
@ -97,23 +89,30 @@ export function ProfileFollowers({name}: {name: string}) {
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
/>
{followers.length > 0 && (
<List
data={followers}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
ListHeaderComponent={<ListHeaderDesktop title={_(msg`Followers`)} />}
ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
)
}
return (
<List
data={followers}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
ListHeaderComponent={<ListHeaderDesktop title={_(msg`Followers`)} />}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
)}
</View>
}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
/>
)
}

View file

@ -1,20 +1,21 @@
import React from 'react'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from './ProfileCard'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useProfileFollowsQuery} from '#/state/queries/profile-follows'
import {useResolveDidQuery} from '#/state/queries/resolve-uri'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {useSession} from 'state/session'
import {
ListFooter,
ListHeaderDesktop,
ListMaybePlaceholder,
} from '#/components/Lists'
import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
import {useSession} from 'state/session'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {List} from '../util/List'
import {ProfileCardWithFollowBtn} from './ProfileCard'
function renderItem({item}: {item: ActorDefs.ProfileViewBasic}) {
return <ProfileCardWithFollowBtn key={item.did} profile={item} />
@ -38,7 +39,6 @@ export function ProfileFollows({name}: {name: string}) {
const {
data,
isLoading: isFollowsLoading,
isFetching,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
@ -46,14 +46,8 @@ export function ProfileFollows({name}: {name: string}) {
refetch,
} = useProfileFollowsQuery(resolvedDid)
const isError = React.useMemo(
() => !!resolveError || !!error,
[resolveError, error],
)
const isMe = React.useMemo(() => {
return resolvedDid === currentAccount?.did
}, [resolvedDid, currentAccount?.did])
const isError = !!resolveError || !!error
const isMe = resolvedDid === currentAccount?.did
const follows = React.useMemo(() => {
if (data?.pages) {
@ -72,20 +66,19 @@ export function ProfileFollows({name}: {name: string}) {
setIsPTRing(false)
}, [refetch, setIsPTRing])
const onEndReached = async () => {
if (isFetching || !hasNextPage || !!error) return
const onEndReached = React.useCallback(async () => {
if (isFetchingNextPage || !hasNextPage || !!error) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more follows', {error: err})
}
}
}, [error, fetchNextPage, hasNextPage, isFetchingNextPage])
return (
<>
if (follows.length < 1) {
return (
<ListMaybePlaceholder
isLoading={isDidLoading || isFollowsLoading}
isEmpty={follows.length < 1}
isError={isError}
emptyType="results"
emptyMessage={
@ -96,23 +89,30 @@ export function ProfileFollows({name}: {name: string}) {
errorMessage={cleanError(resolveError || error)}
onRetry={isError ? refetch : undefined}
/>
{follows.length > 0 && (
<List
data={follows}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
ListHeaderComponent={<ListHeaderDesktop title={_(msg`Following`)} />}
ListFooterComponent={<ListFooter isFetching={isFetchingNextPage} />}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
)
}
return (
<List
data={follows}
renderItem={renderItem}
keyExtractor={keyExtractor}
refreshing={isPTRing}
onRefresh={onRefresh}
onEndReached={onEndReached}
onEndReachedThreshold={4}
ListHeaderComponent={<ListHeaderDesktop title={_(msg`Following`)} />}
ListFooterComponent={
<ListFooter
isFetchingNextPage={isFetchingNextPage}
error={cleanError(error)}
onRetry={fetchNextPage}
/>
)}
</>
}
// @ts-ignore our .web version only -prf
desktopFixedHeight
initialNumToRender={initialNumToRender}
windowSize={11}
/>
)
}