Bluesky app fork with some witchin' additions 💫

Handle notifications on first load (#8629)

* add getInitialURL

* get deep linking working

* actually handle as push instead

* push to route instead of setting initial path

* pop to on top of notifications

* fix comment

* change chat-message handling

* add metrics

* don't reopen due to notif multiple times

* extract data from notification differently on android

* sorry sorry annoying naming nits, align logger

---------

Co-authored-by: Eric Bailey <git@esb.lol>

authored by samuel.fm

Eric Bailey and committed by
GitHub
377d80e2 22bda086

+276 -148
+1 -1
src/App.native.tsx
··· 17 17 import * as Sentry from '@sentry/react-native' 18 18 19 19 import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController' 20 + import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder' 20 21 import {QueryProvider} from '#/lib/react-query' 21 22 import {Provider as StatsigProvider, tryFetchGates} from '#/lib/statsig/statsig' 22 23 import {s} from '#/lib/styles' ··· 73 74 import {Splash} from '#/Splash' 74 75 import {BottomSheetProvider} from '../modules/bottom-sheet' 75 76 import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' 76 - import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' 77 77 78 78 SplashScreen.preventAutoHideAsync() 79 79 if (isIOS) {
+124 -22
src/Navigation.tsx
··· 1 - import * as React from 'react' 1 + import {useCallback, useRef} from 'react' 2 + import * as Notifications from 'expo-notifications' 2 3 import {i18n, type MessageDescriptor} from '@lingui/core' 3 4 import {msg} from '@lingui/macro' 4 5 import { ··· 10 11 createNavigationContainerRef, 11 12 DarkTheme, 12 13 DefaultTheme, 14 + type LinkingOptions, 13 15 NavigationContainer, 14 16 StackActions, 15 17 } from '@react-navigation/native' 16 18 17 19 import {timeout} from '#/lib/async/timeout' 18 20 import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' 21 + import { 22 + getNotificationPayload, 23 + type NotificationPayload, 24 + notificationToURL, 25 + storePayloadForAccountSwitch, 26 + } from '#/lib/hooks/useNotificationHandler' 19 27 import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration' 28 + import {logger as notyLogger} from '#/lib/notifications/util' 20 29 import {buildStateObject} from '#/lib/routes/helpers' 21 30 import { 22 31 type AllNavigatorParams, ··· 71 80 import {ModerationScreen} from '#/screens/Moderation' 72 81 import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings' 73 82 import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings' 83 + import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList' 74 84 import {PostLikedByScreen} from '#/screens/Post/PostLikedBy' 75 85 import {PostQuotesScreen} from '#/screens/Post/PostQuotes' 76 86 import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy' ··· 93 103 import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' 94 104 import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' 95 105 import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' 106 + import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings' 107 + import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' 108 + import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings' 109 + import {LikeNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikeNotificationSettings' 110 + import {LikesOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings' 111 + import {MentionNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MentionNotificationSettings' 112 + import {MiscellaneousNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings' 113 + import {NewFollowerNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/NewFollowerNotificationSettings' 114 + import {QuoteNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/QuoteNotificationSettings' 115 + import {ReplyNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings' 116 + import {RepostNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostNotificationSettings' 117 + import {RepostsOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' 96 118 import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' 97 119 import {SettingsScreen} from '#/screens/Settings/Settings' 98 120 import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' ··· 110 132 } from '#/components/dialogs/EmailDialog' 111 133 import {router} from '#/routes' 112 134 import {Referrer} from '../modules/expo-bluesky-swiss-army' 113 - import {NotificationsActivityListScreen} from './screens/Notifications/ActivityList' 114 - import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings' 115 - import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings' 116 - import {ActivityNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ActivityNotificationSettings' 117 - import {LikeNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikeNotificationSettings' 118 - import {LikesOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings' 119 - import {MentionNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MentionNotificationSettings' 120 - import {MiscellaneousNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MiscellaneousNotificationSettings' 121 - import {NewFollowerNotificationSettingsScreen} from './screens/Settings/NotificationSettings/NewFollowerNotificationSettings' 122 - import {QuoteNotificationSettingsScreen} from './screens/Settings/NotificationSettings/QuoteNotificationSettings' 123 - import {ReplyNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ReplyNotificationSettings' 124 - import {RepostNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostNotificationSettings' 125 - import {RepostsOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' 135 + import {useAccountSwitcher} from './lib/hooks/useAccountSwitcher' 136 + import {useNonReactiveCallback} from './lib/hooks/useNonReactiveCallback' 137 + import {useLoggedOutViewControls} from './state/shell/logged-out' 138 + import {useCloseAllActiveElements} from './state/util' 126 139 127 140 const navigationRef = createNavigationContainerRef<AllNavigatorParams>() 128 141 ··· 595 608 * in 3 distinct tab-stacks with a different root screen on each. 596 609 */ 597 610 function TabsNavigator() { 598 - const tabBar = React.useCallback( 611 + const tabBar = useCallback( 599 612 (props: JSX.IntrinsicAttributes & BottomTabBarProps) => ( 600 613 <BottomBar {...props} /> 601 614 ), ··· 762 775 763 776 const LINKING = { 764 777 // TODO figure out what we are going to use 778 + // note: `bluesky://` is what is used in app.config.js 765 779 prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'], 766 780 767 781 getPathFromState(state: State) { ··· 818 832 return res 819 833 } 820 834 }, 821 - } 835 + } satisfies LinkingOptions<AllNavigatorParams> 836 + 837 + /** 838 + * Used to ensure we don't handle the same notification twice 839 + */ 840 + let lastHandledNotificationDateDedupe: number | undefined 822 841 823 842 function RoutesContainer({children}: React.PropsWithChildren<{}>) { 824 843 const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) 825 - const {currentAccount} = useSession() 826 - const prevLoggedRouteName = React.useRef<string | undefined>(undefined) 844 + const {currentAccount, accounts} = useSession() 845 + const {onPressSwitchAccount} = useAccountSwitcher() 846 + const {setShowLoggedOut} = useLoggedOutViewControls() 847 + const prevLoggedRouteName = useRef<string | undefined>(undefined) 827 848 const emailDialogControl = useEmailDialogControl() 849 + const closeAllActiveElements = useCloseAllActiveElements() 850 + 851 + /** 852 + * Handle navigation to a conversation, or prepares for account switch. 853 + * 854 + * Non-reactive because we need the latest data from some hooks 855 + * after an async call - sfn 856 + */ 857 + const handleChatMessage = useNonReactiveCallback( 858 + (payload: Extract<NotificationPayload, {reason: 'chat-message'}>) => { 859 + notyLogger.debug(`handleChatMessage`, {payload}) 860 + 861 + if (payload.recipientDid !== currentAccount?.did) { 862 + // handled in useNotificationHandler after account switch finishes 863 + storePayloadForAccountSwitch(payload) 864 + closeAllActiveElements() 865 + 866 + const account = accounts.find(a => a.did === payload.recipientDid) 867 + if (account) { 868 + onPressSwitchAccount(account, 'Notification') 869 + } else { 870 + setShowLoggedOut(true) 871 + } 872 + } else { 873 + // @ts-expect-error nested navigators aren't typed -sfn 874 + navigate('MessagesTab', { 875 + screen: 'MessagesConversation', 876 + params: { 877 + conversation: payload.convoId, 878 + }, 879 + }) 880 + } 881 + }, 882 + ) 883 + 884 + async function handlePushNotificationEntry() { 885 + if (!isNative) return 886 + 887 + /** 888 + * The notification that caused the app to open, if applicable 889 + */ 890 + const response = await Notifications.getLastNotificationResponseAsync() 891 + 892 + if (response) { 893 + notyLogger.debug(`handlePushNotificationEntry: response`, {response}) 894 + 895 + if (response.notification.date === lastHandledNotificationDateDedupe) 896 + return 897 + lastHandledNotificationDateDedupe = response.notification.date 898 + 899 + const payload = getNotificationPayload(response.notification) 900 + 901 + if (payload) { 902 + notyLogger.metric( 903 + 'notifications:openApp', 904 + {reason: payload.reason, causedBoot: true}, 905 + {statsig: false}, 906 + ) 907 + 908 + if (payload.reason === 'chat-message') { 909 + handleChatMessage(payload) 910 + } else { 911 + const path = notificationToURL(payload) 912 + 913 + if (path === '/notifications') { 914 + resetToTab('NotificationsTab') 915 + notyLogger.debug(`handlePushNotificationEntry: default navigate`) 916 + } else if (path) { 917 + const [screen, params] = router.matchPath(path) 918 + // @ts-expect-error nested navigators aren't typed -sfn 919 + navigate('HomeTab', {screen, params}) 920 + notyLogger.debug(`handlePushNotificationEntry: navigate`, { 921 + screen, 922 + params, 923 + }) 924 + } 925 + } 926 + } 927 + } 928 + } 828 929 829 930 function onReady() { 830 931 prevLoggedRouteName.current = getCurrentRouteName() ··· 845 946 onStateChange={() => { 846 947 logger.metric( 847 948 'router:navigate', 848 - { 849 - from: prevLoggedRouteName.current, 850 - }, 949 + {from: prevLoggedRouteName.current}, 851 950 {statsig: false}, 852 951 ) 853 952 prevLoggedRouteName.current = getCurrentRouteName() ··· 857 956 logModuleInitTime() 858 957 onReady() 859 958 logger.metric('router:navigate', {}, {statsig: false}) 959 + handlePushNotificationEntry() 860 960 }} 861 961 // WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x 862 962 // However, there's a fair amount of places we do that, especially in when popping to the top of stacks. ··· 906 1006 return Promise.resolve() 907 1007 } 908 1008 909 - function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') { 1009 + function resetToTab( 1010 + tabName: 'HomeTab' | 'SearchTab' | 'MessagesTab' | 'NotificationsTab', 1011 + ) { 910 1012 if (navigationRef.isReady()) { 911 1013 navigate(tabName) 912 1014 if (navigationRef.canGoBack()) {
+136 -114
src/lib/hooks/useNotificationHandler.ts
··· 7 7 import {useQueryClient} from '@tanstack/react-query' 8 8 9 9 import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' 10 + import {logger as notyLogger} from '#/lib/notifications/util' 10 11 import {type NavigationProp} from '#/lib/routes/types' 11 - import {Logger} from '#/logger' 12 - import {isAndroid} from '#/platform/detection' 12 + import {isAndroid, isIOS} from '#/platform/detection' 13 13 import {useCurrentConvoId} from '#/state/messages/current-convo-id' 14 14 import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' 15 15 import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread' ··· 18 18 import {useLoggedOutViewControls} from '#/state/shell/logged-out' 19 19 import {useCloseAllActiveElements} from '#/state/util' 20 20 import {resetToTab} from '#/Navigation' 21 + import {router} from '#/routes' 21 22 22 23 export type NotificationReason = 23 24 | 'like' ··· 39 40 * `notification.request.trigger.payload` being `undefined`, as specified in 40 41 * the source types. 41 42 */ 42 - type NotificationPayload = 43 + export type NotificationPayload = 43 44 | undefined 44 45 | { 45 46 reason: Exclude<NotificationReason, 'chat-message'> 46 47 uri: string 47 48 subject: string 49 + recipientDid: string 48 50 } 49 51 | { 50 52 reason: 'chat-message' ··· 60 62 shouldSetBadge: true, 61 63 } satisfies Notifications.NotificationBehavior 62 64 63 - // These need to stay outside the hook to persist between account switches 64 - let storedPayload: NotificationPayload 65 - let prevDate = 0 65 + /** 66 + * Cached notification payload if we handled a notification while the user was 67 + * using a different account. This is consumed after we finish switching 68 + * accounts. 69 + */ 70 + let storedAccountSwitchPayload: NotificationPayload 66 71 67 - const logger = Logger.create(Logger.Context.Notifications) 72 + /** 73 + * Used to ensure we don't handle the same notification twice 74 + */ 75 + let lastHandledNotificationDateDedupe = 0 68 76 69 77 export function useNotificationsHandler() { 70 78 const queryClient = useQueryClient() ··· 182 190 if (!payload) return 183 191 184 192 if (payload.reason === 'chat-message') { 185 - if (payload.recipientDid !== currentAccount?.did && !storedPayload) { 186 - storedPayload = payload 193 + notyLogger.debug(`useNotificationsHandler: handling chat message`, { 194 + payload, 195 + }) 196 + 197 + if ( 198 + payload.recipientDid !== currentAccount?.did && 199 + !storedAccountSwitchPayload 200 + ) { 201 + storePayloadForAccountSwitch(payload) 187 202 closeAllActiveElements() 188 203 189 204 const account = accounts.find(a => a.did === payload.recipientDid) ··· 227 242 }) 228 243 } 229 244 } else { 230 - switch (payload.reason) { 231 - case 'subscribed-post': 232 - const urip = new AtUri(payload.uri) 233 - if (urip.collection === 'app.bsky.feed.post') { 234 - setTimeout(() => { 235 - // @ts-expect-error types are weird here 236 - navigation.navigate('HomeTab', { 237 - screen: 'PostThread', 238 - params: { 239 - name: urip.host, 240 - rkey: urip.rkey, 241 - }, 242 - }) 243 - }, 500) 244 - } else { 245 - resetToTab('NotificationsTab') 246 - } 247 - break 248 - case 'like': 249 - case 'repost': 250 - case 'follow': 251 - case 'mention': 252 - case 'quote': 253 - case 'reply': 254 - case 'starterpack-joined': 255 - case 'like-via-repost': 256 - case 'repost-via-repost': 257 - case 'verified': 258 - case 'unverified': 259 - default: 260 - resetToTab('NotificationsTab') 261 - break 262 - // TODO implement these after we have an idea of how to handle each individual case 263 - // case 'follow': 264 - // const uri = new AtUri(payload.uri) 265 - // setTimeout(() => { 266 - // // @ts-expect-error types are weird here 267 - // navigation.navigate('HomeTab', { 268 - // screen: 'Profile', 269 - // params: { 270 - // name: uri.host, 271 - // }, 272 - // }) 273 - // }, 500) 274 - // break 275 - // case 'mention': 276 - // case 'reply': 277 - // const urip = new AtUri(payload.uri) 278 - // setTimeout(() => { 279 - // // @ts-expect-error types are weird here 280 - // navigation.navigate('HomeTab', { 281 - // screen: 'PostThread', 282 - // params: { 283 - // name: urip.host, 284 - // rkey: urip.rkey, 285 - // }, 286 - // }) 287 - // }, 500) 245 + const url = notificationToURL(payload) 246 + 247 + if (url === '/notifications') { 248 + resetToTab('NotificationsTab') 249 + } else if (url) { 250 + const [screen, params] = router.matchPath(url) 251 + // @ts-expect-error router is not typed :/ -sfn 252 + navigation.navigate('HomeTab', {screen, params}) 253 + notyLogger.debug(`useNotificationsHandler: navigate`, { 254 + screen, 255 + params, 256 + }) 288 257 } 289 258 } 290 259 } 291 260 292 261 Notifications.setNotificationHandler({ 293 262 handleNotification: async e => { 294 - if ( 295 - e.request.trigger == null || 296 - typeof e.request.trigger !== 'object' || 297 - !('type' in e.request.trigger) || 298 - e.request.trigger.type !== 'push' 299 - ) { 300 - return DEFAULT_HANDLER_OPTIONS 301 - } 263 + const payload = getNotificationPayload(e) 302 264 303 - logger.debug('Notifications: received', {e}) 304 - 305 - const payload = e.request.trigger.payload as NotificationPayload 265 + if (!payload) return DEFAULT_HANDLER_OPTIONS 306 266 307 - if (!payload) { 308 - return DEFAULT_HANDLER_OPTIONS 309 - } 267 + notyLogger.debug('useNotificationsHandler: incoming', {e, payload}) 310 268 311 269 if ( 312 270 payload.reason === 'chat-message' && ··· 329 287 330 288 const responseReceivedListener = 331 289 Notifications.addNotificationResponseReceivedListener(e => { 332 - if (e.notification.date === prevDate) { 333 - return 334 - } 335 - prevDate = e.notification.date 290 + if (e.notification.date === lastHandledNotificationDateDedupe) return 291 + lastHandledNotificationDateDedupe = e.notification.date 336 292 337 - logger.debug('Notifications: response received', { 293 + notyLogger.debug('useNotificationsHandler: response received', { 338 294 actionIdentifier: e.actionIdentifier, 339 295 }) 340 296 341 - if ( 342 - e.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER && 343 - e.notification.request.trigger != null && 344 - typeof e.notification.request.trigger === 'object' && 345 - 'type' in e.notification.request.trigger && 346 - e.notification.request.trigger.type === 'push' 347 - ) { 348 - const payload = e.notification.request.trigger 349 - .payload as NotificationPayload 297 + if (e.actionIdentifier !== Notifications.DEFAULT_ACTION_IDENTIFIER) { 298 + return 299 + } 350 300 351 - if (!payload) { 352 - logger.error('useNotificationsHandler: received no payload', { 353 - identifier: e.notification.request.identifier, 354 - }) 355 - return 356 - } 301 + const payload = getNotificationPayload(e.notification) 302 + 303 + if (payload) { 357 304 if (!payload.reason) { 358 - logger.error('useNotificationsHandler: received unknown payload', { 359 - payload, 360 - identifier: e.notification.request.identifier, 361 - }) 305 + notyLogger.error( 306 + 'useNotificationsHandler: received unknown payload', 307 + { 308 + payload, 309 + identifier: e.notification.request.identifier, 310 + }, 311 + ) 362 312 return 363 313 } 364 314 365 - logger.debug( 315 + notyLogger.debug( 366 316 'User pressed a notification, opening notifications tab', 367 317 {}, 368 318 ) 369 - logger.metric( 319 + notyLogger.metric( 370 320 'notifications:openApp', 371 - {reason: payload.reason}, 321 + {reason: payload.reason, causedBoot: false}, 372 322 {statsig: false}, 373 323 ) 374 324 ··· 383 333 truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')) 384 334 } 385 335 386 - logger.debug('Notifications: handleNotification', { 336 + notyLogger.debug('Notifications: handleNotification', { 387 337 content: e.notification.request.content, 388 - payload: e.notification.request.trigger.payload, 338 + payload: payload, 389 339 }) 390 340 391 341 handleNotification(payload) 392 342 Notifications.dismissAllNotificationsAsync() 343 + } else { 344 + notyLogger.error('useNotificationsHandler: received no payload', { 345 + identifier: e.notification.request.identifier, 346 + }) 393 347 } 394 348 }) 395 349 396 350 // Whenever there's a stored payload, that means we had to switch accounts before handling the notification. 397 351 // Whenever currentAccount changes, we should try to handle it again. 398 352 if ( 399 - storedPayload?.reason === 'chat-message' && 400 - currentAccount?.did === storedPayload.recipientDid 353 + storedAccountSwitchPayload?.reason === 'chat-message' && 354 + currentAccount?.did === storedAccountSwitchPayload.recipientDid 401 355 ) { 402 - handleNotification(storedPayload) 403 - storedPayload = undefined 356 + handleNotification(storedAccountSwitchPayload) 357 + storedAccountSwitchPayload = undefined 404 358 } 405 359 406 360 return () => { ··· 418 372 setShowLoggedOut, 419 373 ]) 420 374 } 375 + 376 + export function storePayloadForAccountSwitch(payload: NotificationPayload) { 377 + storedAccountSwitchPayload = payload 378 + } 379 + 380 + export function getNotificationPayload( 381 + e: Notifications.Notification, 382 + ): NotificationPayload | null { 383 + if ( 384 + e.request.trigger == null || 385 + typeof e.request.trigger !== 'object' || 386 + !('type' in e.request.trigger) || 387 + e.request.trigger.type !== 'push' 388 + ) { 389 + return null 390 + } 391 + 392 + const payload = ( 393 + isIOS ? e.request.trigger.payload : e.request.content.data 394 + ) as NotificationPayload 395 + 396 + if (payload) { 397 + return payload 398 + } else { 399 + return null 400 + } 401 + } 402 + 403 + export function notificationToURL( 404 + payload: NotificationPayload, 405 + ): string | undefined { 406 + switch (payload?.reason) { 407 + case 'like': 408 + case 'repost': 409 + case 'like-via-repost': 410 + case 'repost-via-repost': { 411 + const urip = new AtUri(payload.subject) 412 + if (urip.collection === 'app.bsky.feed.post') { 413 + return `/profile/${urip.host}/post/${urip.rkey}` 414 + } else { 415 + return '/notifications' 416 + } 417 + } 418 + case 'reply': 419 + case 'quote': 420 + case 'mention': 421 + case 'subscribed-post': { 422 + const urip = new AtUri(payload.uri) 423 + if (urip.collection === 'app.bsky.feed.post') { 424 + return `/profile/${urip.host}/post/${urip.rkey}` 425 + } else { 426 + return '/notifications' 427 + } 428 + } 429 + case 'follow': 430 + case 'starterpack-joined': { 431 + const urip = new AtUri(payload.uri) 432 + return `/profile/${urip.host}` 433 + } 434 + case 'chat-message': 435 + // should be handled separately 436 + return undefined 437 + case 'verified': 438 + case 'unverified': 439 + default: 440 + return '/notifications' 441 + } 442 + }
+10 -10
src/lib/notifications/notifications.ts
··· 6 6 import debounce from 'lodash.debounce' 7 7 8 8 import {PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID} from '#/lib/constants' 9 - import {Logger} from '#/logger' 9 + import {logger as notyLogger} from '#/lib/notifications/util' 10 10 import {isNative} from '#/platform/detection' 11 11 import {type SessionAccount, useAgent, useSession} from '#/state/session' 12 12 import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' 13 - 14 - const logger = Logger.create(Logger.Context.Notifications) 15 13 16 14 /** 17 15 * @private ··· 36 34 appId: 'xyz.blueskyweb.app', 37 35 }) 38 36 39 - logger.debug(`registerPushToken: success`, { 37 + notyLogger.debug(`registerPushToken: success`, { 40 38 tokenType: token.type, 41 39 token: token.data, 42 40 }) 43 41 } catch (error) { 44 - logger.error(`registerPushToken: failed`, {safeMessage: error}) 42 + notyLogger.error(`registerPushToken: failed`, {safeMessage: error}) 45 43 } 46 44 } 47 45 ··· 80 78 */ 81 79 async function getPushToken() { 82 80 const granted = (await Notifications.getPermissionsAsync()).granted 83 - logger.debug(`getPushToken`, {granted}) 81 + notyLogger.debug(`getPushToken`, {granted}) 84 82 if (granted) { 85 83 return Notifications.getDevicePushTokenAsync() 86 84 } ··· 115 113 */ 116 114 const token = await getPushToken() 117 115 118 - logger.debug(`useGetAndRegisterPushToken`, {token: token ?? 'undefined'}) 116 + notyLogger.debug(`useGetAndRegisterPushToken`, { 117 + token: token ?? 'undefined', 118 + }) 119 119 120 120 if (token) { 121 121 /** ··· 147 147 */ 148 148 if (!currentAccount) return 149 149 150 - logger.debug(`useNotificationsRegistration`) 150 + notyLogger.debug(`useNotificationsRegistration`) 151 151 152 152 /** 153 153 * Init push token, if permissions are granted already. If they weren't, ··· 168 168 */ 169 169 const subscription = Notifications.addPushTokenListener(async token => { 170 170 registerPushToken({token}) 171 - logger.debug(`addPushTokenListener callback`, {token}) 171 + notyLogger.debug(`addPushTokenListener callback`, {token}) 172 172 }) 173 173 174 174 return () => { ··· 202 202 203 203 const res = await Notifications.requestPermissionsAsync() 204 204 205 - logger.metric(`notifications:request`, { 205 + notyLogger.metric(`notifications:request`, { 206 206 context: context, 207 207 status: res.status, 208 208 })
+3
src/lib/notifications/util.ts
··· 1 + import {Logger} from '#/logger' 2 + 3 + export const logger = Logger.create(Logger.Context.Notifications)
+1 -1
src/lib/routes/links.ts
··· 1 - import {AppBskyGraphDefs, AtUri} from '@atproto/api' 1 + import {type AppBskyGraphDefs, AtUri} from '@atproto/api' 2 2 3 3 import {isInvalidHandle} from '#/lib/strings/handles' 4 4
+1
src/logger/metrics.ts
··· 26 26 } 27 27 'notifications:openApp': { 28 28 reason: NotificationReason 29 + causedBoot: boolean 29 30 } 30 31 'notifications:request': { 31 32 context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home'