Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

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