diff --git a/packages/mobile/src/screens/profile-screen/ProfileNavOverlay.tsx b/packages/mobile/src/screens/profile-screen/ProfileNavOverlay.tsx
index e52694763f1..2cf11aacd5a 100644
--- a/packages/mobile/src/screens/profile-screen/ProfileNavOverlay.tsx
+++ b/packages/mobile/src/screens/profile-screen/ProfileNavOverlay.tsx
@@ -36,7 +36,7 @@ export const PROFILE_NAV_CONTROLS_HEIGHT = 56
// Scroll distance (in px) over which the blur background fades in and the
// button icons transition from white to the neutral theme color.
-const FADE_DISTANCE = 60
+export const PROFILE_NAV_SCROLL_FADE_PX = 60
const useStyles = makeStyles(({ spacing }) => ({
root: {
@@ -93,7 +93,7 @@ export const ProfileNavOverlay = () => {
const blurBackgroundStyle = useAnimatedStyle(() => ({
opacity: scrollY
- ? interpolate(scrollY.value, [0, FADE_DISTANCE], [0, 1], {
+ ? interpolate(scrollY.value, [0, PROFILE_NAV_SCROLL_FADE_PX], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
})
@@ -103,7 +103,7 @@ export const ProfileNavOverlay = () => {
// White icons fade out as the user scrolls past the cover photo.
const whiteIconsStyle = useAnimatedStyle(() => ({
opacity: scrollY
- ? interpolate(scrollY.value, [0, FADE_DISTANCE], [1, 0], {
+ ? interpolate(scrollY.value, [0, PROFILE_NAV_SCROLL_FADE_PX], [1, 0], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
})
@@ -113,7 +113,7 @@ export const ProfileNavOverlay = () => {
// Neutral icons fade in as the user scrolls past the cover photo.
const neutralIconsStyle = useAnimatedStyle(() => ({
opacity: scrollY
- ? interpolate(scrollY.value, [0, FADE_DISTANCE], [0, 1], {
+ ? interpolate(scrollY.value, [0, PROFILE_NAV_SCROLL_FADE_PX], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
})
@@ -124,7 +124,7 @@ export const ProfileNavOverlay = () => {
// the cover photo has scrolled away.
const titleStyle = useAnimatedStyle(() => ({
opacity: scrollY
- ? interpolate(scrollY.value, [0, FADE_DISTANCE], [0, 1], {
+ ? interpolate(scrollY.value, [0, PROFILE_NAV_SCROLL_FADE_PX], [0, 1], {
extrapolateLeft: 'clamp',
extrapolateRight: 'clamp'
})
diff --git a/packages/mobile/src/screens/profile-screen/ProfileScreen.tsx b/packages/mobile/src/screens/profile-screen/ProfileScreen.tsx
index 15a6670e23c..794904fedca 100644
--- a/packages/mobile/src/screens/profile-screen/ProfileScreen.tsx
+++ b/packages/mobile/src/screens/profile-screen/ProfileScreen.tsx
@@ -18,7 +18,6 @@ import { ScreenPrimaryContent } from 'app/components/core/Screen/ScreenPrimaryCo
import { ScreenSecondaryContent } from 'app/components/core/Screen/ScreenSecondaryContent'
import { OfflinePlaceholder } from 'app/components/offline-placeholder'
import { useRoute } from 'app/hooks/useRoute'
-import { useStatusBarStyle } from 'app/hooks/useStatusBarStyle'
import { makeStyles } from 'app/styles'
import { DeactivatedProfileTombstone } from './DeactivatedProfileTombstone'
@@ -27,6 +26,7 @@ import { ProfileNavOverlay } from './ProfileNavOverlay'
import { ProfileScreenSkeleton } from './ProfileScreenSkeleton'
import { ProfileScrollContext } from './ProfileScrollContext'
import { ProfileTabNavigator } from './ProfileTabs/ProfileTabNavigator'
+import { useProfileScrollStatusBar } from './useProfileScrollStatusBar'
import { useRefreshProfile } from './useRefreshProfile'
const { setCurrentUser: setCurrentUserAction } = profilePageActions
const { getIsReachable } = reachabilitySelectors
@@ -96,7 +96,6 @@ export const ProfileScreen = () => {
)
useFocusEffect(setCurrentUser)
- useStatusBarStyle('light-content')
const renderHeader = useCallback(() => , [])
@@ -104,6 +103,7 @@ export const ProfileScreen = () => {
// from inside the collapsible header via `ProfileScrollBridge` and read by
// `ProfileNavOverlay` to animate its blur background and icon colors.
const scrollY = useSharedValue(0)
+ useProfileScrollStatusBar(scrollY)
return (
diff --git a/packages/mobile/src/screens/profile-screen/useProfileScrollStatusBar.ts b/packages/mobile/src/screens/profile-screen/useProfileScrollStatusBar.ts
new file mode 100644
index 00000000000..15ae5988c63
--- /dev/null
+++ b/packages/mobile/src/screens/profile-screen/useProfileScrollStatusBar.ts
@@ -0,0 +1,88 @@
+import { useCallback, useRef } from 'react'
+
+import { useFocusEffect } from '@react-navigation/native'
+import type { StatusBarProps } from 'react-native-bars'
+import { NavigationBar, StatusBar as RNStatusBar } from 'react-native-bars'
+import type { SharedValue } from 'react-native-reanimated'
+import { runOnJS, useAnimatedReaction } from 'react-native-reanimated'
+
+import { PROFILE_NAV_SCROLL_FADE_PX } from './ProfileNavOverlay'
+
+/**
+ * Status bar / nav bar style for profile: light icons over the cover at the top,
+ * default (dark) system bar content once the header has scrolled up — matching
+ * `ProfileNavOverlay` icon transition.
+ */
+export const useProfileScrollStatusBar = (scrollY: SharedValue) => {
+ const statusBarEntryRef = useRef(null)
+ const navigationBarEntryRef = useRef(null)
+ const lastBarStyleRef = useRef<'light-content' | 'dark-content' | null>(null)
+
+ const applyBarStyle = useCallback(
+ (barStyle: 'light-content' | 'dark-content') => {
+ if (!statusBarEntryRef.current || !navigationBarEntryRef.current) {
+ return
+ }
+ if (lastBarStyleRef.current === barStyle) {
+ return
+ }
+ lastBarStyleRef.current = barStyle
+ statusBarEntryRef.current = RNStatusBar.replaceStackEntry(
+ statusBarEntryRef.current,
+ { barStyle }
+ )
+ navigationBarEntryRef.current = NavigationBar.replaceStackEntry(
+ navigationBarEntryRef.current,
+ { barStyle }
+ )
+ },
+ []
+ )
+
+ useFocusEffect(
+ useCallback(() => {
+ lastBarStyleRef.current = null
+ statusBarEntryRef.current = RNStatusBar.pushStackEntry({
+ barStyle: 'light-content'
+ })
+ navigationBarEntryRef.current = NavigationBar.pushStackEntry({
+ barStyle: 'light-content'
+ })
+
+ // Align with restored scroll position (reaction only fires on change).
+ requestAnimationFrame(() => {
+ const barStyle =
+ scrollY.value < PROFILE_NAV_SCROLL_FADE_PX
+ ? 'light-content'
+ : 'dark-content'
+ applyBarStyle(barStyle)
+ })
+
+ return () => {
+ lastBarStyleRef.current = null
+ if (statusBarEntryRef.current) {
+ RNStatusBar.popStackEntry(statusBarEntryRef.current)
+ statusBarEntryRef.current = null
+ }
+ if (navigationBarEntryRef.current) {
+ NavigationBar.popStackEntry(navigationBarEntryRef.current)
+ navigationBarEntryRef.current = null
+ }
+ }
+ }, [applyBarStyle, scrollY])
+ )
+
+ useAnimatedReaction(
+ () => scrollY.value,
+ (y, prev) => {
+ const atTop = y < PROFILE_NAV_SCROLL_FADE_PX
+ const barStyle = atTop ? 'light-content' : 'dark-content'
+ const prevY = prev ?? 0
+ const prevAtTop = prevY < PROFILE_NAV_SCROLL_FADE_PX
+ if (atTop !== prevAtTop) {
+ runOnJS(applyBarStyle)(barStyle)
+ }
+ },
+ [applyBarStyle]
+ )
+}