exclude expo notifications from web bundle
This commit is contained in:
@@ -277,6 +277,11 @@ export default defineConfig(
|
|||||||
message:
|
message:
|
||||||
'React is already in the global type namespace. Use named imports for runtime modules.',
|
'React is already in the global type namespace. Use named imports for runtime modules.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'expo-notifications',
|
||||||
|
message:
|
||||||
|
'Import the helpers from #/lib/notifications/expo-helpers (or the hooks from #/lib/notifications/notifications) instead. expo-notifications is stubbed on web; importing it directly pulls ~70KB of dead weight into the web bundle.',
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -312,6 +317,21 @@ export default defineConfig(
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Native-only files that legitimately wrap expo-notifications. The .web.ts
|
||||||
|
* counterparts to these files don't import the package.
|
||||||
|
*/
|
||||||
|
{
|
||||||
|
files: [
|
||||||
|
'src/lib/notifications/notifications.ts',
|
||||||
|
'src/lib/notifications/expo-helpers.ts',
|
||||||
|
'src/lib/hooks/useNotificationHandler.ts',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'no-restricted-imports': 'off',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test files configuration
|
* Test files configuration
|
||||||
*/
|
*/
|
||||||
|
|||||||
+6
-3
@@ -1,6 +1,5 @@
|
|||||||
import {type JSX, useCallback, useRef} from 'react'
|
import {type JSX, useCallback, useRef} from 'react'
|
||||||
import * as Linking from 'expo-linking'
|
import * as Linking from 'expo-linking'
|
||||||
import * as Notifications from 'expo-notifications'
|
|
||||||
import {i18n, type MessageDescriptor} from '@lingui/core'
|
import {i18n, type MessageDescriptor} from '@lingui/core'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {
|
import {
|
||||||
@@ -29,6 +28,10 @@ import {
|
|||||||
storePayloadForAccountSwitch,
|
storePayloadForAccountSwitch,
|
||||||
} from '#/lib/hooks/useNotificationHandler'
|
} from '#/lib/hooks/useNotificationHandler'
|
||||||
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
|
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
|
||||||
|
import {
|
||||||
|
clearLastNotificationResponse,
|
||||||
|
getLastNotificationResponse,
|
||||||
|
} from '#/lib/notifications/expo-helpers'
|
||||||
import {useCallOnce} from '#/lib/once'
|
import {useCallOnce} from '#/lib/once'
|
||||||
import {buildStateObject} from '#/lib/routes/helpers'
|
import {buildStateObject} from '#/lib/routes/helpers'
|
||||||
import {
|
import {
|
||||||
@@ -976,7 +979,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
// intent urls are handled by `useIntentHandler`
|
// intent urls are handled by `useIntentHandler`
|
||||||
if (linkingUrl) return
|
if (linkingUrl) return
|
||||||
|
|
||||||
const notificationResponse = Notifications.getLastNotificationResponse()
|
const notificationResponse = getLastNotificationResponse()
|
||||||
|
|
||||||
if (notificationResponse) {
|
if (notificationResponse) {
|
||||||
notyLogger.debug(`handlePushNotificationEntry: response`, {
|
notyLogger.debug(`handlePushNotificationEntry: response`, {
|
||||||
@@ -985,7 +988,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
|||||||
|
|
||||||
// Clear the last notification response to ensure it's not used again
|
// Clear the last notification response to ensure it's not used again
|
||||||
try {
|
try {
|
||||||
Notifications.clearLastNotificationResponse()
|
clearLastNotificationResponse()
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notyLogger.error(
|
notyLogger.error(
|
||||||
`handlePushNotificationEntry: error clearing notification response`,
|
`handlePushNotificationEntry: error clearing notification response`,
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// Web shim for #/lib/hooks/useNotificationHandler. expo-notifications doesn't
|
||||||
|
// run on web, so the hook is a no-op and the helper functions only deal with
|
||||||
|
// pure-data payloads we receive from elsewhere.
|
||||||
|
//
|
||||||
|
// Keeping this file expo-notifications-free saves ~70KB on web.
|
||||||
|
import {AtUri} from '@atproto/api'
|
||||||
|
|
||||||
|
export type NotificationReason =
|
||||||
|
| 'like'
|
||||||
|
| 'repost'
|
||||||
|
| 'follow'
|
||||||
|
| 'mention'
|
||||||
|
| 'reply'
|
||||||
|
| 'quote'
|
||||||
|
| 'chat-message'
|
||||||
|
| 'chat-reaction'
|
||||||
|
| 'chat-added-to-group'
|
||||||
|
| 'chat-removed-from-group'
|
||||||
|
| 'chat-join-request-rejected'
|
||||||
|
| 'starterpack-joined'
|
||||||
|
| 'like-via-repost'
|
||||||
|
| 'repost-via-repost'
|
||||||
|
| 'verified'
|
||||||
|
| 'unverified'
|
||||||
|
| 'subscribed-post'
|
||||||
|
|
||||||
|
type ChatNotificationReason = Extract<NotificationReason, `chat-${string}`>
|
||||||
|
|
||||||
|
export type NotificationPayload =
|
||||||
|
| undefined
|
||||||
|
| {
|
||||||
|
reason: Exclude<NotificationReason, ChatNotificationReason>
|
||||||
|
uri: string
|
||||||
|
subject: string
|
||||||
|
recipientDid: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
reason: 'chat-message'
|
||||||
|
convoId: string
|
||||||
|
messageId: string
|
||||||
|
recipientDid: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
reason: 'chat-reaction'
|
||||||
|
convoId: string
|
||||||
|
messageId: string
|
||||||
|
recipientDid: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
reason:
|
||||||
|
| 'chat-added-to-group'
|
||||||
|
| 'chat-removed-from-group'
|
||||||
|
| 'chat-join-request-rejected'
|
||||||
|
convoId: string
|
||||||
|
recipientDid: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ChatNotificationPayload = Extract<
|
||||||
|
NonNullable<NotificationPayload>,
|
||||||
|
{reason: ChatNotificationReason}
|
||||||
|
>
|
||||||
|
|
||||||
|
export function isChatNotificationPayload(
|
||||||
|
payload: NonNullable<NotificationPayload>,
|
||||||
|
): payload is ChatNotificationPayload {
|
||||||
|
return payload.reason.startsWith('chat-')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNotificationsHandler() {}
|
||||||
|
|
||||||
|
export function storePayloadForAccountSwitch(_payload: NotificationPayload) {}
|
||||||
|
|
||||||
|
// On web there's no expo-notifications object to inspect; callers from web
|
||||||
|
// code paths shouldn't invoke this, but we keep a stub that returns null.
|
||||||
|
export function getNotificationPayload(
|
||||||
|
_e: unknown,
|
||||||
|
): NotificationPayload | null {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function notificationToURL(payload: NotificationPayload): string | null {
|
||||||
|
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':
|
||||||
|
case 'chat-reaction':
|
||||||
|
case 'chat-added-to-group':
|
||||||
|
case 'chat-removed-from-group':
|
||||||
|
case 'chat-join-request-rejected':
|
||||||
|
return null
|
||||||
|
case 'verified':
|
||||||
|
case 'unverified':
|
||||||
|
return '/notifications'
|
||||||
|
default:
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Thin wrapper around expo-notifications APIs called outside the dedicated
|
||||||
|
// notifications hooks. The .web.ts variant stubs these out so we don't pull
|
||||||
|
// expo-notifications into the web bundle.
|
||||||
|
import * as Notifications from 'expo-notifications'
|
||||||
|
|
||||||
|
export function getLastNotificationResponse() {
|
||||||
|
return Notifications.getLastNotificationResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLastNotificationResponse() {
|
||||||
|
return Notifications.clearLastNotificationResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPermissionsAsync() {
|
||||||
|
return Notifications.getPermissionsAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestPermissionsAsync() {
|
||||||
|
return Notifications.requestPermissionsAsync()
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationPermissionsStatus =
|
||||||
|
Notifications.NotificationPermissionsStatus
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
// Web stubs. None of these are called on web (callers gate via IS_NATIVE /
|
||||||
|
// IS_WEB), but the imports must resolve to keep expo-notifications out of the
|
||||||
|
// web bundle.
|
||||||
|
|
||||||
|
export function getLastNotificationResponse(): null {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearLastNotificationResponse(): void {}
|
||||||
|
|
||||||
|
export type NotificationPermissionsStatus = {
|
||||||
|
status: 'granted' | 'denied' | 'undetermined'
|
||||||
|
granted: boolean
|
||||||
|
canAskAgain: boolean
|
||||||
|
expires: 'never' | number
|
||||||
|
}
|
||||||
|
|
||||||
|
const denied: NotificationPermissionsStatus = {
|
||||||
|
status: 'denied',
|
||||||
|
granted: false,
|
||||||
|
canAskAgain: false,
|
||||||
|
expires: 'never',
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPermissionsAsync(): Promise<NotificationPermissionsStatus> {
|
||||||
|
return denied
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function requestPermissionsAsync(): Promise<NotificationPermissionsStatus> {
|
||||||
|
return denied
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
// Web shim for #/lib/notifications/notifications. expo-notifications doesn't
|
||||||
|
// run on web, so all of these are no-ops; the corresponding native callers
|
||||||
|
// are gated by IS_NATIVE / Platform.OS, so on web they're never invoked.
|
||||||
|
//
|
||||||
|
// Keeping this file expo-notifications-free saves ~70KB on web.
|
||||||
|
import {type AtpAgent} from '@atproto/api'
|
||||||
|
|
||||||
|
export function useRegisterPushToken() {
|
||||||
|
return () => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useGetAndRegisterPushToken() {
|
||||||
|
return async () => undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNotificationsRegistration() {}
|
||||||
|
|
||||||
|
export function useRequestNotificationsPermission() {
|
||||||
|
return async (
|
||||||
|
_context: 'StartOnboarding' | 'AfterOnboarding' | 'Login' | 'Home',
|
||||||
|
) => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function decrementBadgeCount(_by: number) {}
|
||||||
|
|
||||||
|
export async function resetBadgeCount() {}
|
||||||
|
|
||||||
|
export async function unregisterPushToken(_agents: AtpAgent[]) {}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import {useEffect} from 'react'
|
import {useEffect} from 'react'
|
||||||
import {Linking, View} from 'react-native'
|
import {Linking, View} from 'react-native'
|
||||||
import * as Notification from 'expo-notifications'
|
|
||||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
import {type AppBskyNotificationDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
@@ -8,6 +7,10 @@ import {Trans} from '@lingui/react/macro'
|
|||||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {useAppState} from '#/lib/appState'
|
import {useAppState} from '#/lib/appState'
|
||||||
|
import {
|
||||||
|
getPermissionsAsync,
|
||||||
|
requestPermissionsAsync,
|
||||||
|
} from '#/lib/notifications/expo-helpers'
|
||||||
import {
|
import {
|
||||||
type AllNavigatorParams,
|
type AllNavigatorParams,
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
@@ -47,7 +50,7 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
queryKey: RQKEY,
|
queryKey: RQKEY,
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
if (IS_WEB) return null
|
if (IS_WEB) return null
|
||||||
return await Notification.getPermissionsAsync()
|
return await getPermissionsAsync()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -61,7 +64,7 @@ export function NotificationSettingsScreen({}: Props) {
|
|||||||
const onRequestPermissions = async () => {
|
const onRequestPermissions = async () => {
|
||||||
if (IS_WEB) return
|
if (IS_WEB) return
|
||||||
if (permissions?.canAskAgain) {
|
if (permissions?.canAskAgain) {
|
||||||
const response = await Notification.requestPermissionsAsync()
|
const response = await requestPermissionsAsync()
|
||||||
queryClient.setQueryData(RQKEY, response)
|
queryClient.setQueryData(RQKEY, response)
|
||||||
} else {
|
} else {
|
||||||
if (IS_ANDROID) {
|
if (IS_ANDROID) {
|
||||||
|
|||||||
Reference in New Issue
Block a user