forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
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})