diff options
Diffstat (limited to 'src/view/com/notifications/NotificationFeed.tsx')
-rw-r--r-- | src/view/com/notifications/NotificationFeed.tsx | 197 |
1 files changed, 197 insertions, 0 deletions
diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx new file mode 100644 index 000000000..5168933ae --- /dev/null +++ b/src/view/com/notifications/NotificationFeed.tsx @@ -0,0 +1,197 @@ +import React from 'react' +import { + ActivityIndicator, + ListRenderItemInfo, + StyleSheet, + View, +} from 'react-native' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' +import {usePalette} from '#/lib/hooks/usePalette' +import {cleanError} from '#/lib/strings/errors' +import {s} from '#/lib/styles' +import {logger} from '#/logger' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' +import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread' +import {EmptyState} from '#/view/com/util/EmptyState' +import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' +import {List, ListRef} from '#/view/com/util/List' +import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' +import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' +import {NotificationFeedItem} from './NotificationFeedItem' + +const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} +const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} +const LOADING_ITEM = {_reactKey: '__loading__'} + +export function NotificationFeed({ + scrollElRef, + onPressTryAgain, + onScrolledDownChange, + ListHeaderComponent, + overridePriorityNotifications, +}: { + scrollElRef?: ListRef + onPressTryAgain?: () => void + onScrolledDownChange: (isScrolledDown: boolean) => void + ListHeaderComponent?: () => JSX.Element + overridePriorityNotifications?: boolean +}) { + const initialNumToRender = useInitialNumToRender() + + const [isPTRing, setIsPTRing] = React.useState(false) + const pal = usePalette('default') + + const {_} = useLingui() + const moderationOpts = useModerationOpts() + const {checkUnread} = useUnreadNotificationsApi() + const { + data, + isFetching, + isFetched, + isError, + error, + hasNextPage, + isFetchingNextPage, + fetchNextPage, + } = useNotificationFeedQuery({ + enabled: !!moderationOpts, + overridePriorityNotifications, + }) + const isEmpty = !isFetching && !data?.pages[0]?.items.length + + const items = React.useMemo(() => { + let arr: any[] = [] + if (isFetched) { + if (isEmpty) { + arr = arr.concat([EMPTY_FEED_ITEM]) + } else if (data) { + for (const page of data?.pages) { + arr = arr.concat(page.items) + } + } + if (isError && !isEmpty) { + arr = arr.concat([LOAD_MORE_ERROR_ITEM]) + } + } else { + arr.push(LOADING_ITEM) + } + return arr + }, [isFetched, isError, isEmpty, data]) + + const onRefresh = React.useCallback(async () => { + try { + setIsPTRing(true) + await checkUnread({invalidate: true}) + } catch (err) { + logger.error('Failed to refresh notifications feed', { + message: err, + }) + } finally { + setIsPTRing(false) + } + }, [checkUnread, setIsPTRing]) + + const onEndReached = React.useCallback(async () => { + if (isFetching || !hasNextPage || isError) return + + try { + await fetchNextPage() + } catch (err) { + logger.error('Failed to load more notifications', {message: err}) + } + }, [isFetching, hasNextPage, isError, fetchNextPage]) + + const onPressRetryLoadMore = React.useCallback(() => { + fetchNextPage() + }, [fetchNextPage]) + + const renderItem = React.useCallback( + ({item, index}: ListRenderItemInfo<any>) => { + if (item === EMPTY_FEED_ITEM) { + return ( + <EmptyState + icon="bell" + message={_(msg`No notifications yet!`)} + style={styles.emptyState} + /> + ) + } else if (item === LOAD_MORE_ERROR_ITEM) { + return ( + <LoadMoreRetryBtn + label={_( + msg`There was an issue fetching notifications. Tap here to try again.`, + )} + onPress={onPressRetryLoadMore} + /> + ) + } else if (item === LOADING_ITEM) { + return ( + <View style={[pal.border]}> + <NotificationFeedLoadingPlaceholder /> + </View> + ) + } + return ( + <NotificationFeedItem + item={item} + moderationOpts={moderationOpts!} + hideTopBorder={index === 0} + /> + ) + }, + [moderationOpts, _, onPressRetryLoadMore, pal.border], + ) + + const FeedFooter = React.useCallback( + () => + isFetchingNextPage ? ( + <View style={styles.feedFooter}> + <ActivityIndicator /> + </View> + ) : ( + <View /> + ), + [isFetchingNextPage], + ) + + return ( + <View style={s.hContentRegion}> + {error && ( + <ErrorMessage + message={cleanError(error)} + onPressTryAgain={onPressTryAgain} + /> + )} + <List + testID="notifsFeed" + ref={scrollElRef} + data={items} + keyExtractor={item => item._reactKey} + renderItem={renderItem} + ListHeaderComponent={ListHeaderComponent} + ListFooterComponent={FeedFooter} + refreshing={isPTRing} + onRefresh={onRefresh} + onEndReached={onEndReached} + onEndReachedThreshold={2} + onScrolledDownChange={onScrolledDownChange} + contentContainerStyle={s.contentContainer} + // @ts-ignore our .web version only -prf + desktopFixedHeight + initialNumToRender={initialNumToRender} + windowSize={11} + sideBorders={false} + removeClippedSubviews={true} + /> + </View> + ) +} + +const styles = StyleSheet.create({ + feedFooter: {paddingTop: 20}, + emptyState: {paddingVertical: 40}, +}) |