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}