Skip to content

Commit

Permalink
feat: add advanced token search
Browse files Browse the repository at this point in the history
  • Loading branch information
chybisov committed Aug 8, 2022
1 parent 77660e7 commit 4e4068a
Show file tree
Hide file tree
Showing 11 changed files with 176 additions and 80 deletions.
2 changes: 1 addition & 1 deletion libs/react-scripts/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"jest-resolve": "^28.1.3",
"jest-watch-typeahead": "^2.0.0",
"mini-css-extract-plugin": "^2.6.1",
"postcss": "^8.4.14",
"postcss": "^8.4.16",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-loader": "^7.0.1",
"postcss-normalize": "^10.0.1",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@testing-library/user-event": "^14.4.2",
"@types/big.js": "^6.1.5",
"@types/node": "^18.6.4",
"@types/react": "^18.0.15",
"@types/react": "^18.0.16",
"@types/react-dom": "^18.0.6",
"@types/uuid": "^8.3.4",
"@typescript-eslint/eslint-plugin": "^5.32.0",
Expand Down
3 changes: 1 addition & 2 deletions packages/widget/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,17 +61,16 @@
"@sentry/react": "^7.9.0",
"@sentry/tracing": "^7.9.0",
"@tanstack/react-query": "^4.0.10",
"@tanstack/react-virtual": "^3.0.0-beta.17",
"big.js": "^6.2.1",
"i18next": "^21.8.16",
"immer": "^9.0.15",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.34.0",
"react-i18next": "^11.18.3",
"react-resize-detector": "^7.1.2",
"react-router-dom": "^6.3.0",
"react-timer-hook": "^3.0.5",
"react-virtual": "^2.10.4",
"zustand": "^4.0.0"
},
"eslintConfig": {
Expand Down
82 changes: 31 additions & 51 deletions packages/widget/src/components/TokenList/TokenList.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,29 @@
import { TokenAmount } from '@lifi/sdk';
import { Box, List, Typography } from '@mui/material';
import { FC, useCallback, useEffect, useMemo, useRef } from 'react';
import { useTokenSearch } from '@lifi/widget/hooks/useTokenSearch';
import { Box, Typography } from '@mui/material';
import { FC, useCallback, useMemo, useRef, useState } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { useVirtual } from 'react-virtual';
import { useDebouncedWatch, useTokenBalances } from '../../hooks';
import {
SwapFormKey,
SwapFormKeyHelper,
} from '../../providers/SwapFormProvider';
import { useWallet } from '../../providers/WalletProvider';
import { TokenListItem, TokenListItemSkeleton } from './TokenListItem';
import { TokenListProps } from './types';
import { createTokenAmountSkeletons, skeletonKey } from './utils';
import { createTokenAmountSkeletons } from './utils';
import { VirtualizedTokenList } from './VirtualizedTokenList';

export const TokenList: FC<TokenListProps> = ({
formType,
height,
onClick,
}) => {
const { t } = useTranslation();
const parentRef = useRef<HTMLUListElement | null>(null);
const { account } = useWallet();
const { setValue, getValues } = useFormContext();
const [tokenSkeletons] = useState(createTokenAmountSkeletons);
const [selectedChainId] = useWatch({
name: [SwapFormKeyHelper.getChainKey(formType)],
});
Expand All @@ -37,14 +39,10 @@ export const TokenList: FC<TokenListProps> = ({
isBalanceLoading,
} = useTokenBalances(selectedChainId);

const tokens = tokensWithBalance ?? tokensWithoutBalance;

const chainTokens = useMemo(() => {
let chainTokens = tokens ?? [];
const filteredTokens = useMemo(() => {
let chainTokens = tokensWithBalance ?? tokensWithoutBalance ?? [];
const searchFilter = searchTokensFilter?.toUpperCase() ?? '';
chainTokens = isLoading
? createTokenAmountSkeletons()
: searchTokensFilter
chainTokens = searchTokensFilter
? chainTokens.filter(
(token) =>
token.name.toUpperCase().includes(searchFilter) ||
Expand All @@ -53,22 +51,21 @@ export const TokenList: FC<TokenListProps> = ({
)
: chainTokens;
return chainTokens;
}, [isLoading, searchTokensFilter, tokens]);
}, [searchTokensFilter, tokensWithBalance, tokensWithoutBalance]);

const parentRef = useRef<HTMLUListElement | null>(null);
const tokenSearchEnabled = !filteredTokens.length && !isLoading;

const { virtualItems, totalSize, scrollToIndex } = useVirtual({
size: chainTokens.length,
parentRef,
overscan: 3,
paddingEnd: 12,
estimateSize: useCallback(() => 64, []),
keyExtractor: (index) => chainTokens[index].address ?? index,
});
const { token: searchedToken, isLoading: isSearchedTokenLoading } =
useTokenSearch(searchTokensFilter, selectedChainId, tokenSearchEnabled);

useEffect(() => {
scrollToIndex(0);
}, [scrollToIndex, selectedChainId]);
const tokens =
isLoading || (tokenSearchEnabled && isSearchedTokenLoading)
? tokenSkeletons
: filteredTokens.length
? filteredTokens
: searchedToken
? [searchedToken]
: filteredTokens;

const handleTokenClick = useCallback(
(tokenAddress: string) => {
Expand All @@ -94,36 +91,19 @@ export const TokenList: FC<TokenListProps> = ({

return (
<Box ref={parentRef} style={{ height, overflow: 'auto' }}>
{!virtualItems.length ? (
{!tokens.length ? (
<Typography variant="body1" align="center" py={2} px={3}>
{t('swap.couldntFindTokens')}
</Typography>
) : null}
<List style={{ height: totalSize }} disablePadding>
{virtualItems.map((item) => {
const token = chainTokens[item.index] as TokenAmount;
if (token.name.includes(skeletonKey)) {
return (
<TokenListItemSkeleton
key={item.key}
size={item.size}
start={item.start}
/>
);
}
return (
<TokenListItem
key={item.key}
onClick={handleTokenClick}
size={item.size}
start={item.start}
token={token}
isBalanceLoading={isBalanceLoading}
showBalance={account.isActive}
/>
);
})}
</List>
<VirtualizedTokenList
tokens={tokens as TokenAmount[]}
scrollElementRef={parentRef}
onClick={handleTokenClick}
chainId={selectedChainId}
isBalanceLoading={isBalanceLoading}
showBalance={account.isActive}
/>
</Box>
);
};
57 changes: 57 additions & 0 deletions packages/widget/src/components/TokenList/VirtualizedTokenList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { TokenAmount } from '@lifi/sdk';
import { List } from '@mui/material';
import { useVirtualizer } from '@tanstack/react-virtual';
import { FC, useEffect } from 'react';
import { TokenListItem, TokenListItemSkeleton } from './TokenListItem';
import { VirtualizedTokenListProps } from './types';
import { skeletonKey } from './utils';

export const VirtualizedTokenList: FC<VirtualizedTokenListProps> = ({
tokens,
scrollElementRef,
onClick,
chainId,
isBalanceLoading,
showBalance,
}) => {
const { getVirtualItems, getTotalSize, scrollToIndex } = useVirtualizer({
count: tokens.length,
getScrollElement: () => scrollElementRef.current,
overscan: 3,
paddingEnd: 12,
estimateSize: () => 64,
getItemKey: (index) => tokens[index].address ?? index,
});

useEffect(() => {
scrollToIndex(0, { align: 'start', smoothScroll: false });
}, [scrollToIndex, chainId]);

return (
<List style={{ height: getTotalSize() }} disablePadding>
{getVirtualItems().map((item) => {
const token = tokens[item.index] as TokenAmount;
if (token.name.includes(skeletonKey)) {
return (
<TokenListItemSkeleton
key={item.key}
size={item.size}
start={item.start}
/>
);
}
return (
<TokenListItem
key={item.key}
onClick={onClick}
size={item.size}
start={item.start}
token={token}
isBalanceLoading={isBalanceLoading}
showBalance={showBalance}
/>
);
})}
</List>
);
};
12 changes: 11 additions & 1 deletion packages/widget/src/components/TokenList/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { TokenAmount } from '@lifi/sdk';
import { MutableRefObject } from 'react';
import { SwapFormDirection } from '../../providers/SwapFormProvider';

export interface TokenListProps {
Expand All @@ -7,8 +8,17 @@ export interface TokenListProps {
onClick?(): void;
}

export interface VirtualizedTokenListProps {
tokens: TokenAmount[];
scrollElementRef: MutableRefObject<HTMLElement | null>;
onClick(tokenAddress: string): void;
isBalanceLoading: boolean;
chainId: number;
showBalance?: boolean;
}

export interface TokenListItemBaseProps {
onClick?(token: string): void;
onClick?(tokenAddress: string): void;
size: number;
start: number;
}
Expand Down
5 changes: 3 additions & 2 deletions packages/widget/src/hooks/useTokenBalances.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,16 @@ export const useTokenBalances = (selectedChainId: number) => {
defaultRefetchInterval,
);

const isBalanceLoadingEnabled = Boolean(account.address) && Boolean(tokens);
const isBalanceLoadingEnabled =
Boolean(account.address) && Boolean(tokens?.length);

const {
data: tokensWithBalance,
isLoading: isBalanceLoading,
isFetched: isBalanceFetched,
refetch,
} = useQuery(
['token-balances', selectedChainId, account.address],
['token-balances', selectedChainId, account.address, tokens?.length],
async ({ queryKey: [, , accountAddress] }) => {
if (!accountAddress || !tokens) {
return;
Expand Down
38 changes: 38 additions & 0 deletions packages/widget/src/hooks/useTokenSearch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ChainId, Token } from '@lifi/sdk';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { LiFi } from '../config/lifi';

export const useTokenSearch = (
token: string,
chainId: number,
enabled?: boolean,
) => {
const queryClient = useQueryClient();
const { data, isLoading, isFetching, isFetched } = useQuery(
['token-search', chainId, token],
async ({ queryKey: [, chainId, token], signal }) => {
const data = await LiFi.getToken(chainId as ChainId, token as string, {
signal,
});
if (data) {
queryClient.setQueryData(['tokens', chainId], (tokens?: Token[]) => {
if (!tokens?.some((token) => token.address === data.address)) {
tokens?.push(data);
}
return tokens;
});
}
return data;
},
{
enabled,
retry: false,
},
);
return {
token: data,
isLoading,
isFetching,
isFetched,
};
};
2 changes: 2 additions & 0 deletions packages/widget/src/pages/SelectTokenPage/ChainSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { Card, CardTitle } from '../../components/Card';
import { Select } from '../../components/Select';
import { useChains } from '../../hooks';
import {
SwapFormKey,
SwapFormKeyHelper,
SwapFormTypeProps,
} from '../../providers/SwapFormProvider';
Expand All @@ -31,6 +32,7 @@ export const ChainSelect = ({ formType }: SwapFormTypeProps) => {
setValue(SwapFormKeyHelper.getChainKey(formType), event.target.value);
setValue(SwapFormKeyHelper.getTokenKey(formType), '');
setValue(SwapFormKeyHelper.getAmountKey(formType), '');
setValue(SwapFormKey.SearchTokensFilter, '');
};

return !isLoading ? (
Expand Down
2 changes: 0 additions & 2 deletions packages/widget/src/providers/SwapFormProvider/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ export const SwapFormKeyHelper = {
`${formType}Token`,
getAmountKey: (formType: SwapFormDirection): 'fromAmount' | 'toAmount' =>
`${formType}Amount`,
getSearchTokensFilterKey: (formType: SwapFormDirection) =>
`${formType}SearchTokensFilter`,
};

export interface SwapFormTypeProps {
Expand Down
Loading

0 comments on commit 4e4068a

Please sign in to comment.