From 4e5b1486b4bd790b3e8819a8d6eb7dfc53b3e7e3 Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Mon, 18 May 2026 14:50:50 +0300 Subject: [PATCH] remove main header state from global context --- src/lib/hooks/useMinimalShellTransform.ts | 52 ------------- src/state/shell/index.tsx | 1 - src/state/shell/minimal-mode.tsx | 12 +-- src/view/com/home/HomeHeaderLayoutMobile.tsx | 4 +- src/view/com/util/MainScrollProvider.tsx | 81 +++++++++++++++++++- src/view/screens/Home.tsx | 19 +++-- 6 files changed, 94 insertions(+), 75 deletions(-) diff --git a/src/lib/hooks/useMinimalShellTransform.ts b/src/lib/hooks/useMinimalShellTransform.ts index fc3bb9c9f7..11c08e4dc5 100644 --- a/src/lib/hooks/useMinimalShellTransform.ts +++ b/src/lib/hooks/useMinimalShellTransform.ts @@ -1,62 +1,10 @@ import {interpolate, useAnimatedStyle} from 'react-native-reanimated' -import {useSafeAreaInsets} from 'react-native-safe-area-context' import {useMinimalShellMode} from '#/state/shell/minimal-mode' import {useShellLayout} from '#/state/shell/shell-layout' -import {IS_LIQUID_GLASS} from '#/env' // Keep these separated so that we only pay for useAnimatedStyle that gets used. -export function useMinimalShellHeaderTransform() { - const {headerMode} = useMinimalShellMode() - const {headerHeight} = useShellLayout() - const {top: topInset} = useSafeAreaInsets() - - const headerPinnedHeight = IS_LIQUID_GLASS ? topInset : 0 - - const headerTransform = useAnimatedStyle(() => { - const headerModeValue = headerMode.get() - const hHeight = headerHeight.get() - - if (IS_LIQUID_GLASS) { - // bit of a hackfix, but: the header can get affected by scrollEdgeEffects - // when animating from closed to open. workaround is to trigger a relayout - // by offsetting the top position. the actual value doesn't matter, and we - // simultaneously offset it using the translate transform. - // I think a cleaner way to do it would be to use UIScrollEdgeElementContainerInteraction - // manually or something like that, because this kinda sucks -sfn - const relayoutingOffset = headerModeValue === 0 ? 1 : 0 - return { - top: relayoutingOffset, - pointerEvents: headerModeValue === 0 ? 'auto' : 'none', - opacity: Math.pow(1 - headerModeValue, 2), - transform: [ - { - translateY: - interpolate( - headerModeValue, - [0, 1], - [0, headerPinnedHeight - hHeight], - ) - relayoutingOffset, - }, - ], - } - } - - return { - pointerEvents: headerModeValue === 0 ? 'auto' : 'none', - opacity: Math.pow(1 - headerModeValue, 2), - transform: [ - { - translateY: interpolate(headerModeValue, [0, 1], [0, -hHeight]), - }, - ], - } - }) - - return headerTransform -} - export function useMinimalShellFooterTransform() { const {footerMode} = useMinimalShellMode() const {footerHeight} = useShellLayout() diff --git a/src/state/shell/index.tsx b/src/state/shell/index.tsx index 0fa3ff3042..b63993fb5c 100644 --- a/src/state/shell/index.tsx +++ b/src/state/shell/index.tsx @@ -14,7 +14,6 @@ export { export { useEnableMinimalShellMode, useEnableMinimalShellModeForScreen, - useMinimalShellMode, } from './minimal-mode' export {useOnboardingDispatch, useOnboardingState} from './onboarding' export {useTickEveryMinute} from './tick-every-minute' diff --git a/src/state/shell/minimal-mode.tsx b/src/state/shell/minimal-mode.tsx index a6f48dd867..2572ccc719 100644 --- a/src/state/shell/minimal-mode.tsx +++ b/src/state/shell/minimal-mode.tsx @@ -14,7 +14,6 @@ import { import {useFocusEffect} from '@react-navigation/native' type StateContext = { - headerMode: SharedValue footerMode: SharedValue } type SetContext = { @@ -28,24 +27,18 @@ const setContext = createContext(null) setContext.displayName = 'MinimalModeSetContext' export function Provider({children}: React.PropsWithChildren<{}>) { - const headerMode = useSharedValue(0) const footerMode = useSharedValue(0) const setModeWorklet = useCallback( (v: boolean) => { 'worklet' - headerMode.set(() => - withSpring(v ? 1 : 0, { - overshootClamping: true, - }), - ) footerMode.set(() => withSpring(v ? 1 : 0, { overshootClamping: true, }), ) }, - [headerMode, footerMode], + [footerMode], ) // defaults to "visible", if the count is >0 it gets hidden @@ -74,10 +67,9 @@ export function Provider({children}: React.PropsWithChildren<{}>) { const value = useMemo( () => ({ - headerMode, footerMode, }), - [headerMode, footerMode], + [footerMode], ) return ( diff --git a/src/view/com/home/HomeHeaderLayoutMobile.tsx b/src/view/com/home/HomeHeaderLayoutMobile.tsx index 32b95649ab..3cf91ce7d6 100644 --- a/src/view/com/home/HomeHeaderLayoutMobile.tsx +++ b/src/view/com/home/HomeHeaderLayoutMobile.tsx @@ -8,11 +8,11 @@ import {useNavigation} from '@react-navigation/native' import {HITSLOP_10} from '#/lib/constants' import {PressableScale} from '#/lib/custom-animations/PressableScale' import {useHaptics} from '#/lib/haptics' -import {useMinimalShellHeaderTransform} from '#/lib/hooks/useMinimalShellTransform' import {type NavigationProp} from '#/lib/routes/types' import {emitSoftReset} from '#/state/events' import {useSession} from '#/state/session' import {useShellLayout} from '#/state/shell/shell-layout' +import {useHomeHeaderTransform} from '#/view/com/util/MainScrollProvider' import {Logo} from '#/view/icons/Logo' import {atoms as a, useTheme} from '#/alf' import {ButtonIcon} from '#/components/Button' @@ -31,7 +31,7 @@ export function HomeHeaderLayoutMobile({ const {_} = useLingui() const {headerHeight} = useShellLayout() const insets = useSafeAreaInsets() - const headerMinimalShellTransform = useMinimalShellHeaderTransform() + const headerMinimalShellTransform = useHomeHeaderTransform() const {hasSession} = useSession() const playHaptic = useHaptics() const {navigate} = useNavigation() diff --git a/src/view/com/util/MainScrollProvider.tsx b/src/view/com/util/MainScrollProvider.tsx index 3038ad21e0..1a0580ea4e 100644 --- a/src/view/com/util/MainScrollProvider.tsx +++ b/src/view/com/util/MainScrollProvider.tsx @@ -1,8 +1,10 @@ -import {useCallback, useEffect} from 'react' +import {createContext, useCallback, useContext, useEffect} from 'react' import {type NativeScrollEvent} from 'react-native' import { clamp, interpolate, + type SharedValue, + useAnimatedStyle, useSharedValue, withSpring, } from 'react-native-reanimated' @@ -10,15 +12,88 @@ import {useSafeAreaInsets} from 'react-native-safe-area-context' import {EventEmitter} from 'eventemitter3' import {ScrollProvider} from '#/lib/ScrollContext' -import {useMinimalShellMode} from '#/state/shell' import {useShellLayout} from '#/state/shell/shell-layout' import {IS_LIQUID_GLASS, IS_NATIVE, IS_WEB} from '#/env' const WEB_HIDE_SHELL_THRESHOLD = 200 +const HomeHeaderModeContext = createContext | null>(null) +HomeHeaderModeContext.displayName = 'HomeHeaderModeContext' + +export function HomeHeaderModeProvider({ + children, +}: { + children: React.ReactNode +}) { + const headerMode = useSharedValue(0) + return ( + + {children} + + ) +} + +export function useHomeHeaderMode() { + const headerMode = useContext(HomeHeaderModeContext) + if (!headerMode) { + throw new Error( + 'useHomeHeaderMode must be used within a HomeHeaderModeProvider', + ) + } + return headerMode +} + +export function useHomeHeaderTransform() { + const headerMode = useHomeHeaderMode() + const {headerHeight} = useShellLayout() + const {top: topInset} = useSafeAreaInsets() + + const headerPinnedHeight = IS_LIQUID_GLASS ? topInset : 0 + + return useAnimatedStyle(() => { + const headerModeValue = headerMode.get() + const hHeight = headerHeight.get() + + if (IS_LIQUID_GLASS) { + // bit of a hackfix, but: the header can get affected by scrollEdgeEffects + // when animating from closed to open. workaround is to trigger a relayout + // by offsetting the top position. the actual value doesn't matter, and we + // simultaneously offset it using the translate transform. + // I think a cleaner way to do it would be to use UIScrollEdgeElementContainerInteraction + // manually or something like that, because this kinda sucks -sfn + const relayoutingOffset = headerModeValue === 0 ? 1 : 0 + return { + top: relayoutingOffset, + pointerEvents: headerModeValue === 0 ? 'auto' : 'none', + opacity: Math.pow(1 - headerModeValue, 2), + transform: [ + { + translateY: + interpolate( + headerModeValue, + [0, 1], + [0, headerPinnedHeight - hHeight], + ) - relayoutingOffset, + }, + ], + } + } + + return { + pointerEvents: headerModeValue === 0 ? 'auto' : 'none', + opacity: Math.pow(1 - headerModeValue, 2), + transform: [ + { + translateY: interpolate(headerModeValue, [0, 1], [0, -hHeight]), + }, + ], + } + }) +} + export function MainScrollProvider({children}: {children: React.ReactNode}) { const {headerHeight} = useShellLayout() - const {headerMode} = useMinimalShellMode() + const headerMode = useHomeHeaderMode() const {top: topInset} = useSafeAreaInsets() const headerPinnedHeight = IS_LIQUID_GLASS ? topInset : 0 const startDragOffset = useSharedValue(null) diff --git a/src/view/screens/Home.tsx b/src/view/screens/Home.tsx index c5473d17a8..eeb77e64b0 100644 --- a/src/view/screens/Home.tsx +++ b/src/view/screens/Home.tsx @@ -21,7 +21,6 @@ import {type FeedDescriptor, type FeedParams} from '#/state/queries/post-feed' import {usePreferencesQuery} from '#/state/queries/preferences' import {type UsePreferencesQueryResponse} from '#/state/queries/preferences/types' import {useSession} from '#/state/session' -import {useMinimalShellMode} from '#/state/shell' import {useLoggedOutViewControls} from '#/state/shell/logged-out' import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed' import {FeedPage} from '#/view/com/feeds/FeedPage' @@ -34,6 +33,10 @@ import { import {CustomFeedEmptyState} from '#/view/com/posts/CustomFeedEmptyState' import {FollowingEmptyState} from '#/view/com/posts/FollowingEmptyState' import {FollowingEndOfFeed} from '#/view/com/posts/FollowingEndOfFeed' +import { + HomeHeaderModeProvider, + useHomeHeaderMode, +} from '#/view/com/util/MainScrollProvider' import {NoFeedsPinned} from '#/screens/Home/NoFeedsPinned' import * as Layout from '#/components/Layout' import {useAnalytics} from '#/analytics' @@ -81,11 +84,13 @@ export function HomeScreen(props: Props) { if (preferences && pinnedFeedInfos && !isPinnedFeedsLoading) { return ( - + + + ) } else { @@ -139,7 +144,7 @@ function HomeScreenReady({ }, [selectedIndex]) const {hasSession} = useSession() - const {headerMode} = useMinimalShellMode() + const headerMode = useHomeHeaderMode() const showHeader = useCallback(() => { 'worklet' headerMode.set(() => withSpring(0, {overshootClamping: true}))