exclude expo notifications from web bundle
This commit is contained in:
@@ -277,6 +277,11 @@ export default defineConfig(
|
||||
message:
|
||||
'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
|
||||
*/
|
||||
|
||||
+6
-3
@@ -1,6 +1,5 @@
|
||||
import {type JSX, useCallback, useRef} from 'react'
|
||||
import * as Linking from 'expo-linking'
|
||||
import * as Notifications from 'expo-notifications'
|
||||
import {i18n, type MessageDescriptor} from '@lingui/core'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {
|
||||
@@ -29,6 +28,10 @@ import {
|
||||
storePayloadForAccountSwitch,
|
||||
} from '#/lib/hooks/useNotificationHandler'
|
||||
import {useWebScrollRestoration} from '#/lib/hooks/useWebScrollRestoration'
|
||||
import {
|
||||
clearLastNotificationResponse,
|
||||
getLastNotificationResponse,
|
||||
} from '#/lib/notifications/expo-helpers'
|
||||
import {useCallOnce} from '#/lib/once'
|
||||
import {buildStateObject} from '#/lib/routes/helpers'
|
||||
import {
|
||||
@@ -976,7 +979,7 @@ function RoutesContainer({children}: React.PropsWithChildren<{}>) {
|
||||
// intent urls are handled by `useIntentHandler`
|
||||
if (linkingUrl) return
|
||||
|
||||
const notificationResponse = Notifications.getLastNotificationResponse()
|
||||
const notificationResponse = getLastNotificationResponse()
|
||||
|
||||
if (notificationResponse) {
|
||||
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
|
||||
try {
|
||||
Notifications.clearLastNotificationResponse()
|
||||
clearLastNotificationResponse()
|
||||
} catch (error) {
|
||||
notyLogger.error(
|
||||
`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 {Linking, View} from 'react-native'
|
||||
import * as Notification from 'expo-notifications'
|
||||
import {type AppBskyNotificationDefs} from '@atproto/api'
|
||||
import {msg} from '@lingui/core/macro'
|
||||
import {useLingui} from '@lingui/react'
|
||||
@@ -8,6 +7,10 @@ import {Trans} from '@lingui/react/macro'
|
||||
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
||||
|
||||
import {useAppState} from '#/lib/appState'
|
||||
import {
|
||||
getPermissionsAsync,
|
||||
requestPermissionsAsync,
|
||||
} from '#/lib/notifications/expo-helpers'
|
||||
import {
|
||||
type AllNavigatorParams,
|
||||
type NativeStackScreenProps,
|
||||
@@ -47,7 +50,7 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
queryKey: RQKEY,
|
||||
queryFn: async () => {
|
||||
if (IS_WEB) return null
|
||||
return await Notification.getPermissionsAsync()
|
||||
return await getPermissionsAsync()
|
||||
},
|
||||
})
|
||||
|
||||
@@ -61,7 +64,7 @@ export function NotificationSettingsScreen({}: Props) {
|
||||
const onRequestPermissions = async () => {
|
||||
if (IS_WEB) return
|
||||
if (permissions?.canAskAgain) {
|
||||
const response = await Notification.requestPermissionsAsync()
|
||||
const response = await requestPermissionsAsync()
|
||||
queryClient.setQueryData(RQKEY, response)
|
||||
} else {
|
||||
if (IS_ANDROID) {
|
||||
|
||||
Reference in New Issue
Block a user