From b9c21ab089a943bd0e17f99a20f84a7d4538368c Mon Sep 17 00:00:00 2001 From: Samuel Holmes Date: Tue, 12 Dec 2023 14:16:34 -0800 Subject: [PATCH] Implement SceneDrawer and WalletListSearch --- CHANGELOG.md | 1 + .../components/WalletListHeader.test.tsx | 1 - .../__snapshots__/MenuTabs.test.tsx.snap | 1 - .../WalletListHeader.test.tsx.snap | 60 +--- ...reateWalletAccountSetupScene.test.tsx.snap | 26 ++ .../CreateWalletImportScene.test.tsx.snap | 26 ++ ...reateWalletSelectCryptoScene.test.tsx.snap | 26 ++ .../CreateWalletSelectFiatScene.test.tsx.snap | 26 ++ .../CryptoExchangeQuoteScene.test.tsx.snap | 26 ++ .../CurrencyNotificationScene.test.tsx.snap | 26 ++ .../CurrencySettings.ui.test.tsx.snap | 26 ++ .../EdgeLoginScene.test.tsx.snap | 26 ++ .../__snapshots__/SendScene2.ui.test.tsx.snap | 260 ++++++++++++++++++ .../__snapshots__/SettingsScene.test.tsx.snap | 52 ++++ .../TransactionDetailsScene.test.tsx.snap | 52 ++++ src/components/common/SceneWrapper.tsx | 18 +- src/components/scenes/WalletListScene.tsx | 52 +++- src/components/themed/MenuTabs.tsx | 42 ++- src/components/themed/SceneDrawer.tsx | 48 ++++ src/components/themed/WalletListHeader.tsx | 37 +-- src/components/themed/WalletListSearch.tsx | 106 +++++++ src/state/SceneDrawerState.tsx | 75 +++-- 22 files changed, 851 insertions(+), 162 deletions(-) create mode 100644 src/components/themed/SceneDrawer.tsx create mode 100644 src/components/themed/WalletListSearch.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d38c005f21..54c18b24f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - changed: Update various scenes with UI4 components - changed: Light account re-enabled at 50% distribution - changed: New dynamic menu tabs that responds to scene scroll +- changed: New dynamic wallet list search drawer above tabs - changed: Scene layout to support transparent and blurred header and tab-bar - fixed: USP vs legacy landing experiment distribution - fixed: Paybis sell from Tron USDT diff --git a/src/__tests__/components/WalletListHeader.test.tsx b/src/__tests__/components/WalletListHeader.test.tsx index 5f7a341ad27..4e7d1c35287 100644 --- a/src/__tests__/components/WalletListHeader.test.tsx +++ b/src/__tests__/components/WalletListHeader.test.tsx @@ -15,7 +15,6 @@ describe('WalletListHeader', () => { navigation={fakeNavigation} sorting searching - searchText="string" openSortModal={() => undefined} onChangeSearchText={() => undefined} onChangeSearchingState={searching => undefined} diff --git a/src/__tests__/components/__snapshots__/MenuTabs.test.tsx.snap b/src/__tests__/components/__snapshots__/MenuTabs.test.tsx.snap index 09d8e14b61d..bbf8d527af9 100644 --- a/src/__tests__/components/__snapshots__/MenuTabs.test.tsx.snap +++ b/src/__tests__/components/__snapshots__/MenuTabs.test.tsx.snap @@ -2,7 +2,6 @@ exports[`MenuTabs should render with loading props 1`] = ` - - - - - - - Done - - - - -`; +exports[`WalletListHeader should render with loading props 1`] = ``; diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap index e486363d008..5a1d31b84e5 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletAccountSetupScene.test.tsx.snap @@ -515,6 +515,32 @@ exports[`CreateWalletAccountSelect renders 1`] = ` + , ] diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap index 49ae7ee9911..812ca507df5 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletImportScene.test.tsx.snap @@ -530,6 +530,32 @@ exports[`CreateWalletImportScene should render with loading props 1`] = ` + , ] `; diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap index 0d8df31d3ef..bc8085ecd29 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletSelectCryptoScene.test.tsx.snap @@ -898,6 +898,32 @@ exports[`CreateWalletSelectCrypto should render with loading props 1`] = ` + , ] `; diff --git a/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap index 613ca3ca859..4661758e993 100644 --- a/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CreateWalletSelectFiatScene.test.tsx.snap @@ -828,6 +828,32 @@ exports[`CreateWalletSelectFiatComponent should render with loading props 1`] = + , ] `; diff --git a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap index 474310f809f..54dab81710a 100644 --- a/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CryptoExchangeQuoteScene.test.tsx.snap @@ -1458,6 +1458,32 @@ exports[`CryptoExchangeQuoteScreenComponent should render with loading props 1`] } } /> + , ] `; diff --git a/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap index 0aea2fea9ec..87ea3fa8a08 100644 --- a/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CurrencyNotificationScene.test.tsx.snap @@ -283,6 +283,32 @@ exports[`CurrencyNotificationComponent should render with loading props 1`] = ` + , ] `; diff --git a/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap index 4eab60a2f29..0498496f7e3 100644 --- a/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/CurrencySettings.ui.test.tsx.snap @@ -489,6 +489,32 @@ exports[`CurrencySettings should render 1`] = ` + , ] `; diff --git a/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap index 9ac99244e6d..b2959d2ae88 100644 --- a/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/EdgeLoginScene.test.tsx.snap @@ -455,6 +455,32 @@ exports[`EdgeLoginScene should render with loading props 1`] = ` + , ] `; diff --git a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap index 14229108f88..1077b7c581a 100644 --- a/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SendScene2.ui.test.tsx.snap @@ -798,6 +798,32 @@ exports[`SendScene2 1 spendTarget 1`] = ` } } /> + , ] `; @@ -1810,6 +1836,32 @@ exports[`SendScene2 1 spendTarget with info tiles 1`] = ` } } /> + , ] `; @@ -2744,6 +2796,32 @@ exports[`SendScene2 2 spendTargets 1`] = ` } } /> + , ] `; @@ -3548,6 +3626,32 @@ exports[`SendScene2 2 spendTargets hide tiles 1`] = ` } } /> + , ] `; @@ -4330,6 +4434,32 @@ exports[`SendScene2 2 spendTargets hide tiles 2`] = ` } } /> + , ] `; @@ -4982,6 +5112,32 @@ exports[`SendScene2 2 spendTargets hide tiles 3`] = ` } } /> + , ] `; @@ -5889,6 +6045,32 @@ exports[`SendScene2 2 spendTargets lock tiles 1`] = ` } } /> + , ] `; @@ -6769,6 +6951,32 @@ exports[`SendScene2 2 spendTargets lock tiles 2`] = ` } } /> + , ] `; @@ -7622,6 +7830,32 @@ exports[`SendScene2 2 spendTargets lock tiles 3`] = ` } } /> + , ] `; @@ -8534,6 +8768,32 @@ exports[`SendScene2 Render SendScene 1`] = ` } } /> + , ] `; diff --git a/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap index cc539f1f90a..eb91f3843c0 100644 --- a/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/SettingsScene.test.tsx.snap @@ -2827,6 +2827,32 @@ exports[`MyComponent should render Locked SettingsOverview 1`] = ` + , ] `; @@ -5658,6 +5684,32 @@ exports[`MyComponent should render UnLocked SettingsOverview 1`] = ` + , ] `; diff --git a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap index 637a6ed335e..40047263e67 100644 --- a/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap +++ b/src/__tests__/scenes/__snapshots__/TransactionDetailsScene.test.tsx.snap @@ -1539,6 +1539,32 @@ exports[`TransactionDetailsScene should render 1`] = ` } } /> + , ] @@ -3083,6 +3109,32 @@ exports[`TransactionDetailsScene should render with negative nativeAmount and fi } } /> + , ] diff --git a/src/components/common/SceneWrapper.tsx b/src/components/common/SceneWrapper.tsx index a6e3cab0ad7..5d2a8d43001 100644 --- a/src/components/common/SceneWrapper.tsx +++ b/src/components/common/SceneWrapper.tsx @@ -4,13 +4,14 @@ import { Animated, Dimensions, ScrollView, StyleSheet, View } from 'react-native import LinearGradient from 'react-native-linear-gradient' import { EdgeInsets, useSafeAreaFrame, useSafeAreaInsets } from 'react-native-safe-area-context' -import { useDrawerOpenRatio } from '../../state/SceneDrawerState' +import { useSceneDrawerState } from '../../state/SceneDrawerState' import { useSelector } from '../../types/reactRedux' import { maybeComponent } from '../hoc/maybeComponent' import { styled } from '../hoc/styled' import { NotificationView } from '../notification/NotificationView' import { useTheme } from '../services/ThemeContext' import { MAX_TAB_BAR_HEIGHT } from '../themed/MenuTabs' +import { SceneDrawer } from '../themed/SceneDrawer' import { KeyboardTracker } from './KeyboardTracker' const windowDimensions = Dimensions.get('window') @@ -52,6 +53,9 @@ interface SceneWrapperProps { // Padding to add inside the scene border: padding?: number + // Render function to render component for the tab drawer + renderDrawer?: () => React.ReactNode + // True to make the scene scrolling (if avoidKeyboard is false): scroll?: boolean } @@ -67,6 +71,7 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { avoidKeyboard = false, background = 'theme', children, + renderDrawer, hasHeader = true, hasNotifications = false, hasTabs = false, @@ -79,7 +84,7 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { const activeUsername = useSelector(state => state.core.account.username) const isLightAccount = accountId != null && activeUsername == null - const { drawerHeight } = useDrawerOpenRatio() + const { tabDrawerHeight = 0 } = useSceneDrawerState() const theme = useTheme() @@ -95,6 +100,12 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { const hasKeyboardAnimation = keyboardAnimation != null const isFuncChildren = typeof children === 'function' + // Derive the keyboard height by getting the difference between screen height + // and trackerValue. This value should be from zero to keyboard height + // depending on the open state of the keyboard + const keyboardHeight = frame.height - trackerValue + const isKeyboardOpen = keyboardHeight !== 0 + // These are the safeAreaInsets including the app's header and tab-bar // heights. const insets: EdgeInsets = { @@ -109,7 +120,7 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { // used for the ScrollView component internal to the SceneWrapper. const insetStyles: InsetStyles = { paddingTop: insets.top, - paddingBottom: insets.bottom + drawerHeight.value + paddingBottom: insets.bottom + tabDrawerHeight } const maybeInsetStyles = isFuncChildren ? {} : insetStyles @@ -134,6 +145,7 @@ export function SceneWrapper(props: SceneWrapperProps): JSX.Element { {isFuncChildren ? children({ safeAreaInsets, insets, insetStyles: insetStyles }) : children} {hasNotifications ? : null} + {renderDrawer == null ? null : renderDrawer()} diff --git a/src/components/scenes/WalletListScene.tsx b/src/components/scenes/WalletListScene.tsx index bc882286c8e..50b9d3333fc 100644 --- a/src/components/scenes/WalletListScene.tsx +++ b/src/components/scenes/WalletListScene.tsx @@ -17,6 +17,7 @@ import { cacheStyles, Theme, useTheme } from '../services/ThemeContext' import { EdgeText } from '../themed/EdgeText' import { WalletListFooter } from '../themed/WalletListFooter' import { WalletListHeader } from '../themed/WalletListHeader' +import { WalletListSearch } from '../themed/WalletListSearch' import { WalletListSortable } from '../themed/WalletListSortable' import { WalletListSwipeable } from '../themed/WalletListSwipeable' import { WiredProgressBar } from '../themed/WiredProgressBar' @@ -30,7 +31,7 @@ export function WalletListScene(props: Props) { const dispatch = useDispatch() const [sorting, setSorting] = React.useState(false) - const [searching, setSearching] = React.useState(false) + const [isSearching, setIsSearching] = React.useState(false) const [searchText, setSearchText] = React.useState('') const needsPasswordCheck = useSelector(state => state.ui.passwordReminder.needsPasswordCheck) @@ -47,13 +48,25 @@ export function WalletListScene(props: Props) { }) const handleRefresh = useHandler(() => { - setSearching(true) + setIsSearching(true) }) - // Turn off searching mode when a wallet is selected - const handlReset = useHandler(() => { + const handleReset = useHandler(() => { setSearchText('') - setSearching(false) + setIsSearching(false) + }) + + const handleStartSearching = useHandler(() => { + setIsSearching(true) + }) + + const handleDoneSearching = useHandler(() => { + setSearchText('') + setIsSearching(false) + }) + + const handleChangeText = useHandler((value: string) => { + setSearchText(value) }) // Show the password reminder on mount if required: @@ -78,19 +91,30 @@ export function WalletListScene(props: Props) { {}} + onChangeSearchingState={() => {}} /> ) - }, [handleSort, navigation, searchText, searching, sorting]) + }, [handleSort, navigation, isSearching, sorting]) const handlePressDone = useHandler(() => setSorting(false)) + const renderDrawer = () => { + return ( + + ) + } + return ( - + {({ insetStyles }) => ( <> @@ -107,13 +131,13 @@ export function WalletListScene(props: Props) { diff --git a/src/components/themed/MenuTabs.tsx b/src/components/themed/MenuTabs.tsx index 73d7776e2dd..d8582235591 100644 --- a/src/components/themed/MenuTabs.tsx +++ b/src/components/themed/MenuTabs.tsx @@ -13,7 +13,7 @@ import { Fontello } from '../../assets/vector/index' import { useHandler } from '../../hooks/useHandler' import { LocaleStringKey } from '../../locales/en_US' import { lstrings } from '../../locales/strings' -import { useDrawerOpenRatio } from '../../state/SceneDrawerState' +import { useDrawerOpenRatio, useLayoutHeightInTabBar } from '../../state/SceneDrawerState' import { config } from '../../theme/appConfig' import { styled } from '../hoc/styled' import { useTheme } from '../services/ThemeContext' @@ -22,6 +22,7 @@ import { VectorIcon } from './VectorIcon' const extraTabString: LocaleStringKey = config.extraTab?.tabTitleKey ?? 'title_map' export const MAX_TAB_BAR_HEIGHT = 92 +export const MIN_TAB_BAR_HEIGHT = 74 const title: { readonly [key: string]: string } = { homeTab: lstrings.title_home, @@ -53,15 +54,15 @@ export const MenuTabs = (props: BottomTabBarProps) => { [state.routes] ) - const insets = useSafeAreaInsets() - const activeTabRoute = state.routes[activeTabFullIndex] const activeTabIndex = routes.findIndex(route => route.name === activeTabRoute.name) - const { drawerOpenRatio, handleDrawerLayout, isRatioDisabled = false, resetDrawerRatio } = useDrawerOpenRatio() + const { drawerOpenRatio, resetDrawerRatio } = useDrawerOpenRatio() + + const handleLayout = useLayoutHeightInTabBar() return ( - + @@ -74,7 +75,6 @@ export const MenuTabs = (props: BottomTabBarProps) => { isActive={activeTabIndex === index} drawerOpenRatio={drawerOpenRatio} resetDrawerRatio={resetDrawerRatio} - isRatioDisabled={isRatioDisabled} /> ))} @@ -83,7 +83,7 @@ export const MenuTabs = (props: BottomTabBarProps) => { ) } -const Container = styled(View)<{ bottom: number; height?: number }>({ +const Container = styled(View)({ position: 'absolute', left: 0, right: 0, @@ -104,15 +104,13 @@ const Tab = ({ drawerOpenRatio, resetDrawerRatio, currentName, - isRatioDisabled, navigation }: { isActive: boolean currentName: string route: BottomTabBarProps['state']['routes'][number] - drawerOpenRatio: SharedValue | undefined + drawerOpenRatio: SharedValue resetDrawerRatio: () => void - isRatioDisabled: boolean navigation: NavigationHelpers }) => { const theme = useTheme() @@ -150,34 +148,25 @@ const Tab = ({ return ( {icon[route.name]} - ) } -const TabContainer = styled(TouchableOpacity)<{ insetBottom: number }>(theme => props => ({ +const TabContainer = styled(TouchableOpacity)<{ insetBottom: number }>(theme => ({ insetBottom }) => ({ flex: 1, paddingTop: theme.rem(0.75), - paddingBottom: Math.max(theme.rem(0.75), props.insetBottom), + paddingBottom: Math.max(theme.rem(0.75), insetBottom), justifyContent: 'center', alignItems: 'center' })) const Label = styled(Animated.Text)<{ isActive: boolean - isRatioDisabled: boolean - openRatio: SharedValue | undefined -}>(theme => ({ isActive, isRatioDisabled, openRatio }) => { + openRatio: SharedValue +}>(theme => ({ isActive, openRatio }) => { const rem = theme.rem(1) return [ { @@ -191,9 +180,10 @@ const Label = styled(Animated.Text)<{ }, useAnimatedStyle(() => { 'worklet' + if (openRatio == null) return {} return { - height: isRatioDisabled ? undefined : openRatio == null ? undefined : rem * openRatio.value, - opacity: isRatioDisabled ? undefined : openRatio == null ? undefined : openRatio.value + height: rem * openRatio.value, + opacity: openRatio.value } }) ] diff --git a/src/components/themed/SceneDrawer.tsx b/src/components/themed/SceneDrawer.tsx new file mode 100644 index 00000000000..6aeb4e38325 --- /dev/null +++ b/src/components/themed/SceneDrawer.tsx @@ -0,0 +1,48 @@ +import React from 'react' +import Animated, { interpolate, SharedValue, useAnimatedStyle } from 'react-native-reanimated' + +import { useDrawerOpenRatio, useLayoutHeightInTabBar } from '../../state/SceneDrawerState' +import { styled } from '../hoc/styled' +import { MAX_TAB_BAR_HEIGHT, MIN_TAB_BAR_HEIGHT } from './MenuTabs' + +export interface SceneDrawerProps { + children: React.ReactNode + isKeyboardOpen: boolean +} + +export const SceneDrawer = (props: SceneDrawerProps) => { + const { children, isKeyboardOpen } = props + const { drawerOpenRatio } = useDrawerOpenRatio() + const handleLayout = useLayoutHeightInTabBar() + + return ( + <> + + {children} + + + ) +} + +const Drawer = styled(Animated.View)<{ + drawerOpenRatio: SharedValue + isKeyboardOpen: boolean +}>(theme => ({ drawerOpenRatio, isKeyboardOpen }) => { + return [ + { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + flexDirection: 'column', + justifyContent: 'flex-start', + alignItems: 'stretch', + overflow: 'hidden' + }, + useAnimatedStyle(() => { + return { + bottom: isKeyboardOpen ? 0 : interpolate(drawerOpenRatio.value, [0, 1], [MIN_TAB_BAR_HEIGHT, MAX_TAB_BAR_HEIGHT]) + } + }) + ] +}) diff --git a/src/components/themed/WalletListHeader.tsx b/src/components/themed/WalletListHeader.tsx index 392e2df7cf6..d8d4c266ae2 100644 --- a/src/components/themed/WalletListHeader.tsx +++ b/src/components/themed/WalletListHeader.tsx @@ -7,16 +7,14 @@ import { lstrings } from '../../locales/strings' import { NavigationBase } from '../../types/routerTypes' import { PromoCard } from '../cards/PromoCard' import { cacheStyles, Theme, ThemeProps, withTheme } from '../services/ThemeContext' -import { EdgeText } from '../themed/EdgeText' import { BalanceCardUi4 } from '../ui4/BalanceCardUi4' import { SectionHeaderUi4 } from '../ui4/SectionHeaderUi4' -import { OutlinedTextInput, OutlinedTextInputRef } from './OutlinedTextInput' +import { OutlinedTextInputRef } from './OutlinedTextInput' interface OwnProps { navigation: NavigationBase sorting: boolean searching: boolean - searchText: string openSortModal: () => void onChangeSearchText: (search: string) => void onChangeSearchingState: (searching: boolean) => void @@ -47,7 +45,7 @@ export class WalletListHeaderComponent extends React.PureComponent { } render() { - const { navigation, sorting, searching, searchText, theme } = this.props + const { navigation, sorting, searching, theme } = this.props const styles = getStyles(theme) const addSortButtons = ( @@ -63,25 +61,6 @@ export class WalletListHeaderComponent extends React.PureComponent { return ( <> - - - - - {searching && ( - - {lstrings.string_done_cap} - - )} - {searching ? null : } {sorting || searching ? null : } @@ -99,18 +78,6 @@ const getStyles = cacheStyles((theme: Theme) => ({ }, addButton: { marginRight: theme.rem(0.5) - }, - - searchContainer: { - flexDirection: 'row', - alignItems: 'center', - marginTop: theme.rem(0.5), - marginHorizontal: theme.rem(0.5) - }, - searchDoneButton: { - justifyContent: 'center', - paddingLeft: theme.rem(0.75), - paddingBottom: theme.rem(1) } })) diff --git a/src/components/themed/WalletListSearch.tsx b/src/components/themed/WalletListSearch.tsx new file mode 100644 index 00000000000..0f0f0c2ca60 --- /dev/null +++ b/src/components/themed/WalletListSearch.tsx @@ -0,0 +1,106 @@ +import React, { useEffect, useState } from 'react' +import { LayoutChangeEvent, StyleSheet } from 'react-native' +import Animated, { SharedValue, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated' +import { BlurView } from 'rn-id-blurview' + +import { useHandler } from '../../hooks/useHandler' +import { lstrings } from '../../locales/strings' +import { useDrawerOpenRatio } from '../../state/SceneDrawerState' +import { styled } from '../hoc/styled' +import { SearchIconAnimated } from '../icons/ThemedIcons' +import { Space } from '../layout/Space' +import { useTheme } from '../services/ThemeContext' +import { SimpleTextInput, SimpleTextInputRef } from './SimpleTextInput' + +interface WalletListSearchProps { + isSearching: boolean + searchText: string + + onChangeText: (value: string) => void + onDoneSearching: () => void + onStartSearching: () => void +} + +export const WalletListSearch = (props: WalletListSearchProps) => { + const { isSearching, searchText, onChangeText, onDoneSearching, onStartSearching } = props + const theme = useTheme() + + const textInputRef = React.useRef(null) + + const { drawerOpenRatio, setKeepOpen } = useDrawerOpenRatio() + const [containerHeight, setContainerHeight] = useState(undefined) + + const inputScale = useDerivedValue(() => drawerOpenRatio.value) + + const handleLayout = useHandler((event: LayoutChangeEvent) => { + if (containerHeight != null) return + setContainerHeight(event.nativeEvent.layout.height) + }) + + const handleSearchChangeText = useHandler((text: string) => { + onChangeText(text) + }) + + const handleSearchBlur = useHandler(() => { + if (searchText === '') { + onDoneSearching() + } + }) + + const handleSearchClear = useHandler(() => { + if (!textInputRef.current?.isFocused()) { + onDoneSearching() + } + }) + + const handleSearchFocus = useHandler(() => { + onStartSearching() + }) + + useEffect(() => { + if (setKeepOpen != null) setKeepOpen(isSearching) + if (isSearching && textInputRef.current) { + textInputRef.current.focus() + } + if (!isSearching && textInputRef.current) { + textInputRef.current.blur() + } + }, [isSearching, setKeepOpen]) + + return ( + <> + + + + + + + + ) +} + +const ContainerAnimatedView = styled(Animated.View)<{ + containerHeight?: number + drawerOpenRatio: SharedValue +}>(() => ({ containerHeight, drawerOpenRatio }) => [ + { + overflow: 'hidden' + }, + useAnimatedStyle(() => { + if (containerHeight == null) return {} + return { + height: containerHeight * drawerOpenRatio.value + } + }) +]) diff --git a/src/state/SceneDrawerState.tsx b/src/state/SceneDrawerState.tsx index b6515cc69af..0f6436abe71 100644 --- a/src/state/SceneDrawerState.tsx +++ b/src/state/SceneDrawerState.tsx @@ -1,4 +1,5 @@ -import { LayoutChangeEvent, Platform } from 'react-native' +import { useEffect } from 'react' +import { Dimensions, LayoutChangeEvent, Platform } from 'react-native' import { runOnJS, useAnimatedReaction, useSharedValue, withTiming } from 'react-native-reanimated' import { withContextProvider } from '../components/hoc/withContextProvider' @@ -8,14 +9,18 @@ import { useState } from '../types/reactHooks' import { makeUseContextValue } from '../util/makeUseContextValue' import { useSceneScrollContext } from './SceneScrollState' +const SCROLL_DISTANCE = Dimensions.get('window').height / 10 + export const [SceneDrawerProvider, SceneDrawerContext] = withContextProvider(() => { - const [isRatioDisabled, setIsRatioDisabled] = useState(false) + const [keepOpen, setKeepOpen] = useState(false) + const [tabDrawerHeight, setTabDrawerHeight] = useState(undefined) return { - drawerHeight: useSharedValue(0), drawerOpenRatio: useSharedValue(1), drawerOpenRatioStart: useSharedValue(1), - isRatioDisabled, - setIsRatioDisabled + keepOpen, + setKeepOpen, + tabDrawerHeight, + setTabDrawerHeight } }) export const useSceneDrawerState = makeUseContextValue(SceneDrawerContext) @@ -25,7 +30,7 @@ export const useDrawerOpenRatio = () => { const scrollYStart = useSharedValue(undefined) const snapTo = useSharedValue(undefined) - const { drawerHeight, drawerOpenRatio, drawerOpenRatioStart, isRatioDisabled, setIsRatioDisabled } = useSceneDrawerState() + const { drawerOpenRatio, drawerOpenRatioStart, keepOpen, setKeepOpen } = useSceneDrawerState() function resetDrawerRatio() { snapTo.value = 1 @@ -77,14 +82,14 @@ export const useDrawerOpenRatio = () => { useAnimatedReaction( () => { - // Drawer height is not ready - if (drawerHeight.value === 0) return drawerOpenRatio.value + // Keep it open when disabled + if (keepOpen) return 1 // Scrolling hasn't started yet if (scrollYStart.value == null) return const scrollYDelta = scrollY.value - scrollYStart.value - const ratioDelta = scrollYDelta / drawerHeight.value / 2 // Constant is to lower jumpy-ness + const ratioDelta = scrollYDelta / SCROLL_DISTANCE // Constant is to lower jumpy-ness return Math.min(1, Math.max(0, drawerOpenRatioStart.value - ratioDelta)) }, @@ -109,31 +114,55 @@ export const useDrawerOpenRatio = () => { snapTo.value = undefined drawerOpenRatio.value = currentValue }, - [] + [keepOpen] ) useAnimatedReaction( - () => snapTo.value, + () => { + // Keep it open when disabled + if (keepOpen) return 1 + + return snapTo.value + }, (currentValue, previousValue) => { if (currentValue === previousValue) return if (currentValue == null) return drawerOpenRatio.value = withTiming(currentValue, { duration: 300 }) - } + }, + [keepOpen] ) - const handleDrawerLayout = useHandler((event: LayoutChangeEvent) => { - // Only handle the initial layout (re-layout is not yet supported): - if (drawerHeight.value !== 0) return - drawerHeight.value = event.nativeEvent.layout.height - }) - return { - drawerHeight, drawerOpenRatio, - isRatioDisabled, - setIsRatioDisabled, - handleDrawerLayout, - resetDrawerRatio + resetDrawerRatio, + + keepOpen, + setKeepOpen } } + +export const useLayoutHeightInTabBar = (): ((event: LayoutChangeEvent) => void) => { + const { setTabDrawerHeight } = useSceneDrawerState() + + const [layoutHeight, setLayoutHeight] = useState(undefined) + + // One-time layout measurement handler: + const handleLayout = useHandler((event: LayoutChangeEvent) => { + if (layoutHeight == null) { + const layoutHeight = event.nativeEvent.layout.height + setLayoutHeight((prev = 0) => prev + layoutHeight) + } + }) + + // Add/subtract container height to the tab-bar height when mounted/unmounted + useEffect(() => { + if (layoutHeight == null) return + setTabDrawerHeight((prev = 0) => prev + layoutHeight) + return () => { + setTabDrawerHeight((prev = 0) => prev - layoutHeight) + } + }, [layoutHeight, setTabDrawerHeight]) + + return handleLayout +}