bsky-app/src/state/queries/notifications/unread.tsx
Paul Frazee 4b3ec54add
Fix infinite query reloading behavior (reset, not invalidate) (#2031)
* Reset, not invalidate, notification queries

* Reset, not invalidate, feed queries
2023-11-29 11:15:35 -08:00

178 lines
5.1 KiB
TypeScript

/**
* A kind of companion API to ./feed.ts. See that file for more info.
*/
import React from 'react'
import * as Notifications from 'expo-notifications'
import {useQueryClient} from '@tanstack/react-query'
import BroadcastChannel from '#/lib/broadcast'
import {useSession, getAgent} from '#/state/session'
import {useModerationOpts} from '../preferences'
import {fetchPage} from './util'
import {CachedFeedPage, FeedPage} from './types'
import {isNative} from '#/platform/detection'
import {useMutedThreads} from '#/state/muted-threads'
import {RQKEY as RQKEY_NOTIFS} from './feed'
import {logger} from '#/logger'
const UPDATE_INTERVAL = 30 * 1e3 // 30sec
const broadcast = new BroadcastChannel('NOTIFS_BROADCAST_CHANNEL')
type StateContext = string
interface ApiContext {
markAllRead: () => Promise<void>
checkUnread: (opts?: {invalidate?: boolean}) => Promise<void>
getCachedUnreadPage: () => FeedPage | undefined
}
const stateContext = React.createContext<StateContext>('')
const apiContext = React.createContext<ApiContext>({
async markAllRead() {},
async checkUnread() {},
getCachedUnreadPage: () => undefined,
})
export function Provider({children}: React.PropsWithChildren<{}>) {
const {hasSession, currentAccount} = useSession()
const queryClient = useQueryClient()
const moderationOpts = useModerationOpts()
const threadMutes = useMutedThreads()
const [numUnread, setNumUnread] = React.useState('')
const checkUnreadRef = React.useRef<ApiContext['checkUnread'] | null>(null)
const cacheRef = React.useRef<CachedFeedPage>({
sessDid: currentAccount?.did || '',
syncedAt: new Date(),
data: undefined,
})
// periodic sync
React.useEffect(() => {
if (!hasSession || !checkUnreadRef.current) {
return
}
checkUnreadRef.current() // fire on init
const interval = setInterval(checkUnreadRef.current, UPDATE_INTERVAL)
return () => clearInterval(interval)
}, [hasSession])
// listen for broadcasts
React.useEffect(() => {
const listener = ({data}: MessageEvent) => {
cacheRef.current = {
sessDid: currentAccount?.did || '',
syncedAt: new Date(),
data: undefined,
}
setNumUnread(data.event)
}
broadcast.addEventListener('message', listener)
return () => {
broadcast.removeEventListener('message', listener)
}
}, [setNumUnread, currentAccount])
// create API
const api = React.useMemo<ApiContext>(() => {
return {
async markAllRead() {
// update server
await getAgent().updateSeenNotifications(
cacheRef.current.syncedAt.toISOString(),
)
// update & broadcast
setNumUnread('')
broadcast.postMessage({event: ''})
},
async checkUnread({invalidate}: {invalidate?: boolean} = {}) {
try {
if (!getAgent().session) return
// count
const page = await fetchPage({
cursor: undefined,
limit: 40,
queryClient,
moderationOpts,
threadMutes,
})
const unreadCount = countUnread(page)
const unreadCountStr =
unreadCount >= 30
? '30+'
: unreadCount === 0
? ''
: String(unreadCount)
if (isNative) {
Notifications.setBadgeCountAsync(Math.min(unreadCount, 30))
}
// track last sync
const now = new Date()
const lastIndexed =
page.items[0] && new Date(page.items[0].notification.indexedAt)
cacheRef.current = {
sessDid: currentAccount?.did || '',
data: page,
syncedAt: !lastIndexed || now > lastIndexed ? now : lastIndexed,
}
// update & broadcast
setNumUnread(unreadCountStr)
if (invalidate) {
queryClient.resetQueries({queryKey: RQKEY_NOTIFS()})
}
broadcast.postMessage({event: unreadCountStr})
} catch (e) {
logger.error('Failed to check unread notifications', {error: e})
}
},
getCachedUnreadPage() {
// return cached page if was for the current user
// (protects against session changes serving data from the past session)
if (cacheRef.current.sessDid === currentAccount?.did) {
return cacheRef.current.data
}
},
}
}, [setNumUnread, queryClient, moderationOpts, threadMutes, currentAccount])
checkUnreadRef.current = api.checkUnread
return (
<stateContext.Provider value={numUnread}>
<apiContext.Provider value={api}>{children}</apiContext.Provider>
</stateContext.Provider>
)
}
export function useUnreadNotifications() {
return React.useContext(stateContext)
}
export function useUnreadNotificationsApi() {
return React.useContext(apiContext)
}
function countUnread(page: FeedPage) {
let num = 0
for (const item of page.items) {
if (!item.notification.isRead) {
num++
}
if (item.additional) {
for (const item2 of item.additional) {
if (!item2.isRead) {
num++
}
}
}
}
return num
}