From a770f5635b549f2a87ffeaedd031dfe8e37b58c8 Mon Sep 17 00:00:00 2001 From: Mathieu Acthernoene Date: Tue, 22 Apr 2025 18:16:50 +0200 Subject: Edge to edge support (#7497) --- app.config.js | 10 ++-- package.json | 5 +- plugins/withAndroidDayNightThemePlugin.js | 27 +++++++++++ ...ndroidSplashScreenStatusBarTranslucentPlugin.js | 28 ----------- plugins/withAndroidStylesAccentColorPlugin.js | 2 +- src/App.native.tsx | 7 +-- src/App.web.tsx | 7 +-- src/alf/util/navigationBar.ts | 21 -------- src/alf/util/systemUI.ts | 14 ++++++ src/components/ContextMenu/index.tsx | 2 +- src/components/Dialog/sheet-wrapper.ts | 23 +++++---- src/lib/hooks/useEnableKeyboardController.tsx | 5 +- src/screens/Login/index.tsx | 2 +- src/screens/Messages/components/MessageInput.tsx | 5 +- src/screens/SignupQueued.tsx | 4 +- src/screens/Takendown.tsx | 4 +- src/screens/VideoFeed/index.tsx | 8 ++-- src/state/shell/light-status-bar.tsx | 49 +++++-------------- src/view/com/composer/Composer.tsx | 3 +- src/view/com/lightbox/ImageViewing/index.tsx | 56 ++++++++-------------- src/view/com/modals/CreateOrEditList.tsx | 4 +- src/view/shell/index.tsx | 25 +++++----- yarn.lock | 26 ++++------ 23 files changed, 137 insertions(+), 200 deletions(-) create mode 100644 plugins/withAndroidDayNightThemePlugin.js delete mode 100644 plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js delete mode 100644 src/alf/util/navigationBar.ts create mode 100644 src/alf/util/systemUI.ts diff --git a/app.config.js b/app.config.js index 7b4937d3b..e197e6770 100644 --- a/app.config.js +++ b/app.config.js @@ -1,7 +1,5 @@ const pkg = require('./package.json') -const DARK_SPLASH_ANDROID_BACKGROUND = '#0f141b' - module.exports = function (config) { /** * App version number. Should be incremented as part of a release cycle. @@ -140,12 +138,10 @@ module.exports = function (config) { }, androidStatusBar: { barStyle: 'light-content', - backgroundColor: '#00000000', }, // Dark nav bar in light mode is better than light nav bar in dark mode androidNavigationBar: { barStyle: 'light-content', - backgroundColor: DARK_SPLASH_ANDROID_BACKGROUND, }, android: { icon: './assets/app-icons/android_icon_default_light.png', @@ -197,6 +193,10 @@ module.exports = function (config) { plugins: [ 'expo-video', 'expo-localization', + [ + 'react-native-edge-to-edge', + {android: {enforceNavigationBarContrast: false}}, + ], USE_SENTRY && [ '@sentry/react-native/expo', { @@ -240,7 +240,7 @@ module.exports = function (config) { './plugins/withAndroidManifestPlugin.js', './plugins/withAndroidManifestFCMIconPlugin.js', './plugins/withAndroidStylesAccentColorPlugin.js', - './plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js', + './plugins/withAndroidDayNightThemePlugin.js', './plugins/withAndroidNoJitpackPlugin.js', './plugins/withNoBundleCompression.js', './plugins/shareExtension/withShareExtensions.js', diff --git a/package.json b/package.json index 235b1a91b..8448fbd1e 100644 --- a/package.json +++ b/package.json @@ -141,12 +141,10 @@ "expo-linking": "~7.0.5", "expo-localization": "~16.0.1", "expo-media-library": "~17.0.6", - "expo-navigation-bar": "~4.0.9", "expo-notifications": "~0.29.14", "expo-screen-orientation": "~8.0.4", "expo-sharing": "~13.0.1", "expo-splash-screen": "~0.29.22", - "expo-status-bar": "~2.0.1", "expo-system-ui": "~4.0.9", "expo-task-manager": "~12.0.6", "expo-updates": "~0.27.4", @@ -178,12 +176,13 @@ "react-native-compressor": "1.10.3", "react-native-date-picker": "^5.0.7", "react-native-drawer-layout": "^4.1.1", + "react-native-edge-to-edge": "^1.6.0", "react-native-emoji-popup": "^0.1.2", "react-native-gesture-handler": "2.20.2", "react-native-get-random-values": "~1.11.0", "react-native-image-crop-picker": "^0.41.6", "react-native-ios-context-menu": "^1.15.3", - "react-native-keyboard-controller": "^1.14.5", + "react-native-keyboard-controller": "^1.17.1", "react-native-mmkv": "^2.12.2", "react-native-pager-view": "6.5.1", "react-native-picker-select": "^9.3.1", diff --git a/plugins/withAndroidDayNightThemePlugin.js b/plugins/withAndroidDayNightThemePlugin.js new file mode 100644 index 000000000..d9bc1b211 --- /dev/null +++ b/plugins/withAndroidDayNightThemePlugin.js @@ -0,0 +1,27 @@ +// Based on https://github.com/expo/expo/pull/33957 +// Could be removed once the app has been updated to Expo 53 +const {withAndroidStyles} = require('@expo/config-plugins') + +module.exports = function withAndroidDayNightThemePlugin(appConfig) { + const cleanupList = new Set([ + 'colorPrimary', + 'android:editTextBackground', + 'android:textColor', + 'android:editTextStyle', + ]) + + return withAndroidStyles(appConfig, config => { + config.modResults.resources.style = config.modResults.resources.style + ?.map(style => { + if (style.$.name === 'AppTheme' && style.item != null) { + style.item = style.item.filter(item => !cleanupList.has(item.$.name)) + } + return style + }) + .filter(style => { + return style.$.name !== 'ResetEditText' + }) + + return config + }) +} diff --git a/plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js b/plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js deleted file mode 100644 index 704ead054..000000000 --- a/plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js +++ /dev/null @@ -1,28 +0,0 @@ -const {withStringsXml, AndroidConfig} = require('@expo/config-plugins') - -module.exports = function withAndroidSplashScreenStatusBarTranslucentPlugin( - appConfig, -) { - return withStringsXml(appConfig, function (decoratedAppConfig) { - try { - decoratedAppConfig.modResults = AndroidConfig.Strings.setStringItem( - [ - { - _: 'true', - $: { - name: 'expo_splash_screen_status_bar_translucent', - translatable: 'false', - }, - }, - ], - decoratedAppConfig.modResults, - ) - } catch (e) { - console.error( - `withAndroidSplashScreenStatusBarTranslucentPlugin failed`, - e, - ) - } - return decoratedAppConfig - }) -} diff --git a/plugins/withAndroidStylesAccentColorPlugin.js b/plugins/withAndroidStylesAccentColorPlugin.js index c45553788..51dd44f35 100644 --- a/plugins/withAndroidStylesAccentColorPlugin.js +++ b/plugins/withAndroidStylesAccentColorPlugin.js @@ -12,7 +12,7 @@ module.exports = function withAndroidStylesAccentColorPlugin(appConfig) { decoratedAppConfig.modResults, { add: true, - parent: AndroidConfig.Styles.getAppThemeLightNoActionBarGroup(), + parent: AndroidConfig.Styles.getAppThemeGroup(), name: 'colorAccent', value: '@color/colorPrimary', }, diff --git a/src/App.native.tsx b/src/App.native.tsx index ac985e560..5023c48bb 100644 --- a/src/App.native.tsx +++ b/src/App.native.tsx @@ -46,14 +46,13 @@ import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' import { Provider as SessionProvider, - SessionAccount, + type SessionAccount, useSession, useSessionApi, } from '#/state/session' import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ComposerProvider} from '#/state/shell/composer' -import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' @@ -219,9 +218,7 @@ function App() { - - - + diff --git a/src/App.web.tsx b/src/App.web.tsx index af39bee47..bbe23e5a5 100644 --- a/src/App.web.tsx +++ b/src/App.web.tsx @@ -35,14 +35,13 @@ import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation import {Provider as UnreadNotifsProvider} from '#/state/queries/notifications/unread' import { Provider as SessionProvider, - SessionAccount, + type SessionAccount, useSession, useSessionApi, } from '#/state/session' import {readLastActiveAccount} from '#/state/session/util' import {Provider as ShellStateProvider} from '#/state/shell' import {Provider as ComposerProvider} from '#/state/shell/composer' -import {Provider as LightStatusBarProvider} from '#/state/shell/light-status-bar' import {Provider as LoggedOutViewProvider} from '#/state/shell/logged-out' import {Provider as ProgressGuideProvider} from '#/state/shell/progress-guide' import {Provider as SelectedFeedProvider} from '#/state/shell/selected-feed' @@ -193,9 +192,7 @@ function App() { - - - + diff --git a/src/alf/util/navigationBar.ts b/src/alf/util/navigationBar.ts deleted file mode 100644 index cb315f70a..000000000 --- a/src/alf/util/navigationBar.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as NavigationBar from 'expo-navigation-bar' -import * as SystemUI from 'expo-system-ui' - -import {isAndroid} from '#/platform/detection' -import {Theme} from '../types' - -export function setNavigationBar(themeType: 'theme' | 'lightbox', t: Theme) { - if (isAndroid) { - if (themeType === 'theme') { - NavigationBar.setBackgroundColorAsync(t.atoms.bg.backgroundColor) - NavigationBar.setBorderColorAsync(t.atoms.bg.backgroundColor) - NavigationBar.setButtonStyleAsync(t.name !== 'light' ? 'light' : 'dark') - SystemUI.setBackgroundColorAsync(t.atoms.bg.backgroundColor) - } else { - NavigationBar.setBackgroundColorAsync('black') - NavigationBar.setBorderColorAsync('black') - NavigationBar.setButtonStyleAsync('light') - SystemUI.setBackgroundColorAsync('black') - } - } -} diff --git a/src/alf/util/systemUI.ts b/src/alf/util/systemUI.ts new file mode 100644 index 000000000..c973e10ea --- /dev/null +++ b/src/alf/util/systemUI.ts @@ -0,0 +1,14 @@ +import * as SystemUI from 'expo-system-ui' + +import {isAndroid} from '#/platform/detection' +import {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') + } + } +} diff --git a/src/components/ContextMenu/index.tsx b/src/components/ContextMenu/index.tsx index aebed6419..4a0814dfe 100644 --- a/src/components/ContextMenu/index.tsx +++ b/src/components/ContextMenu/index.tsx @@ -556,7 +556,7 @@ export function Outer({ // pure vibes based const TOP_INSET = insets.top + 80 const BOTTOM_INSET_IOS = insets.bottom + 20 - const BOTTOM_INSET_ANDROID = 12 // TODO: revisit when edge-to-edge mode is enabled -sfn + const BOTTOM_INSET_ANDROID = insets.bottom + 12 const {height} = evt.nativeEvent.layout const topPosition = diff --git a/src/components/Dialog/sheet-wrapper.ts b/src/components/Dialog/sheet-wrapper.ts index 37c663383..b655dde00 100644 --- a/src/components/Dialog/sheet-wrapper.ts +++ b/src/components/Dialog/sheet-wrapper.ts @@ -1,20 +1,25 @@ import {useCallback} from 'react' +import {SystemBars} from 'react-native-edge-to-edge' -import {useDialogStateControlContext} from '#/state/dialogs' +import {isIOS} from '#/platform/detection' /** * If we're calling a system API like the image picker that opens a sheet * wrap it in this function to make sure the status bar is the correct color. */ export function useSheetWrapper() { - const {setFullyExpandedCount} = useDialogStateControlContext() - return useCallback( - async (promise: Promise): Promise => { - setFullyExpandedCount(c => c + 1) + return useCallback(async (promise: Promise): Promise => { + if (isIOS) { + const entry = SystemBars.pushStackEntry({ + style: { + statusBar: 'light', + }, + }) const res = await promise - setFullyExpandedCount(c => c - 1) + SystemBars.popStackEntry(entry) return res - }, - [setFullyExpandedCount], - ) + } else { + return await promise + } + }, []) } diff --git a/src/lib/hooks/useEnableKeyboardController.tsx b/src/lib/hooks/useEnableKeyboardController.tsx index 366791c0c..858f6943a 100644 --- a/src/lib/hooks/useEnableKeyboardController.tsx +++ b/src/lib/hooks/useEnableKeyboardController.tsx @@ -26,10 +26,7 @@ export function KeyboardControllerProvider({ children: React.ReactNode }) { return ( - + {children} diff --git a/src/screens/Login/index.tsx b/src/screens/Login/index.tsx index 8ed8d2da8..e4e2f43f0 100644 --- a/src/screens/Login/index.tsx +++ b/src/screens/Login/index.tsx @@ -8,7 +8,7 @@ import {DEFAULT_SERVICE} from '#/lib/constants' import {logEvent} from '#/lib/statsig/statsig' import {logger} from '#/logger' import {useServiceQuery} from '#/state/queries/service' -import {SessionAccount, useSession} from '#/state/session' +import {type SessionAccount, useSession} from '#/state/session' import {useLoggedOutView} from '#/state/shell/logged-out' import {LoggedOutLayout} from '#/view/com/util/layouts/LoggedOutLayout' import {ForgotPasswordForm} from '#/screens/Login/ForgotPasswordForm' diff --git a/src/screens/Messages/components/MessageInput.tsx b/src/screens/Messages/components/MessageInput.tsx index ac0f7969f..69cba07f7 100644 --- a/src/screens/Messages/components/MessageInput.tsx +++ b/src/screens/Messages/components/MessageInput.tsx @@ -24,9 +24,9 @@ import { useMessageDraft, useSaveMessageDraft, } from '#/state/messages/message-drafts' -import {EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker.web' +import {type EmojiPickerPosition} from '#/view/com/composer/text-input/web/EmojiPicker.web' import * as Toast from '#/view/com/util/Toast' -import {atoms as a, useTheme} from '#/alf' +import {android, atoms as a, useTheme} from '#/alf' import {useSharedInputStyles} from '#/components/forms/TextField' import {PaperPlane_Stroke2_Corner0_Rounded as PaperPlane} from '#/components/icons/PaperPlane' import {useExtractEmbedFromFacets} from './MessageInputEmbed' @@ -174,6 +174,7 @@ export function MessageInput({ a.text_md, a.px_sm, t.atoms.text, + android({paddingTop: 0}), {paddingBottom: isIOS ? 5 : 0}, animatedStyle, ]} diff --git a/src/screens/SignupQueued.tsx b/src/screens/SignupQueued.tsx index 823ed0784..6a2c5bbc7 100644 --- a/src/screens/SignupQueued.tsx +++ b/src/screens/SignupQueued.tsx @@ -1,7 +1,7 @@ import React from 'react' import {Modal, ScrollView, View} from 'react-native' +import {SystemBars} from 'react-native-edge-to-edge' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {StatusBar} from 'expo-status-bar' import {msg, plural, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -106,7 +106,7 @@ export function SignupQueued() { animationType={native('slide')} presentationStyle="formSheet" style={[web(a.util_screen_outer)]}> - {isIOS && } + {isIOS && } - {isIOS && } + {isIOS && } { setMinShellMode(true) - setNavigationBar('lightbox', t) + setSystemUITheme('lightbox', t) return () => { setMinShellMode(false) - setNavigationBar('theme', t) + setSystemUITheme('theme', t) } }, [setMinShellMode, t]), ) @@ -140,6 +141,7 @@ export function VideoFeed({}: NativeStackScreenProps< return ( + (false) -const SetLightStatusBarRefCountContext = createContext -> | null>(null) - -export function useLightStatusBar() { - return useContext(LightStatusBarRefCountContext) -} +import {useEffect} from 'react' +import {SystemBars} from 'react-native-edge-to-edge' export function useSetLightStatusBar(enabled: boolean) { - const setRefCount = useContext(SetLightStatusBarRefCountContext) useEffect(() => { - // noop on web -sfn - if (isWeb) return - - if (!setRefCount) { - if (__DEV__) - console.error( - 'useLightStatusBar was used without a SetLightStatusBarRefCountContext provider', - ) - return - } if (enabled) { - setRefCount(prev => prev + 1) - return () => setRefCount(prev => prev - 1) + const entry = SystemBars.pushStackEntry({ + style: { + statusBar: 'light', + }, + }) + return () => { + SystemBars.popStackEntry(entry) + } } - }, [enabled, setRefCount]) -} - -export function Provider({children}: React.PropsWithChildren<{}>) { - const [refCount, setRefCount] = useState(0) - - return ( - - 0}> - {children} - - - ) + }, [enabled]) } diff --git a/src/view/com/composer/Composer.tsx b/src/view/com/composer/Composer.tsx index 4384783dc..aa27adb3d 100644 --- a/src/view/com/composer/Composer.tsx +++ b/src/view/com/composer/Composer.tsx @@ -1464,8 +1464,7 @@ function useKeyboardVerticalOffset() { // Android etc if (!isIOS) { - // if Android <35 or web, bottom is 0 anyway. if >=35, this is needed to account - // for the edge-to-edge nav bar + // need to account for the edge-to-edge nav bar return bottom * -1 } diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index 7018d753a..41a54eba6 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -9,22 +9,17 @@ // https://github.com/jobtoday/react-native-image-viewing import React, {useCallback, useEffect, useMemo, useState} from 'react' -import { - LayoutAnimation, - PixelRatio, - Platform, - StyleSheet, - View, -} from 'react-native' +import {LayoutAnimation, PixelRatio, StyleSheet, View} from 'react-native' +import {SystemBars} from 'react-native-edge-to-edge' import {Gesture} from 'react-native-gesture-handler' import PagerView from 'react-native-pager-view' import Animated, { - AnimatedRef, + type AnimatedRef, cancelAnimation, interpolate, measure, runOnJS, - SharedValue, + type SharedValue, useAnimatedReaction, useAnimatedRef, useAnimatedStyle, @@ -32,30 +27,28 @@ import Animated, { useSharedValue, withDecay, withSpring, - WithSpringConfig, + type WithSpringConfig, } from 'react-native-reanimated' import { - Edge, SafeAreaView, useSafeAreaFrame, useSafeAreaInsets, } from 'react-native-safe-area-context' import * as ScreenOrientation from 'expo-screen-orientation' -import {StatusBar} from 'expo-status-bar' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {Trans} from '@lingui/macro' -import {Dimensions} from '#/lib/media/types' +import {type Dimensions} from '#/lib/media/types' import {colors, s} from '#/lib/styles' import {isIOS} from '#/platform/detection' -import {Lightbox} from '#/state/lightbox' +import {type Lightbox} from '#/state/lightbox' import {Button} from '#/view/com/util/forms/Button' import {Text} from '#/view/com/util/text/Text' import {ScrollView} from '#/view/com/util/Views' -import {ios, useTheme} from '#/alf' -import {setNavigationBar} from '#/alf/util/navigationBar' +import {useTheme} from '#/alf' +import {setSystemUITheme} from '#/alf/util/systemUI' import {PlatformInfo} from '../../../../../modules/expo-bluesky-swiss-army' -import {ImageSource, Transform} from './@types' +import {type ImageSource, type Transform} from './@types' import ImageDefaultHeader from './components/ImageDefaultHeader' import ImageItem from './components/ImageItem/ImageItem' @@ -63,10 +56,6 @@ type Rect = {x: number; y: number; width: number; height: number} const PORTRAIT_UP = ScreenOrientation.OrientationLock.PORTRAIT_UP const PIXEL_RATIO = PixelRatio.get() -const EDGES = - Platform.OS === 'android' && Platform.Version < 35 - ? (['top', 'bottom', 'left', 'right'] satisfies Edge[]) - : ([] satisfies Edge[]) // iOS or Android 15+ bleeds into safe area const SLOW_SPRING: WithSpringConfig = { mass: isIOS ? 1.25 : 0.75, @@ -167,9 +156,8 @@ export default function ImageViewRoot({ return ( // Keep it always mounted to avoid flicker on the first frame. - @@ -197,7 +185,7 @@ export default function ImageViewRoot({ /> )} - + ) } @@ -325,25 +313,23 @@ function ImageView({ }, ) - // style nav bar on android + // style system ui on android const t = useTheme() useEffect(() => { - setNavigationBar('lightbox', t) + setSystemUITheme('lightbox', t) return () => { - setNavigationBar('theme', t) + setSystemUITheme('theme', t) } }, [t]) return ( -