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