diff --git a/apps/mobile/src/components/common/SafeNavigationScrollView.tsx b/apps/mobile/src/components/common/SafeNavigationScrollView.tsx index a472d6ca8f..ca19a05bb1 100644 --- a/apps/mobile/src/components/common/SafeNavigationScrollView.tsx +++ b/apps/mobile/src/components/common/SafeNavigationScrollView.tsx @@ -1,9 +1,9 @@ import { useBottomTabBarHeight } from "@react-navigation/bottom-tabs" -import { useHeaderHeight } from "@react-navigation/elements" +import { Header, useHeaderHeight } from "@react-navigation/elements" import type { NativeStackNavigationOptions } from "@react-navigation/native-stack" import { router, Stack, useNavigation } from "expo-router" import type { FC, PropsWithChildren } from "react" -import { createContext, useContext, useEffect, useMemo, useState } from "react" +import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react" import type { ScrollViewProps } from "react-native" import { Animated as RNAnimated, @@ -12,10 +12,12 @@ import { useAnimatedValue, View, } from "react-native" +import Animated, { useSharedValue, withTiming } from "react-native-reanimated" import type { ReanimatedScrollEvent } from "react-native-reanimated/lib/typescript/hook/commonTypes" import { useSafeAreaInsets } from "react-native-safe-area-context" import { useColor } from "react-native-uikit-colors" +import { useDefaultHeaderHeight } from "@/src/hooks/useDefaultHeaderHeight" import { MingcuteLeftLineIcon } from "@/src/icons/mingcute_left_line" import { AnimatedScrollView } from "./AnimatedComponents" @@ -69,9 +71,13 @@ export const SafeNavigationScrollView: FC = ({ export const NavigationBlurEffectHeader = ({ blurThreshold = 0, + headerHideableBottomHeight = 50, + headerHideableBottom, ...props }: NativeStackNavigationOptions & { blurThreshold?: number + headerHideableBottomHeight?: number + headerHideableBottom?: () => React.ReactNode }) => { const label = useColor("label") @@ -83,15 +89,39 @@ export const NavigationBlurEffectHeader = ({ const [opacity, setOpacity] = useState(0) + const originalDefaultHeaderHeight = useDefaultHeaderHeight() + const largeDefaultHeaderHeight = originalDefaultHeaderHeight + headerHideableBottomHeight + + const largeHeaderHeight = useSharedValue(largeDefaultHeaderHeight) + + const lastScrollY = useRef(0) + useEffect(() => { const id = scrollY.addListener(({ value }) => { - setOpacity(Math.min(1, Math.max(0, Math.min(1, (value + blurThreshold) / 10)))) + setOpacity(Math.max(0, Math.min(1, (value + blurThreshold) / 10))) + if (headerHideableBottom && value > 0) { + if (value > lastScrollY.current) { + largeHeaderHeight.value = withTiming(originalDefaultHeaderHeight) + } else { + largeHeaderHeight.value = withTiming(largeDefaultHeaderHeight) + } + lastScrollY.current = value + } }) return () => { scrollY.removeListener(id) } - }, [blurThreshold, scrollY]) + }, [ + blurThreshold, + scrollY, + headerHideableBottom, + largeHeaderHeight, + originalDefaultHeaderHeight, + largeDefaultHeaderHeight, + ]) + + const hideableBottom = headerHideableBottom?.() return ( { + return ( + + + {options.headerBackground?.()} + +
null} /> + {hideableBottom} + + ) + } + : undefined, + ...props, }} /> diff --git a/apps/mobile/src/hooks/useDefaultHeaderHeight.ts b/apps/mobile/src/hooks/useDefaultHeaderHeight.ts new file mode 100644 index 0000000000..d55be6ae98 --- /dev/null +++ b/apps/mobile/src/hooks/useDefaultHeaderHeight.ts @@ -0,0 +1,22 @@ +// https://github.com/react-navigation/react-navigation/blob/main/packages/native-stack/src/views/NativeStackView.native.tsx + +import { getDefaultHeaderHeight } from "@react-navigation/elements" +import { Platform } from "react-native" +import { useSafeAreaFrame, useSafeAreaInsets } from "react-native-safe-area-context" + +export const useDefaultHeaderHeight = () => { + const insets = useSafeAreaInsets() + const frame = useSafeAreaFrame() + + const isIPhone = Platform.OS === "ios" && !(Platform.isPad || Platform.isTV) + const isLandscape = frame.width > frame.height + const topInset = isIPhone && isLandscape ? 0 : insets.top + const ANDROID_DEFAULT_HEADER_HEIGHT = 56 + + const defaultHeaderHeight = Platform.select({ + android: ANDROID_DEFAULT_HEADER_HEIGHT + topInset, + default: getDefaultHeaderHeight(frame, false, topInset), + }) + + return defaultHeaderHeight +} diff --git a/apps/mobile/src/modules/entry-list/action.tsx b/apps/mobile/src/modules/entry-list/action.tsx index fd078c3fe8..f5b40cec27 100644 --- a/apps/mobile/src/modules/entry-list/action.tsx +++ b/apps/mobile/src/modules/entry-list/action.tsx @@ -19,7 +19,11 @@ export function LeftAction() { const insets = useActionPadding() return ( - + ) @@ -29,7 +33,7 @@ export function RightAction() { const insets = useActionPadding() return ( - + diff --git a/apps/mobile/src/modules/entry-list/entry-list.tsx b/apps/mobile/src/modules/entry-list/entry-list.tsx index 461466c6b0..78396c643e 100644 --- a/apps/mobile/src/modules/entry-list/entry-list.tsx +++ b/apps/mobile/src/modules/entry-list/entry-list.tsx @@ -14,7 +14,7 @@ import { NavigationContext, } from "@/src/components/common/SafeNavigationScrollView" import { ItemPressable } from "@/src/components/ui/pressable/item-pressable" -import { useSelectedFeed } from "@/src/modules/feed-drawer/atoms" +import { useSelectedFeed, useSelectedFeedTitle } from "@/src/modules/feed-drawer/atoms" import { useEntry } from "@/src/store/entry/hooks" import { ViewSelector } from "../feed-drawer/view-selector" @@ -25,12 +25,13 @@ export function EntryListScreen({ entryIds }: { entryIds: string[] }) { const scrollY = useAnimatedValue(0) const selectedFeed = useSelectedFeed() const view = selectedFeed.type === "view" ? selectedFeed.viewId : null + const viewTitle = useSelectedFeedTitle() return ( ({ scrollY }), [scrollY])}> ( @@ -43,6 +44,8 @@ export function EntryListScreen({ entryIds }: { entryIds: string[] }) { ), [], )} + headerHideableBottomHeight={45} + headerHideableBottom={ViewSelector} /> {view === FeedViewType.Pictures || view === FeedViewType.Videos ? ( diff --git a/apps/mobile/src/modules/feed-drawer/view-selector.tsx b/apps/mobile/src/modules/feed-drawer/view-selector.tsx index 446bf5d6b6..0e7820726d 100644 --- a/apps/mobile/src/modules/feed-drawer/view-selector.tsx +++ b/apps/mobile/src/modules/feed-drawer/view-selector.tsx @@ -1,5 +1,5 @@ import { Image } from "expo-image" -import { ScrollView, TouchableOpacity } from "react-native" +import { ScrollView, TouchableOpacity, View } from "react-native" import { Grayscale } from "react-native-color-matrix-image-filters" import { FallbackIcon } from "@/src/components/ui/icon/fallback-icon" @@ -15,14 +15,16 @@ export function ViewSelector() { const lists = useAllListSubscription() return ( - - {views.map((view) => ( - - ))} - {lists.map((listId) => ( - - ))} - + + + {views.map((view) => ( + + ))} + {lists.map((listId) => ( + + ))} + + ) }