Refactor notifications to use react-query (#1878)

* Move broadcast channel to lib

* Refactor view/com/post/Post and remove temporary 2 components

* Add useModerationOpts hook

* Refactor notifications to use react-query

* Fix: only trigger updates in useModerationOpts when the values have changed

* Implement unread notification tracking

* Add moderation filtering to notifications

* Handle native/push notifications

* Remove dead code

---------

Co-authored-by: Eric Bailey <git@esb.lol>
This commit is contained in:
Paul Frazee 2023-11-12 18:13:11 -08:00 committed by GitHub
parent c584a3378d
commit b445c15cc9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
29 changed files with 941 additions and 1739 deletions

View file

@ -1,8 +1,6 @@
import React, {MutableRefObject} from 'react'
import {observer} from 'mobx-react-lite'
import {CenteredView, FlatList} from '../util/Views'
import {ActivityIndicator, RefreshControl, StyleSheet, View} from 'react-native'
import {NotificationsFeedModel} from 'state/models/feeds/notifications'
import {FeedItem} from './FeedItem'
import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
import {ErrorMessage} from '../util/error/ErrorMessage'
@ -12,20 +10,22 @@ import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED'
import {s} from 'lib/styles'
import {usePalette} from 'lib/hooks/usePalette'
import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
import {logger} from '#/logger'
import {cleanError} from '#/lib/strings/errors'
import {useModerationOpts} from '#/state/queries/preferences'
const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
const LOADING_SPINNER = {_reactKey: '__loading_spinner__'}
const LOADING_ITEM = {_reactKey: '__loading__'}
export const Feed = observer(function Feed({
view,
export function Feed({
scrollElRef,
onPressTryAgain,
onScroll,
ListHeaderComponent,
}: {
view: NotificationsFeedModel
scrollElRef?: MutableRefObject<FlatList<any> | null>
onPressTryAgain?: () => void
onScroll?: OnScrollHandler
@ -33,35 +33,54 @@ export const Feed = observer(function Feed({
}) {
const pal = usePalette('default')
const [isPTRing, setIsPTRing] = React.useState(false)
const data = React.useMemo(() => {
let feedItems: any[] = []
if (view.isRefreshing && !isPTRing) {
feedItems = [LOADING_SPINNER]
const moderationOpts = useModerationOpts()
const {markAllRead} = useUnreadNotificationsApi()
const {
data,
dataUpdatedAt,
isFetching,
isFetched,
isError,
error,
refetch,
hasNextPage,
isFetchingNextPage,
fetchNextPage,
} = useNotificationFeedQuery({enabled: !!moderationOpts})
const isEmpty = !isFetching && !data?.pages[0]?.items.length
const firstItem = data?.pages[0]?.items[0]
// mark all read on fresh data
React.useEffect(() => {
if (firstItem) {
markAllRead()
}
if (view.hasLoaded) {
if (view.isEmpty) {
feedItems = feedItems.concat([EMPTY_FEED_ITEM])
} else {
feedItems = feedItems.concat(view.notifications)
}, [firstItem, markAllRead])
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)
}
if (view.loadMoreError) {
feedItems = (feedItems || []).concat([LOAD_MORE_ERROR_ITEM])
}
return feedItems
}, [
view.hasLoaded,
view.isEmpty,
view.notifications,
view.loadMoreError,
view.isRefreshing,
isPTRing,
])
return arr
}, [isFetched, isError, isEmpty, data])
const onRefresh = React.useCallback(async () => {
try {
setIsPTRing(true)
await view.refresh()
await refetch()
} catch (err) {
logger.error('Failed to refresh notifications feed', {
error: err,
@ -69,21 +88,21 @@ export const Feed = observer(function Feed({
} finally {
setIsPTRing(false)
}
}, [view, setIsPTRing])
}, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => {
if (isFetching || !hasNextPage || isError) return
try {
await view.loadMore()
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more notifications', {
error: err,
})
logger.error('Failed to load more notifications', {error: err})
}
}, [view])
}, [isFetching, hasNextPage, isError, fetchNextPage])
const onPressRetryLoadMore = React.useCallback(() => {
view.retryLoadMore()
}, [view])
fetchNextPage()
}, [fetchNextPage])
// TODO optimize renderItem or FeedItem, we're getting this notice from RN: -prf
// VirtualizedList: You have a large list that is slow to update - make sure your
@ -106,78 +125,72 @@ export const Feed = observer(function Feed({
onPress={onPressRetryLoadMore}
/>
)
} else if (item === LOADING_SPINNER) {
return (
<View style={styles.loading}>
<ActivityIndicator size="small" />
</View>
)
} else if (item === LOADING_ITEM) {
return <NotificationFeedLoadingPlaceholder />
}
return <FeedItem item={item} />
return (
<FeedItem
item={item}
dataUpdatedAt={dataUpdatedAt}
moderationOpts={moderationOpts!}
/>
)
},
[onPressRetryLoadMore],
[onPressRetryLoadMore, dataUpdatedAt, moderationOpts],
)
const FeedFooter = React.useCallback(
() =>
view.isLoading ? (
isFetchingNextPage ? (
<View style={styles.feedFooter}>
<ActivityIndicator />
</View>
) : (
<View />
),
[view],
[isFetchingNextPage],
)
const scrollHandler = useAnimatedScrollHandler(onScroll || {})
return (
<View style={s.hContentRegion}>
<CenteredView>
{view.isLoading && !data.length && (
<NotificationFeedLoadingPlaceholder />
)}
{view.hasError && (
{error && (
<CenteredView>
<ErrorMessage
message={view.error}
message={cleanError(error)}
onPressTryAgain={onPressTryAgain}
/>
)}
</CenteredView>
{data.length ? (
<FlatList
testID="notifsFeed"
ref={scrollElRef}
data={data}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={FeedFooter}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
onScroll={scrollHandler}
scrollEventThrottle={1}
contentContainerStyle={s.contentContainer}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
) : null}
</CenteredView>
)}
<FlatList
testID="notifsFeed"
ref={scrollElRef}
data={items}
keyExtractor={item => item._reactKey}
renderItem={renderItem}
ListHeaderComponent={ListHeaderComponent}
ListFooterComponent={FeedFooter}
refreshControl={
<RefreshControl
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached}
onEndReachedThreshold={0.6}
onScroll={scrollHandler}
scrollEventThrottle={1}
contentContainerStyle={s.contentContainer}
// @ts-ignore our .web version only -prf
desktopFixedHeight
/>
</View>
)
})
}
const styles = StyleSheet.create({
loading: {
paddingVertical: 20,
},
feedFooter: {paddingTop: 20},
emptyState: {paddingVertical: 40},
})