mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React from 'react' 2import * as Notifications from 'expo-notifications' 3import {CommonActions, useNavigation} from '@react-navigation/native' 4import {useQueryClient} from '@tanstack/react-query' 5 6import {logger} from '#/logger' 7import {track} from 'lib/analytics/analytics' 8import {useAccountSwitcher} from 'lib/hooks/useAccountSwitcher' 9import {NavigationProp} from 'lib/routes/types' 10import {logEvent} from 'lib/statsig/statsig' 11import {isAndroid} from 'platform/detection' 12import {useCurrentConvoId} from 'state/messages/current-convo-id' 13import {RQKEY as RQKEY_NOTIFS} from 'state/queries/notifications/feed' 14import {invalidateCachedUnreadPage} from 'state/queries/notifications/unread' 15import {truncateAndInvalidate} from 'state/queries/util' 16import {useSession} from 'state/session' 17import {useLoggedOutViewControls} from 'state/shell/logged-out' 18import {useCloseAllActiveElements} from 'state/util' 19import {resetToTab} from '#/Navigation' 20 21type NotificationReason = 22 | 'like' 23 | 'repost' 24 | 'follow' 25 | 'mention' 26 | 'reply' 27 | 'quote' 28 | 'chat-message' 29 30type NotificationPayload = 31 | { 32 reason: Exclude<NotificationReason, 'chat-message'> 33 uri: string 34 subject: string 35 } 36 | { 37 reason: 'chat-message' 38 convoId: string 39 messageId: string 40 recipientDid: string 41 } 42 43const DEFAULT_HANDLER_OPTIONS = { 44 shouldShowAlert: false, 45 shouldPlaySound: false, 46 shouldSetBadge: true, 47} 48 49// These need to stay outside the hook to persist between account switches 50let storedPayload: NotificationPayload | undefined 51let prevDate = 0 52 53export function useNotificationsHandler() { 54 const queryClient = useQueryClient() 55 const {currentAccount, accounts} = useSession() 56 const {onPressSwitchAccount} = useAccountSwitcher() 57 const navigation = useNavigation<NavigationProp>() 58 const {currentConvoId} = useCurrentConvoId() 59 const {setShowLoggedOut} = useLoggedOutViewControls() 60 const closeAllActiveElements = useCloseAllActiveElements() 61 62 // On Android, we cannot control which sound is used for a notification on Android 63 // 28 or higher. Instead, we have to configure a notification channel ahead of time 64 // which has the sounds we want in the configuration for that channel. These two 65 // channels allow for the mute/unmute functionality we want for the background 66 // handler. 67 React.useEffect(() => { 68 if (!isAndroid) return 69 Notifications.setNotificationChannelAsync('chat-messages', { 70 name: 'Chat', 71 importance: Notifications.AndroidImportance.MAX, 72 sound: 'dm.mp3', 73 showBadge: true, 74 vibrationPattern: [250], 75 lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE, 76 }) 77 78 Notifications.setNotificationChannelAsync('chat-messages-muted', { 79 name: 'Chat - Muted', 80 importance: Notifications.AndroidImportance.MAX, 81 sound: null, 82 showBadge: true, 83 vibrationPattern: [250], 84 lockscreenVisibility: Notifications.AndroidNotificationVisibility.PRIVATE, 85 }) 86 }, []) 87 88 React.useEffect(() => { 89 const handleNotification = (payload?: NotificationPayload) => { 90 if (!payload) return 91 92 if (payload.reason === 'chat-message') { 93 if (payload.recipientDid !== currentAccount?.did && !storedPayload) { 94 storedPayload = payload 95 closeAllActiveElements() 96 97 const account = accounts.find(a => a.did === payload.recipientDid) 98 if (account) { 99 onPressSwitchAccount(account, 'Notification') 100 } else { 101 setShowLoggedOut(true) 102 } 103 } else { 104 navigation.dispatch(state => { 105 if (state.routes[0].name === 'Messages') { 106 if ( 107 state.routes[state.routes.length - 1].name === 108 'MessagesConversation' 109 ) { 110 return CommonActions.reset({ 111 ...state, 112 routes: [ 113 ...state.routes.slice(0, state.routes.length - 1), 114 { 115 name: 'MessagesConversation', 116 params: { 117 conversation: payload.convoId, 118 }, 119 }, 120 ], 121 }) 122 } else { 123 return CommonActions.navigate('MessagesConversation', { 124 conversation: payload.convoId, 125 }) 126 } 127 } else { 128 return CommonActions.navigate('MessagesTab', { 129 screen: 'Messages', 130 params: { 131 pushToConversation: payload.convoId, 132 }, 133 }) 134 } 135 }) 136 } 137 } else { 138 switch (payload.reason) { 139 case 'like': 140 case 'repost': 141 case 'follow': 142 case 'mention': 143 case 'quote': 144 case 'reply': 145 resetToTab('NotificationsTab') 146 break 147 // TODO implement these after we have an idea of how to handle each individual case 148 // case 'follow': 149 // const uri = new AtUri(payload.uri) 150 // setTimeout(() => { 151 // // @ts-expect-error types are weird here 152 // navigation.navigate('HomeTab', { 153 // screen: 'Profile', 154 // params: { 155 // name: uri.host, 156 // }, 157 // }) 158 // }, 500) 159 // break 160 // case 'mention': 161 // case 'reply': 162 // const urip = new AtUri(payload.uri) 163 // setTimeout(() => { 164 // // @ts-expect-error types are weird here 165 // navigation.navigate('HomeTab', { 166 // screen: 'PostThread', 167 // params: { 168 // name: urip.host, 169 // rkey: urip.rkey, 170 // }, 171 // }) 172 // }, 500) 173 } 174 } 175 } 176 177 Notifications.setNotificationHandler({ 178 handleNotification: async e => { 179 if (e.request.trigger.type !== 'push') return DEFAULT_HANDLER_OPTIONS 180 181 logger.debug( 182 'Notifications: received', 183 {e}, 184 logger.DebugContext.notifications, 185 ) 186 187 const payload = e.request.trigger.payload as NotificationPayload 188 if ( 189 payload.reason === 'chat-message' && 190 payload.recipientDid === currentAccount?.did 191 ) { 192 return { 193 shouldShowAlert: payload.convoId !== currentConvoId, 194 shouldPlaySound: false, 195 shouldSetBadge: false, 196 } 197 } 198 199 // Any notification other than a chat message should invalidate the unread page 200 invalidateCachedUnreadPage() 201 return DEFAULT_HANDLER_OPTIONS 202 }, 203 }) 204 205 const responseReceivedListener = 206 Notifications.addNotificationResponseReceivedListener(e => { 207 if (e.notification.date === prevDate) { 208 return 209 } 210 prevDate = e.notification.date 211 212 logger.debug( 213 'Notifications: response received', 214 { 215 actionIdentifier: e.actionIdentifier, 216 }, 217 logger.DebugContext.notifications, 218 ) 219 220 if ( 221 e.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER && 222 e.notification.request.trigger.type === 'push' 223 ) { 224 logger.debug( 225 'User pressed a notification, opening notifications tab', 226 {}, 227 logger.DebugContext.notifications, 228 ) 229 track('Notificatons:OpenApp') 230 logEvent('notifications:openApp', {}) 231 invalidateCachedUnreadPage() 232 truncateAndInvalidate(queryClient, RQKEY_NOTIFS()) 233 logger.debug('Notifications: handleNotification', { 234 content: e.notification.request.content, 235 payload: e.notification.request.trigger.payload, 236 }) 237 handleNotification( 238 e.notification.request.trigger.payload as NotificationPayload, 239 ) 240 Notifications.dismissAllNotificationsAsync() 241 } 242 }) 243 244 // Whenever there's a stored payload, that means we had to switch accounts before handling the notification. 245 // Whenever currentAccount changes, we should try to handle it again. 246 if ( 247 storedPayload?.reason === 'chat-message' && 248 currentAccount?.did === storedPayload.recipientDid 249 ) { 250 handleNotification(storedPayload) 251 storedPayload = undefined 252 } 253 254 return () => { 255 responseReceivedListener.remove() 256 } 257 }, [ 258 queryClient, 259 currentAccount, 260 currentConvoId, 261 accounts, 262 closeAllActiveElements, 263 currentAccount?.did, 264 navigation, 265 onPressSwitchAccount, 266 setShowLoggedOut, 267 ]) 268}