diff --git a/jest/test-utils.tsx b/jest/test-utils.tsx index 0a22d792b..264b31fae 100644 --- a/jest/test-utils.tsx +++ b/jest/test-utils.tsx @@ -1,22 +1,18 @@ -import React from 'react' -import {render} from '@testing-library/react-native' import {GestureHandlerRootView} from 'react-native-gesture-handler' -import {RootSiblingParent} from 'react-native-root-siblings' import {SafeAreaProvider} from 'react-native-safe-area-context' -import {RootStoreProvider, RootStoreModel} from '../src/state' +import {render} from '@testing-library/react-native' + import {ThemeProvider} from '../src/lib/ThemeContext' +import {type RootStoreModel, RootStoreProvider} from '../src/state' const customRender = (ui: any, rootStore: RootStoreModel) => render( - // eslint-disable-next-line react-native/no-inline-styles - - - - {ui} - - - + + + {ui} + + , ) diff --git a/package.json b/package.json index df8155d0c..7a26f88e0 100644 --- a/package.json +++ b/package.json @@ -196,7 +196,6 @@ "react-native-progress": "bluesky-social/react-native-progress", "react-native-qrcode-styled": "^0.3.3", "react-native-reanimated": "^3.19.1", - "react-native-root-siblings": "^5.0.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", "react-native-svg": "15.12.1", diff --git a/src/App.native.tsx b/src/App.native.tsx index 104a7ecae..30a5e8129 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -4,7 +4,6 @@ import '#/view/icons' import React, {useEffect, useState} from 'react' import {GestureHandlerRootView} from 'react-native-gesture-handler' -import {RootSiblingParent} from 'react-native-root-siblings' import { initialWindowMetrics, SafeAreaProvider, @@ -84,7 +83,11 @@ if (isIOS) { } if (isAndroid) { // iOS is handled by the config plugin -sfn - ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP) + ScreenOrientation.lockAsync( + ScreenOrientation.OrientationLock.PORTRAIT_UP, + ).catch(error => + logger.debug('Could not lock orientation', {safeMessage: error}), + ) } /** @@ -133,64 +136,62 @@ function InnerApp() { - - - - - - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/App.web.tsx b/src/App.web.tsx index 569c9be79..b7cba6122 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -3,7 +3,6 @@ import '#/view/icons' import './style.css' import React, {useEffect, useState} from 'react' -import {RootSiblingParent} from 'react-native-root-siblings' import {SafeAreaProvider} from 'react-native-safe-area-context' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -111,62 +110,60 @@ function InnerApp() { - - - - - - - - - - - {/* LabelDefsProvider MUST come before ModerationOptsProvider */} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + {/* LabelDefsProvider MUST come before ModerationOptsProvider */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/alf/util/systemUI.ts b/src/alf/util/systemUI.ts index 9e5769c4c..d8c899cb1 100644 --- a/src/alf/util/systemUI.ts +++ b/src/alf/util/systemUI.ts @@ -1,14 +1,20 @@ import * as SystemUI from 'expo-system-ui' +import {logger} from '#/logger' import {isAndroid} from '#/platform/detection' import {type Theme} from '../types' export function setSystemUITheme(themeType: 'theme' | 'lightbox', t: Theme) { if (isAndroid) { - if (themeType === 'theme') { - SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor) - } else { - SystemUI.setBackgroundColorAsync('black') + try { + if (themeType === 'theme') { + SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor) + } else { + SystemUI.setBackgroundColorAsync('black') + } + } catch (error) { + // Can reject with 'The current activity is no longer available' - no big deal + logger.debug('Could not set system UI theme', {safeMessage: error}) } } } diff --git a/src/components/Link.tsx b/src/components/Link.tsx index 421a7fe9d..2b533b599 100644 --- a/src/components/Link.tsx +++ b/src/components/Link.tsx @@ -165,8 +165,8 @@ export function useLink({ if (isNative && screen !== 'NotFound') { const state = navigation.getState() // if screen is not in the current navigator, it means it's - // most likely a tab screen - if (!state.routeNames.includes(screen)) { + // most likely a tab screen. note: state can be undefined + if (!state?.routeNames.includes(screen)) { const parent = navigation.getParent() if ( parent && diff --git a/src/lib/hooks/useOTAUpdates.ts b/src/lib/hooks/useOTAUpdates.ts index 9c5750606..466d03208 100644 --- a/src/lib/hooks/useOTAUpdates.ts +++ b/src/lib/hooks/useOTAUpdates.ts @@ -10,6 +10,7 @@ import { useUpdates, } from 'expo-updates' +import {isNetworkError} from '#/lib/strings/errors' import {logger} from '#/logger' import {isIOS} from '#/platform/detection' import {IS_TESTFLIGHT} from '#/env' @@ -145,8 +146,10 @@ export function useOTAUpdates() { } else { logger.debug('No update available.') } - } catch (e) { - logger.error('OTA Update Error', {error: `${e}`}) + } catch (err) { + if (!isNetworkError(err)) { + logger.error('OTA Update Error', {safeMessage: err}) + } } }, 10e3) }, []) @@ -154,8 +157,10 @@ export function useOTAUpdates() { const onIsTestFlight = React.useCallback(async () => { try { await updateTestflight() - } catch (e: any) { - logger.error('Internal OTA Update Error', {error: `${e}`}) + } catch (err: any) { + if (!isNetworkError(err)) { + logger.error('Internal OTA Update Error', {safeMessage: err}) + } } }, []) diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx index 88d9a47d0..e56bdc2bd 100644 --- a/src/state/feed-feedback.tsx +++ b/src/state/feed-feedback.tsx @@ -28,9 +28,21 @@ import {useAgent} from './session' export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS] -export const DIRECT_FEEDBACK_INTERACTIONS = new Set< +export const THIRD_PARTY_ALLOWED_INTERACTIONS = new Set< AppBskyFeedDefs.Interaction['event'] ->(['app.bsky.feed.defs#requestLess', 'app.bsky.feed.defs#requestMore']) +>([ + // These are explicit actions and are therefore fine to send. + 'app.bsky.feed.defs#requestLess', + 'app.bsky.feed.defs#requestMore', + // These can be inferred from the firehose and are therefore fine to send. + 'app.bsky.feed.defs#interactionLike', + 'app.bsky.feed.defs#interactionQuote', + 'app.bsky.feed.defs#interactionReply', + 'app.bsky.feed.defs#interactionRepost', + // This can be inferred from pagination requests for everything except the very last page + // so it is fine to send. It is crucial for third party algorithmic feeds to receive these. + 'app.bsky.feed.defs#interactionSeen', +]) const logger = Logger.create(Logger.Context.FeedFeedback) @@ -228,7 +240,7 @@ function isInteractionAllowed( return false } const isDiscover = isDiscoverFeed(feed.feedDescriptor) - return isDiscover ? true : DIRECT_FEEDBACK_INTERACTIONS.has(interaction) + return isDiscover ? true : THIRD_PARTY_ALLOWED_INTERACTIONS.has(interaction) } function toString(interaction: AppBskyFeedDefs.Interaction): string { diff --git a/src/state/geolocation/useSyncedDeviceGeolocation.ts b/src/state/geolocation/useSyncedDeviceGeolocation.ts index 602f29a30..fea6198d4 100644 --- a/src/state/geolocation/useSyncedDeviceGeolocation.ts +++ b/src/state/geolocation/useSyncedDeviceGeolocation.ts @@ -1,10 +1,45 @@ import {useEffect, useRef} from 'react' import * as Location from 'expo-location' +import {createPermissionHook} from 'expo-modules-core' import {logger} from '#/state/geolocation/logger' import {getDeviceGeolocation} from '#/state/geolocation/util' import {device, useStorage} from '#/storage' +/** + * Location.useForegroundPermissions on web just errors if the navigator.permissions API is not available. + * We need to catch and ignore it, since it's effectively denied. + * @see https://github.com/expo/expo/blob/72f1562ed9cce5ff6dfe04aa415b71632a3d4b87/packages/expo-location/src/Location.ts#L290-L293 + */ +const useForegroundPermissions = createPermissionHook({ + getMethod: () => + Location.getForegroundPermissionsAsync().catch(error => { + logger.debug( + 'useForegroundPermission: error getting location permissions', + {safeMessage: error}, + ) + return { + status: Location.PermissionStatus.DENIED, + granted: false, + canAskAgain: false, + expires: 0, + } + }), + requestMethod: () => + Location.requestForegroundPermissionsAsync().catch(error => { + logger.debug( + 'useForegroundPermission: error requesting location permissions', + {safeMessage: error}, + ) + return { + status: Location.PermissionStatus.DENIED, + granted: false, + canAskAgain: false, + expires: 0, + } + }), +}) + /** * Hook to get and sync the device geolocation from the device GPS and store it * using device storage. If permissions are not granted, it will clear any cached @@ -12,7 +47,7 @@ import {device, useStorage} from '#/storage' */ export function useSyncedDeviceGeolocation() { const synced = useRef(false) - const [status] = Location.useForegroundPermissions() + const [status] = useForegroundPermissions() const [deviceGeolocation, setDeviceGeolocation] = useStorage(device, [ 'deviceGeolocation', ]) diff --git a/yarn.lock b/yarn.lock index 8c09828e7..3d5ab1fa3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17064,11 +17064,6 @@ react-native-reanimated@^3.19.1: invariant "^2.2.4" react-native-is-edge-to-edge "1.1.7" -react-native-root-siblings@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/react-native-root-siblings/-/react-native-root-siblings-5.0.1.tgz#97e050e5155228f65810fb1c466ff8e769c5272c" - integrity sha512-Ay3k/fBj6ReUkWX5WNS+oEAcgPLEGOK8n7K/L7D85mf3xvd8rm/b4spsv26E4HlFzluVx5HKbxEt9cl0wQ1u3g== - react-native-safe-area-context@~5.6.0: version "5.6.1" resolved "https://registry.yarnpkg.com/react-native-safe-area-context/-/react-native-safe-area-context-5.6.1.tgz#cb4d249ef1a6f7e8fd0cfdfa9764838dffda26b6"