Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Swaps: token list and search improvements #5834

Merged
merged 18 commits into from
Jun 12, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/__swaps__/screens/Swap/Swap.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect } from 'react';
import React, { useCallback, useEffect, useMemo } from 'react';
import { StyleSheet, StatusBar } from 'react-native';
import Animated, { runOnJS, useAnimatedReaction } from 'react-native-reanimated';
import { ScreenCornerRadius } from 'react-native-screen-corner-radius';
Expand Down Expand Up @@ -165,8 +165,15 @@ const WalletAddressObserver = () => {
return null;
};

const areBothAssetsPrefilled = () => {
const { inputAsset, outputAsset } = useSwapsStore.getState();
return !!inputAsset && !!outputAsset;
};

const SliderAndKeyboardAndBottomControls = () => {
const shouldMount = useDelayedMount();
const skipDelayedMount = useMemo(() => areBothAssetsPrefilled(), []);
const shouldMount = useDelayedMount({ skipDelayedMount });

const { AnimatedSwapStyles } = useSwapContext();

return shouldMount ? (
Expand Down
73 changes: 31 additions & 42 deletions src/__swaps__/screens/Swap/components/CoinRow.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
import React, { useCallback, useMemo } from 'react';
import React, { memo, useCallback, useMemo } from 'react';
import { ButtonPressAnimation } from '@/components/animations';
import { Box, Column, Columns, HitSlop, Inline, Text } from '@/design-system';
import { TextColor } from '@/design-system/color/palettes';
import { CoinRowButton } from '@/__swaps__/screens/Swap/components/CoinRowButton';
import { BalancePill } from '@/__swaps__/screens/Swap/components/BalancePill';
import { toggleFavorite, useFavorites } from '@/resources/favorites';
import { StyleSheet } from 'react-native';
import { toggleFavorite } from '@/resources/favorites';
import { SwapCoinIcon } from './SwapCoinIcon';
import { ethereumUtils, haptics, showActionSheetWithOptions } from '@/utils';
import { ContextMenuButton, OnPressMenuItemEventObject } from 'react-native-ios-context-menu';
Expand All @@ -15,59 +13,56 @@ import { setClipboard } from '@/hooks/useClipboard';
import { RainbowNetworks } from '@/networks';
import * as i18n from '@/languages';
import { ETH_ADDRESS } from '@/references';
import { trimTrailingZeros } from '@/__swaps__/utils/swaps';
import { ParsedSearchAsset } from '@/__swaps__/types/assets';
import { userAssetsStore } from '@/state/assets/userAssets';
import { SearchAsset } from '@/__swaps__/types/search';
import { ChainId } from '@/__swaps__/types/chains';

export const COIN_ROW_WITH_PADDING_HEIGHT = 56;

christianbaroni marked this conversation as resolved.
Show resolved Hide resolved
interface InputCoinRowProps {
isFavorite?: boolean;
isTrending?: boolean;
nativePriceChange?: string;
onPress: (asset: ParsedSearchAsset | null) => void;
output?: false | undefined;
uniqueId: string;
}

type PartialAsset = Pick<SearchAsset, 'address' | 'mainnetAddress' | 'chainId' | 'icon_url' | 'name' | 'colors' | 'symbol' | 'uniqueId'>;
type PartialAsset = Pick<SearchAsset, 'address' | 'chainId' | 'colors' | 'icon_url' | 'mainnetAddress' | 'name' | 'symbol' | 'uniqueId'>;

interface OutputCoinRowProps extends PartialAsset {
isFavorite: boolean;
onPress: () => void;
output: true;
nativePriceChange?: string;
isTrending?: boolean;
}

type CoinRowProps = InputCoinRowProps | OutputCoinRowProps;

export const CoinRow = React.memo(function CoinRow({ onPress, output, uniqueId, isTrending, ...assetProps }: CoinRowProps) {
const { favoritesMetadata } = useFavorites();

export const CoinRow = memo(function CoinRow({ isFavorite, onPress, output, uniqueId, ...assetProps }: CoinRowProps) {
const inputAsset = userAssetsStore(state => (output ? undefined : state.getUserAsset(uniqueId)));
const outputAsset = output ? (assetProps as PartialAsset) : undefined;

const asset = output ? outputAsset : inputAsset;
const { address, chainId, colors, icon_url, mainnetAddress, name, symbol } = asset || {};

const percentChange = useMemo(() => {
if (isTrending) {
const rawChange = Math.random() * 30;
const isNegative = Math.random() < 0.2;
const prefix = isNegative ? '-' : '+';
const color: TextColor = isNegative ? 'red' : 'green';
const change = `${trimTrailingZeros(Math.abs(rawChange).toFixed(1))}%`;

return { change, color, prefix };
}
}, [isTrending]);

const isFavorite = useMemo(() => {
return Object.values(favoritesMetadata).find(fav => {
if (mainnetAddress?.toLowerCase() === ETH_ADDRESS) {
return fav.address.toLowerCase() === ETH_ADDRESS;
}

return fav.address?.toLowerCase() === address?.toLowerCase();
});
}, [favoritesMetadata, address, mainnetAddress]);
/**
* ⚠️ TODO: Re-enable when trending tokens are added
*
* const percentChange = useMemo(() => {
* if (isTrending && nativePriceChange) {
* const rawChange = parseFloat(nativePriceChange);
* const isNegative = rawChange < 0;
* const prefix = isNegative ? '-' : '+';
* const color: TextColor = isNegative ? 'red' : 'green';
* const change = `${trimTrailingZeros(Math.abs(rawChange).toFixed(1))}%`;

* return { change, color, prefix };
* }
* }, [isTrending, nativePriceChange]);
*/

const favoritesIconColor = useMemo(() => {
return isFavorite ? '#FFCB0F' : undefined;
Expand All @@ -76,14 +71,14 @@ export const CoinRow = React.memo(function CoinRow({ onPress, output, uniqueId,
const handleToggleFavorite = useCallback(() => {
// NOTE: It's important to always fetch ETH favorite on mainnet
if (address) {
return toggleFavorite(address, mainnetAddress === ETH_ADDRESS ? 1 : chainId);
return toggleFavorite(address, mainnetAddress === ETH_ADDRESS ? ChainId.mainnet : chainId);
}
}, [address, mainnetAddress, chainId]);

if (!address || !chainId) return null;

return (
<Box>
<Box style={{ height: COIN_ROW_WITH_PADDING_HEIGHT, width: '100%' }}>
<Columns alignVertical="center">
<Column>
<ButtonPressAnimation disallowInterruption onPress={output ? onPress : () => onPress(inputAsset || null)} scaleTo={0.95}>
Expand Down Expand Up @@ -116,7 +111,7 @@ export const CoinRow = React.memo(function CoinRow({ onPress, output, uniqueId,
<Text color="labelTertiary" numberOfLines={1} size="13pt" weight="semibold">
{output ? symbol : `${inputAsset?.balance.display}`}
</Text>
{isTrending && percentChange && (
{/* {nativePriceChange && percenChange && (
<Inline alignVertical="center" space={{ custom: 1 }} wrap={false}>
<Text align="center" color={percentChange.color} size="12pt" weight="bold">
{percentChange.prefix}
Expand All @@ -125,7 +120,7 @@ export const CoinRow = React.memo(function CoinRow({ onPress, output, uniqueId,
{percentChange.change}
</Text>
</Inline>
)}
)} */}
</Inline>
</Box>
</Box>
Expand All @@ -137,7 +132,7 @@ export const CoinRow = React.memo(function CoinRow({ onPress, output, uniqueId,
{output && (
<Column width="content">
<Box paddingLeft="12px" paddingRight="20px">
<Inline space="8px">
<Inline space="10px">
<InfoButton address={address} chainId={chainId} />
<CoinRowButton color={favoritesIconColor} onPress={handleToggleFavorite} icon="􀋃" weight="black" />
</Inline>
Expand Down Expand Up @@ -225,10 +220,10 @@ const InfoButton = ({ address, chainId }: { address: string; chainId: ChainId })
return (
<ContextMenuButton
activeOpacity={0}
isMenuPrimaryAction
// @ts-expect-error Types of property 'menuItems' are incompatible
menuConfig={menuConfig}
onPress={IS_ANDROID ? onPressAndroid : undefined}
isMenuPrimaryAction
onPressMenuItem={handlePressMenuItem}
useActionSheetFallback={false}
wrapNativeComponent={false}
Expand All @@ -237,9 +232,3 @@ const InfoButton = ({ address, chainId }: { address: string; chainId: ChainId })
</ContextMenuButton>
);
};

export const styles = StyleSheet.create({
solidColorCoinIcon: {
opacity: 0.4,
},
});
2 changes: 1 addition & 1 deletion src/__swaps__/screens/Swap/components/CoinRowButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export const CoinRowButton = ({
backgroundColor: outline
? 'transparent'
: color
? opacity(color, 0.25)
? opacity(color, isDarkMode ? 0.16 : 0.25)
: isDarkMode
? fillQuaternary
: opacity(fillTertiary, 0.04),
Expand Down
5 changes: 1 addition & 4 deletions src/__swaps__/screens/Swap/components/SwapInputAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ export function SwapInputAsset() {
</Box>
<Box
as={Animated.View}
padding={{ custom: INPUT_PADDING }}
paddingTop={{ custom: INPUT_PADDING }}
paddingBottom={{ custom: 14.5 }}
position="absolute"
style={AnimatedSwapStyles.inputTokenListStyle}
Expand Down Expand Up @@ -181,9 +181,6 @@ export const styles = StyleSheet.create({
overflow: 'hidden',
marginTop: StatusBar.currentHeight ?? 0,
},
solidColorCoinIcon: {
opacity: 0.4,
},
staticInputContainerStyles: {
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
Expand Down
36 changes: 16 additions & 20 deletions src/__swaps__/screens/Swap/components/SwapNavbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,13 @@ export function SwapNavbar() {
const { isDarkMode } = useColorMode();
const { navigate, goBack } = useNavigation();

const { AnimatedSwapStyles, internalSelectedInputAsset, internalSelectedOutputAsset } = useSwapContext();
const { AnimatedSwapStyles, swapInfo } = useSwapContext();

// const separatorSecondary = useForegroundColor('separatorSecondary');
// const separatorTertiary = useForegroundColor('separatorTertiary');

const swapOrBridgeLabel = useDerivedValue(() => {
const areBothAssetsSelected = internalSelectedInputAsset.value && internalSelectedOutputAsset.value;
const isBridging =
areBothAssetsSelected && internalSelectedInputAsset.value?.mainnetAddress === internalSelectedOutputAsset.value?.mainnetAddress;

return isBridging ? BRIDGE_TITLE_LABEL : SWAP_TITLE_LABEL;
return swapInfo.value.isBridging ? BRIDGE_TITLE_LABEL : SWAP_TITLE_LABEL;
});

const onChangeWallet = React.useCallback(() => {
Expand Down Expand Up @@ -125,17 +121,17 @@ export function SwapNavbar() {
);
}

export const styles = StyleSheet.create({
headerButton: {
borderRadius: 18,
borderWidth: THICK_BORDER_WIDTH,
height: 36,
width: 36,
},
headerTextShadow: {
padding: 12,
textShadowColor: 'rgba(0, 0, 0, 0.2)',
textShadowOffset: { width: 0, height: 2 },
textShadowRadius: 6,
},
});
// export const styles = StyleSheet.create({
// headerButton: {
// borderRadius: 18,
// borderWidth: THICK_BORDER_WIDTH,
// height: 36,
// width: 36,
// },
// headerTextShadow: {
// padding: 12,
// textShadowColor: 'rgba(0, 0, 0, 0.2)',
// textShadowOffset: { width: 0, height: 2 },
// textShadowRadius: 6,
// },
// });
christianbaroni marked this conversation as resolved.
Show resolved Hide resolved
5 changes: 1 addition & 4 deletions src/__swaps__/screens/Swap/components/SwapOutputAsset.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ export function SwapOutputAsset() {
<Box
as={Animated.View}
height="full"
padding={{ custom: INPUT_PADDING }}
paddingTop={{ custom: INPUT_PADDING }}
christianbaroni marked this conversation as resolved.
Show resolved Hide resolved
paddingBottom={{ custom: 14.5 }}
position="absolute"
style={AnimatedSwapStyles.outputTokenListStyle}
Expand Down Expand Up @@ -210,9 +210,6 @@ export const styles = StyleSheet.create({
overflow: 'hidden',
marginTop: StatusBar.currentHeight ?? 0,
},
solidColorCoinIcon: {
opacity: 0.4,
},
staticInputContainerStyles: {
shadowOffset: { width: 0, height: 6 },
shadowOpacity: 0.1,
Expand Down
45 changes: 24 additions & 21 deletions src/__swaps__/screens/Swap/components/SwapSlider.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
/* eslint-disable no-nested-ternary */
import React, { useCallback, useRef } from 'react';
import { StyleSheet, TouchableOpacity, View } from 'react-native';
import { StyleSheet, View } from 'react-native';
import * as i18n from '@/languages';
import { PanGestureHandler, TapGestureHandler, TapGestureHandlerGestureEvent } from 'react-native-gesture-handler';
import Animated, {
interpolate,
interpolateColor,
runOnJS,
runOnUI,
useAnimatedGestureHandler,
useAnimatedReaction,
useAnimatedStyle,
Expand All @@ -34,6 +33,7 @@ import {
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { clamp, getColorValueForThemeWorklet, opacity, opacityWorklet } from '@/__swaps__/utils/swaps';
import { AnimatedSwapCoinIcon } from './AnimatedSwapCoinIcon';
import { GestureHandlerV1Button } from './GestureHandlerV1Button';

type SwapSliderProps = {
dualColor?: boolean;
Expand Down Expand Up @@ -64,10 +64,12 @@ export const SwapSlider = ({
isQuoteStale,
sliderPressProgress,
sliderXPosition,
swapInfo,
} = useSwapContext();

const panRef = useRef();
const tapRef = useRef();
const maxButtonRef = useRef();

const fillSecondary = useForegroundColor('fillSecondary');
const labelSecondary = useForegroundColor('labelSecondary');
Expand Down Expand Up @@ -361,11 +363,7 @@ export const SwapSlider = ({
});

const sellingOrBridgingLabel = useDerivedValue(() => {
const areBothAssetsSelected = internalSelectedInputAsset.value && internalSelectedOutputAsset.value;
const isBridging =
areBothAssetsSelected && internalSelectedInputAsset.value?.mainnetAddress === internalSelectedOutputAsset.value?.mainnetAddress;

return isBridging ? BRIDGE_TITLE_LABEL : SWAP_TITLE_LABEL;
return swapInfo.value.isBridging ? BRIDGE_TITLE_LABEL : SWAP_TITLE_LABEL;
});

const maxTextColor = useAnimatedStyle(() => {
Expand All @@ -379,7 +377,7 @@ export const SwapSlider = ({
<PanGestureHandler activeOffsetX={[0, 0]} activeOffsetY={[0, 0]} onGestureEvent={onSlide} simultaneousHandlers={[tapRef]}>
<Animated.View style={AnimatedSwapStyles.hideWhileReviewingOrConfiguringGas}>
{/* @ts-expect-error Property 'children' does not exist on type */}
<TapGestureHandler onGestureEvent={onPressDown} simultaneousHandlers={[panRef]}>
<TapGestureHandler onGestureEvent={onPressDown} simultaneousHandlers={[maxButtonRef, panRef]}>
<Animated.View style={{ gap: 14, paddingBottom: 20, paddingHorizontal: 20 }}>
<View style={{ zIndex: 10 }}>
<Columns alignHorizontal="justify" alignVertical="center">
Expand All @@ -402,22 +400,30 @@ export const SwapSlider = ({
</Inline>
</Inline>
<Column width="content">
<TouchableOpacity
activeOpacity={0.4}
hitSlop={8}
onPress={() => {
runOnUI(() => {
<GestureHandlerV1Button
style={{ margin: -12, padding: 12 }}
onPressWorklet={() => {
'worklet';
SwapInputController.inputMethod.value = 'slider';
const isAlreadyMax = sliderXPosition.value === width;
christianbaroni marked this conversation as resolved.
Show resolved Hide resolved

if (isAlreadyMax) {
runOnJS(triggerHapticFeedback)('impactMedium');
} else {
SwapInputController.quoteFetchingInterval.stop();
SwapInputController.inputMethod.value = 'slider';
sliderXPosition.value = withSpring(width, SPRING_CONFIGS.snappySpringConfig);
runOnJS(onChangeWrapper)(1);
})();
sliderXPosition.value = withSpring(width, SPRING_CONFIGS.snappySpringConfig, isFinished => {
if (isFinished) {
runOnJS(onChangeWrapper)(1);
}
});
}
}}
ref={maxButtonRef}
>
<AnimatedText align="center" size="15pt" style={maxTextColor} weight="heavy">
{MAX_LABEL}
</AnimatedText>
</TouchableOpacity>
</GestureHandlerV1Button>
</Column>
</Columns>
</View>
Expand Down Expand Up @@ -489,7 +495,4 @@ const styles = StyleSheet.create({
justifyContent: 'center',
width: SCRUBBER_WIDTH,
},
solidColorCoinIcon: {
opacity: 0.4,
},
});
Loading
Loading