[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
This commit is contained in:
@@ -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()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -872,7 +872,13 @@ function MockNotifItem({
|
||||
</P>
|
||||
)
|
||||
}
|
||||
return <NotificationFeedItem item={notif} moderationOpts={moderationOpts} />
|
||||
return (
|
||||
<NotificationFeedItem
|
||||
item={notif}
|
||||
moderationOpts={moderationOpts}
|
||||
highlightUnread
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function MockAccountCard({
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user