Refactor feeds to use react-query (#1862)

* Update to react-query v5

* Introduce post-feed react query

* Add feed refresh behaviors

* Only fetch feeds of visible pages

* Implement polling for latest on feeds

* Add moderation filtering to slices

* Handle block errors

* Update feed error messages

* Remove old models

* Replace simple-feed option with disable-tuner option

* Add missing useMemo

* Implement the mergefeed and fixes to polling

* Correctly handle failed load more state

* Improve error and empty state behaviors

* Clearer naming
This commit is contained in:
Paul Frazee 2023-11-10 15:34:25 -08:00 committed by GitHub
parent 51f04b9620
commit c8c308e31e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 904 additions and 1081 deletions

View file

@ -1,5 +1,4 @@
import React, {MutableRefObject} from 'react'
import {observer} from 'mobx-react-lite'
import {
ActivityIndicator,
Dimensions,
@ -12,7 +11,6 @@ import {
import {FlatList} from '../util/Views'
import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {FeedErrorMessage} from './FeedErrorMessage'
import {PostsFeedModel} from 'state/models/feeds/posts'
import {FeedSlice} from './FeedSlice'
import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
@ -21,17 +19,26 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {useTheme} from 'lib/ThemeContext'
import {logger} from '#/logger'
import {
FeedDescriptor,
FeedParams,
usePostFeedQuery,
} from '#/state/queries/post-feed'
const LOADING_ITEM = {_reactKey: '__loading__'}
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const ERROR_ITEM = {_reactKey: '__error__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
export const Feed = observer(function Feed({
export function Feed({
feed,
feedParams,
style,
enabled,
pollInterval,
scrollElRef,
onScroll,
onHasNew,
scrollEventThrottle,
renderEmptyState,
renderEndOfFeed,
@ -41,9 +48,13 @@ export const Feed = observer(function Feed({
ListHeaderComponent,
extraData,
}: {
feed: PostsFeedModel
feed: FeedDescriptor
feedParams?: FeedParams
style?: StyleProp<ViewStyle>
enabled?: boolean
pollInterval?: number
scrollElRef?: MutableRefObject<FlatList<any> | null>
onHasNew?: (v: boolean) => void
onScroll?: OnScrollHandler
scrollEventThrottle?: number
renderEmptyState: () => JSX.Element
@ -58,32 +69,68 @@ export const Feed = observer(function Feed({
const theme = useTheme()
const {track} = useAnalytics()
const [isRefreshing, setIsRefreshing] = React.useState(false)
const checkForNewRef = React.useRef<(() => void) | null>(null)
const data = React.useMemo(() => {
let feedItems: any[] = []
if (feed.hasLoaded) {
if (feed.hasError) {
feedItems = feedItems.concat([ERROR_ITEM])
const opts = React.useMemo(() => ({enabled}), [enabled])
const {
data,
dataUpdatedAt,
isFetching,
isFetched,
isError,
error,
refetch,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
pollLatest,
} = usePostFeedQuery(feed, feedParams, opts)
const isEmpty = !isFetching && !data?.pages[0]?.slices.length
const checkForNew = React.useCallback(async () => {
if (!isFetched || isFetching || !onHasNew) {
return
}
try {
if (await pollLatest()) {
onHasNew(true)
}
if (feed.isEmpty) {
feedItems = feedItems.concat([EMPTY_FEED_ITEM])
} else {
feedItems = feedItems.concat(feed.slices)
} catch (e) {
logger.error('Poll latest failed', {feed, error: String(e)})
}
}, [feed, isFetched, isFetching, pollLatest, onHasNew])
React.useEffect(() => {
// we store the interval handler in a ref to avoid needless
// reassignments of the interval
checkForNewRef.current = checkForNew
}, [checkForNew])
React.useEffect(() => {
const i = setInterval(() => checkForNewRef.current?.(), pollInterval)
return () => clearInterval(i)
}, [pollInterval])
const feedItems = React.useMemo(() => {
let arr: any[] = []
if (isFetched) {
if (isError && isEmpty) {
arr = arr.concat([ERROR_ITEM])
}
if (feed.loadMoreError) {
feedItems = feedItems.concat([LOAD_MORE_ERROR_ITEM])
if (isEmpty) {
arr = arr.concat([EMPTY_FEED_ITEM])
} else if (data) {
for (const page of data?.pages) {
arr = arr.concat(page.slices)
}
}
if (isError && !isEmpty) {
arr = arr.concat([LOAD_MORE_ERROR_ITEM])
}
} else {
feedItems.push(LOADING_ITEM)
arr.push(LOADING_ITEM)
}
return feedItems
}, [
feed.hasError,
feed.hasLoaded,
feed.isEmpty,
feed.slices,
feed.loadMoreError,
])
return arr
}, [isFetched, isError, isEmpty, data])
// events
// =
@ -92,31 +139,33 @@ export const Feed = observer(function Feed({
track('Feed:onRefresh')
setIsRefreshing(true)
try {
await feed.refresh()
await refetch()
onHasNew?.(false)
} catch (err) {
logger.error('Failed to refresh posts feed', {error: err})
}
setIsRefreshing(false)
}, [feed, track, setIsRefreshing])
}, [refetch, track, setIsRefreshing, onHasNew])
const onEndReached = React.useCallback(async () => {
if (!feed.hasLoaded || !feed.hasMore) return
if (isFetching || !hasNextPage || isError) return
track('Feed:onEndReached')
try {
await feed.loadMore()
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more posts', {error: err})
}
}, [feed, track])
}, [isFetching, hasNextPage, isError, fetchNextPage, track])
const onPressTryAgain = React.useCallback(() => {
feed.refresh()
}, [feed])
refetch()
onHasNew?.(false)
}, [refetch, onHasNew])
const onPressRetryLoadMore = React.useCallback(() => {
feed.retryLoadMore()
}, [feed])
fetchNextPage()
}, [fetchNextPage])
// rendering
// =
@ -127,7 +176,11 @@ export const Feed = observer(function Feed({
return renderEmptyState()
} else if (item === ERROR_ITEM) {
return (
<FeedErrorMessage feed={feed} onPressTryAgain={onPressTryAgain} />
<FeedErrorMessage
feedDesc={feed}
error={error}
onPressTryAgain={onPressTryAgain}
/>
)
} else if (item === LOAD_MORE_ERROR_ITEM) {
return (
@ -139,23 +192,32 @@ export const Feed = observer(function Feed({
} else if (item === LOADING_ITEM) {
return <PostFeedLoadingPlaceholder />
}
return <FeedSlice slice={item} />
return <FeedSlice slice={item} dataUpdatedAt={dataUpdatedAt} />
},
[feed, onPressTryAgain, onPressRetryLoadMore, renderEmptyState],
[
feed,
dataUpdatedAt,
error,
onPressTryAgain,
onPressRetryLoadMore,
renderEmptyState,
],
)
const shouldRenderEndOfFeed =
!hasNextPage && !isEmpty && !isFetching && !isError && !!renderEndOfFeed
const FeedFooter = React.useCallback(
() =>
feed.isLoadingMore ? (
isFetchingNextPage ? (
<View style={styles.feedFooter}>
<ActivityIndicator />
</View>
) : !feed.hasMore && !feed.isEmpty && renderEndOfFeed ? (
) : shouldRenderEndOfFeed ? (
renderEndOfFeed()
) : (
<View />
),
[feed.isLoadingMore, feed.hasMore, feed.isEmpty, renderEndOfFeed],
[isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed],
)
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
@ -164,7 +226,7 @@ export const Feed = observer(function Feed({
<FlatList
testID={testID ? `${testID}-flatlist` : undefined}
ref={scrollElRef}
data={data}
data={feedItems}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListFooterComponent={FeedFooter}
@ -197,7 +259,7 @@ export const Feed = observer(function Feed({
/>
</View>
)
})
}
const styles = StyleSheet.create({
feedFooter: {paddingTop: 20},