From d90e32a71b6e185ec6829dfbcf071f47a6ba6003 Mon Sep 17 00:00:00 2001 From: Matthew Wall Date: Fri, 20 Dec 2024 07:01:18 -0500 Subject: [PATCH] Fix selected state for network, timeframe, sort (#6352) --- src/components/Discover/DiscoverHome.tsx | 3 +- src/components/Discover/TrendingTokens.tsx | 283 +++++++++++------- .../Discover/useTrackDiscoverScreenTime.ts | 6 +- src/components/NetworkSwitcher.tsx | 150 +++++++--- src/components/coin-icon/ChainImage.tsx | 2 - src/languages/en_US.json | 12 +- 6 files changed, 287 insertions(+), 169 deletions(-) diff --git a/src/components/Discover/DiscoverHome.tsx b/src/components/Discover/DiscoverHome.tsx index 019328a92b6..8da0ba974af 100644 --- a/src/components/Discover/DiscoverHome.tsx +++ b/src/components/Discover/DiscoverHome.tsx @@ -70,10 +70,11 @@ export default function DiscoverHome() { {isProfilesEnabled && } + {trendingTokensEnabled && ( <> - + )} diff --git a/src/components/Discover/TrendingTokens.tsx b/src/components/Discover/TrendingTokens.tsx index 0c3e0fba658..3a14f94011a 100644 --- a/src/components/Discover/TrendingTokens.tsx +++ b/src/components/Discover/TrendingTokens.tsx @@ -1,5 +1,5 @@ import { DropdownMenu } from '@/components/DropdownMenu'; -import { globalColors, Text, useBackgroundColor } from '@/design-system'; +import { globalColors, Text, TextIcon, useBackgroundColor, useColorMode } from '@/design-system'; import { useForegroundColor } from '@/design-system/color/useForegroundColor'; import { SwapCoinIcon } from '@/__swaps__/screens/Swap/components/SwapCoinIcon'; @@ -8,7 +8,7 @@ import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks import { ChainId } from '@/state/backendNetworks/types'; import { ChainImage } from '@/components/coin-icon/ChainImage'; import Skeleton, { FakeAvatar, FakeText } from '@/components/skeleton/Skeleton'; -import { SortDirection, TrendingCategory, TrendingSort } from '@/graphql/__generated__/arc'; +import { SortDirection, Timeframe, TrendingCategory, TrendingSort } from '@/graphql/__generated__/arc'; import { formatCurrency, formatNumber } from '@/helpers/strings'; import * as i18n from '@/languages'; import { Navigation } from '@/navigation'; @@ -22,71 +22,95 @@ import { darkModeThemeColors } from '@/styles/colors'; import { useTheme } from '@/theme'; import { useCallback, useEffect, useMemo } from 'react'; import React, { Dimensions, FlatList, View } from 'react-native'; -import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; -import Animated, { runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; +import Animated, { runOnJS, useSharedValue } from 'react-native-reanimated'; import { ButtonPressAnimation } from '../animations'; import { useFarcasterAccountForWallets } from '@/hooks/useFarcasterAccountForWallets'; import { ImgixImage } from '../images'; import { useRemoteConfig } from '@/model/remoteConfig'; import { useAccountSettings } from '@/hooks'; +import { getColorWorklet, getMixedColor, opacity } from '@/__swaps__/utils/swaps'; +import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; +import { IS_IOS } from '@/env'; const t = i18n.l.trending_tokens; -const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient); - -function FilterButton({ icon, label, onPress }: { onPress?: VoidFunction; label: string; icon: string | JSX.Element }) { - const pressed = useSharedValue(false); - - const tap = Gesture.Tap() - .onBegin(() => { - pressed.value = true; - if (onPress) runOnJS(onPress)(); - }) - .onFinalize(() => (pressed.value = false)); - - const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], - })); - - const backgroundColor = useBackgroundColor('fillTertiary'); - const borderColor = useBackgroundColor('fillSecondary'); - - const iconColor = useForegroundColor('labelQuaternary'); +function FilterButton({ + icon, + label, + onPress, + selected, + iconColor, + highlightedBackgroundColor, +}: { + onPress?: VoidFunction; + label: string; + icon: string | JSX.Element; + selected: boolean; + iconColor?: string; + highlightedBackgroundColor?: string; +}) { + const { isDarkMode } = useColorMode(); + const fillTertiary = useBackgroundColor('fillTertiary'); + const separatorSecondary = useForegroundColor('separatorSecondary'); + const borderColor = selected && isDarkMode ? globalColors.white80 : separatorSecondary; + const defaultIconColor = getColorWorklet('labelSecondary', selected ? false : isDarkMode); + + const gradientColors = useMemo(() => { + if (!selected) return [fillTertiary, fillTertiary]; + return highlightedBackgroundColor + ? [highlightedBackgroundColor, globalColors.white100] + : [ + isDarkMode ? opacity(globalColors.white100, 0.72) : opacity(fillTertiary, 0.2), + isDarkMode ? opacity(globalColors.white100, 0.92) : opacity(fillTertiary, 0), + ]; + }, [fillTertiary, highlightedBackgroundColor, selected, isDarkMode]); return ( - - + {typeof icon === 'string' ? ( - + {icon} - + ) : ( icon )} - - {label} - - + + {/* This first Text element sets the width of the container */} + + {label} + + {/* This second Text element is the visible label, positioned absolutely within the established frame */} + + {label} + + + 􀆏 - - + + ); } @@ -146,69 +170,70 @@ function CategoryFilterButton({ }: { category: TrendingCategory; icon: string; - iconColor: string; + iconColor: string | { default: string; selected: string }; highlightedBackgroundColor: string; iconWidth?: number; label: string; }) { - const { isDarkMode } = useTheme(); + const { isDarkMode } = useColorMode(); const fillTertiary = useBackgroundColor('fillTertiary'); - const fillSecondary = useBackgroundColor('fillSecondary'); + const separatorSecondary = useForegroundColor('separatorSecondary'); const selected = useTrendingTokensStore(state => state.category === category); - const borderColor = selected && isDarkMode ? globalColors.white80 : fillSecondary; + const borderColor = selected && isDarkMode ? globalColors.white80 : separatorSecondary; - const pressed = useSharedValue(false); + const gradientColors = useMemo(() => { + if (!selected) return [fillTertiary, fillTertiary]; + return [highlightedBackgroundColor, globalColors.white100]; + }, [fillTertiary, highlightedBackgroundColor, selected]); const selectCategory = useCallback(() => { useTrendingTokensStore.getState().setCategory(category); }, [category]); - const tap = Gesture.Tap() - .onBegin(() => { - pressed.value = true; - }) - .onEnd(() => { - pressed.value = false; - runOnJS(selectCategory)(); - }); - - const animatedStyles = useAnimatedStyle(() => ({ - transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], - })); - return ( - - + - - {icon} - - - {label} - - - + {icon} + + + {/* This first Text element sets the width of the container */} + + {label} + + {/* This second Text element is the visible label, positioned absolutely within the established frame */} + + {label} + + + + ); } @@ -266,7 +291,7 @@ function FriendHolders({ friends }: { friends: FarcasterUser[] }) { function TrendingTokenLoadingRow() { const backgroundColor = useBackgroundColor('surfacePrimary'); - const { isDarkMode } = useTheme(); + const { isDarkMode } = useColorMode(); return ( @@ -464,7 +489,7 @@ function TrendingTokenRow({ token }: { token: TrendingToken }) { } function NoResults() { - const { isDarkMode } = useTheme(); + const { isDarkMode } = useColorMode(); const fillQuaternary = useBackgroundColor('fillQuaternary'); const backgroundColor = isDarkMode ? '#191A1C' : fillQuaternary; @@ -488,12 +513,27 @@ function NoResults() { } function NetworkFilter() { - const selected = useSharedValue(undefined); + const { isDarkMode } = useColorMode(); + const { colors } = useTheme(); - const { chainId, setChainId } = useTrendingTokensStore(state => ({ - chainId: state.chainId, - setChainId: state.setChainId, - })); + const selected = useSharedValue(undefined); + const chainId = useTrendingTokensStore(state => state.chainId); + const setChainId = useTrendingTokensStore(state => state.setChainId); + + const { icon, label, lightenedNetworkColor } = useMemo(() => { + if (!chainId) return { icon: '􀤆', label: i18n.t(t.all), lightenedNetworkColor: undefined }; + return { + icon: ( + + + + ), + label: useBackendNetworksStore.getState().getChainsLabel()[chainId], + lightenedNetworkColor: colors.networkColors[chainId] + ? getMixedColor(colors.networkColors[chainId], globalColors.white100, isDarkMode ? 0.55 : 0.6) + : undefined, + }; + }, [chainId, colors.networkColors, isDarkMode]); const setSelected = useCallback( (chainId: ChainId | undefined) => { @@ -504,13 +544,6 @@ function NetworkFilter() { [selected, setChainId] ); - const label = !chainId ? i18n.t(t.all) : useBackendNetworksStore.getState().getChainsLabel()[chainId]; - - const icon = useMemo(() => { - if (!chainId) return '􀤆'; - return ; - }, [chainId]); - const navigateToNetworkSelector = useCallback(() => { Navigation.handleAction(Routes.NETWORK_SELECTOR, { selected, @@ -518,11 +551,20 @@ function NetworkFilter() { }); }, [selected, setSelected]); - return ; + return ( + + ); } function TimeFilter() { const timeframe = useTrendingTokensStore(state => state.timeframe); + const shouldAbbreviate = timeframe === Timeframe.H24; return ( useTrendingTokensStore.getState().setTimeframe(timeframe)} > - + ); } function SortFilter() { const sort = useTrendingTokensStore(state => state.sort); + const selected = sort !== TrendingSort.Recommended; - const iconColor = useForegroundColor('labelQuaternary'); + const iconColor = useForegroundColor(selected ? 'labelSecondary' : 'labelTertiary'); return ( @@ -591,8 +643,8 @@ function TrendingTokenData() { return ( } data={trendingTokens} renderItem={({ item }) => } @@ -603,6 +655,7 @@ function TrendingTokenData() { const padding = 20; export function TrendingTokens() { + const { isDarkMode } = useColorMode(); return ( @@ -623,8 +676,8 @@ export function TrendingTokens() { category={TrendingCategory.New} label={i18n.t(t.filters.categories.NEW)} icon="􀋃" - iconColor={'#FFDA24'} - highlightedBackgroundColor={'#F9EAA1'} + iconColor={{ default: isDarkMode ? globalColors.yellow60 : '#FFBB00', selected: '#F5A200' }} + highlightedBackgroundColor={'#FFEAC2'} iconWidth={18} /> { - const activeSwipeRoute = useNavigationStore(state => state.activeSwipeRoute); + const isOnDiscoverScreen = useNavigationStore(state => state.isRouteActive(Routes.DISCOVER_SCREEN)); + useEffect(() => { - const isOnDiscoverScreen = activeSwipeRoute === Routes.DISCOVER_SCREEN; const data = currentlyTrackedMetrics.get(PerformanceMetrics.timeSpentOnDiscoverScreen); if (!isOnDiscoverScreen && data?.startTimestamp) { @@ -17,5 +17,5 @@ export const useTrackDiscoverScreenTime = () => { if (isOnDiscoverScreen) { PerformanceTracking.startMeasuring(PerformanceMetrics.timeSpentOnDiscoverScreen); } - }, [activeSwipeRoute]); + }, [isOnDiscoverScreen]); }; diff --git a/src/components/NetworkSwitcher.tsx b/src/components/NetworkSwitcher.tsx index 50c4e13d639..65a66961d7a 100644 --- a/src/components/NetworkSwitcher.tsx +++ b/src/components/NetworkSwitcher.tsx @@ -1,15 +1,14 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ -import { getChainColorWorklet } from '@/__swaps__/utils/swaps'; +import { getChainColorWorklet, opacity } from '@/__swaps__/utils/swaps'; import { useBackendNetworksStore } from '@/state/backendNetworks/backendNetworks'; import { ChainId } from '@/state/backendNetworks/types'; import { AnimatedBlurView } from '@/components/AnimatedComponents/AnimatedBlurView'; import { ButtonPressAnimation } from '@/components/animations'; import { SPRING_CONFIGS, TIMING_CONFIGS } from '@/components/animations/animationConfigs'; -import { AnimatedChainImage, ChainImage } from '@/components/coin-icon/ChainImage'; +import { ChainImage } from '@/components/coin-icon/ChainImage'; import { AnimatedText, Box, DesignSystemProvider, globalColors, Separator, Text, useBackgroundColor, useColorMode } from '@/design-system'; import { useForegroundColor } from '@/design-system/color/useForegroundColor'; import * as i18n from '@/languages'; -import { useTheme } from '@/theme'; import deviceUtils, { DEVICE_WIDTH } from '@/utils/deviceUtils'; import MaskedView from '@react-native-masked-view/masked-view'; import chroma from 'chroma-js'; @@ -19,6 +18,7 @@ import { RouteProp, useRoute } from '@react-navigation/native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import LinearGradient from 'react-native-linear-gradient'; import Animated, { + Easing, FadeIn, FadeOutUp, LinearTransition, @@ -46,6 +46,8 @@ import { IS_IOS } from '@/env'; import { safeAreaInsetValues } from '@/utils'; import { noop } from 'lodash'; import { TapToDismiss } from './DappBrowser/control-panel/ControlPanel'; +import { THICK_BORDER_WIDTH } from '@/__swaps__/screens/Swap/constants'; +import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton'; const t = i18n.l.network_switcher; @@ -72,11 +74,18 @@ function EditButton({ editing }: { editing: SharedValue }) { editing.value = !editing.value; }} scaleTo={0.95} - style={[ - { position: 'absolute', right: 0 }, - { paddingHorizontal: 10, height: 28, justifyContent: 'center' }, - { borderColor, borderWidth: 1.33, borderRadius: 14 }, - ]} + style={{ + borderColor, + borderCurve: 'continuous', + borderRadius: 14, + borderWidth: THICK_BORDER_WIDTH, + height: 28, + justifyContent: 'center', + overflow: 'hidden', + paddingHorizontal: 10, + position: 'absolute', + right: 0, + }} > {text} @@ -96,7 +105,17 @@ function Header({ editing }: { editing: SharedValue }) { return ( - + @@ -199,16 +218,28 @@ const CustomizeNetworksBanner = !shouldShowCustomizeNetworksBanner(customizeNetw ); }; +const BADGE_BORDER_COLORS = { + default: { + dark: globalColors.white10, + light: '#F2F3F4', + }, + selected: { + dark: '#1E2E40', + light: '#D7E9FD', + }, +}; + const useNetworkOptionStyle = (isSelected: SharedValue, color?: string) => { const { isDarkMode } = useColorMode(); const label = useForegroundColor('labelTertiary'); const surfacePrimary = useBackgroundColor('surfacePrimary'); const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; + const separatorTertiary = useForegroundColor('separatorTertiary'); const defaultStyle = { backgroundColor: isDarkMode ? globalColors.white10 : globalColors.grey20, - borderColor: '#F5F8FF05', + borderColor: isDarkMode ? opacity(separatorTertiary, 0.02) : separatorTertiary, }; const selectedStyle = { backgroundColor: chroma @@ -222,9 +253,12 @@ const useNetworkOptionStyle = (isSelected: SharedValue, color?: string) const scale = useSharedValue(1); useAnimatedReaction( () => isSelected.value, - current => { - if (current === true) { - scale.value = withSequence(withTiming(0.95, { duration: 50 }), withTiming(1, { duration: 80 })); + (current, prev) => { + if (current === true && prev === false) { + scale.value = withSequence( + withTiming(0.9, { duration: 120, easing: Easing.bezier(0.25, 0.46, 0.45, 0.94) }), + withTiming(1, TIMING_CONFIGS.fadeConfig) + ); } } ); @@ -252,29 +286,28 @@ function AllNetworksOption({ selected: SharedValue; setSelected: (chainId: ChainId | undefined) => void; }) { + const { isDarkMode } = useColorMode(); const blue = useForegroundColor('blue'); const isSelected = useDerivedValue(() => selected.value === undefined); - const { animatedStyle, selectedStyle, defaultStyle } = useNetworkOptionStyle(isSelected, blue); + const { animatedStyle } = useNetworkOptionStyle(isSelected, blue); const overlappingBadge = useAnimatedStyle(() => { return { - borderColor: isSelected.value ? selectedStyle.backgroundColor : defaultStyle.backgroundColor, - borderWidth: 1.67, - borderRadius: 16, - marginLeft: -9, - width: 16 + 1.67 * 2, // size + borders - height: 16 + 1.67 * 2, + borderColor: isSelected.value + ? BADGE_BORDER_COLORS.selected[isDarkMode ? 'dark' : 'light'] + : BADGE_BORDER_COLORS.default[isDarkMode ? 'dark' : 'light'], }; }); - const tapGesture = Gesture.Tap().onTouchesDown(() => { - 'worklet'; - setSelected(undefined); - }); - return ( - + { + 'worklet'; + setSelected(undefined); + }} + scaleTo={0.95} + > - - - - - + + + + + + + + + + + + + {i18n.t(t.all_networks)} - + ); } @@ -338,9 +381,17 @@ function NetworkOption({ chainId, selected }: { chainId: ChainId; selected: Shar @@ -358,6 +409,10 @@ const GAP = 12; const ITEM_WIDTH = (DEVICE_WIDTH - SHEET_INNER_PADDING * 2 - SHEET_OUTER_INSET * 2 - GAP) / 2; const ITEM_HEIGHT = 48; const SEPARATOR_HEIGHT = 68; + +const ALL_NETWORKS_BADGE_SIZE = 16; +const THICKER_BORDER_WIDTH = 5 / 3; + const enum Section { pinned, separator, @@ -482,7 +537,7 @@ function SectionSeparator({ const showMoreOrLessIcon = useDerivedValue(() => (expanded.value ? '􀆇' : '􀆈') as string); const showMoreOrLessIconStyle = useAnimatedStyle(() => ({ opacity: editing.value ? 0 : 1 })); - const { isDarkMode } = useTheme(); + const { isDarkMode } = useColorMode(); const separatorContainerStyles = useAnimatedStyle(() => { if (showExpandButtonAsNetworkChip.value) { @@ -495,7 +550,7 @@ function SectionSeparator({ flexDirection: 'row', alignItems: 'center', borderRadius: 24, - borderWidth: 1.33, + borderWidth: THICK_BORDER_WIDTH, transform: [{ translateX: position.x }, { translateY: position.y }], }; } @@ -559,13 +614,13 @@ function EmptyUnpinnedPlaceholder({ transform: [{ translateY: sectionsOffsets.value[Section.unpinned].y }], }; }); - const { isDarkMode } = useTheme(); + const { isDarkMode } = useColorMode(); return ( ; onClose: VoidFunction }>) { - const { isDarkMode } = useTheme(); + const { isDarkMode } = useColorMode(); const surfacePrimary = useBackgroundColor('surfacePrimary'); const backgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; const separatorSecondary = useForegroundColor('separatorSecondary'); @@ -760,7 +815,7 @@ function Sheet({ children, editing, onClose }: PropsWithChildren<{ editing: Shar sx.sheet, { backgroundColor, - borderColor: separatorSecondary, + borderColor: isDarkMode ? separatorSecondary : globalColors.white100, }, ]} > @@ -789,6 +844,13 @@ export function NetworkSelector() { } const sx = StyleSheet.create({ + overlappingBadge: { + borderWidth: THICKER_BORDER_WIDTH, + borderRadius: ALL_NETWORKS_BADGE_SIZE, + marginLeft: -9, + width: ALL_NETWORKS_BADGE_SIZE + THICKER_BORDER_WIDTH * 2, + height: ALL_NETWORKS_BADGE_SIZE + THICKER_BORDER_WIDTH * 2, + }, sheet: { flex: 1, width: deviceUtils.dimensions.width - 16, @@ -799,7 +861,9 @@ const sx = StyleSheet.create({ left: 8, right: 8, paddingHorizontal: 16, + borderCurve: 'continuous', borderRadius: 42, - borderWidth: 1.33, + borderWidth: THICK_BORDER_WIDTH, + overflow: 'hidden', }, }); diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx index 671296c8445..90f8ffbc960 100644 --- a/src/components/coin-icon/ChainImage.tsx +++ b/src/components/coin-icon/ChainImage.tsx @@ -89,5 +89,3 @@ export const ChainImage = forwardRef(function ChainImage( /> ); }); - -export const AnimatedChainImage = Animated.createAnimatedComponent(ChainImage); diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 4bfe7a65a0d..040e9da9ab7 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -3043,8 +3043,10 @@ "TOP_LOSERS": "Top Losers" }, "time": { - "H12": "12h", - "H24": "24h", + "H12": "12 Hours", + "H12_ABBREVIATED": "12h", + "H24": "24 Hours", + "H24_ABBREVIATED": "24h", "D7": "1 Week", "D3": "1 Month" } @@ -3056,11 +3058,11 @@ "tap_the": "Tap the", "button_to_set_up": "button below to set up" }, - "drag_here_to_unpin": "Drag here to unpin networks", + "drag_here_to_unpin": "Drop Here to Unpin", "edit": "Edit", "networks": "Networks", - "drag_to_rearrange": "Drag to rearrange", - "show_less": "Show less", + "drag_to_rearrange": "Drag to Rearrange", + "show_less": "Show Less", "more": "More", "show_more": "More Networks", "all_networks": "All Networks"