Skip to content

Commit

Permalink
refactor: move header directly in the screen component
Browse files Browse the repository at this point in the history
Allows to remove shared header context
Simplify a bit by removing the need for insets on some children components
Share most of the animation logic
Fix some edge cases with the scrollHandler logic to not diff if no begin drag was done and to not start on a negative value
Going with headerShown false requires us to use the SafeAreaView on our screens
All tests still work after big refactor (no implementation detail in the tests makes this possible)
  • Loading branch information
Justkant committed Aug 9, 2024
1 parent b75593c commit 437246f
Show file tree
Hide file tree
Showing 21 changed files with 430 additions and 558 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export const PlatformAPIWebview = forwardRef<WebviewAPI, WebviewProps>(
currencies: safeCurrencyIds,
includeTokens,
});
// handle no curencies selected case
// handle no currencies selected case
const cryptoCurrencyIds =
safeCurrencyIds && safeCurrencyIds.length > 0
? safeCurrencyIds
Expand Down

This file was deleted.

74 changes: 15 additions & 59 deletions apps/ledger-live-mobile/src/newArch/features/Web3Hub/Navigator.tsx
Original file line number Diff line number Diff line change
@@ -1,73 +1,29 @@
import React, { useState } from "react";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { useSharedValue } from "react-native-reanimated";
import React from "react";
import {
createNativeStackNavigator,
NativeStackNavigationOptions,
} from "@react-navigation/native-stack";
import { ScreenName } from "~/const";
import { HeaderContext } from "./HeaderContext";
import Web3HubSearch from "./screens/Web3HubSearch";
import Web3HubSearchHeader from "./screens/Web3HubSearch/components/Header";
import Web3HubTabs from "./screens/Web3HubTabs";
import Web3HubTabsHeader from "./screens/Web3HubTabs/components/Header";
import Web3HubApp from "./screens/Web3HubApp";
import Web3HubAppHeader from "./screens/Web3HubApp/components/Header";
import type { AppProps, SearchProps, TabsProps, Web3HubStackParamList } from "./types";
import type { Web3HubStackParamList } from "./types";

// Uncomment to use mocks (you need to reload the app)
// process.env.MOCK_WEB3HUB = "1";

const Stack = createNativeStackNavigator<Web3HubStackParamList>();

