diff options
author | dan <dan.abramov@gmail.com> | 2024-12-12 17:37:07 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-12-12 17:37:07 +0000 |
commit | f8cdd6b9ae46a9a4efe0be87e55ee1debded4f91 (patch) | |
tree | 87884914d17ca8d466bcb57a1a668f77a1d7338f /src | |
parent | 10e241e7eb5a45a0c35802618f52b411dcecd0b0 (diff) | |
download | voidsky-f8cdd6b9ae46a9a4efe0be87e55ee1debded4f91.tar.zst |
[Notifications] Add a Mentions tab (#7044)
* Split out NotificationsTab * Remove unused route parameter * Refine the split between components * Hoist some logic out of NotificationFeed * Remove unused option * Add all|conversations to query, hardcode "all" * Add a Conversations tab * Rename to Mentions * Bump packages * Rename fields * Fix oopsie * Simplify header * Track active tab * Fix types * Separate logic for tabs * Better border for first unread * Highlight unread for all only * Fix spinner races * Fix fetchPage races * Fix bottom bar border being obscured by glimmer * Remember last tab within the session * One tab at a time * Fix TS * Handle all RQKEY usages * Nit
Diffstat (limited to 'src')
-rw-r--r-- | src/lib/hooks/useNotificationHandler.ts | 15 | ||||
-rw-r--r-- | src/lib/routes/types.ts | 6 | ||||
-rw-r--r-- | src/screens/Settings/NotificationSettings.tsx | 8 | ||||
-rw-r--r-- | src/state/queries/notifications/feed.ts | 34 | ||||
-rw-r--r-- | src/state/queries/notifications/settings.ts | 9 | ||||
-rw-r--r-- | src/state/queries/notifications/unread.tsx | 16 | ||||
-rw-r--r-- | src/state/queries/notifications/util.ts | 5 | ||||
-rw-r--r-- | src/state/queries/util.ts | 4 | ||||
-rw-r--r-- | src/view/com/notifications/NotificationFeed.tsx | 33 | ||||
-rw-r--r-- | src/view/com/notifications/NotificationFeedItem.tsx | 13 | ||||
-rw-r--r-- | src/view/com/pager/Pager.tsx | 14 | ||||
-rw-r--r-- | src/view/screens/DebugMod.tsx | 8 | ||||
-rw-r--r-- | src/view/screens/Notifications.tsx | 256 |
13 files changed, 277 insertions, 144 deletions
diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index 69ae536d0..2ec3fcb79 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -239,14 +239,21 @@ export function useNotificationsHandler() { ) logEvent('notifications:openApp', {}) invalidateCachedUnreadPage() - truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) + const payload = e.notification.request.trigger + .payload as NotificationPayload + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all')) + if ( + payload.reason === 'mention' || + payload.reason === 'quote' || + payload.reason === 'reply' + ) { + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')) + } logger.debug('Notifications: handleNotification', { content: e.notification.request.content, payload: e.notification.request.trigger.payload, }) - handleNotification( - e.notification.request.trigger.payload as NotificationPayload, - ) + handleNotification(payload) Notifications.dismissAllNotificationsAsync() } }) diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index 9e3407261..238e4be4c 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -75,7 +75,7 @@ export type SearchTabNavigatorParams = CommonNavigatorParams & { } export type NotificationsTabNavigatorParams = CommonNavigatorParams & { - Notifications: {show?: 'all'} + Notifications: undefined } export type MyProfileTabNavigatorParams = CommonNavigatorParams & { @@ -90,7 +90,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & { Home: undefined Search: {q?: string} Feeds: undefined - Notifications: {show?: 'all'} + Notifications: undefined Hashtag: {tag: string; author?: string} Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} } @@ -102,7 +102,7 @@ export type AllNavigatorParams = CommonNavigatorParams & { Search: {q?: string} Feeds: undefined NotificationsTab: undefined - Notifications: {show?: 'all'} + Notifications: undefined MyProfileTab: undefined Hashtag: {tag: string; author?: string} MessagesTab: undefined diff --git a/src/screens/Settings/NotificationSettings.tsx b/src/screens/Settings/NotificationSettings.tsx index 1c77b3148..ebb230c2c 100644 --- a/src/screens/Settings/NotificationSettings.tsx +++ b/src/screens/Settings/NotificationSettings.tsx @@ -18,7 +18,13 @@ type Props = NativeStackScreenProps<AllNavigatorParams, 'NotificationSettings'> export function NotificationSettingsScreen({}: Props) { const {_} = useLingui() - const {data, isError: isQueryError, refetch} = useNotificationFeedQuery() + const { + data, + isError: isQueryError, + refetch, + } = useNotificationFeedQuery({ + filter: 'all', + }) const serverPriority = data?.pages.at(0)?.priority const { diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts index 19a92fc3c..72100a624 100644 --- a/src/state/queries/notifications/feed.ts +++ b/src/state/queries/notifications/feed.ts @@ -52,25 +52,22 @@ const PAGE_SIZE = 30 type RQPageParam = string | undefined const RQKEY_ROOT = 'notification-feed' -export function RQKEY(priority?: false) { - return [RQKEY_ROOT, priority] +export function RQKEY(filter: 'all' | 'mentions') { + return [RQKEY_ROOT, filter] } -export function useNotificationFeedQuery(opts?: { +export function useNotificationFeedQuery(opts: { enabled?: boolean - overridePriorityNotifications?: boolean + filter: 'all' | 'mentions' }) { const agent = useAgent() const queryClient = useQueryClient() const moderationOpts = useModerationOpts() const unreads = useUnreadNotificationsApi() - const enabled = opts?.enabled !== false + const enabled = opts.enabled !== false + const filter = opts.filter const {uris: hiddenReplyUris} = useThreadgateHiddenReplyUris() - // false: force showing all notifications - // undefined: let the server decide - const priority = opts?.overridePriorityNotifications ? false : undefined - const selectArgs = useMemo(() => { return { moderationOpts, @@ -91,14 +88,23 @@ export function useNotificationFeedQuery(opts?: { RQPageParam >({ staleTime: STALE.INFINITY, - queryKey: RQKEY(priority), + queryKey: RQKEY(filter), async queryFn({pageParam}: {pageParam: RQPageParam}) { let page - if (!pageParam) { + if (filter === 'all' && !pageParam) { // for the first page, we check the cached page held by the unread-checker first page = unreads.getCachedUnreadPage() } if (!page) { + let reasons: string[] = [] + if (filter === 'mentions') { + reasons = [ + // Anything that's a post + 'mention', + 'reply', + 'quote', + ] + } const {page: fetchedPage} = await fetchPage({ agent, limit: PAGE_SIZE, @@ -106,13 +112,13 @@ export function useNotificationFeedQuery(opts?: { queryClient, moderationOpts, fetchAdditionalData: true, - priority, + reasons, }) page = fetchedPage } - // if the first page has an unread, mark all read - if (!pageParam) { + if (filter === 'all' && !pageParam) { + // if the first page has an unread, mark all read unreads.markAllRead() } diff --git a/src/state/queries/notifications/settings.ts b/src/state/queries/notifications/settings.ts index a17fce832..e552b6520 100644 --- a/src/state/queries/notifications/settings.ts +++ b/src/state/queries/notifications/settings.ts @@ -45,7 +45,8 @@ export function useNotificationSettingsMutation() { }, onSettled: () => { invalidateCachedUnreadPage() - queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS()}) + queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('all')}) + queryClient.invalidateQueries({queryKey: RQKEY_NOTIFS('mentions')}) }, }) } @@ -54,7 +55,7 @@ function eagerlySetCachedPriority( queryClient: ReturnType<typeof useQueryClient>, enabled: boolean, ) { - queryClient.setQueryData(RQKEY_NOTIFS(), (old: any) => { + function updateData(old: any) { if (!old) return old return { ...old, @@ -65,5 +66,7 @@ function eagerlySetCachedPriority( } }), } - }) + } + queryClient.setQueryData(RQKEY_NOTIFS('all'), updateData) + queryClient.setQueryData(RQKEY_NOTIFS('mentions'), updateData) } diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx index 2ade04246..ba2377a78 100644 --- a/src/state/queries/notifications/unread.tsx +++ b/src/state/queries/notifications/unread.tsx @@ -2,7 +2,7 @@ * A kind of companion API to ./feed.ts. See that file for more info. */ -import React from 'react' +import React, {useRef} from 'react' import {AppState} from 'react-native' import {useQueryClient} from '@tanstack/react-query' import EventEmitter from 'eventemitter3' @@ -105,6 +105,8 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } }, [setNumUnread]) + const isFetchingRef = useRef(false) + // create API const api = React.useMemo<ApiContext>(() => { return { @@ -138,6 +140,12 @@ export function Provider({children}: React.PropsWithChildren<{}>) { } } + if (isFetchingRef.current) { + return + } + // Do not move this without ensuring it gets a symmetrical reset in the finally block. + isFetchingRef.current = true + // count const {page, indexedAt: lastIndexed} = await fetchPage({ agent, @@ -145,6 +153,7 @@ export function Provider({children}: React.PropsWithChildren<{}>) { limit: 40, queryClient, moderationOpts, + reasons: [], // only fetch subjects when the page is going to be used // in the notifications query, otherwise skip it @@ -174,11 +183,14 @@ export function Provider({children}: React.PropsWithChildren<{}>) { // update & broadcast setNumUnread(unreadCountStr) if (invalidate) { - truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('all')) + truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')) } broadcast.postMessage({event: unreadCountStr}) } catch (e) { logger.warn('Failed to check unread notifications', {error: e}) + } finally { + isFetchingRef.current = false } }, diff --git a/src/state/queries/notifications/util.ts b/src/state/queries/notifications/util.ts index a251d170e..0d72e9e92 100644 --- a/src/state/queries/notifications/util.ts +++ b/src/state/queries/notifications/util.ts @@ -31,6 +31,7 @@ export async function fetchPage({ queryClient, moderationOpts, fetchAdditionalData, + reasons, }: { agent: BskyAgent cursor: string | undefined @@ -38,7 +39,7 @@ export async function fetchPage({ queryClient: QueryClient moderationOpts: ModerationOpts | undefined fetchAdditionalData: boolean - priority?: boolean + reasons: string[] }): Promise<{ page: FeedPage indexedAt: string | undefined @@ -46,7 +47,7 @@ export async function fetchPage({ const res = await agent.listNotifications({ limit, cursor, - // priority, + reasons, }) const indexedAt = res.data.notifications[0]?.indexedAt diff --git a/src/state/queries/util.ts b/src/state/queries/util.ts index 0d6a8e99a..887c1df0a 100644 --- a/src/state/queries/util.ts +++ b/src/state/queries/util.ts @@ -8,7 +8,7 @@ import { } from '@atproto/api' import {InfiniteData, QueryClient, QueryKey} from '@tanstack/react-query' -export function truncateAndInvalidate<T = any>( +export async function truncateAndInvalidate<T = any>( queryClient: QueryClient, queryKey: QueryKey, ) { @@ -21,7 +21,7 @@ export function truncateAndInvalidate<T = any>( } return data }) - queryClient.invalidateQueries({queryKey}) + return queryClient.invalidateQueries({queryKey}) } // Given an AtUri, this function will check if the AtUri matches a diff --git a/src/view/com/notifications/NotificationFeed.tsx b/src/view/com/notifications/NotificationFeed.tsx index 5168933ae..0b814e68d 100644 --- a/src/view/com/notifications/NotificationFeed.tsx +++ b/src/view/com/notifications/NotificationFeed.tsx @@ -9,13 +9,11 @@ import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useInitialNumToRender} from '#/lib/hooks/useInitialNumToRender' -import {usePalette} from '#/lib/hooks/usePalette' import {cleanError} from '#/lib/strings/errors' import {s} from '#/lib/styles' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useNotificationFeedQuery} from '#/state/queries/notifications/feed' -import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread' import {EmptyState} from '#/view/com/util/EmptyState' import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' import {List, ListRef} from '#/view/com/util/List' @@ -28,26 +26,26 @@ const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'} const LOADING_ITEM = {_reactKey: '__loading__'} export function NotificationFeed({ + filter, + enabled, scrollElRef, onPressTryAgain, onScrolledDownChange, ListHeaderComponent, - overridePriorityNotifications, + refreshNotifications, }: { + filter: 'all' | 'mentions' + enabled: boolean scrollElRef?: ListRef onPressTryAgain?: () => void onScrolledDownChange: (isScrolledDown: boolean) => void ListHeaderComponent?: () => JSX.Element - overridePriorityNotifications?: boolean + refreshNotifications: () => Promise<void> }) { const initialNumToRender = useInitialNumToRender() - const [isPTRing, setIsPTRing] = React.useState(false) - const pal = usePalette('default') - const {_} = useLingui() const moderationOpts = useModerationOpts() - const {checkUnread} = useUnreadNotificationsApi() const { data, isFetching, @@ -58,8 +56,8 @@ export function NotificationFeed({ isFetchingNextPage, fetchNextPage, } = useNotificationFeedQuery({ - enabled: !!moderationOpts, - overridePriorityNotifications, + enabled: enabled && !!moderationOpts, + filter, }) const isEmpty = !isFetching && !data?.pages[0]?.items.length @@ -85,7 +83,7 @@ export function NotificationFeed({ const onRefresh = React.useCallback(async () => { try { setIsPTRing(true) - await checkUnread({invalidate: true}) + await refreshNotifications() } catch (err) { logger.error('Failed to refresh notifications feed', { message: err, @@ -93,7 +91,7 @@ export function NotificationFeed({ } finally { setIsPTRing(false) } - }, [checkUnread, setIsPTRing]) + }, [refreshNotifications, setIsPTRing]) const onEndReached = React.useCallback(async () => { if (isFetching || !hasNextPage || isError) return @@ -129,21 +127,18 @@ export function NotificationFeed({ /> ) } else if (item === LOADING_ITEM) { - return ( - <View style={[pal.border]}> - <NotificationFeedLoadingPlaceholder /> - </View> - ) + return <NotificationFeedLoadingPlaceholder /> } return ( <NotificationFeedItem + highlightUnread={filter === 'all'} item={item} moderationOpts={moderationOpts!} - hideTopBorder={index === 0} + hideTopBorder={index === 0 && item.notification.isRead} /> ) }, - [moderationOpts, _, onPressRetryLoadMore, pal.border], + [moderationOpts, _, onPressRetryLoadMore, filter], ) const FeedFooter = React.useCallback( diff --git a/src/view/com/notifications/NotificationFeedItem.tsx b/src/view/com/notifications/NotificationFeedItem.tsx index 4902e66bc..1267ce089 100644 --- a/src/view/com/notifications/NotificationFeedItem.tsx +++ b/src/view/com/notifications/NotificationFeedItem.tsx @@ -79,10 +79,12 @@ interface Author { let NotificationFeedItem = ({ item, moderationOpts, + highlightUnread, hideTopBorder, }: { item: FeedNotification moderationOpts: ModerationOpts + highlightUnread: boolean hideTopBorder?: boolean }): React.ReactNode => { const queryClient = useQueryClient() @@ -151,6 +153,7 @@ let NotificationFeedItem = ({ if (!item.subject) { return null } + const isHighlighted = highlightUnread && !item.notification.isRead return ( <Link testID={`feedItem-by-${item.notification.author.handle}`} @@ -160,12 +163,10 @@ let NotificationFeedItem = ({ <Post post={item.subject} style={ - item.notification.isRead - ? undefined - : { - backgroundColor: pal.colors.unreadNotifBg, - borderColor: pal.colors.unreadNotifBorder, - } + isHighlighted && { + backgroundColor: pal.colors.unreadNotifBg, + borderColor: pal.colors.unreadNotifBorder, + } } hideTopBorder={hideTopBorder} /> diff --git a/src/view/com/pager/Pager.tsx b/src/view/com/pager/Pager.tsx index 2c0bbee52..b3f936ddc 100644 --- a/src/view/com/pager/Pager.tsx +++ b/src/view/com/pager/Pager.tsx @@ -136,12 +136,14 @@ export const Pager = forwardRef<PagerRef, React.PropsWithChildren<Props>>( return ( <View testID={testID} style={[a.flex_1, native(a.overflow_hidden)]}> - {renderTabBar({ - selectedPage, - onSelect: onTabBarSelect, - dragProgress, - dragState, - })} + <View style={a.z_10 /* Let tabbar bottom border cover the glimmer */}> + {renderTabBar({ + selectedPage, + onSelect: onTabBarSelect, + dragProgress, + dragState, + })} + </View> <GestureDetector gesture={nativeGesture}> <AnimatedPagerView ref={pagerView} diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx index 74a58a56a..4ff0a4b8b 100644 --- a/src/view/screens/DebugMod.tsx +++ b/src/view/screens/DebugMod.tsx @@ -872,7 +872,13 @@ function MockNotifItem({ </P> ) } - return <NotificationFeedItem item={notif} moderationOpts={moderationOpts} /> + return ( + <NotificationFeedItem + item={notif} + moderationOpts={moderationOpts} + highlightUnread + /> + ) } function MockAccountCard({ diff --git a/src/view/screens/Notifications.tsx b/src/view/screens/Notifications.tsx index 70ab32db0..82c68dde6 100644 --- a/src/view/screens/Notifications.tsx +++ b/src/view/screens/Notifications.tsx @@ -13,7 +13,7 @@ import { } from '#/lib/routes/types' import {s} from '#/lib/styles' import {logger} from '#/logger' -import {isNative, isWeb} from '#/platform/detection' +import {isNative} from '#/platform/detection' import {emitSoftReset, listenSoftReset} from '#/state/events' import {RQKEY as NOTIFS_RQKEY} from '#/state/queries/notifications/feed' import { @@ -24,35 +24,173 @@ import {truncateAndInvalidate} from '#/state/queries/util' import {useSetMinimalShellMode} from '#/state/shell' import {useComposerControls} from '#/state/shell/composer' import {NotificationFeed} from '#/view/com/notifications/NotificationFeed' +import {Pager} from '#/view/com/pager/Pager' +import {TabBar} from '#/view/com/pager/TabBar' import {FAB} from '#/view/com/util/fab/FAB' import {ListMethods} from '#/view/com/util/List' import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn' import {MainScrollProvider} from '#/view/com/util/MainScrollProvider' -import {atoms as a, useBreakpoints, useTheme} from '#/alf' -import {Button, ButtonIcon} from '#/components/Button' +import {atoms as a} from '#/alf' +import {web} from '#/alf' +import {ButtonIcon} from '#/components/Button' import {SettingsGear2_Stroke2_Corner0_Rounded as SettingsIcon} from '#/components/icons/SettingsGear2' import * as Layout from '#/components/Layout' import {Link} from '#/components/Link' import {Loader} from '#/components/Loader' +// We don't currently persist this across reloads since +// you gotta visit All to clear the badge anyway. +// But let's at least persist it during the sesssion. +let lastActiveTab = 0 + type Props = NativeStackScreenProps< NotificationsTabNavigatorParams, 'Notifications' > -export function NotificationsScreen({route: {params}}: Props) { - const t = useTheme() - const {gtTablet} = useBreakpoints() +export function NotificationsScreen({}: Props) { + const {_} = useLingui() + const {openComposer} = useComposerControls() + const unreadNotifs = useUnreadNotifications() + const hasNew = !!unreadNotifs + const {checkUnread: checkUnreadAll} = useUnreadNotificationsApi() + const [isLoadingAll, setIsLoadingAll] = React.useState(false) + const [isLoadingMentions, setIsLoadingMentions] = React.useState(false) + const initialActiveTab = lastActiveTab + const [activeTab, setActiveTab] = React.useState(initialActiveTab) + const isLoading = activeTab === 0 ? isLoadingAll : isLoadingMentions + + const onPageSelected = React.useCallback( + (index: number) => { + setActiveTab(index) + lastActiveTab = index + }, + [setActiveTab], + ) + + const queryClient = useQueryClient() + const checkUnreadMentions = React.useCallback( + async ({invalidate}: {invalidate: boolean}) => { + if (invalidate) { + return truncateAndInvalidate(queryClient, NOTIFS_RQKEY('mentions')) + } else { + // Background polling is not implemented for the mentions tab. + // Just ignore it. + } + }, + [queryClient], + ) + + const sections = React.useMemo(() => { + return [ + { + title: _(msg`All`), + component: ( + <NotificationsTab + filter="all" + isActive={activeTab === 0} + isLoading={isLoadingAll} + hasNew={hasNew} + setIsLoadingLatest={setIsLoadingAll} + checkUnread={checkUnreadAll} + /> + ), + }, + { + title: _(msg`Mentions`), + component: ( + <NotificationsTab + filter="mentions" + isActive={activeTab === 1} + isLoading={isLoadingMentions} + hasNew={false /* We don't know for sure */} + setIsLoadingLatest={setIsLoadingMentions} + checkUnread={checkUnreadMentions} + /> + ), + }, + ] + }, [ + _, + hasNew, + checkUnreadAll, + checkUnreadMentions, + activeTab, + isLoadingAll, + isLoadingMentions, + ]) + + return ( + <Layout.Screen testID="notificationsScreen"> + <Layout.Header.Outer noBottomBorder> + <Layout.Header.MenuButton /> + <Layout.Header.Content> + <Layout.Header.TitleText> + <Trans>Notifications</Trans> + </Layout.Header.TitleText> + </Layout.Header.Content> + <Layout.Header.Slot> + <Link + to="/notifications/settings" + label={_(msg`Notification settings`)} + size="small" + variant="ghost" + color="secondary" + shape="round" + style={[a.justify_center]}> + <ButtonIcon icon={isLoading ? Loader : SettingsIcon} size="lg" /> + </Link> + </Layout.Header.Slot> + </Layout.Header.Outer> + <Pager + onPageSelected={onPageSelected} + renderTabBar={props => ( + <Layout.Center style={web([a.sticky, a.z_10, {top: 0}])}> + <TabBar + {...props} + items={sections.map(section => section.title)} + onPressSelected={() => emitSoftReset()} + /> + </Layout.Center> + )} + initialPage={initialActiveTab}> + {sections.map((section, i) => ( + <View key={i}>{section.component}</View> + ))} + </Pager> + <FAB + testID="composeFAB" + onPress={() => openComposer({})} + icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} + accessibilityRole="button" + accessibilityLabel={_(msg`New post`)} + accessibilityHint="" + /> + </Layout.Screen> + ) +} + +function NotificationsTab({ + filter, + isActive, + isLoading, + hasNew, + checkUnread, + setIsLoadingLatest, +}: { + filter: 'all' | 'mentions' + isActive: boolean + isLoading: boolean + hasNew: boolean + checkUnread: ({invalidate}: {invalidate: boolean}) => Promise<void> + setIsLoadingLatest: (v: boolean) => void +}) { const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const [isScrolledDown, setIsScrolledDown] = React.useState(false) - const [isLoadingLatest, setIsLoadingLatest] = React.useState(false) const scrollElRef = React.useRef<ListMethods>(null) const queryClient = useQueryClient() - const unreadNotifs = useUnreadNotifications() - const unreadApi = useUnreadNotificationsApi() - const hasNew = !!unreadNotifs const isScreenFocused = useIsFocused() - const {openComposer} = useComposerControls() + const isFocusedAndActive = isScreenFocused && isActive // event handlers // = @@ -65,16 +203,23 @@ export function NotificationsScreen({route: {params}}: Props) { scrollToTop() if (hasNew) { // render what we have now - truncateAndInvalidate(queryClient, NOTIFS_RQKEY()) - } else { + truncateAndInvalidate(queryClient, NOTIFS_RQKEY(filter)) + } else if (!isLoading) { // check with the server setIsLoadingLatest(true) - unreadApi - .checkUnread({invalidate: true}) + checkUnread({invalidate: true}) .catch(() => undefined) .then(() => setIsLoadingLatest(false)) } - }, [scrollToTop, queryClient, unreadApi, hasNew, setIsLoadingLatest]) + }, [ + scrollToTop, + queryClient, + checkUnread, + hasNew, + isLoading, + setIsLoadingLatest, + filter, + ]) const onFocusCheckLatest = useNonReactiveCallback(() => { // on focus, check for latest, but only invalidate if the user @@ -87,79 +232,36 @@ export function NotificationsScreen({route: {params}}: Props) { // we're just going to look it up synchronously. currentIsScrolledDown = window.scrollY > 200 } - unreadApi.checkUnread({invalidate: !currentIsScrolledDown}) + checkUnread({invalidate: !currentIsScrolledDown}) }) // on-visible setup // = useFocusEffect( React.useCallback(() => { - setMinimalShellMode(false) - logger.debug('NotificationsScreen: Focus') - onFocusCheckLatest() - }, [setMinimalShellMode, onFocusCheckLatest]), + if (isFocusedAndActive) { + setMinimalShellMode(false) + logger.debug('NotificationsScreen: Focus') + onFocusCheckLatest() + } + }, [setMinimalShellMode, onFocusCheckLatest, isFocusedAndActive]), ) React.useEffect(() => { - if (!isScreenFocused) { + if (!isFocusedAndActive) { return } return listenSoftReset(onPressLoadLatest) - }, [onPressLoadLatest, isScreenFocused]) + }, [onPressLoadLatest, isFocusedAndActive]) return ( - <Layout.Screen testID="notificationsScreen"> - <Layout.Header.Outer> - <Layout.Header.MenuButton /> - <Layout.Header.Content> - <Button - label={_(msg`Notifications`)} - accessibilityHint={_(msg`Refresh notifications`)} - onPress={emitSoftReset} - style={[a.justify_start]}> - {({hovered}) => ( - <Layout.Header.TitleText - style={[a.w_full, hovered && a.underline]}> - <Trans>Notifications</Trans> - {isWeb && gtTablet && hasNew && ( - <View - style={[ - a.rounded_full, - { - width: 8, - height: 8, - bottom: 3, - left: 6, - backgroundColor: t.palette.primary_500, - }, - ]} - /> - )} - </Layout.Header.TitleText> - )} - </Button> - </Layout.Header.Content> - <Layout.Header.Slot> - <Link - to="/notifications/settings" - label={_(msg`Notification settings`)} - size="small" - variant="ghost" - color="secondary" - shape="round" - style={[a.justify_center]}> - <ButtonIcon - icon={isLoadingLatest ? Loader : SettingsIcon} - size="lg" - /> - </Link> - </Layout.Header.Slot> - </Layout.Header.Outer> - + <> <MainScrollProvider> <NotificationFeed + enabled={isFocusedAndActive} + filter={filter} + refreshNotifications={() => checkUnread({invalidate: true})} onScrolledDownChange={setIsScrolledDown} scrollElRef={scrollElRef} - overridePriorityNotifications={params?.show === 'all'} /> </MainScrollProvider> {(isScrolledDown || hasNew) && ( @@ -169,14 +271,6 @@ export function NotificationsScreen({route: {params}}: Props) { showIndicator={hasNew} /> )} - <FAB - testID="composeFAB" - onPress={() => openComposer({})} - icon={<ComposeIcon2 strokeWidth={1.5} size={29} style={s.white} />} - accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} - accessibilityHint="" - /> - </Layout.Screen> + </> ) } |