Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 218 lines 6.5 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/core/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 React.useEffect(() => { 168 if (!enabled) { 169 setIsPTRing(false) 170 } 171 }, [enabled]) 172 173 return ( 174 <View style={s.hContentRegion}> 175 {error && ( 176 <ErrorMessage 177 message={cleanError(error)} 178 onPressTryAgain={onPressTryAgain} 179 /> 180 )} 181 <List 182 testID="notifsFeed" 183 ref={scrollElRef} 184 data={items} 185 keyExtractor={item => item._reactKey} 186 renderItem={renderItem} 187 ListHeaderComponent={ListHeaderComponent} 188 ListFooterComponent={FeedFooter} 189 refreshing={isPTRing} 190 onRefresh={onRefresh} 191 onEndReached={onEndReached} 192 onEndReachedThreshold={2} 193 onScrolledDownChange={onScrolledDownChange} 194 onItemSeen={item => { 195 if ( 196 (item.type === 'reply' || 197 item.type === 'mention' || 198 item.type === 'quote') && 199 item.subject 200 ) { 201 trackPostView(item.subject) 202 } 203 }} 204 contentContainerStyle={s.contentContainer} 205 desktopFixedHeight 206 initialNumToRender={initialNumToRender} 207 windowSize={11} 208 sideBorders={false} 209 removeClippedSubviews={true} 210 /> 211 </View> 212 ) 213} 214 215const styles = StyleSheet.create({ 216 feedFooter: {paddingTop: 20}, 217 emptyState: {paddingVertical: 40}, 218})