-
Notifications
You must be signed in to change notification settings - Fork 326
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: move header directly in the screen component
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
Showing
21 changed files
with
430 additions
and
558 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
9 changes: 0 additions & 9 deletions
9
apps/ledger-live-mobile/src/newArch/features/Web3Hub/HeaderContext.tsx
This file was deleted.
Oops, something went wrong.
74 changes: 15 additions & 59 deletions
74
apps/ledger-live-mobile/src/newArch/features/Web3Hub/Navigator.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
46 changes: 12 additions & 34 deletions
46
apps/ledger-live-mobile/src/newArch/features/Web3Hub/TabNavigator.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
86 changes: 86 additions & 0 deletions
86
apps/ledger-live-mobile/src/newArch/features/Web3Hub/components/AnimatedBar/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
76 changes: 76 additions & 0 deletions
76
apps/ledger-live-mobile/src/newArch/features/Web3Hub/hooks/useScrollHandler.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; | ||
} |
Oops, something went wrong.