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:
parent
51f04b9620
commit
c8c308e31e
31 changed files with 904 additions and 1081 deletions
|
@ -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},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue