about summary refs log tree commit diff
path: root/src/state/queries/notifications/unread.tsx
blob: 36bc6528f911d74413bebdf72363b21b5945d349 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
import React from 'react'
import * as Notifications from 'expo-notifications'
import BroadcastChannel from '#/lib/broadcast'
import {useSession, getAgent} from '#/state/session'
import {useModerationOpts} from '../preferences'
import {shouldFilterNotif} from './util'
import {isNative} from '#/platform/detection'

const UPDATE_INTERVAL = 30 * 1e3 // 30sec

const broadcast = new BroadcastChannel('NOTIFS_BROADCAST_CHANNEL')

type StateContext = string

interface ApiContext {
  markAllRead: () => Promise<void>
  checkUnread: () => Promise<void>
}

const stateContext = React.createContext<StateContext>('')

const apiContext = React.createContext<ApiContext>({
  async markAllRead() {},
  async checkUnread() {},
})

export function Provider({children}: React.PropsWithChildren<{}>) {
  const {hasSession} = useSession()
  const moderationOpts = useModerationOpts()

  const [numUnread, setNumUnread] = React.useState('')

  const checkUnreadRef = React.useRef<(() => Promise<void>) | null>(null)
  const lastSyncRef = React.useRef<Date>(new Date())

  // 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) => {
      lastSyncRef.current = new Date()
      setNumUnread(data.event)
    }
    broadcast.addEventListener('message', listener)
    return () => {
      broadcast.removeEventListener('message', listener)
    }
  }, [setNumUnread])

  // create API
  const api = React.useMemo<ApiContext>(() => {
    return {
      async markAllRead() {
        // update server
        await getAgent().updateSeenNotifications(
          lastSyncRef.current.toISOString(),
        )

        // update & broadcast
        setNumUnread('')
        broadcast.postMessage({event: ''})
      },

      async checkUnread() {
        const agent = getAgent()

        if (!agent.session) return

        // count
        const res = await agent.listNotifications({limit: 40})
        const filtered = res.data.notifications.filter(
          notif => !notif.isRead && !shouldFilterNotif(notif, moderationOpts),
        )
        const num =
          filtered.length >= 30
            ? '30+'
            : filtered.length === 0
            ? ''
            : String(filtered.length)
        if (isNative) {
          Notifications.setBadgeCountAsync(Math.min(filtered.length, 30))
        }

        // track last sync
        const now = new Date()
        const lastIndexed = filtered[0] && new Date(filtered[0].indexedAt)
        lastSyncRef.current =
          !lastIndexed || now > lastIndexed ? now : lastIndexed

        // update & broadcast
        setNumUnread(num)
        broadcast.postMessage({event: num})
      },
    }
  }, [setNumUnread, moderationOpts])
  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)
}