diff options
Diffstat (limited to 'src/lib/hooks/useNotificationHandler.ts')
-rw-r--r-- | src/lib/hooks/useNotificationHandler.ts | 225 |
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, + ]) +} |