about summary refs log tree commit diff
path: root/src/lib/hooks/useNotificationHandler.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/hooks/useNotificationHandler.ts')
-rw-r--r--src/lib/hooks/useNotificationHandler.ts225
1 files changed, 225 insertions, 0 deletions
diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts
new file mode 100644
index 000000000..12e150572
--- /dev/null
+++ b/src/lib/hooks/useNotificationHandler.ts
@@ -0,0 +1,225 @@
+import React from 'react'
+import * as Notifications from 'expo-notifications'
+import {CommonActions, useNavigation} from '@react-navigation/native'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {logger} from '#/logger'
+import {track} from 'lib/analytics/analytics'
+import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher'
+import {NavigationProp} from 'lib/routes/types'
+import {logEvent} from 'lib/statsig/statsig'
+import {useCurrentConvoId} from 'state/messages/current-convo-id'
+import {RQKEY as RQKEY_NOTIFS} from 'state/queries/notifications/feed'
+import {invalidateCachedUnreadPage} from 'state/queries/notifications/unread'
+import {truncateAndInvalidate} from 'state/queries/util'
+import {useSession} from 'state/session'
+import {useLoggedOutViewControls} from 'state/shell/logged-out'
+import {useCloseAllActiveElements} from 'state/util'
+import {resetToTab} from '#/Navigation'
+
+type NotificationReason =
+  | 'like'
+  | 'repost'
+  | 'follow'
+  | 'mention'
+  | 'reply'
+  | 'quote'
+  | 'chat-message'
+
+type NotificationPayload =
+  | {
+      reason: Exclude<NotificationReason, 'chat-message'>
+      uri: string
+      subject: string
+    }
+  | {
+      reason: 'chat-message'
+      convoId: string
+      messageId: string
+      recipientDid: string
+    }
+
+const DEFAULT_HANDLER_OPTIONS = {
+  shouldShowAlert: false,
+  shouldPlaySound: false,
+  shouldSetBadge: true,
+}
+
+// This needs to stay outside the hook to persist between account switches
+let storedPayload: NotificationPayload | undefined
+
+export function useNotificationsHandler() {
+  const queryClient = useQueryClient()
+  const {currentAccount, accounts} = useSession()
+  const {onPressSwitchAccount} = useAccountSwitcher()
+  const navigation = useNavigation<NavigationProp>()
+  const {currentConvoId} = useCurrentConvoId()
+  const {setShowLoggedOut} = useLoggedOutViewControls()
+  const closeAllActiveElements = useCloseAllActiveElements()
+
+  // Safety to prevent double handling of the same notification
+  const prevIdentifier = React.useRef('')
+
+  React.useEffect(() => {
+    const handleNotification = (payload?: NotificationPayload) => {
+      if (!payload) return
+
+      if (payload.reason === 'chat-message') {
+        if (payload.recipientDid !== currentAccount?.did && !storedPayload) {
+          storedPayload = payload
+          closeAllActiveElements()
+
+          const account = accounts.find(a => a.did === payload.recipientDid)
+          if (account) {
+            onPressSwitchAccount(account, 'Notification')
+          } else {
+            setShowLoggedOut(true)
+          }
+        } else {
+          navigation.dispatch(state => {
+            if (state.routes[0].name === 'Messages') {
+              return CommonActions.navigate('MessagesConversation', {
+                conversation: payload.convoId,
+              })
+            } else {
+              return CommonActions.navigate('MessagesTab', {
+                screen: 'Messages',
+                params: {
+                  pushToConversation: payload.convoId,
+                },
+              })
+            }
+          })
+        }
+      } else {
+        switch (payload.reason) {
+          case 'like':
+          case 'repost':
+          case 'follow':
+          case 'mention':
+          case 'quote':
+          case 'reply':
+            resetToTab('NotificationsTab')
+            break
+          // TODO implement these after we have an idea of how to handle each individual case
+          // case 'follow':
+          //   const uri = new AtUri(payload.uri)
+          //   setTimeout(() => {
+          //     // @ts-expect-error types are weird here
+          //     navigation.navigate('HomeTab', {
+          //       screen: 'Profile',
+          //       params: {
+          //         name: uri.host,
+          //       },
+          //     })
+          //   }, 500)
+          //   break
+          // case 'mention':
+          // case 'reply':
+          //   const urip = new AtUri(payload.uri)
+          //   setTimeout(() => {
+          //     // @ts-expect-error types are weird here
+          //     navigation.navigate('HomeTab', {
+          //       screen: 'PostThread',
+          //       params: {
+          //         name: urip.host,
+          //         rkey: urip.rkey,
+          //       },
+          //     })
+          //   }, 500)
+        }
+      }
+    }
+
+    Notifications.setNotificationHandler({
+      handleNotification: async e => {
+        if (e.request.trigger.type !== 'push') return DEFAULT_HANDLER_OPTIONS
+
+        logger.debug(
+          'Notifications: received',
+          {e},
+          logger.DebugContext.notifications,
+        )
+
+        const payload = e.request.trigger.payload as NotificationPayload
+        if (
+          payload.reason === 'chat-message' &&
+          payload.recipientDid === currentAccount?.did
+        ) {
+          return {
+            shouldShowAlert: payload.convoId !== currentConvoId,
+            shouldPlaySound: false,
+            shouldSetBadge: false,
+          }
+        }
+
+        // Any notification other than a chat message should invalidate the unread page
+        invalidateCachedUnreadPage()
+        return DEFAULT_HANDLER_OPTIONS
+      },
+    })
+
+    const responseReceivedListener =
+      Notifications.addNotificationResponseReceivedListener(e => {
+        if (e.notification.request.identifier === prevIdentifier.current) {
+          return
+        }
+        prevIdentifier.current = e.notification.request.identifier
+
+        logger.debug(
+          'Notifications: response received',
+          {
+            actionIdentifier: e.actionIdentifier,
+          },
+          logger.DebugContext.notifications,
+        )
+
+        if (
+          e.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER &&
+          e.notification.request.trigger.type === 'push'
+        ) {
+          logger.debug(
+            'User pressed a notification, opening notifications tab',
+            {},
+            logger.DebugContext.notifications,
+          )
+          track('Notificatons:OpenApp')
+          logEvent('notifications:openApp', {})
+          invalidateCachedUnreadPage()
+          truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
+          logger.debug('Notifications: handleNotification', {
+            content: e.notification.request.content,
+            payload: e.notification.request.trigger.payload,
+          })
+          handleNotification(
+            e.notification.request.trigger.payload as NotificationPayload,
+          )
+          Notifications.dismissAllNotificationsAsync()
+        }
+      })
+
+    // Whenever there's a stored payload, that means we had to switch accounts before handling the notification.
+    // Whenever currentAccount changes, we should try to handle it again.
+    if (
+      storedPayload?.reason === 'chat-message' &&
+      currentAccount?.did === storedPayload.recipientDid
+    ) {
+      handleNotification(storedPayload)
+      storedPayload = undefined
+    }
+
+    return () => {
+      responseReceivedListener.remove()
+    }
+  }, [
+    queryClient,
+    currentAccount,
+    currentConvoId,
+    accounts,
+    closeAllActiveElements,
+    currentAccount?.did,
+    navigation,
+    onPressSwitchAccount,
+    setShowLoggedOut,
+  ])
+}