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] + ) +}