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 | '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}