diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/App.native.tsx | 2 | ||||
-rw-r--r-- | src/Navigation.tsx | 146 | ||||
-rw-r--r-- | src/lib/hooks/useNotificationHandler.ts | 250 | ||||
-rw-r--r-- | src/lib/notifications/notifications.ts | 20 | ||||
-rw-r--r-- | src/lib/notifications/util.ts | 3 | ||||
-rw-r--r-- | src/lib/routes/links.ts | 2 | ||||
-rw-r--r-- | src/logger/metrics.ts | 1 |
7 files changed, 276 insertions, 148 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx index c42b11746..2278b73de 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -17,6 +17,7 @@ import {useLingui} from '@lingui/react' import * as Sentry from '@sentry/react-native' import {KeyboardControllerProvider} from '#/lib/hooks/useEnableKeyboardController' +import {Provider as HideBottomBarBorderProvider} from '#/lib/hooks/useHideBottomBarBorder' import {QueryProvider} from '#/lib/react-query' import {Provider as StatsigProvider, tryFetchGates} from '#/lib/statsig/statsig' import {s} from '#/lib/styles' @@ -73,7 +74,6 @@ import {Provider as VideoVolumeProvider} from '#/components/Post/Embed/VideoEmbe import {Splash} from '#/Splash' import {BottomSheetProvider} from '../modules/bottom-sheet' import {BackgroundNotificationPreferencesProvider} from '../modules/expo-background-notification-handler/src/BackgroundNotificationHandlerProvider' -import {Provider as HideBottomBarBorderProvider} from './lib/hooks/useHideBottomBarBorder' SplashScreen.preventAutoHideAsync() if (isIOS) { diff --git a/src/Navigation.tsx b/src/Navigation.tsx index 26a2b2a2a..e71148a2c 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -1,4 +1,5 @@ -import * as React from 'react' +import {useCallback, useRef} from 'react' +import * as Notifications from 'expo-notifications' import {i18n, type MessageDescriptor} from '@lingui/core' import {msg} from '@lingui/macro' import { @@ -10,13 +11,21 @@ import { createNavigationContainerRef, DarkTheme, DefaultTheme, + type LinkingOptions, NavigationContainer, StackActions, } from '@react-navigation/native' import {timeout} from '#/lib/async/timeout' import {useColorSchemeStyle} from '#/lib/hooks/useColorSchemeStyle' +import { + getNotificationPayload, + type NotificationPayload, + notificationToURL, + storePayloadForAccountSwitch, +} from '#/lib/hooks/useNotificationHandler' import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration' +import {logger as notyLogger} from '#/lib/notifications/util' import {buildStateObject} from '#/lib/routes/helpers' import { type AllNavigatorParams, @@ -71,6 +80,7 @@ import {MessagesSettingsScreen} from '#/screens/Messages/Settings' import {ModerationScreen} from '#/screens/Moderation' import {Screen as ModerationVerificationSettings} from '#/screens/Moderation/VerificationSettings' import {Screen as ModerationInteractionSettings} from '#/screens/ModerationInteractionSettings' +import {NotificationsActivityListScreen} from '#/screens/Notifications/ActivityList' import {PostLikedByScreen} from '#/screens/Post/PostLikedBy' import {PostQuotesScreen} from '#/screens/Post/PostQuotes' import {PostRepostedByScreen} from '#/screens/Post/PostRepostedBy' @@ -93,6 +103,18 @@ import {ExternalMediaPreferencesScreen} from '#/screens/Settings/ExternalMediaPr import {FollowingFeedPreferencesScreen} from '#/screens/Settings/FollowingFeedPreferences' import {InterestsSettingsScreen} from '#/screens/Settings/InterestsSettings' import {LanguageSettingsScreen} from '#/screens/Settings/LanguageSettings' +import {LegacyNotificationSettingsScreen} from '#/screens/Settings/LegacyNotificationSettings' +import {NotificationSettingsScreen} from '#/screens/Settings/NotificationSettings' +import {ActivityNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ActivityNotificationSettings' +import {LikeNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikeNotificationSettings' +import {LikesOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings' +import {MentionNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MentionNotificationSettings' +import {MiscellaneousNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/MiscellaneousNotificationSettings' +import {NewFollowerNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/NewFollowerNotificationSettings' +import {QuoteNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/QuoteNotificationSettings' +import {ReplyNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/ReplyNotificationSettings' +import {RepostNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostNotificationSettings' +import {RepostsOnRepostsNotificationSettingsScreen} from '#/screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' import {PrivacyAndSecuritySettingsScreen} from '#/screens/Settings/PrivacyAndSecuritySettings' import {SettingsScreen} from '#/screens/Settings/Settings' import {ThreadPreferencesScreen} from '#/screens/Settings/ThreadPreferences' @@ -110,19 +132,10 @@ import { } from '#/components/dialogs/EmailDialog' import {router} from '#/routes' import {Referrer} from '../modules/expo-bluesky-swiss-army' -import {NotificationsActivityListScreen} from './screens/Notifications/ActivityList' -import {LegacyNotificationSettingsScreen} from './screens/Settings/LegacyNotificationSettings' -import {NotificationSettingsScreen} from './screens/Settings/NotificationSettings' -import {ActivityNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ActivityNotificationSettings' -import {LikeNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikeNotificationSettings' -import {LikesOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/LikesOnRepostsNotificationSettings' -import {MentionNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MentionNotificationSettings' -import {MiscellaneousNotificationSettingsScreen} from './screens/Settings/NotificationSettings/MiscellaneousNotificationSettings' -import {NewFollowerNotificationSettingsScreen} from './screens/Settings/NotificationSettings/NewFollowerNotificationSettings' -import {QuoteNotificationSettingsScreen} from './screens/Settings/NotificationSettings/QuoteNotificationSettings' -import {ReplyNotificationSettingsScreen} from './screens/Settings/NotificationSettings/ReplyNotificationSettings' -import {RepostNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostNotificationSettings' -import {RepostsOnRepostsNotificationSettingsScreen} from './screens/Settings/NotificationSettings/RepostsOnRepostsNotificationSettings' +import {useAccountSwitcher} from './lib/hooks/useAccountSwitcher' +import {useNonReactiveCallback} from './lib/hooks/useNonReactiveCallback' +import {useLoggedOutViewControls} from './state/shell/logged-out' +import {useCloseAllActiveElements} from './state/util' const navigationRef = createNavigationContainerRef<AllNavigatorParams>() @@ -595,7 +608,7 @@ function commonScreens(Stack: typeof Flat, unreadCountLabel?: string) { * in 3 distinct tab-stacks with a different root screen on each. */ function TabsNavigator() { - const tabBar = React.useCallback( + const tabBar = useCallback( (props: JSX.IntrinsicAttributes & BottomTabBarProps) => ( <BottomBar {...props} /> ), @@ -762,6 +775,7 @@ const FlatNavigator = () => { const LINKING = { // TODO figure out what we are going to use + // note: `bluesky://` is what is used in app.config.js prefixes: ['bsky://', 'bluesky://', 'https://bsky.app'], getPathFromState(state: State) { @@ -818,13 +832,100 @@ const LINKING = { return res } }, -} +} satisfies LinkingOptions<AllNavigatorParams> + +/** + * Used to ensure we don't handle the same notification twice + */ +let lastHandledNotificationDateDedupe: number | undefined function RoutesContainer({children}: React.PropsWithChildren<{}>) { const theme = useColorSchemeStyle(DefaultTheme, DarkTheme) - const {currentAccount} = useSession() - const prevLoggedRouteName = React.useRef<string | undefined>(undefined) + const {currentAccount, accounts} = useSession() + const {onPressSwitchAccount} = useAccountSwitcher() + const {setShowLoggedOut} = useLoggedOutViewControls() + const prevLoggedRouteName = useRef<string | undefined>(undefined) const emailDialogControl = useEmailDialogControl() + const closeAllActiveElements = useCloseAllActiveElements() + + /** + * Handle navigation to a conversation, or prepares for account switch. + * + * Non-reactive because we need the latest data from some hooks + * after an async call - sfn + */ + const handleChatMessage = useNonReactiveCallback( + (payload: Extract<NotificationPayload, {reason: 'chat-message'}>) => { + notyLogger.debug(`handleChatMessage`, {payload}) + + if (payload.recipientDid !== currentAccount?.did) { + // handled in useNotificationHandler after account switch finishes + storePayloadForAccountSwitch(payload) + closeAllActiveElements() + + const account = accounts.find(a => a.did === payload.recipientDid) + if (account) { + onPressSwitchAccount(account, 'Notification') + } else { + setShowLoggedOut(true) + } + } else { + // @ts-expect-error nested navigators aren't typed -sfn + navigate('MessagesTab', { + screen: 'MessagesConversation', + params: { + conversation: payload.convoId, + }, + }) + } + }, + ) + + async function handlePushNotificationEntry() { + if (!isNative) return + + /** + * The notification that caused the app to open, if applicable + */ + const response = await Notifications.getLastNotificationResponseAsync() + + if (response) { + notyLogger.debug(`handlePushNotificationEntry: response`, {response}) + + if (response.notification.date === lastHandledNotificationDateDedupe) + return + lastHandledNotificationDateDedupe = response.notification.date + + const payload = getNotificationPayload(response.notification) + + if (payload) { + notyLogger.metric( + 'notifications:openApp', + {reason: payload.reason, causedBoot: true}, + {statsig: false}, + ) + + if (payload.reason === 'chat-message') { + handleChatMessage(payload) + } else { + const path = notificationToURL(payload) + + if (path === '/notifications') { + resetToTab('NotificationsTab') + notyLogger.debug(`handlePushNotificationEntry: default navigate`) + } else if (path) { + const [screen, params] = router.matchPath(path) + // @ts-expect-error nested navigators aren't typed -sfn + navigate('HomeTab', {screen, params}) + notyLogger.debug(`handlePushNotificationEntry: navigate`, { + screen, + params, + }) + } + } + } + } + } function onReady() { prevLoggedRouteName.current = getCurrentRouteName() @@ -845,9 +946,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { onStateChange={() => { logger.metric( 'router:navigate', - { - from: prevLoggedRouteName.current, - }, + {from: prevLoggedRouteName.current}, {statsig: false}, ) prevLoggedRouteName.current = getCurrentRouteName() @@ -857,6 +956,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) { logModuleInitTime() onReady() logger.metric('router:navigate', {}, {statsig: false}) + handlePushNotificationEntry() }} // WARNING: Implicit navigation to nested navigators is depreciated in React Navigation 7.x // However, there's a fair amount of places we do that, especially in when popping to the top of stacks. @@ -906,7 +1006,9 @@ function navigate<K extends keyof AllNavigatorParams>( return Promise.resolve() } -function resetToTab(tabName: 'HomeTab' | 'SearchTab' | 'NotificationsTab') { +function resetToTab( + tabName: 'HomeTab' | 'SearchTab' | 'MessagesTab' | 'NotificationsTab', +) { if (navigationRef.isReady()) { navigate(tabName) if (navigationRef.canGoBack()) { diff --git a/src/lib/hooks/useNotificationHandler.ts b/src/lib/hooks/useNotificationHandler.ts index 6c3e7deb8..ddee11fb5 100644 --- a/src/lib/hooks/useNotificationHandler.ts +++ b/src/lib/hooks/useNotificationHandler.ts @@ -7,9 +7,9 @@ import {CommonActions, useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {useAccountSwitcher} from '#/lib/hooks/useAccountSwitcher' +import {logger as notyLogger} from '#/lib/notifications/util' import {type NavigationProp} from '#/lib/routes/types' -import {Logger} from '#/logger' -import {isAndroid} from '#/platform/detection' +import {isAndroid, isIOS} from '#/platform/detection' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {RQKEY as RQKEY_NOTIFS} from '#/state/queries/notifications/feed' import {invalidateCachedUnreadPage} from '#/state/queries/notifications/unread' @@ -18,6 +18,7 @@ import {useSession} from '#/state/session' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useCloseAllActiveElements} from '#/state/util' import {resetToTab} from '#/Navigation' +import {router} from '#/routes' export type NotificationReason = | 'like' @@ -39,12 +40,13 @@ export type NotificationReason = * `notification.request.trigger.payload` being `undefined`, as specified in * the source types. */ -type NotificationPayload = +export type NotificationPayload = | undefined | { reason: Exclude<NotificationReason, 'chat-message'> uri: string subject: string + recipientDid: string } | { reason: 'chat-message' @@ -60,11 +62,17 @@ const DEFAULT_HANDLER_OPTIONS = { shouldSetBadge: true, } satisfies Notifications.NotificationBehavior -// These need to stay outside the hook to persist between account switches -let storedPayload: NotificationPayload -let prevDate = 0 +/** + * Cached notification payload if we handled a notification while the user was + * using a different account. This is consumed after we finish switching + * accounts. + */ +let storedAccountSwitchPayload: NotificationPayload -const logger = Logger.create(Logger.Context.Notifications) +/** + * Used to ensure we don't handle the same notification twice + */ +let lastHandledNotificationDateDedupe = 0 export function useNotificationsHandler() { const queryClient = useQueryClient() @@ -182,8 +190,15 @@ export function useNotificationsHandler() { if (!payload) return if (payload.reason === 'chat-message') { - if (payload.recipientDid !== currentAccount?.did && !storedPayload) { - storedPayload = payload + notyLogger.debug(`useNotificationsHandler: handling chat message`, { + payload, + }) + + if ( + payload.recipientDid !== currentAccount?.did && + !storedAccountSwitchPayload + ) { + storePayloadForAccountSwitch(payload) closeAllActiveElements() const account = accounts.find(a => a.did === payload.recipientDid) @@ -227,86 +242,29 @@ export function useNotificationsHandler() { }) } } else { - switch (payload.reason) { - case 'subscribed-post': - const urip = new AtUri(payload.uri) - if (urip.collection === 'app.bsky.feed.post') { - setTimeout(() => { - // @ts-expect-error types are weird here - navigation.navigate('HomeTab', { - screen: 'PostThread', - params: { - name: urip.host, - rkey: urip.rkey, - }, - }) - }, 500) - } else { - resetToTab('NotificationsTab') - } - break - case 'like': - case 'repost': - case 'follow': - case 'mention': - case 'quote': - case 'reply': - case 'starterpack-joined': - case 'like-via-repost': - case 'repost-via-repost': - case 'verified': - case 'unverified': - default: - resetToTab('NotificationsTab') - break - // TODO implement these after we have an idea of how to handle each individual case - // case 'follow': - // const uri = new AtUri(payload.uri) - // setTimeout(() => { - // // @ts-expect-error types are weird here - // navigation.navigate('HomeTab', { - // screen: 'Profile', - // params: { - // name: uri.host, - // }, - // }) - // }, 500) - // break - // case 'mention': - // case 'reply': - // const urip = new AtUri(payload.uri) - // setTimeout(() => { - // // @ts-expect-error types are weird here - // navigation.navigate('HomeTab', { - // screen: 'PostThread', - // params: { - // name: urip.host, - // rkey: urip.rkey, - // }, - // }) - // }, 500) + const url = notificationToURL(payload) + + if (url === '/notifications') { + resetToTab('NotificationsTab') + } else if (url) { + const [screen, params] = router.matchPath(url) + // @ts-expect-error router is not typed :/ -sfn + navigation.navigate('HomeTab', {screen, params}) + notyLogger.debug(`useNotificationsHandler: navigate`, { + screen, + params, + }) } } } Notifications.setNotificationHandler({ handleNotification: async e => { - if ( - e.request.trigger == null || - typeof e.request.trigger !== 'object' || - !('type' in e.request.trigger) || - e.request.trigger.type !== 'push' - ) { - return DEFAULT_HANDLER_OPTIONS - } + const payload = getNotificationPayload(e) - logger.debug('Notifications: received', {e}) + if (!payload) return DEFAULT_HANDLER_OPTIONS - const payload = e.request.trigger.payload as NotificationPayload - - if (!payload) { - return DEFAULT_HANDLER_OPTIONS - } + notyLogger.debug('useNotificationsHandler: incoming', {e, payload}) if ( payload.reason === 'chat-message' && @@ -329,46 +287,38 @@ export function useNotificationsHandler() { const responseReceivedListener = Notifications.addNotificationResponseReceivedListener(e => { - if (e.notification.date === prevDate) { - return - } - prevDate = e.notification.date + if (e.notification.date === lastHandledNotificationDateDedupe) return + lastHandledNotificationDateDedupe = e.notification.date - logger.debug('Notifications: response received', { + notyLogger.debug('useNotificationsHandler: response received', { actionIdentifier: e.actionIdentifier, }) - if ( - e.actionIdentifier === Notifications.DEFAULT_ACTION_IDENTIFIER && - e.notification.request.trigger != null && - typeof e.notification.request.trigger === 'object' && - 'type' in e.notification.request.trigger && - e.notification.request.trigger.type === 'push' - ) { - const payload = e.notification.request.trigger - .payload as NotificationPayload + if (e.actionIdentifier !== Notifications.DEFAULT_ACTION_IDENTIFIER) { + return + } - if (!payload) { - logger.error('useNotificationsHandler: received no payload', { - identifier: e.notification.request.identifier, - }) - return - } + const payload = getNotificationPayload(e.notification) + + if (payload) { if (!payload.reason) { - logger.error('useNotificationsHandler: received unknown payload', { - payload, - identifier: e.notification.request.identifier, - }) + notyLogger.error( + 'useNotificationsHandler: received unknown payload', + { + payload, + identifier: e.notification.request.identifier, + }, + ) return } - logger.debug( + notyLogger.debug( 'User pressed a notification, opening notifications tab', {}, ) - logger.metric( + notyLogger.metric( 'notifications:openApp', - {reason: payload.reason}, + {reason: payload.reason, causedBoot: false}, {statsig: false}, ) @@ -383,24 +333,28 @@ export function useNotificationsHandler() { truncateAndInvalidate(queryClient, RQKEY_NOTIFS('mentions')) } - logger.debug('Notifications: handleNotification', { + notyLogger.debug('Notifications: handleNotification', { content: e.notification.request.content, - payload: e.notification.request.trigger.payload, + payload: payload, }) handleNotification(payload) Notifications.dismissAllNotificationsAsync() + } else { + notyLogger.error('useNotificationsHandler: received no payload', { + identifier: e.notification.request.identifier, + }) } }) // Whenever there's a stored payload, that means we had to switch accounts before handling the notification. // Whenever currentAccount changes, we should try to handle it again. if ( - storedPayload?.reason === 'chat-message' && - currentAccount?.did === storedPayload.recipientDid + storedAccountSwitchPayload?.reason === 'chat-message' && + currentAccount?.did === storedAccountSwitchPayload.recipientDid ) { - handleNotification(storedPayload) - storedPayload = undefined + handleNotification(storedAccountSwitchPayload) + storedAccountSwitchPayload = undefined } return () => { @@ -418,3 +372,71 @@ export function useNotificationsHandler() { setShowLoggedOut, ]) } + +export function storePayloadForAccountSwitch(payload: NotificationPayload) { + storedAccountSwitchPayload = payload +} + +export function getNotificationPayload( + e: Notifications.Notification, +): NotificationPayload | null { + if ( + e.request.trigger == null || + typeof e.request.trigger !== 'object' || + !('type' in e.request.trigger) || + e.request.trigger.type !== 'push' + ) { + return null + } + + const payload = ( + isIOS ? e.request.trigger.payload : e.request.content.data + ) as NotificationPayload + + if (payload) { + return payload + } else { + return null + } +} + +export function notificationToURL( + payload: NotificationPayload, +): string | undefined { + switch (payload?.reason) { + case 'like': + case 'repost': + case 'like-via-repost': + case 'repost-via-repost': { + const urip = new AtUri(payload.subject) + if (urip.collection === 'app.bsky.feed.post') { + return `/profile/${urip.host}/post/${urip.rkey}` + } else { + return '/notifications' + } + } + case 'reply': + case 'quote': + case 'mention': + case 'subscribed-post': { + const urip = new AtUri(payload.uri) + if (urip.collection === 'app.bsky.feed.post') { + return `/profile/${urip.host}/post/${urip.rkey}` + } else { + return '/notifications' + } + } + case 'follow': + case 'starterpack-joined': { + const urip = new AtUri(payload.uri) + return `/profile/${urip.host}` + } + case 'chat-message': + // should be handled separately + return undefined + case 'verified': + case 'unverified': + default: + return '/notifications' + } +} diff --git a/src/lib/notifications/notifications.ts b/src/lib/notifications/notifications.ts index 2c0487ab7..94b3f6de3 100644 --- a/src/lib/notifications/notifications.ts +++ b/src/lib/notifications/notifications.ts @@ -6,13 +6,11 @@ import {type AtpAgent} from '@atproto/api' import debounce from 'lodash.debounce' import {PUBLIC_APPVIEW_DID, PUBLIC_STAGING_APPVIEW_DID} from '#/lib/constants' -import {Logger} from '#/logger' +import {logger as notyLogger} from '#/lib/notifications/util' import {isNative} from '#/platform/detection' import {type SessionAccount, useAgent, useSession} from '#/state/session' import BackgroundNotificationHandler from '#/../modules/expo-background-notification-handler' -const logger = Logger.create(Logger.Context.Notifications) - /** * @private * Registers the device's push notification token with the Bluesky server. @@ -36,12 +34,12 @@ async function _registerPushToken({ appId: 'xyz.blueskyweb.app', }) - logger.debug(`registerPushToken: success`, { + notyLogger.debug(`registerPushToken: success`, { tokenType: token.type, token: token.data, }) } catch (error) { - logger.error(`registerPushToken: failed`, {safeMessage: error}) + notyLogger.error(`registerPushToken: failed`, {safeMessage: error}) } } @@ -80,7 +78,7 @@ export function useRegisterPushToken() { */ async function getPushToken() { const granted = (await Notifications.getPermissionsAsync()).granted - logger.debug(`getPushToken`, {granted}) + notyLogger.debug(`getPushToken`, {granted}) if (granted) { return Notifications.getDevicePushTokenAsync() } @@ -115,7 +113,9 @@ export function useGetAndRegisterPushToken() { */ const token = await getPushToken() - logger.debug(`useGetAndRegisterPushToken`, {token: token ?? 'undefined'}) + notyLogger.debug(`useGetAndRegisterPushToken`, { + token: token ?? 'undefined', + }) if (token) { /** @@ -147,7 +147,7 @@ export function useNotificationsRegistration() { */ if (!currentAccount) return - logger.debug(`useNotificationsRegistration`) + notyLogger.debug(`useNotificationsRegistration`) /** * Init push token, if permissions are granted already. If they weren't, @@ -168,7 +168,7 @@ export function useNotificationsRegistration() { */ const subscription = Notifications.addPushTokenListener(async token => { registerPushToken({token}) - logger.debug(`addPushTokenListener callback`, {token}) + notyLogger.debug(`addPushTokenListener callback`, {token}) }) return () => { @@ -202,7 +202,7 @@ export function useRequestNotificationsPermission() { const res = await Notifications.requestPermissionsAsync() - logger.metric(`notifications:request`, { + notyLogger.metric(`notifications:request`, { context: context, status: res.status, }) diff --git a/src/lib/notifications/util.ts b/src/lib/notifications/util.ts new file mode 100644 index 000000000..6d00aa0e3 --- /dev/null +++ b/src/lib/notifications/util.ts @@ -0,0 +1,3 @@ +import {Logger} from '#/logger' + +export const logger = Logger.create(Logger.Context.Notifications) diff --git a/src/lib/routes/links.ts b/src/lib/routes/links.ts index 10c99b62d..908db7cba 100644 --- a/src/lib/routes/links.ts +++ b/src/lib/routes/links.ts @@ -1,4 +1,4 @@ -import {AppBskyGraphDefs, AtUri} from '@atproto/api' +import {type AppBskyGraphDefs, AtUri} from '@atproto/api' import {isInvalidHandle} from '#/lib/strings/handles' diff --git a/src/logger/metrics.ts b/src/logger/metrics.ts index d18e69122..3390c4b4b 100644 --- a/src/logger/metrics.ts +++ b/src/logger/metrics.ts @@ -26,6 +26,7 @@ export type MetricEvents = { } 'notifications:openApp': { reason: NotificationReason + causedBoot: boolean } 'notifications:request': { context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home' |