about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMathieu Acthernoene <zoontek@gmail.com>2025-04-22 18:16:50 +0200
committerGitHub <noreply@github.com>2025-04-22 19:16:50 +0300
commita770f5635b549f2a87ffeaedd031dfe8e37b58c8 (patch)
tree2e935d294227e57b2e81ba79ba96a6a85f268971
parent6e80b340c825900524bfe981ba29cfd0c6cf5934 (diff)
downloadvoidsky-a770f5635b549f2a87ffeaedd031dfe8e37b58c8.tar.zst
Edge to edge support (#7497)
-rw-r--r--app.config.js10
-rw-r--r--package.json5
-rw-r--r--plugins/withAndroidDayNightThemePlugin.js27
-rw-r--r--plugins/withAndroidSplashScreenStatusBarTranslucentPlugin.js28
-rw-r--r--plugins/withAndroidStylesAccentColorPlugin.js2
-rw-r--r--src/App.native.tsx7
-rw-r--r--src/App.web.tsx7
-rw-r--r--src/alf/util/navigationBar.ts21
-rw-r--r--src/alf/util/systemUI.ts14
-rw-r--r--src/components/ContextMenu/index.tsx2
-rw-r--r--src/components/Dialog/sheet-wrapper.ts23
-rw-r--r--src/lib/hooks/useEnableKeyboardController.tsx5
-rw-r--r--src/screens/Login/index.tsx2
-rw-r--r--src/screens/Messages/components/MessageInput.tsx5
-rw-r--r--src/screens/SignupQueued.tsx4
-rw-r--r--src/screens/Takendown.tsx4
-rw-r--r--src/screens/VideoFeed/index.tsx8
-rw-r--r--src/state/shell/light-status-bar.tsx49
-rw-r--r--src/view/com/composer/Composer.tsx3
-rw-r--r--src/view/com/lightbox/ImageViewing/index.tsx56
-rw-r--r--src/view/com/modals/CreateOrEditList.tsx4
-rw-r--r--src/view/shell/index.tsx25
-rw-r--r--yarn.lock26
23 files changed, 137 insertions, 200 deletions
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() {
                               <StarterPackProvider>
                                 <SafeAreaProvider
                                   initialMetrics={initialWindowMetrics}>
-                                  <LightStatusBarProvider>
-                                    <InnerApp />
-                                  </LightStatusBarProvider>
+                                  <InnerApp />
                                 </SafeAreaProvider>
                               </StarterPackProvider>
                             </BottomSheetProvider>
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() {
                       <LightboxStateProvider>
                         <PortalProvider>
                           <StarterPackProvider>
-                            <LightStatusBarProvider>
-                              <InnerApp />
-                            </LightStatusBarProvider>
+                            <InnerApp />
                           </StarterPackProvider>
                         </PortalProvider>
                       </LightboxStateProvider>
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 <T>(promise: Promise<T>): Promise<T> => {
-      setFullyExpandedCount(c => c + 1)
+  return useCallback(async <T>(promise: Promise<T>): Promise<T> => {
+    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 (
-    <KeyboardProvider
-      enabled={false}
-      // I don't think this is necessary, but Chesterton's fence and all that -sfn
-      statusBarTranslucent={true}>
+    <KeyboardProvider enabled={false}>
       <KeyboardControllerProviderInner>
         {children}
       </KeyboardControllerProviderInner>
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 && <StatusBar style="light" />}
+      {isIOS && <SystemBars style={{statusBar: 'light'}} />}
       <ScrollView
         style={[a.flex_1, t.atoms.bg]}
         contentContainerStyle={{borderWidth: 0}}
diff --git a/src/screens/Takendown.tsx b/src/screens/Takendown.tsx
index c714a775e..ef3e93658 100644
--- a/src/screens/Takendown.tsx
+++ b/src/screens/Takendown.tsx
@@ -1,8 +1,8 @@
 import {useMemo, useState} from 'react'
 import {Modal, View} from 'react-native'
+import {SystemBars} from 'react-native-edge-to-edge'
 import {KeyboardAwareScrollView} from 'react-native-keyboard-controller'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {StatusBar} from 'expo-status-bar'
 import {ComAtprotoAdminDefs, ComAtprotoModerationDefs} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
@@ -126,7 +126,7 @@ export function Takendown() {
       animationType={native('slide')}
       presentationStyle="formSheet"
       style={[web(a.util_screen_outer)]}>
-      {isIOS && <StatusBar style="light" />}
+      {isIOS && <SystemBars style={{statusBar: 'light'}} />}
       <KeyboardAwareScrollView style={[a.flex_1, t.atoms.bg]} centerContent>
         <View
           style={[
diff --git a/src/screens/VideoFeed/index.tsx b/src/screens/VideoFeed/index.tsx
index 04c2d7792..344b93429 100644
--- a/src/screens/VideoFeed/index.tsx
+++ b/src/screens/VideoFeed/index.tsx
@@ -8,6 +8,7 @@ import {
   ViewabilityConfig,
   ViewToken,
 } from 'react-native'
+import {SystemBars} from 'react-native-edge-to-edge'
 import {
   Gesture,
   GestureDetector,
@@ -77,7 +78,7 @@ import {PostCtrls} from '#/view/com/util/post-ctrls/PostCtrls'
 import {UserAvatar} from '#/view/com/util/UserAvatar'
 import {Header} from '#/screens/VideoFeed/components/Header'
 import {atoms as a, ios, platform, ThemeProvider, useTheme} from '#/alf'
-import {setNavigationBar} from '#/alf/util/navigationBar'
+import {setSystemUITheme} from '#/alf/util/systemUI'
 import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import {Divider} from '#/components/Divider'
 import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'
@@ -126,10 +127,10 @@ export function VideoFeed({}: NativeStackScreenProps<
   useFocusEffect(
     useCallback(() => {
       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 (
     <ThemeProvider theme="dark">
       <Layout.Screen noInsetTop style={{backgroundColor: 'black'}}>
+        <SystemBars style={{statusBar: 'light', navigationBar: 'light'}} />
         <View
           style={[
             a.absolute,
diff --git a/src/state/shell/light-status-bar.tsx b/src/state/shell/light-status-bar.tsx
index 6f47689d1..80df9ad90 100644
--- a/src/state/shell/light-status-bar.tsx
+++ b/src/state/shell/light-status-bar.tsx
@@ -1,44 +1,17 @@
-import {createContext, useContext, useEffect, useState} from 'react'
-
-import {isWeb} from '#/platform/detection'
-
-const LightStatusBarRefCountContext = createContext<boolean>(false)
-const SetLightStatusBarRefCountContext = createContext<React.Dispatch<
-  React.SetStateAction<number>
-> | 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 (
-    <SetLightStatusBarRefCountContext.Provider value={setRefCount}>
-      <LightStatusBarRefCountContext.Provider value={refCount > 0}>
-        {children}
-      </LightStatusBarRefCountContext.Provider>
-    </SetLightStatusBarRefCountContext.Provider>
-  )
+  }, [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.
-    <SafeAreaView
+    <View
       style={[styles.screen, !activeLightbox && styles.screenHidden]}
-      edges={EDGES}
       aria-modal
       accessibilityViewIsModal
       aria-hidden={!activeLightbox}>
@@ -197,7 +185,7 @@ export default function ImageViewRoot({
           />
         )}
       </Animated.View>
-    </SafeAreaView>
+    </View>
   )
 }
 
@@ -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 (
     <Animated.View style={[styles.container, containerStyle]}>
-      <StatusBar
-        animated
-        style="light"
-        hideTransitionAnimation="slide"
-        backgroundColor="black"
-        // hiding causes layout shifts on android,
-        // so avoid until we add edge-to-edge mode
-        hidden={ios(isScaled || !showControls)}
+      <SystemBars
+        style={{statusBar: 'light', navigationBar: 'light'}}
+        hidden={{
+          statusBar: isScaled || !showControls,
+          navigationBar: false,
+        }}
       />
       <Animated.View
         style={[styles.backdrop, backdropStyle]}
diff --git a/src/view/com/modals/CreateOrEditList.tsx b/src/view/com/modals/CreateOrEditList.tsx
index 3a1678954..0e4e23b97 100644
--- a/src/view/com/modals/CreateOrEditList.tsx
+++ b/src/view/com/modals/CreateOrEditList.tsx
@@ -8,9 +8,9 @@ import {
   TouchableOpacity,
   View,
 } from 'react-native'
-import {Image as RNImage} from 'react-native-image-crop-picker'
+import {type Image as RNImage} from 'react-native-image-crop-picker'
 import {LinearGradient} from 'expo-linear-gradient'
-import {AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
+import {type AppBskyGraphDefs, RichText as RichTextAPI} from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
diff --git a/src/view/shell/index.tsx b/src/view/shell/index.tsx
index 3fbc9a3f3..3d3a5520c 100644
--- a/src/view/shell/index.tsx
+++ b/src/view/shell/index.tsx
@@ -1,9 +1,9 @@
 import {useCallback, useEffect, useState} from 'react'
 import {BackHandler, useWindowDimensions, View} from 'react-native'
 import {Drawer} from 'react-native-drawer-layout'
+import {SystemBars} from 'react-native-edge-to-edge'
 import {Gesture} from 'react-native-gesture-handler'
 import {useSafeAreaInsets} from 'react-native-safe-area-context'
-import {StatusBar} from 'expo-status-bar'
 import {useNavigation, useNavigationState} from '@react-navigation/native'
 
 import {useDedupe} from '#/lib/hooks/useDedupe'
@@ -19,13 +19,12 @@ import {
   useIsDrawerSwipeDisabled,
   useSetDrawerOpen,
 } from '#/state/shell'
-import {useLightStatusBar} from '#/state/shell/light-status-bar'
 import {useCloseAnyActiveElement} from '#/state/util'
 import {Lightbox} from '#/view/com/lightbox/Lightbox'
 import {ModalsContainer} from '#/view/com/modals/Modal'
 import {ErrorBoundary} from '#/view/com/util/ErrorBoundary'
 import {atoms as a, select, useTheme} from '#/alf'
-import {setNavigationBar} from '#/alf/util/navigationBar'
+import {setSystemUITheme} from '#/alf/util/systemUI'
 import {MutedWordsDialog} from '#/components/dialogs/MutedWords'
 import {SigninDialog} from '#/components/dialogs/Signin'
 import {Outlet as PortalOutlet} from '#/components/Portal'
@@ -161,25 +160,23 @@ function ShellInner() {
 
 export const Shell: React.FC = function ShellImpl() {
   const {fullyExpandedCount} = useDialogStateControlContext()
-  const lightStatusBar = useLightStatusBar()
   const t = useTheme()
   useIntentHandler()
 
   useEffect(() => {
-    setNavigationBar('theme', t)
+    setSystemUITheme('theme', t)
   }, [t])
 
   return (
     <View testID="mobileShellView" style={[a.h_full, t.atoms.bg]}>
-      <StatusBar
-        style={
-          t.name !== 'light' ||
-          (isIOS && fullyExpandedCount > 0) ||
-          lightStatusBar
-            ? 'light'
-            : 'dark'
-        }
-        animated
+      <SystemBars
+        style={{
+          statusBar:
+            t.name !== 'light' || (isIOS && fullyExpandedCount > 0)
+              ? 'light'
+              : 'dark',
+          navigationBar: t.name !== 'light' ? 'light' : 'dark',
+        }}
       />
       <RoutesContainer>
         <ShellInner />
diff --git a/yarn.lock b/yarn.lock
index 71631527a..073c147ba 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11067,14 +11067,6 @@ expo-modules-core@^2.1.1:
   dependencies:
     invariant "^2.2.4"
 
-expo-navigation-bar@~4.0.9:
-  version "4.0.9"
-  resolved "https://registry.yarnpkg.com/expo-navigation-bar/-/expo-navigation-bar-4.0.9.tgz#e0409c2db8f6384d12c87f45c2674effc9fae1b6"
-  integrity sha512-dCJ04yPixFOUixJaWlmCZafGeQ1L1g6vWn+oX8rqPnYN9kYCMUz2aRNnhRRoK5MBGFTK/nue2D49TE/AwwWt9w==
-  dependencies:
-    "@react-native/normalize-colors" "0.76.8"
-    debug "^4.3.2"
-
 expo-notifications@~0.29.14:
   version "0.29.14"
   resolved "https://registry.yarnpkg.com/expo-notifications/-/expo-notifications-0.29.14.tgz#77beb6bc74b1b1abfa3adcab77fb6c9ea5d7d1b0"
@@ -11115,11 +11107,6 @@ expo-splash-screen@~0.29.22:
   dependencies:
     "@expo/prebuild-config" "^8.0.27"
 
-expo-status-bar@~2.0.1:
-  version "2.0.1"
-  resolved "https://registry.yarnpkg.com/expo-status-bar/-/expo-status-bar-2.0.1.tgz#fc07726346dc30fbb68aadb0d7890b34fba42eee"
-  integrity sha512-AkIPX7jWHRPp83UBZ1iXtVvyr0g+DgBVvIXTtlmPtmUsm8Vq9Bb5IGj86PW8osuFlgoTVAg7HI/+Ok7yEYwiRg==
-
 expo-structured-headers@~4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/expo-structured-headers/-/expo-structured-headers-4.0.0.tgz#85537ae6daec61ebfb214ede4107c8841c6e16d0"
@@ -16761,6 +16748,11 @@ react-native-drawer-layout@^4.1.1:
   dependencies:
     use-latest-callback "^0.2.1"
 
+react-native-edge-to-edge@^1.6.0:
+  version "1.6.0"
+  resolved "https://registry.yarnpkg.com/react-native-edge-to-edge/-/react-native-edge-to-edge-1.6.0.tgz#2ba63b941704a7f713e298185c26cde4d9e4b973"
+  integrity sha512-2WCNdE3Qd6Fwg9+4BpbATUxCLcouF6YRY7K+J36KJ4l3y+tWN6XCqAC4DuoGblAAbb2sLkhEDp4FOlbOIot2Og==
+
 react-native-emoji-popup@^0.1.2:
   version "0.1.2"
   resolved "https://registry.yarnpkg.com/react-native-emoji-popup/-/react-native-emoji-popup-0.1.2.tgz#7cd3874ba0496031e6f3e24de77e0df895168ce6"
@@ -16807,10 +16799,10 @@ react-native-is-edge-to-edge@^1.1.6:
   resolved "https://registry.yarnpkg.com/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.1.6.tgz#69ec13f70d76e9245e275eed4140d0873a78f902"
   integrity sha512-1pHnFTlBahins6UAajXUqeCOHew9l9C2C8tErnpGC3IyLJzvxD+TpYAixnCbrVS52f7+NvMttbiSI290XfwN0w==
 
-react-native-keyboard-controller@^1.14.5:
-  version "1.14.5"
-  resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.14.5.tgz#ec1e7d1fb8ee18b69ced4d8ddd6fd99bdaaf14bb"
-  integrity sha512-Cx7+SWI/P50i4PKJZN4T43RqoFkJ3GBoxjQ5ysrzZGoImHTF4j3atSwcBQGMmunKCem1yGOOQ84or+Vbcor6wQ==
+react-native-keyboard-controller@^1.17.1:
+  version "1.17.1"
+  resolved "https://registry.yarnpkg.com/react-native-keyboard-controller/-/react-native-keyboard-controller-1.17.1.tgz#46efe148c1bdd0ee22094dcb2660f70f81e6544e"
+  integrity sha512-YM3GYvtkuWimCKkZFURn5hIb1WiKOQqi2DijdwZSF5QSSzGqfqwzEEC3bm1xCN8HGHAEIXAaWIBUsc3Xp5L+Ng==
   dependencies:
     react-native-is-edge-to-edge "^1.1.6"