Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269import React from 'react'import * as Notifications from 'expo-notifications'import {CommonActions, useNavigation} from '@react-navigation/native'import {useQueryClient} from '@tanstack/react-query'
import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher'import {NavigationProp} from '#/lib/routes/types'import {logEvent} from '#/lib/statsig/statsig'import {logger} from '#/logger'import {isAndroid} from '#/platform/detection'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' | 'starterpack-joined'
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,}
// These need to stay outside the hook to persist between account switcheslet storedPayload: NotificationPayload | undefinedlet prevDate = 0
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()
// On Android, we cannot control which sound is used for a notification on Android // 28 or higher. Instead, we have to configure a notification channel ahead of time // which has the sounds we want in the configuration for that channel. These two // channels allow for the mute/unmute functionality we want for the background // handler. React.useEffect(() => { if (!isAndroid) return Notifications.setNotificationChannelAsync('chat-messages', { name: 'Chat', importance: Notifications.AndroidImportance.MAX, sound: 'dm.mp3', showBadge: true, vibrationPattern: [250], lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE, })
Notifications.setNotificationChannelAsync('chat-messages-muted', { name: 'Chat - Muted', importance: Notifications.AndroidImportance.MAX, sound: null, showBadge: true, vibrationPattern: [250], lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE, }) }, [])
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') { if ( state.routes[state.routes.length - 1].name === 'MessagesConversation' ) { return CommonActions.reset({ ...state, routes: [ ...state.routes.slice(0, state.routes.length - 1), { name: 'MessagesConversation', params: { conversation: payload.convoId, }, }, ], }) } else { 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': case 'starterpack-joined': 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.date === prevDate) { return } prevDate = e.notification.date
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, ) 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, ])}