diff --git a/src/components/coin-icon/ChainImage.tsx b/src/components/coin-icon/ChainImage.tsx index 4db59d9c61f..6c63ddc5890 100644 --- a/src/components/coin-icon/ChainImage.tsx +++ b/src/components/coin-icon/ChainImage.tsx @@ -12,9 +12,17 @@ import AvalancheBadge from '@/assets/badges/avalanche.png'; import BlastBadge from '@/assets/badges/blast.png'; import DegenBadge from '@/assets/badges/degen.png'; import ApechainBadge from '@/assets/badges/apechain.png'; -import FastImage, { Source } from 'react-native-fast-image'; +import FastImage, { FastImageProps, Source } from 'react-native-fast-image'; -export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | undefined; size?: number }) { +export function ChainImage({ + chainId, + size = 20, + style, +}: { + chainId: ChainId | null | undefined; + size?: number; + style?: FastImageProps['style']; +}) { const source = useMemo(() => { switch (chainId) { case ChainId.apechain: @@ -47,6 +55,10 @@ export function ChainImage({ chainId, size = 20 }: { chainId: ChainId | null | u if (!chainId) return null; return ( - + ); } diff --git a/src/languages/en_US.json b/src/languages/en_US.json index 0d2e0fcb380..ad8b7f0aaab 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -2986,6 +2986,7 @@ "farcaster": "Farcaster" }, "sort": { + "sort": "Sort", "volume": "Volume", "market_cap": "Market Cap", "top_gainers": "Top Gainers", diff --git a/src/screens/discover/components/NetworkSwitcher.tsx b/src/screens/discover/components/NetworkSwitcher.tsx index f4f1b6f78eb..10bb5df4eca 100644 --- a/src/screens/discover/components/NetworkSwitcher.tsx +++ b/src/screens/discover/components/NetworkSwitcher.tsx @@ -2,22 +2,28 @@ import { getChainColorWorklet } from '@/__swaps__/utils/swaps'; import { chainsLabel, SUPPORTED_CHAIN_IDS_ALPHABETICAL } from '@/chains'; import { ChainId } from '@/chains/types'; import { AbsolutePortal } from '@/components/AbsolutePortal'; +import { AnimatedBlurView } from '@/components/AnimatedComponents/AnimatedBlurView'; import { ChainImage } from '@/components/coin-icon/ChainImage'; -import { globalColors, Text, useBackgroundColor, useColorMode } from '@/design-system'; +import { DesignSystemProvider, globalColors, Separator, Text, useBackgroundColor, useColorMode } from '@/design-system'; import { useForegroundColor } from '@/design-system/color/useForegroundColor'; -import hiddenTokens from '@/redux/hiddenTokens'; import { createRainbowStore } from '@/state/internal/createRainbowStore'; import { nonceStore } from '@/state/nonces'; - +import { useTheme } from '@/theme'; +import MaskedView from '@react-native-masked-view/masked-view'; import chroma from 'chroma-js'; -import { useReducer, useState } from 'react'; -import React, { StyleSheet, View } from 'react-native'; +import { Component, forwardRef, useReducer, useState } from 'react'; +import React, { Pressable, View, ViewStyle } from 'react-native'; import { Gesture, GestureDetector, GestureStateChangeEvent, TapGestureHandlerEventPayload } from 'react-native-gesture-handler'; +import LinearGradient from 'react-native-linear-gradient'; import Animated, { + BounceIn, FadeIn, FadeOut, + FadeOutUp, LinearTransition, + makeMutable, runOnJS, + SharedValue, SlideInDown, SlideOutDown, useAnimatedReaction, @@ -27,6 +33,7 @@ import Animated, { withSpring, withTiming, } from 'react-native-reanimated'; +import Svg, { Path } from 'react-native-svg'; function getMostUsedChains() { const noncesByAddress = nonceStore.getState().nonces; @@ -45,17 +52,16 @@ function getMostUsedChains() { } const initialPinnedNetworks = getMostUsedChains().slice(0, 5); -const useNetworkSwitcherStore = createRainbowStore<{ pinnedNetworks: ChainId[]; unpinnedNetworks: ChainId[] }>( +const useNetworkSwitcherStore = createRainbowStore<{ pinnedNetworks: ChainId[] }>( (set, get) => ({ pinnedNetworks: initialPinnedNetworks, - unpinnedNetworks: SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !initialPinnedNetworks.includes(chainId)), }) // { // storageKey: 'network-switcher', - // version: 1, + // // version: 0, // } ); -const setNetworkSwitcherState = (s: { pinnedNetworks: ChainId[]; unpinnedNetworks: ChainId[] }) => { +const setNetworkSwitcherState = (s: { pinnedNetworks: ChainId[] }) => { useNetworkSwitcherStore.setState(s); }; @@ -102,24 +108,93 @@ function EditButton({ text, onPress }: { text: string; onPress: VoidFunction }) ); } -function NetworkOption({ - chainId, +function ExpandNetworks({ + hiddenNetworksLength, + isExpanded, + toggleExpanded, +}: { + hiddenNetworksLength: number; + toggleExpanded: (expanded: boolean) => void; + isExpanded: boolean; +}) { + const pressed = useSharedValue(false); + + const tap = Gesture.Tap() + .onBegin(() => { + pressed.value = true; + }) + .onFinalize(() => { + pressed.value = false; + runOnJS(toggleExpanded)(!isExpanded); + }); + + const animatedStyles = useAnimatedStyle(() => ({ + transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], + })); + + return ( + + + {!isExpanded && ( + + + {hiddenNetworksLength} + + + )} + + {isExpanded ? 'Show Less' : 'More Networks'} + + + + {isExpanded ? '􀆇' : '􀆈'} + + + + + ); +} + +function AllNetworksOption({ selected, onPress, }: { - chainId: ChainId; selected: boolean; onPress?: (e: GestureStateChangeEvent) => void; }) { - const name = chainsLabel[chainId]; - if (!name) throw new Error(`No chain name for chainId ${chainId}`); - const { isDarkMode } = useColorMode(); - const surfaceSecondary = useBackgroundColor('fillSecondary'); - const chainColor = chroma(getChainColorWorklet(chainId, true)).alpha(0.16).hex(); - const backgroundColor = selected ? chainColor : isDarkMode ? globalColors.white10 : globalColors.grey20; + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; + + const blue = useForegroundColor('blue'); - const borderColor = selected ? chainColor : '#F5F8FF05'; + const backgroundColor = selected + ? chroma.scale([networkSwitcherBackgroundColor, blue])(0.16).hex() + : isDarkMode + ? globalColors.white10 + : '#F2F3F4'; + const borderColor = selected ? chroma(blue).alpha(0.16).hex() : '#F5F8FF05'; const pressed = useSharedValue(false); @@ -137,14 +212,22 @@ function NetworkOption({ transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], })); + const overlappingBadge = { + borderColor: backgroundColor, + borderWidth: 1.67, + borderRadius: 16, + marginLeft: -9, + width: 16 + 1.67 * 2, + height: 16 + 1.67 * 2, + }; + return ( - + + + + + + - {name} + All Networks ); } -function ExpandNetworks({ - hiddenNetworksLength, - isExpanded, - toggleExpanded, +function NetworkOption({ + chainId, + selected, + onPress, + style, }: { - hiddenNetworksLength: number; - toggleExpanded: (expanded: boolean) => void; - isExpanded: boolean; + chainId: ChainId; + selected: boolean; + onPress?: (e: GestureStateChangeEvent) => void; + style?: ViewStyle; }) { + const name = chainsLabel[chainId]; + if (!name) throw new Error(`: No chain name for chainId ${chainId}`); + + const { isDarkMode } = useColorMode(); + + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const networkSwitcherBackgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; + + const chainColor = getChainColorWorklet(chainId, true); + const backgroundColor = selected + ? chroma.scale([networkSwitcherBackgroundColor, chainColor])(0.16).hex() + : isDarkMode + ? globalColors.white10 + : globalColors.grey20; + + const borderColor = selected ? chroma(chainColor).alpha(0.16).hex() : '#F5F8FF05'; + const pressed = useSharedValue(false); const tap = Gesture.Tap() - .onBegin(() => { + .onBegin(e => { pressed.value = true; + if (onPress) runOnJS(onPress)(e); }) .onFinalize(() => { pressed.value = false; - runOnJS(toggleExpanded)(!isExpanded); - }); + }) + .enabled(!!onPress); const animatedStyles = useAnimatedStyle(() => ({ transform: [{ scale: withTiming(pressed.value ? 0.95 : 1, { duration: 100 }) }], @@ -192,42 +300,27 @@ function ExpandNetworks({ return ( - {!isExpanded && ( - - - {hiddenNetworksLength} - - - )} - - {isExpanded ? 'Show Less' : 'More Networks'} + + + {name} - - - {isExpanded ? '􀆇' : '􀆈'} - - ); @@ -240,8 +333,6 @@ const ITEM_WIDTH = 164.5; const GAP = 12; const HALF_GAP = 6; -const styles = StyleSheet.create({ draggingItem: { zIndex: 2, position: 'absolute', height: ITEM_HEIGHT, width: ITEM_WIDTH } }); - /* - what to do if user pins all networks (where to drag to unpin) @@ -249,6 +340,32 @@ const styles = StyleSheet.create({ draggingItem: { zIndex: 2, position: 'absolut */ +function DraggingItem({ + chainId, + selected, + transform, +}: { + chainId: ChainId | null; + transform: SharedValue; + selected: boolean; +}) { + const draggingStyles = useAnimatedStyle(() => { + if (!transform.value) return { opacity: 0 }; + return { + opacity: 1, + transform: [{ scale: transform.value.scale }], + left: transform.value.x, + top: transform.value.y, + }; + }); + + return ( + + {chainId && } + + ); +} + function NetworksGrid({ editing, selected, @@ -260,9 +377,10 @@ function NetworksGrid({ unselect: (chainId: ChainId) => void; select: (chainId: ChainId) => void; }) { - const networks = useSharedValue(useNetworkSwitcherStore()); - const pinnedNetworks = useDerivedValue(() => networks.value.pinnedNetworks); - const unpinnedNetworks = useDerivedValue(() => networks.value.unpinnedNetworks); + const pinnedNetworks = useSharedValue(useNetworkSwitcherStore(s => s.pinnedNetworks)); + const unpinnedNetworks = useDerivedValue(() => + SUPPORTED_CHAIN_IDS_ALPHABETICAL.filter(chainId => !pinnedNetworks.value.includes(chainId)) + ); const dragging = useSharedValue(null); const draggingTransform = useSharedValue(null); @@ -273,15 +391,21 @@ function NetworksGrid({ // Sync back to store useAnimatedReaction( - () => networks.value, - current => runOnJS(setNetworkSwitcherState)(current) + () => pinnedNetworks.value, + (pinnedNetworks, prev) => { + if (!prev) return; // no need to react on initial value + runOnJS(setNetworkSwitcherState)({ pinnedNetworks }); + } ); // Force rerender when dragging or dropping changes const [, rerender] = useReducer(x => x + 1, 0); useAnimatedReaction( () => [dropping.value, dragging.value], - () => runOnJS(rerender)() + () => { + console.log('force rerendering'); + runOnJS(rerender)(); + } ); const positionIndex = (x: number, y: number) => { @@ -308,17 +432,22 @@ function NetworksGrid({ 'worklet'; const isTargetUnpinned = e.y > unpinnedGridY.value; - if (isTargetUnpinned && pinnedNetworks.value.length === 1) return; + if (!isTargetUnpinned && pinnedNetworks.value.length === 1) return; const index = positionIndex(e.x, e.y); const position = indexPosition(index, isTargetUnpinned); draggingTransform.value = { x: position.x, y: position.y, scale: 1 }; // initial position is the grid slot - draggingTransform.value = withSpring({ x: e.x - ITEM_WIDTH * 0.5, y: e.y - ITEM_HEIGHT * 0.5, scale: 1.05 }); // animate into the center of the pointer const targetArray = isTargetUnpinned ? unpinnedNetworks.value : pinnedNetworks.value; const chainId = targetArray[index]; dragging.value = chainId; + + draggingTransform.value = withSpring({ + x: e.x - ITEM_WIDTH * 0.5, + y: e.y - ITEM_HEIGHT * 0.5, + scale: 1.05, + }); // animate into the center of the pointer }) .onChange(e => { 'worklet'; @@ -334,37 +463,51 @@ function NetworksGrid({ const isDraggingOverUnpinned = e.y > unpinnedGridY.value; - const targetArrayKey = isDraggingOverUnpinned ? 'unpinnedNetworks' : 'pinnedNetworks'; - const otherArrayKey = isDraggingOverUnpinned ? 'pinnedNetworks' : 'unpinnedNetworks'; + const currentIndexAtPinned = pinnedNetworks.value.indexOf(chainId); + const isPinned = currentIndexAtPinned !== -1; - const targetArray = networks.value[targetArrayKey]; - const targetIndex = Math.min(positionIndex(e.x, e.y), targetArray.length - 1); - const indexInTarget = targetArray.indexOf(chainId); + // We don't reorder unpinned networks + if (isDraggingOverUnpinned && !isPinned) return; - if (indexInTarget === -1) { - networks.modify(v => { - // Pin/Unpin - v[otherArrayKey] = v[otherArrayKey].filter(id => id !== chainId); - v[targetArrayKey].splice(targetIndex, 0, chainId); - return v; + // Unpin + if (isDraggingOverUnpinned && isPinned) { + pinnedNetworks.modify(networks => { + networks.splice(currentIndexAtPinned, 1); + return networks; }); - } else if (indexInTarget !== targetIndex) { - // Reorder - networks.modify(v => { - const [movedChainId] = v[targetArrayKey].splice(indexInTarget, 1); - v[targetArrayKey].splice(targetIndex, 0, movedChainId); - return v; + return; + } + + // Pin + if (!isDraggingOverUnpinned && !isPinned) { + pinnedNetworks.modify(networks => { + networks.push(chainId); + return networks; + }); + return; + } + + // Reorder + const newIndex = Math.min(positionIndex(e.x, e.y), pinnedNetworks.value.length - 1); + if (newIndex !== currentIndexAtPinned) { + pinnedNetworks.modify(networks => { + networks.splice(currentIndexAtPinned, 1); + networks.splice(newIndex, 0, chainId); + return networks; }); } }) .onFinalize(e => { 'worklet'; - if (!dragging.value) return; + const chainId = dragging.value; + if (!chainId) return; const isDroppingInUnpinned = e.y > unpinnedGridY.value; - const targetArray = isDroppingInUnpinned ? pinnedNetworks.value : unpinnedNetworks.value; - const index = Math.min(positionIndex(e.x, e.y), targetArray.length - 1); + const index = isDroppingInUnpinned + ? unpinnedNetworks.value.indexOf(chainId) + : Math.min(positionIndex(e.x, e.y), pinnedNetworks.value.length - 1); + const { x, y } = indexPosition(index, isDroppingInUnpinned); droppingTransform.value = draggingTransform.value; @@ -380,32 +523,14 @@ function NetworksGrid({ () => unpinnedGridY.value, (newY, prevY) => { // the layout can recalculate after the drop started - if (!prevY || !droppingTransform.value || droppingTransform.value.y < prevY) return; - const { x, y, scale } = droppingTransform.value; - droppingTransform.value = withSpring({ x, y: y + (prevY - newY), scale }, { mass: 0.6 }, completed => { + if (!prevY || !droppingTransform.value) return; + const { x, y } = droppingTransform.value; // TODO: does not work + droppingTransform.value = withSpring({ x, y: y + (prevY - newY), scale: 1 }, { mass: 0.6 }, completed => { if (completed) dropping.value = null; }); } ); - const draggingStyles = useAnimatedStyle(() => { - if (!draggingTransform.value) return {}; - return { - transform: [{ scale: draggingTransform.value.scale }], - left: draggingTransform.value.x, - top: draggingTransform.value.y, - }; - }); - - const droppingStyles = useAnimatedStyle(() => { - if (!droppingTransform.value) return {}; - return { - transform: [{ scale: droppingTransform.value.scale }], - left: droppingTransform.value.x, - top: droppingTransform.value.y, - }; - }); - const [isExpanded, setExpanded] = useState(false); const toggleSelected = (chainId: ChainId) => { @@ -413,30 +538,31 @@ function NetworksGrid({ else select(chainId); }; - // const mainGridNetworks = editing - // ? pinnedNetworks.value - // : [...pinnedNetworks.value, ...unpinnedNetworks.value.filter(chainId => selected.includes(chainId))]; + const pinnedGrid = editing + ? pinnedNetworks.value + : [...pinnedNetworks.value, ...unpinnedNetworks.value.filter(chainId => selected.includes(chainId))]; + + const unpinnedGrid = editing ? unpinnedNetworks.value : unpinnedNetworks.value.filter(chainId => !selected.includes(chainId)); return ( - {!!dropping.value && ( - - - - )} - - {!!dragging.value && ( - - - - )} + + - {pinnedNetworks.value.map(chainId => + {pinnedGrid.map(chainId => chainId === dragging.value || chainId === dropping.value ? ( ) : ( @@ -465,17 +591,16 @@ function NetworksGrid({ onLayout={e => (unpinnedGridY.value = e.nativeEvent.layout.y)} layout={LinearTransition} entering={FadeIn.duration(250).delay(125)} - exiting={FadeOut.duration(50)} style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap', paddingVertical: 12 }} > - {unpinnedNetworks.value.length === 0 && ( + {unpinnedGrid.length === 0 && ( Drag here to unpin networks )} - {unpinnedNetworks.value.map(chainId => + {unpinnedGrid.map(chainId => chainId === dragging.value || chainId === dropping.value ? ( ) : ( @@ -494,12 +619,115 @@ function NetworksGrid({ ); } +const useCustomizeNetworksBanner = createRainbowStore<{ + dismissedAt: number; // timestamp +}>(() => ({ dismissedAt: 0 }), { + storageKey: 'CustomizeNetworksBanner', + version: 0, +}); +const twoWeeks = 1000 * 60 * 60 * 24 * 7 * 2; +const dismissCustomizeNetworksBanner = () => { + const { dismissedAt } = useCustomizeNetworksBanner.getState(); + if (Date.now() - dismissedAt < twoWeeks) return; + useCustomizeNetworksBanner.setState({ dismissedAt: Date.now() }); +}; + +function CustomizeNetworksBanner() { + const blue = '#268FFF'; + + const dismissedAt = useCustomizeNetworksBanner(s => s.dismissedAt); + const isOpen = Date.now() - dismissedAt > twoWeeks; + + if (!isOpen) return null; + + return ( + + + + + + } + > + + + + + 􀍱 + + + + + Customize Your Networks + + + Tap the{' '} + + Edit + {' '} + button below to set up + + + + + 􀆄 + + + + + + + + ); +} + export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: VoidFunction; onSelect: VoidFunction; multiple?: boolean }) { - const backgroundColor = '#191A1C'; + const { isDarkMode } = useTheme(); + const surfacePrimary = useBackgroundColor('surfacePrimary'); + const backgroundColor = isDarkMode ? '#191A1C' : surfacePrimary; const separatorSecondary = useForegroundColor('separatorSecondary'); const separatorTertiary = useForegroundColor('separatorTertiary'); const fill = useForegroundColor('fill'); + console.log('rerender NetworkSelector'); + const translationY = useSharedValue(0); const swipeToClose = Gesture.Pan() @@ -518,9 +746,17 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void transform: [{ translateY: translationY.value }], })); - const [selected, setSelected] = useState([]); - const unselect = (chainId: ChainId) => setSelected(s => s.filter(id => id !== chainId)); - const select = (chainId: ChainId) => setSelected(s => (multiple ? [...s, chainId] : [chainId])); + const [selected, setSelected] = useState([]); + const unselect = (chainId: ChainId) => + setSelected(s => { + if (s === 'all') return []; + return s.filter(id => id !== chainId); + }); + const select = (chainId: ChainId) => + setSelected(s => { + if (s === 'all') return [chainId]; + return multiple ? [...s, chainId] : [chainId]; + }); return ( @@ -553,6 +789,7 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void animatedStyles, ]} > + @@ -564,12 +801,25 @@ export function NetworkSelector({ onClose, onSelect, multiple }: { onClose: Void {isEditing ? 'Edit' : 'Network'} - setEditing(s => !s)} /> + { + dismissCustomizeNetworksBanner(); + setEditing(s => !s); + }} + /> - + {multiple && !isEditing && ( + <> + setSelected('all')} /> + + + )} + + diff --git a/src/screens/discover/components/TrendingTokens.tsx b/src/screens/discover/components/TrendingTokens.tsx index a92fdbd012c..d9f5d38bb52 100644 --- a/src/screens/discover/components/TrendingTokens.tsx +++ b/src/screens/discover/components/TrendingTokens.tsx @@ -13,8 +13,7 @@ import LinearGradient from 'react-native-linear-gradient'; import Animated, { LinearTransition, runOnJS, useAnimatedStyle, useSharedValue, withTiming } from 'react-native-reanimated'; import { NetworkSelector } from './NetworkSwitcher'; import * as i18n from '@/languages'; - -const TRANSLATIONS = i18n.l.cards.ens_search; +import { useTheme } from '@/theme'; const AnimatedLinearGradient = Animated.createAnimatedComponent(LinearGradient); @@ -84,8 +83,11 @@ function CategoryFilterButton({ iconWidth?: number; label: string; }) { - const backgroundColor = useBackgroundColor('fillTertiary'); - const borderColor = useBackgroundColor('fillSecondary'); + const { isDarkMode } = useTheme(); + const fillTertiary = useBackgroundColor('fillTertiary'); + const fillSecondary = useBackgroundColor('fillSecondary'); + + const borderColor = selected && isDarkMode ? globalColors.white80 : fillSecondary; const pressed = useSharedValue(false); @@ -103,7 +105,7 @@ function CategoryFilterButton({ return ( - + @@ -258,7 +277,9 @@ function TrendingTokenRow() { const t = i18n.l.trending_tokens; function NoResults() { - const backgroundColor = '#191A1C'; // useBackgroundColor('fillQuaternary'); + const { isDarkMode } = useTheme(); + const fillQuaternary = useBackgroundColor('fillQuaternary'); + const backgroundColor = isDarkMode ? '#191A1C' : fillQuaternary; return ( @@ -349,7 +370,7 @@ export function TrendingTokens() { side="bottom" onPressMenuItem={timeframe => setFilter(filter => ({ ...filter, timeframe }))} > - + - +