export default function Navigator() {
const layoutY = useSharedValue(0);
const [search, setSearch] = useState("");
const screenOptions: NativeStackNavigationOptions = {
headerShown: false,
};

export default function Navigator() {
return (
<HeaderContext.Provider
value={{
layoutY,
search,
}}
>
<Stack.Navigator>
<Stack.Screen
name={ScreenName.Web3HubSearch}
component={Web3HubSearch}
options={{
header: props => (
<Web3HubSearchHeader
// Using as here because we cannot use generics on the header props
navigation={props.navigation as SearchProps["navigation"]}
onSearch={setSearch}
/>
),
}}
/>
<Stack.Screen
name={ScreenName.Web3HubTabs}
component={Web3HubTabs}
options={{
title: "N Tabs", // Temporary, will probably be changed
header: props => (
<Web3HubTabsHeader
title={props.options.title}
// Using as here because we cannot use generics on the header props
navigation={props.navigation as TabsProps["navigation"]}
/>
),
}}
/>
<Stack.Screen
name={ScreenName.Web3HubApp}
component={Web3HubApp}
options={{
header: props => (
<Web3HubAppHeader
// Using as here because we cannot use generics on the header props
navigation={props.navigation as AppProps["navigation"]}
/>
),
}}
/>
</Stack.Navigator>
</HeaderContext.Provider>
<Stack.Navigator screenOptions={screenOptions}>
<Stack.Screen name={ScreenName.Web3HubSearch} component={Web3HubSearch} />
<Stack.Screen name={ScreenName.Web3HubTabs} component={Web3HubTabs} />
<Stack.Screen name={ScreenName.Web3HubApp} component={Web3HubApp} />
</Stack.Navigator>
);
}
Original file line number Diff line number Diff line change
@@ -1,44 +1,22 @@
import React from "react";
import { useTranslation } from "react-i18next";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { useSharedValue } from "react-native-reanimated";
import {
createNativeStackNavigator,
NativeStackNavigationOptions,
} from "@react-navigation/native-stack";
import { ScreenName } from "~/const";
import { HeaderContext } from "./HeaderContext";
import Web3HubMain from "./screens/Web3HubMain";
import Web3HubMainHeader from "./screens/Web3HubMain/components/Header";
import { MainProps, Web3HubTabStackParamList } from "./types";
import { Web3HubTabStackParamList } from "./types";

const Stack = createNativeStackNavigator<Web3HubTabStackParamList>();

export default function TabNavigator() {
const { t } = useTranslation();
const layoutY = useSharedValue(0);
const screenOptions: NativeStackNavigationOptions = {
headerShown: false,
};

export default function TabNavigator() {
return (
<HeaderContext.Provider
value={{
layoutY,
}}
>
<Stack.Navigator initialRouteName={ScreenName.Web3HubMain}>
<Stack.Screen
name={ScreenName.Web3HubMain}
component={Web3HubMain}
options={{
title: t("web3hub.main.header.title"),
// Never just pass a component to header like `header: Web3HubMainHeader,`
// as it would break the fast-refresh for the header
header: props => (
<Web3HubMainHeader
title={props.options.title}
// Using as here because we cannot use generics on the header props
navigation={props.navigation as MainProps["navigation"]}
/>
),
animation: "none",
}}
/>
</Stack.Navigator>
</HeaderContext.Provider>
<Stack.Navigator initialRouteName={ScreenName.Web3HubMain} screenOptions={screenOptions}>
<Stack.Screen name={ScreenName.Web3HubMain} component={Web3HubMain} />
</Stack.Navigator>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import React, { PropsWithChildren } from "react";
import { ColorValue, StyleProp, ViewStyle } from "react-native";
import Animated, {
useAnimatedStyle,
interpolate,
Extrapolation,
SharedValue,
AnimatedStyle,
} from "react-native-reanimated";

type Props = PropsWithChildren<{
pt?: number;
style?: StyleProp<AnimatedStyle<StyleProp<ViewStyle>>>;
layoutY: SharedValue<number>;
totalHeight: number;
opacityHeight: number;
animationHeight: number;
backgroundColor?: ColorValue;
opacityChildren?: React.ReactNode;
}>;

export default function AnimatedBar({
pt = 0,
style,
layoutY,
backgroundColor,
totalHeight,
opacityHeight,
animationHeight,
opacityChildren,
children,
}: Props) {
const heightStyle = useAnimatedStyle(() => {
if (!layoutY) return {};

const headerHeight = interpolate(
layoutY.value,
[0, animationHeight],
[totalHeight, totalHeight - animationHeight],
Extrapolation.CLAMP,
);

return {
backgroundColor: backgroundColor,
paddingTop: pt,
height: headerHeight + pt,
};
});

const transformStyle = useAnimatedStyle(() => {
if (!layoutY) return {};

return {
// Height necessary for proper transform
height: totalHeight,
transform: [
{
translateY: interpolate(
layoutY.value,
[0, animationHeight],
[0, -animationHeight],
Extrapolation.CLAMP,
),
},
],
};
});

const opacityStyle = useAnimatedStyle(() => {
if (!layoutY) return {};

return {
height: opacityHeight,
opacity: interpolate(layoutY.value, [0, animationHeight], [1, 0], Extrapolation.CLAMP),
};
});

return (
<Animated.View style={[style, heightStyle]}>
<Animated.View style={transformStyle}>
<Animated.View style={opacityStyle}>{opacityChildren}</Animated.View>
{children}
</Animated.View>
</Animated.View>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export default function Disclaimer({

{description ? (
<Flex mt={6}>
<Text fontSize={14} lineHeight={"22px"} color="smoke">
<Text numberOfLines={12} fontSize={14} lineHeight={"22px"} color="smoke">
{description}
</Text>
</Flex>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import React, { ComponentProps, useCallback, useState } from "react";
import { View } from "react-native";
import Animated from "react-native-reanimated";
import { useTranslation } from "react-i18next";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { FlashList, FlashListProps } from "@shopify/flash-list";
import { Box, Text } from "@ledgerhq/native-ui";
import { AppManifest } from "@ledgerhq/live-common/wallet-api/types";
Expand Down Expand Up @@ -42,7 +41,6 @@ const renderItem = ({

export default function ManifestsList({ navigation, onScroll, title, pt = 0, pb = 0 }: Props) {
const { t } = useTranslation();
const insets = useSafeAreaInsets();
const [selectedCategory, selectCategory] = useState("all");
const { data, isLoading, onEndReached } = useManifestsListViewModel(selectedCategory);

Expand All @@ -65,8 +63,8 @@ export default function ManifestsList({ navigation, onScroll, title, pt = 0, pb
<Disclaimer disclaimer={disclaimer} />
<AnimatedFlashList
contentContainerStyle={{
paddingTop: pt ? pt + insets.top : pt,
paddingBottom: pb + insets.bottom,
paddingTop: pt,
paddingBottom: pb,
}}
ListHeaderComponent={
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { useCallback, useRef } from "react";
import { useSharedValue, useAnimatedScrollHandler } from "react-native-reanimated";
import { WebviewProps } from "~/components/Web3AppWebview/types";

const clamp = (value: number, lowerBound: number, upperBound: number) => {
"worklet";
return Math.min(Math.max(lowerBound, value), upperBound);
};

export default function useScrollHandler(clampUpperBound: number) {
const layoutY = useSharedValue(0);

const scrollHandler = useAnimatedScrollHandler<{ prevY: number; prevLayoutY: number }>({
onScroll: (event, ctx) => {
if (!layoutY || ctx.prevLayoutY === undefined || ctx.prevY === undefined) return;

const diff = event.contentOffset.y - ctx.prevY;

layoutY.value = clamp(ctx.prevLayoutY + diff, 0, clampUpperBound);
},
onBeginDrag: (event, ctx) => {
if (layoutY) {
ctx.prevLayoutY = layoutY.value;
}
// Avoid negative values to start with
// if you beginDrag after a bounce drag
ctx.prevY = Math.max(event.contentOffset.y, 0);
},
});

return {
layoutY,
scrollHandler,
};
}

type NoOptionals<T> = {
[K in keyof T]-?: T[K];
};

const initialTimeoutRef = {
prevY: 0,
prevLayoutY: 0,
};

export function useWebviewScrollHandler(clampUpperBound: number) {
const layoutY = useSharedValue(0);

// Trick until we can properly use reanimated with the webview
const timeoutRef = useRef<{ timeout?: NodeJS.Timeout; prevY: number; prevLayoutY: number }>(
initialTimeoutRef,
);

const onScroll = useCallback(
(event: Parameters<NoOptionals<WebviewProps>["onScroll"]>[0]) => {
if (!layoutY) return;
clearTimeout(timeoutRef.current.timeout);

const currentY = event.nativeEvent.contentOffset.y;

const diff = currentY - timeoutRef.current.prevY;
layoutY.value = clamp(timeoutRef.current.prevLayoutY + diff, 0, clampUpperBound);

timeoutRef.current.timeout = setTimeout(() => {
timeoutRef.current.prevY = Math.max(currentY, 0);
timeoutRef.current.prevLayoutY = layoutY.value;
}, 100);
},
[clampUpperBound, layoutY],
);

return {
layoutY,
onScroll,
};
}
Loading

0 comments on commit 437246f

Please sign in to comment.