Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
12 changes: 9 additions & 3 deletions apps/next/src/hooks/useAllTokensFromEnabledNetworks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,14 @@ import { useNetworkContext } from '@core/ui';
import { useAllTokens } from './useAllTokens';

// TODO: Currently the hook is using favoriteNetwork. It should be changed to enabledNetworks once added.
export const useAllTokensFromEnabledNetworks = () => {
const { favoriteNetworks } = useNetworkContext();
export const useAllTokensFromEnabledNetworks = (
onlyTokensWithBalances?: boolean,
hideMalicious?: boolean,
) => {
const { enabledNetworks } = useNetworkContext();
const tokens = useAllTokens(enabledNetworks, hideMalicious);

return useAllTokens(favoriteNetworks, false);
return !onlyTokensWithBalances
? tokens
: tokens.filter((token) => token.balance);
};
2 changes: 1 addition & 1 deletion apps/next/src/hooks/useTokensForAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ const isNativeToken = (
): token is FungibleTokenBalance & { type: TokenType.NATIVE } =>
token.type === TokenType.NATIVE;

const isAvaxToken = (
export const isAvaxToken = (
token: FungibleTokenBalance,
): token is FungibleTokenBalance & { type: TokenType.NATIVE } =>
token.type === TokenType.NATIVE && token.symbol === 'AVAX';
Expand Down
4 changes: 4 additions & 0 deletions apps/next/src/localization/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"Add using Ledger": "Add using Ledger",
"Address copied!": "Address copied!",
"All": "All",
"All Networks": "All Networks",
"All keys contained in this file are already imported.": "All keys contained in this file are already imported.",
"All networks": "All networks",
"Allow Chrome to access your camera to scan the QR code.": "Allow Chrome to access your camera to scan the QR code.",
Expand Down Expand Up @@ -88,6 +89,7 @@
"Average password - this will do": "Average password - this will do",
"Awesome!": "Awesome!",
"Back": "Back",
"Balance": "Balance",
"Balance change unavailable.": "Balance change unavailable.",
"Balances loading...": "Balances loading...",
"Best price available": "Best price available",
Expand Down Expand Up @@ -470,6 +472,7 @@
"Protocol": "Protocol",
"Provider": "Provider",
"Public key not found": "Public key not found",
"Quantity": "Quantity",
"Quote includes an {{coreFee}} Core fee": "Quote includes an {{coreFee}} Core fee",
"RPC URL reset successfully": "RPC URL reset successfully",
"Rate": "Rate",
Expand Down Expand Up @@ -620,6 +623,7 @@
"To use Solana in Core you will need to add an account from your Ledger device. You can always add this later at any time": "To use Solana in Core you will need to add an account from your Ledger device. You can always add this later at any time",
"Token": "Token",
"Token Added": "Token Added",
"Token Price": "Token Price",
"Token already exists in the wallet.": "Token already exists in the wallet.",
"Token contract address": "Token contract address",
"Token name": "Token name",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { useAllTokensFromEnabledNetworks } from '@/hooks/useAllTokensFromEnabledNetworks';
import { Box } from '@avalabs/k2-alpine';
import { TokenType } from '@avalabs/vm-module-types';
import { isTokenMalicious } from '@core/common';
import { FungibleTokenBalance } from '@core/types';
import { FC, useMemo } from 'react';
import { FixedSizeList, ListChildComponentProps } from 'react-window';
Expand All @@ -16,41 +14,29 @@ interface Props {

export const TokenSwitchList: FC<Props> = ({ filter, spam }) => {
const [height, containerRef] = useContainerHeight<HTMLDivElement>(400);
const tokensWithBalances = useAllTokensFromEnabledNetworks();
const tokensWithBalances = useAllTokensFromEnabledNetworks(false, !spam);

const nonNative = useMemo(() => {
return tokensWithBalances.filter(
(token) => token.type !== TokenType.NATIVE,
);
}, [tokensWithBalances]);

const spamless = useMemo(
() =>
spam ? nonNative : nonNative.filter((token) => !isTokenMalicious(token)),
[spam, nonNative],
);

const filtered = useMemo(
const filteredTokensList = useMemo(
() =>
filter
? spamless.filter((token) => {
? tokensWithBalances.filter((token) => {
const normalizedFilter = filter.toLowerCase();
return (
token.name.toLowerCase().includes(normalizedFilter) ||
token.symbol.toLowerCase().includes(normalizedFilter)
);
})
: spamless,
[filter, spamless],
: tokensWithBalances,
[filter, tokensWithBalances],
);

return (
<Box height={1} ref={containerRef}>
<FixedSizeList
height={height}
width="100%"
itemData={filtered}
itemCount={filtered.length}
itemData={filteredTokensList}
itemCount={filteredTokensList.length}
itemSize={54}
overscanCount={5}
style={{ overflow: 'auto', scrollbarWidth: 'none' }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ const TabsContainer = styled(Stack)(({ theme }) => ({
bottom: 0,
paddingTop: theme.spacing(1),
background: `linear-gradient(180deg, ${getHexAlpha(theme.palette.background.default, 0)} 0%, ${theme.palette.background.default} 16px)`,

zIndex: theme.zIndex.appBar,
'> div': {
background: 'unset',
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import {
Box,
ChevronRightIcon,
Stack,
Theme,
Typography,
useTheme,
} from '@avalabs/k2-alpine';
import { ProfitAndLoss } from './ProfitAndLoss';
import { FungibleTokenBalance } from '@core/types';
import { TokenAvatar } from '@/components/TokenAvatar';
import { Card } from '@/components/Card';

interface AssetCardProps {
asset: FungibleTokenBalance;
onClick?: () => void;
}

const AVATAR_SIZE = 40;
const BADGE_SIZE = 18;
const CHEVRON_SIZE = 20;
const CARD_BORDER_RADIUS = 2;
const CARD_GAP = 1.5;
const CARD_PADDING_X = 1.5;
const CARD_PADDING_Y = 1;

const getBadgeBorderColor = (theme: Theme): string => {
return theme.palette.mode === 'dark'
? '#47474c'
: theme.palette.surface.primary;
};

const formatTokenBalance = (balance: string, symbol: string): string => {
return `${balance} ${symbol}`;
};

export const AssetCard = ({ asset, onClick }: AssetCardProps) => {
const theme = useTheme();

const handleClick = () => {
onClick?.();
// TODO: Navigate to asset details page when route is available
// const history = useHistory();
// history.push(`/asset/${asset.symbol}`);
};

const badgeBorderColor = getBadgeBorderColor(theme);
const tokenBalanceText = formatTokenBalance(
asset.balanceDisplayValue,
asset.symbol,
);

return (
<Card sx={{ width: '100%', borderRadius: CARD_BORDER_RADIUS }}>
<Stack
role="button"
onClick={handleClick}
direction="row"
alignItems="center"
gap={CARD_GAP}
sx={{
cursor: onClick ? 'pointer' : 'default',
px: theme.spacing(CARD_PADDING_X),
py: theme.spacing(CARD_PADDING_Y),
}}
>
<Box flexShrink={0}>
<TokenAvatar
token={asset}
size={AVATAR_SIZE}
badgeSize={BADGE_SIZE}
badgeSx={{
borderColor: badgeBorderColor,
}}
/>
</Box>

<Stack flexGrow={1} minWidth={0}>
<Typography
variant="subtitle3"
noWrap
fontWeight="600"
color="text.primary"
>
{asset.name}
</Typography>
<Typography color="text.primary" variant="body3" noWrap>
{tokenBalanceText}
</Typography>
</Stack>

<Stack alignItems="flex-end" flexShrink={0}>
<ProfitAndLoss asset={asset} />
</Stack>

<Box
display="flex"
flexShrink={0}
alignItems="center"
justifyContent="center"
>
<ChevronRightIcon size={CHEVRON_SIZE} color="text.secondary" />
</Box>
</Stack>
</Card>
);
};
Original file line number Diff line number Diff line change
@@ -1,21 +1,70 @@
import {
Box,
Button,
ButtonProps,
Stack,
styled,
toast,
} from '@avalabs/k2-alpine';
import { FC } from 'react';
import { Box, Button, ButtonProps, Stack, styled } from '@avalabs/k2-alpine';
import { FC, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { MdCurrencyBitcoin, MdKeyboardArrowDown } from 'react-icons/md';
import { MdKeyboardArrowDown } from 'react-icons/md';
import { useHistory } from 'react-router-dom';
import { UnderConstruction } from './UnderConstruction';
import { useAllTokensFromEnabledNetworks } from '@/hooks/useAllTokensFromEnabledNetworks';
import { TrendingTokenBanner } from '@/pages/TrendingTokens/components/banner/TrendingTokenBanner';
import { getUniqueTokenId } from '@core/types';
import { useNetworkContext } from '@core/ui';

import { AssetCard } from './AssetCard';
import {
filterAssetsByNetworks,
getAvailableNetworksFromAssets,
} from './assetFiltering';
import { FilterMenu } from './FilterMenu';
import { SortMenu } from './SortMenu';
import { AssetSortOption, sortAssets } from './assetSorting';

export const AssetsTab: FC = () => {
const { t } = useTranslation();
const { push } = useHistory();
const { getNetwork } = useNetworkContext();
const [filterMenuElement, setFilterMenuElement] =
useState<HTMLButtonElement | null>(null);
const [sortMenuElement, setSortMenuElement] =
useState<HTMLButtonElement | null>(null);
const [sort, setSort] = useState<AssetSortOption | null>(null);
const [selectedNetworks, setSelectedNetworks] = useState<Set<number>>(
new Set(),
);

const assets = useAllTokensFromEnabledNetworks(true, true);

const availableNetworks = useMemo(
() => getAvailableNetworksFromAssets(assets, getNetwork),
[assets, getNetwork],
);

const filteredAssets = useMemo(
() => filterAssetsByNetworks(assets, selectedNetworks),
[assets, selectedNetworks],
);

const sortedAssets = useMemo(
() => sortAssets(filteredAssets, sort),
[filteredAssets, sort],
);

const handleFilterMenuClick = (
event: React.MouseEvent<HTMLButtonElement>,
) => {
setFilterMenuElement(event.currentTarget);
};

const handleFilterMenuClose = () => {
setFilterMenuElement(null);
};

const handleSortMenuClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setSortMenuElement(event.currentTarget);
};

const handleSortMenuClose = () => {
setSortMenuElement(null);
};

return (
<Stack direction="column" gap={1.25} height={1}>
<Box bgcolor="background.paper" borderRadius={2} px={2}>
Expand All @@ -24,13 +73,13 @@ export const AssetsTab: FC = () => {
<Stack direction="row" gap={1.25}>
<StyledButton
endIcon={<MdKeyboardArrowDown size={20} />}
onClick={() => toast.info('Coming soon')}
onClick={handleFilterMenuClick}
>
{t('Filter')}
</StyledButton>
<StyledButton
endIcon={<MdKeyboardArrowDown size={20} />}
onClick={() => toast.info('Coming soon')}
onClick={handleSortMenuClick}
>
{t('Sort')}
</StyledButton>
Expand All @@ -40,10 +89,27 @@ export const AssetsTab: FC = () => {
</StyledButton>
</Box>
</Stack>
<UnderConstruction
title="Assets"
description="Your assets will be displayed here. We're working hard to bring you this feature soon!"
icon={<MdCurrencyBitcoin size={24} />}
<Stack width="100%" flexGrow={1} gap={1}>
{sortedAssets.map((token) => (
<AssetCard key={getUniqueTokenId(token)} asset={token} />
))}
</Stack>
<FilterMenu
id="filter-menu"
anchorEl={filterMenuElement}
selectedNetworks={selectedNetworks}
setSelectedNetworks={setSelectedNetworks}
availableNetworks={availableNetworks}
open={Boolean(filterMenuElement)}
onClose={handleFilterMenuClose}
/>
<SortMenu
id="sort-menu"
anchorEl={sortMenuElement}
sort={sort}
setSort={setSort}
open={Boolean(sortMenuElement)}
onClose={handleSortMenuClose}
/>
</Stack>
);
Expand Down
Loading
Loading