Bluesky app fork with some witchin' additions 馃挮
at readme-update 212 lines 6.4 kB view raw
1import React from 'react' 2import { 3 ActivityIndicator, 4 type ListRenderItemInfo, 5 StyleSheet, 6 View, 7} from 'react-native' 8import {msg} from '@lingui/macro' 9import {useLingui} from '@lingui/react' 10 11import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' 12import {usePostViewTracking} from '#/lib/hooks/usePostViewTracking' 13import {cleanError} from '#/lib/strings/errors' 14import {s} from '#/lib/styles' 15import {logger} from '#/logger' 16import {useModerationOpts} from '#/state/preferences/moderation-opts' 17import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' 18import {EmptyState} from '#/view/com/util/EmptyState' 19import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 20import {List, type ListProps, type ListRef} from '#/view/com/util/List' 21import {NotificationFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 22import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 23import {useTheme} from '#/alf' 24import {Bell_Stroke2_Corner0_Rounded as BellIcon} from '#/components/icons/Bell' 25import {NotificationFeedItem} from './NotificationFeedItem' 26 27const EMPTY_FEED_ITEM = {_reactKey: '__empty__'} 28const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} 29const LOADING_ITEM = {_reactKey: '__loading__'} 30 31export function NotificationFeed({ 32 filter, 33 enabled, 34 scrollElRef, 35 onPressTryAgain, 36 onScrolledDownChange, 37 ListHeaderComponent, 38 refreshNotifications, 39}: { 40 filter: 'all' | 'mentions' 41 enabled: boolean 42 scrollElRef?: ListRef 43 onPressTryAgain?: () => void 44 onScrolledDownChange: (isScrolledDown: boolean) => void 45 ListHeaderComponent?: ListProps['ListHeaderComponent'] 46 refreshNotifications: () => Promise<void> 47}) { 48 const initialNumToRender = useInitialNumToRender() 49 const [isPTRing, setIsPTRing] = React.useState(false) 50 const t = useTheme() 51 const {_} = useLingui() 52 const moderationOpts = useModerationOpts() 53 const trackPostView = usePostViewTracking('Notifications') 54 const { 55 data, 56 isFetching, 57 isFetched, 58 isError, 59 error, 60 hasNextPage, 61 isFetchingNextPage, 62 fetchNextPage, 63 } = useNotificationFeedQuery({ 64 enabled: enabled && !!moderationOpts, 65 filter, 66 }) 67 // previously, this was `!isFetching && !data?.pages[0]?.items.length` 68 // however, if the first page had no items (can happen in the mentions tab!) 69 // it would flicker the empty state whenever it was loading. 70 // therefore, we need to find if *any* page has items. in 99.9% of cases, 71 // the `.find()` won't need to go any further than the first page -sfn 72 const isEmpty = 73 !isFetching && !data?.pages.find(page => page.items.length > 0) 74 75 const items = React.useMemo(() => { 76 let arr: any[] = [] 77 if (isFetched) { 78 if (isEmpty) { 79 arr = arr.concat([EMPTY_FEED_ITEM]) 80 } else if (data) { 81 for (const page of data?.pages) { 82 arr = arr.concat(page.items) 83 } 84 } 85 if (isError && !isEmpty) { 86 arr = arr.concat([LOAD_MORE_ERROR_ITEM]) 87 } 88 } else { 89 arr.push(LOADING_ITEM) 90 } 91 return arr 92 }, [isFetched, isError, isEmpty, data]) 93 94 const onRefresh = React.useCallback(async () => { 95 try { 96 setIsPTRing(true) 97 await refreshNotifications() 98 } catch (err) { 99 logger.error('Failed to refresh notifications feed', { 100 message: err, 101 }) 102 } finally { 103 setIsPTRing(false) 104 } 105 }, [refreshNotifications, setIsPTRing]) 106 107 const onEndReached = React.useCallback(async () => { 108 if (isFetching || !hasNextPage || isError) return 109 110 try { 111 await fetchNextPage() 112 } catch (err) { 113 logger.error('Failed to load more notifications', {message: err}) 114 } 115 }, [isFetching, hasNextPage, isError, fetchNextPage]) 116 117 const onPressRetryLoadMore = React.useCallback(() => { 118 fetchNextPage() 119 }, [fetchNextPage]) 120 121 const renderItem = React.useCallback( 122 ({item, index}: ListRenderItemInfo<any>) => { 123 if (item === EMPTY_FEED_ITEM) { 124 return ( 125 <EmptyState 126 icon={BellIcon} 127 message={_(msg`No notifications yet!`)} 128 style={styles.emptyState} 129 /> 130 ) 131 } else if (item === LOAD_MORE_ERROR_ITEM) { 132 return ( 133 <LoadMoreRetryBtn 134 label={_( 135 msg`There was an issue fetching notifications. Tap here to try again.`, 136 )} 137 onPress={onPressRetryLoadMore} 138 /> 139 ) 140 } else if (item === LOADING_ITEM) { 141 return <NotificationFeedLoadingPlaceholder /> 142 } 143 return ( 144 <NotificationFeedItem 145 highlightUnread={filter === 'all'} 146 item={item} 147 moderationOpts={moderationOpts!} 148 hideTopBorder={index === 0} 149 /> 150 ) 151 }, 152 [moderationOpts, _, onPressRetryLoadMore, filter], 153 ) 154 155 const FeedFooter = React.useCallback( 156 () => 157 isFetchingNextPage ? ( 158 <View style={styles.feedFooter}> 159 <ActivityIndicator color={t.palette.primary_500} /> 160 </View> 161 ) : ( 162 <View /> 163 ), 164 [isFetchingNextPage, t.palette.primary_500], 165 ) 166 167 return ( 168 <View style={s.hContentRegion}> 169 {error && ( 170 <ErrorMessage 171 message={cleanError(error)} 172 onPressTryAgain={onPressTryAgain} 173 /> 174 )} 175 <List 176 testID="notifsFeed" 177 ref={scrollElRef} 178 data={items} 179 keyExtractor={item => item._reactKey} 180 renderItem={renderItem} 181 ListHeaderComponent={ListHeaderComponent} 182 ListFooterComponent={FeedFooter} 183 refreshing={isPTRing} 184 onRefresh={onRefresh} 185 onEndReached={onEndReached} 186 onEndReachedThreshold={2} 187 onScrolledDownChange={onScrolledDownChange} 188 onItemSeen={item => { 189 if ( 190 (item.type === 'reply' || 191 item.type === 'mention' || 192 item.type === 'quote') && 193 item.subject 194 ) { 195 trackPostView(item.subject) 196 } 197 }} 198 contentContainerStyle={s.contentContainer} 199 desktopFixedHeight 200 initialNumToRender={initialNumToRender} 201 windowSize={11} 202 sideBorders={false} 203 removeClippedSubviews={true} 204 /> 205 </View> 206 ) 207} 208 209const styles = StyleSheet.create({ 210 feedFooter: {paddingTop: 20}, 211 emptyState: {paddingVertical: 40}, 212})