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