about summary refs log tree commit diff
path: root/src/lib
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib')
-rw-r--r--src/lib/hooks/useNotificationHandler.ts225
-rw-r--r--src/lib/notifications/notifications.ts74
-rw-r--r--src/lib/routes/types.ts4
-rw-r--r--src/lib/statsig/events.ts7
4 files changed, 234 insertions, 76 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,
+  ])
+}
diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts
index b0bbc1bf9..38c18bf3f 100644
--- a/src/lib/notifications/notifications.ts
+++ b/src/lib/notifications/notifications.ts
@@ -1,17 +1,9 @@
-import {useEffect} from 'react'
 import * as Notifications from 'expo-notifications'
 import {BskyAgent} from '@atproto/api'
-import {QueryClient} from '@tanstack/react-query'
 
 import {logger} from '#/logger'
-import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed'
-import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread'
-import {truncateAndInvalidate} from '#/state/queries/util'
 import {SessionAccount} from '#/state/session'
-import {track} from 'lib/analytics/analytics'
-import {devicePlatform, isIOS} from 'platform/detection'
-import {resetToTab} from '../../Navigation'
-import {logEvent} from '../statsig/statsig'
+import {devicePlatform} from 'platform/detection'
 
 const SERVICE_DID = (serviceUrl?: string) =>
   serviceUrl?.includes('staging')
@@ -85,67 +77,3 @@ export function registerTokenChangeHandler(
     sub.remove()
   }
 }
-
-export function useNotificationsListener(queryClient: QueryClient) {
-  useEffect(() => {
-    // handle notifications that are received, both in the foreground or background
-    // NOTE: currently just here for debug logging
-    const sub1 = Notifications.addNotificationReceivedListener(event => {
-      invalidateCachedUnreadPage()
-      logger.debug(
-        'Notifications: received',
-        {event},
-        logger.DebugContext.notifications,
-      )
-      if (event.request.trigger.type === 'push') {
-        // handle payload-based deeplinks
-        let payload
-        if (isIOS) {
-          payload = event.request.trigger.payload
-        } else {
-          // TODO: handle android payload deeplink
-        }
-        if (payload) {
-          logger.debug(
-            'Notifications: received payload',
-            payload,
-            logger.DebugContext.notifications,
-          )
-          // TODO: deeplink notif here
-        }
-      }
-    })
-
-    // handle notifications that are tapped on
-    const sub2 = Notifications.addNotificationResponseReceivedListener(
-      response => {
-        logger.debug(
-          'Notifications: response received',
-          {
-            actionIdentifier: response.actionIdentifier,
-          },
-          logger.DebugContext.notifications,
-        )
-        if (
-          response.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER
-        ) {
-          logger.debug(
-            'User pressed a notification, opening notifications tab',
-            {},
-            logger.DebugContext.notifications,
-          )
-          track('Notificatons:OpenApp')
-          logEvent('notifications:openApp', {})
-          invalidateCachedUnreadPage()
-          truncateAndInvalidate(queryClient, RQKEY_NOTIFS())
-          resetToTab('NotificationsTab') // open notifications tab
-        }
-      },
-    )
-
-    return () => {
-      sub1.remove()
-      sub2.remove()
-    }
-  }, [queryClient])
-}
diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts
index f7e8544b8..31133cb1b 100644
--- a/src/lib/routes/types.ts
+++ b/src/lib/routes/types.ts
@@ -72,7 +72,7 @@ export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
 }
 
 export type MessagesTabNavigatorParams = CommonNavigatorParams & {
-  Messages: undefined
+  Messages: {pushToConversation?: string}
 }
 
 export type FlatNavigatorParams = CommonNavigatorParams & {
@@ -81,7 +81,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
   Feeds: undefined
   Notifications: undefined
   Hashtag: {tag: string; author?: string}
-  Messages: undefined
+  Messages: {pushToConversation?: string}
 }
 
 export type AllNavigatorParams = CommonNavigatorParams & {
diff --git a/src/lib/statsig/events.ts b/src/lib/statsig/events.ts
index 1cfdcbb6a..3371cd140 100644
--- a/src/lib/statsig/events.ts
+++ b/src/lib/statsig/events.ts
@@ -4,7 +4,12 @@ export type LogEvents = {
     initMs: number
   }
   'account:loggedIn': {
-    logContext: 'LoginForm' | 'SwitchAccount' | 'ChooseAccountForm' | 'Settings'
+    logContext:
+      | 'LoginForm'
+      | 'SwitchAccount'
+      | 'ChooseAccountForm'
+      | 'Settings'
+      | 'Notification'
     withPassword: boolean
   }
   'account:loggedOut': {