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: Input List #5728

Closed
wants to merge 17 commits into from
166 changes: 166 additions & 0 deletions src/__swaps__/screens/Swap/components/CoinRow2.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React, { useCallback, useMemo } from 'react';
import { ButtonPressAnimation } from '@/components/animations';
import { Box, HitSlop, Inline, Stack, 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 Animated from 'react-native-reanimated';
import { StyleSheet } from 'react-native';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { UniqueId } from '@/__swaps__/types/assets';
import { userAssetsStore } from '@/state/assets/userAssets';
import { parseSearchAsset } from '@/__swaps__/utils/assets';
import { SwapAssetType } from '@/__swaps__/types/swap';

const CoinName = ({ assetId }: { assetId: UniqueId }) => {
const name = userAssetsStore(state => state.getUserAsset(assetId).name);
return (
<Text color="label" size="17pt" weight="semibold">
{name}
</Text>
);
};

const CoinUserBalance = ({ assetId }: { assetId: UniqueId }) => {
const balance = userAssetsStore(state => state.getUserAsset(assetId).balance.display);
return (
<Text color="labelTertiary" size="13pt" weight="semibold">
{balance}
</Text>
);
};

const CoinSymbol = ({ assetId }: { assetId: UniqueId }) => {
const symbol = userAssetsStore(state => state.getUserAsset(assetId).symbol);
return (
<Text color="labelTertiary" size="13pt" weight="semibold">
{symbol}
</Text>
);
};

const CoinPercentChange = ({ assetId }: { assetId: UniqueId }) => {
const isTrending = false; // fix this when implementing token to sell list

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 = `${rawChange.toFixed(1)}%`;

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

if (!isTrending || !percentChange) return null;

return (
<Inline alignVertical="center" space={{ custom: 1 }}>
<Text align="center" color={percentChange.color} size="12pt" weight="bold">
{percentChange.prefix}
</Text>
<Text color={percentChange.color} size="13pt" weight="semibold">
{percentChange.change}
</Text>
</Inline>
);
};

const CoinIcon = ({ assetId }: { assetId: UniqueId }) => {
const { AnimatedSwapStyles } = useSwapContext();
return (
<Box
as={Animated.View}
borderRadius={18}
height={{ custom: 36 }}
style={[styles.solidColorCoinIcon, AnimatedSwapStyles.assetToSellIconStyle]}
width={{ custom: 36 }}
/>
);
};

const CoinInfoButton = ({ assetId }: { assetId: UniqueId }) => {
return <CoinRowButton icon="􀅳" outline size="icon 14px" />;
};

const CoinFavoriteButton = ({ assetId }: { assetId: UniqueId }) => {
const address = userAssetsStore(state => state.getUserAsset(assetId).address);
const isFavorited = userAssetsStore(state => state.isFavorite(assetId));
return (
<CoinRowButton
color={isFavorited ? '#FFCB0F' : undefined}
onPress={() => userAssetsStore.getState().toggleFavorite(address)}
icon="􀋃"
weight="black"
/>
);
};

const CoinActions = ({ assetId }: { assetId: UniqueId }) => {
return (
<Inline space="8px">
<CoinInfoButton assetId={assetId} />
<CoinFavoriteButton assetId={assetId} />
</Inline>
);
};

const CoinBalance = ({ assetId }: { assetId: UniqueId }) => {
const nativeBalance = userAssetsStore(state => state.getUserAsset(assetId).native.balance.display);
return <BalancePill balance={nativeBalance} />;
};

export const CoinRow2 = React.memo(({ assetId, output = false }: { assetId: string; output?: boolean }) => {
const { setAsset } = useSwapContext();

const handleSelectToken = useCallback(() => {
const userAsset = userAssetsStore.getState().getUserAsset(assetId);
const parsedAsset = parseSearchAsset({
assetWithPrice: undefined,
searchAsset: userAsset,
userAsset,
});

setAsset({
type: SwapAssetType.inputAsset,
asset: parsedAsset,
});
}, [assetId, setAsset]);

return (
<ButtonPressAnimation disallowInterruption onPress={handleSelectToken} scaleTo={0.95}>
<HitSlop vertical="10px">
<Box
alignItems="center"
paddingVertical="10px"
paddingHorizontal="20px"
flexDirection="row"
justifyContent="space-between"
width="full"
>
<Inline alignVertical="center" space="10px">
<CoinIcon assetId={assetId} />
<Stack space="10px">
<CoinName assetId={assetId} />
<Inline alignVertical="center" space={{ custom: 5 }}>
{!output ? <CoinUserBalance assetId={assetId} /> : <CoinSymbol assetId={assetId} />}
<CoinPercentChange assetId={assetId} />
</Inline>
</Stack>
</Inline>
{output ? <CoinActions assetId={assetId} /> : <CoinBalance assetId={assetId} />}
</Box>
</HitSlop>
</ButtonPressAnimation>
);
});

CoinRow2.displayName = 'CoinRow';

export const styles = StyleSheet.create({
solidColorCoinIcon: {
opacity: 0.4,
},
});
21 changes: 18 additions & 3 deletions src/__swaps__/screens/Swap/components/TokenList/TokenList.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import Animated, { SharedValue, useAnimatedProps } from 'react-native-reanimated';
import { StyleSheet } from 'react-native';
import { InteractionManager, StyleSheet } from 'react-native';
import { Separator, Stack } from '@/design-system';
import { useDimensions } from '@/hooks';
import { EXPANDED_INPUT_HEIGHT, FOCUSED_INPUT_HEIGHT } from '@/__swaps__/screens/Swap/constants';
import { SearchInput } from '@/__swaps__/screens/Swap/components/SearchInput';
import { TokenToSellList } from '@/__swaps__/screens/Swap/components/TokenList/TokenToSellList';
import { TokenToBuyList } from '@/__swaps__/screens/Swap/components/TokenList/TokenToBuyList';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { ExtendedAnimatedAssetWithColors } from '@/__swaps__/types/assets';
import { TokenToBuyList } from './TokenToBuyList';

export const TokenList = ({
asset,
Expand All @@ -21,6 +21,7 @@ export const TokenList = ({
handleFocusSearch: () => void;
output?: boolean;
}) => {
const [isSwapMounted, setIsSwapMounted] = useState(false);
const { inputProgress, outputProgress } = useSwapContext();
const { width: deviceWidth } = useDimensions();

Expand All @@ -35,6 +36,20 @@ export const TokenList = ({
};
});

useEffect(() => {
InteractionManager.runAfterInteractions(() => {
setIsSwapMounted(true);
});

return () => {
setIsSwapMounted(false);
};
}, []);

if (!isSwapMounted) {
return null;
}

return (
<Stack>
<Stack space="20px">
Expand Down
62 changes: 13 additions & 49 deletions src/__swaps__/screens/Swap/components/TokenList/TokenToSellList.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,29 @@
import React, { useCallback } from 'react';
import React from 'react';
import { StyleSheet } from 'react-native';
import { CoinRow } from '@/__swaps__/screens/Swap/components/CoinRow';
import { useAssetsToSell } from '@/__swaps__/screens/Swap/hooks/useAssetsToSell';
import { ParsedSearchAsset } from '@/__swaps__/types/assets';
import { CoinRow2 } from '@/__swaps__/screens/Swap/components/CoinRow2';
import { Stack } from '@/design-system';
import Animated from 'react-native-reanimated';
import { useSwapContext } from '@/__swaps__/screens/Swap/providers/swap-provider';
import { parseSearchAsset } from '@/__swaps__/utils/assets';
import { ListEmpty } from '@/__swaps__/screens/Swap/components/TokenList/ListEmpty';
import { FlashList } from '@shopify/flash-list';
import { ChainSelection } from './ChainSelection';
import { SwapAssetType } from '@/__swaps__/types/swap';
import { userAssetsStore } from '@/state/assets/userAssets';

const AnimatedFlashListComponent = Animated.createAnimatedComponent(FlashList<ParsedSearchAsset>);
import { COIN_ROW_LIST_WIDTH, COIN_ROW_LIST_HEIGHT, COIN_ROW_HEIGHT } from '../../constants';

export const TokenToSellList = () => {
const { setAsset } = useSwapContext();
const userAssets = useAssetsToSell();

const handleSelectToken = useCallback(
(token: ParsedSearchAsset) => {
const userAsset = userAssetsStore.getState().getUserAsset(token.uniqueId);
const parsedAsset = parseSearchAsset({
assetWithPrice: undefined,
searchAsset: token,
userAsset,
});

setAsset({
type: SwapAssetType.inputAsset,
asset: parsedAsset,
});
},
[setAsset]
);
const assetIds = userAssetsStore(state => state.filteredUserAssetsById);

return (
<Stack space="20px">
<ChainSelection allText="All Networks" output={false} />

<AnimatedFlashListComponent
data={userAssets}
<FlashList
data={assetIds}
estimatedItemSize={COIN_ROW_HEIGHT}
estimatedListSize={{
height: COIN_ROW_LIST_HEIGHT,
width: COIN_ROW_LIST_WIDTH,
}}
ListEmptyComponent={<ListEmpty />}
keyExtractor={item => item.uniqueId}
renderItem={({ item }) => (
<CoinRow
// key={item.uniqueId}
chainId={item.chainId}
color={item.colors?.primary ?? item.colors?.fallback}
iconUrl={item.icon_url}
address={item.address}
mainnetAddress={item.mainnetAddress}
balance={item.balance.display}
name={item.name}
onPress={() => handleSelectToken(item)}
nativeBalance={item.native.balance.display}
output={false}
symbol={item.symbol}
/>
)}
keyExtractor={item => item}
renderItem={({ item }) => <CoinRow2 assetId={item} />}
/>
</Stack>
);
Expand Down
27 changes: 24 additions & 3 deletions src/__swaps__/screens/Swap/components/UserAssetsSync.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useUserAssets } from '../resources/assets';
import { selectUserAssetsList, selectorFilterByUserChains } from '@/__swaps__/screens/Swap/resources/_selectors/assets';
import { Hex } from 'viem';
import { userAssetsStore } from '@/state/assets/userAssets';
import { ParsedSearchAsset } from '@/__swaps__/types/assets';
import { ParsedSearchAsset, UniqueId } from '@/__swaps__/types/assets';

export const UserAssetsSync = () => {
const { accountAddress: currentAddress, nativeCurrency: currentCurrency } = useAccountSettings();
Expand All @@ -21,9 +21,30 @@ export const UserAssetsSync = () => {
selector: selectUserAssetsList,
}),
onSuccess: data => {
const searchQuery = userAssetsStore.getState().searchQuery.toLowerCase();
const filter = userAssetsStore.getState().filter;

const filteredUserAssetsById: UniqueId[] = [];
const userAssets = new Map<UniqueId, ParsedSearchAsset>();
data.forEach(asset => {
if (filter === 'all' || asset.chainId === filter) {
if (searchQuery) {
const nameMatch = asset.name.toLowerCase().includes(searchQuery);
const symbolMatch = asset.symbol.toLowerCase().startsWith(searchQuery);
const addressMatch = asset.address.toLowerCase().startsWith(searchQuery);
if (nameMatch || symbolMatch || addressMatch) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the change here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh hmm i see nvm, good catch

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can prob improve later also. for example, rank startsWith matches for name higher than includes matches for name, if that makes sense.

filteredUserAssetsById.push(asset.uniqueId);
}
} else {
filteredUserAssetsById.push(asset.uniqueId);
}
}
userAssets.set(asset.uniqueId, asset as ParsedSearchAsset);
});

userAssetsStore.setState({
userAssetsById: new Set(data.map(d => d.uniqueId)),
userAssets: new Map(data.map(d => [d.uniqueId, d as ParsedSearchAsset])),
filteredUserAssetsById,
userAssets,
});
},
}
Expand Down
4 changes: 4 additions & 0 deletions src/__swaps__/screens/Swap/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ export const THICK_BORDER_WIDTH = 4 / 3;
export const INPUT_PADDING = 20 - THICK_BORDER_WIDTH;
export const INPUT_INNER_WIDTH = BASE_INPUT_WIDTH - THICK_BORDER_WIDTH * 2;

export const COIN_ROW_HEIGHT = 76;
export const COIN_ROW_LIST_HEIGHT = EXPANDED_INPUT_HEIGHT - 77;
export const COIN_ROW_LIST_WIDTH = deviceUtils.dimensions.width - 24;

export const SLIDER_HEIGHT = 16;
export const SLIDER_COLLAPSED_HEIGHT = 10;
export const SLIDER_WIDTH = deviceUtils.dimensions.width - 40;
Expand Down
27 changes: 27 additions & 0 deletions src/migrations/migrations/migrateFavoritesToZustand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { EthereumAddress } from '@rainbow-me/swaps';
import { Migration, MigrationName } from '../types';
import { RainbowToken } from '@/entities';
import { queryClient } from '@/react-query';
import { favoritesQueryKey } from '@/resources/favorites';
import { Hex } from 'viem';
import { userAssetsStore } from '@/state/assets/userAssets';

export function migrateFavoritesToZustand(): Migration {
return {
name: MigrationName.migrateFavoritesToZustand,
async migrate() {
const favorites = queryClient.getQueryData<Record<EthereumAddress, RainbowToken>>(favoritesQueryKey);

if (favorites) {
const favoriteAddresses: Hex[] = [];
Object.keys(favorites).forEach((address: string) => {
favoriteAddresses.push(address as Hex);
});

userAssetsStore.setState({
favorites: new Set(favoriteAddresses),
});
}
},
};
}
1 change: 1 addition & 0 deletions src/migrations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export enum MigrationName {
migratePinnedAndHiddenTokenUniqueIds = 'migration_migratePinnedAndHiddenTokenUniqueIds',
migrateUnlockableAppIconStorage = 'migration_migrateUnlockableAppIconStorage',
migratePersistedQueriesToMMKV = 'migration_migratePersistedQueriesToMMKV',
migrateFavoritesToZustand = 'migration_migrateFavoritesToZustand',
}

export type Migration = {
Expand Down
Loading
Loading