import React, {memo, MutableRefObject} from 'react' import { ActivityIndicator, Dimensions, RefreshControl, StyleProp, StyleSheet, View, ViewStyle, } from 'react-native' import {FlatList} from '../util/Views' import {PostFeedLoadingPlaceholder} from '../util/LoadingPlaceholder' import {FeedErrorMessage} from './FeedErrorMessage' import {FeedSlice} from './FeedSlice' import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn' import {OnScrollHandler} from 'lib/hooks/useOnMainScroll' import {useAnalytics} from 'lib/analytics/analytics' 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, pollLatest, } from '#/state/queries/post-feed' import {useModerationOpts} from '#/state/queries/preferences' 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__'} let Feed = ({ feed, feedParams, style, enabled, pollInterval, scrollElRef, onScroll, onHasNew, scrollEventThrottle, renderEmptyState, renderEndOfFeed, testID, headerOffset = 0, desktopFixedHeightOffset, ListHeaderComponent, extraData, }: { feed: FeedDescriptor feedParams?: FeedParams style?: StyleProp enabled?: boolean pollInterval?: number scrollElRef?: MutableRefObject | null> onHasNew?: (v: boolean) => void onScroll?: OnScrollHandler scrollEventThrottle?: number renderEmptyState: () => JSX.Element renderEndOfFeed?: () => JSX.Element testID?: string headerOffset?: number desktopFixedHeightOffset?: number ListHeaderComponent?: () => JSX.Element extraData?: any }): React.ReactNode => { const pal = usePalette('default') const theme = useTheme() const {track} = useAnalytics() const [isPTRing, setIsPTRing] = React.useState(false) const checkForNewRef = React.useRef<(() => void) | null>(null) const moderationOpts = useModerationOpts() const opts = React.useMemo(() => ({enabled}), [enabled]) const { data, isFetching, isFetched, isError, error, refetch, hasNextPage, isFetchingNextPage, fetchNextPage, } = usePostFeedQuery(feed, feedParams, opts) const isEmpty = !isFetching && !data?.pages[0]?.slices.length const checkForNew = React.useCallback(async () => { if (!data?.pages[0] || isFetching || !onHasNew) { return } try { if (await pollLatest(data.pages[0])) { onHasNew(true) } } catch (e) { logger.error('Poll latest failed', {feed, error: String(e)}) } }, [feed, data, isFetching, onHasNew]) React.useEffect(() => { // we store the interval handler in a ref to avoid needless // reassignments of the interval checkForNewRef.current = checkForNew }, [checkForNew]) React.useEffect(() => { if (!pollInterval) { return } const i = setInterval(() => checkForNewRef.current?.(), pollInterval) return () => clearInterval(i) }, [pollInterval]) const feedItems = React.useMemo(() => { let arr: any[] = [] if (isFetched && moderationOpts) { if (isError && isEmpty) { arr = arr.concat([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 { arr.push(LOADING_ITEM) } return arr }, [isFetched, isError, isEmpty, data, moderationOpts]) // events // = const onRefresh = React.useCallback(async () => { track('Feed:onRefresh') setIsPTRing(true) try { await refetch() onHasNew?.(false) } catch (err) { logger.error('Failed to refresh posts feed', {error: err}) } setIsPTRing(false) }, [refetch, track, setIsPTRing, onHasNew]) const onEndReached = React.useCallback(async () => { if (isFetching || !hasNextPage || isError) return track('Feed:onEndReached') try { await fetchNextPage() } catch (err) { logger.error('Failed to load more posts', {error: err}) } }, [isFetching, hasNextPage, isError, fetchNextPage, track]) const onPressTryAgain = React.useCallback(() => { refetch() onHasNew?.(false) }, [refetch, onHasNew]) const onPressRetryLoadMore = React.useCallback(() => { fetchNextPage() }, [fetchNextPage]) // rendering // = const renderItem = React.useCallback( ({item}: {item: any}) => { if (item === EMPTY_FEED_ITEM) { return renderEmptyState() } else if (item === ERROR_ITEM) { return ( ) } else if (item === LOAD_MORE_ERROR_ITEM) { return ( ) } else if (item === LOADING_ITEM) { return } return ( ) }, [ feed, error, onPressTryAgain, onPressRetryLoadMore, renderEmptyState, moderationOpts, ], ) const shouldRenderEndOfFeed = !hasNextPage && !isEmpty && !isFetching && !isError && !!renderEndOfFeed const FeedFooter = React.useCallback( () => isFetchingNextPage ? ( ) : shouldRenderEndOfFeed ? ( renderEndOfFeed() ) : ( ), [isFetchingNextPage, shouldRenderEndOfFeed, renderEndOfFeed], ) const scrollHandler = useAnimatedScrollHandler(onScroll || {}) return ( item._reactKey} renderItem={renderItem} ListFooterComponent={FeedFooter} ListHeaderComponent={ListHeaderComponent} refreshControl={ } contentContainerStyle={{ minHeight: Dimensions.get('window').height * 1.5, }} style={{paddingTop: headerOffset}} onScroll={onScroll != null ? scrollHandler : undefined} scrollEventThrottle={scrollEventThrottle} indicatorStyle={theme.colorScheme === 'dark' ? 'white' : 'black'} onEndReached={onEndReached} onEndReachedThreshold={2} removeClippedSubviews={true} contentOffset={{x: 0, y: headerOffset * -1}} extraData={extraData} // @ts-ignore our .web version only -prf desktopFixedHeight={ desktopFixedHeightOffset ? desktopFixedHeightOffset : true } /> ) } Feed = memo(Feed) export {Feed} const styles = StyleSheet.create({ feedFooter: {paddingTop: 20}, })