Commit 56a0937f authored by isstuev's avatar isstuev

fixes and tests

parent 75836a7e
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
<path fill="currentColor" fill-opacity=".8" fill-rule="evenodd" d="M9.767 2.074a.7.7 0 0 1 .626 0l7.38 3.69a.7.7 0 0 1 0 1.252l-7.38 3.69a.7.7 0 0 1-.626 0l-7.38-3.69a.7.7 0 0 1 0-1.252l7.38-3.69ZM4.266 6.39l5.814 2.907 5.815-2.907-5.815-2.907L4.266 6.39Zm-2.192 7.067a.7.7 0 0 1 .94-.313l7.066 3.534 7.067-3.534a.7.7 0 0 1 .627 1.252l-7.38 3.69a.7.7 0 0 1-.627 0l-7.38-3.69a.7.7 0 0 1-.313-.939Zm.94-4.003a.7.7 0 0 0-.627 1.252l7.38 3.69a.7.7 0 0 0 .626 0l7.38-3.69a.7.7 0 1 0-.626-1.252l-7.067 3.534-7.067-3.534Z" clip-rule="evenodd"/>
</svg>
import type { AddressTokenBalance } from 'types/api/address'; import type { AddressCollectionsResponse, AddressNFTsResponse, AddressTokenBalance } from 'types/api/address';
import * as tokens from 'mocks/tokens/tokenInfo'; import * as tokens from 'mocks/tokens/tokenInfo';
import * as tokenInstance from 'mocks/tokens/tokenInstance'; import * as tokenInstance from 'mocks/tokens/tokenInstance';
...@@ -117,3 +117,49 @@ export const erc1155List = { ...@@ -117,3 +117,49 @@ export const erc1155List = {
erc1155b, erc1155b,
], ],
}; };
export const nfts: AddressNFTsResponse = {
items: [
{
...tokenInstance.base,
token_type: 'ERC-1155',
value: '11',
},
{
...tokenInstance.unique,
token_type: 'ERC-721',
value: '1',
},
],
next_page_params: null,
};
const nftInstance = {
...tokenInstance.base,
token_type: 'ERC-1155',
value: '11',
};
export const collections: AddressCollectionsResponse = {
items: [
{
token: tokens.tokenInfoERC1155a,
amount: '100',
token_instances: Array(5).fill(nftInstance),
},
{
token: tokens.tokenInfoERC20LongSymbol,
amount: '100',
token_instances: Array(5).fill(nftInstance),
},
{
token: tokens.tokenInfoERC1155WithoutName,
amount: '1',
token_instances: [ nftInstance ],
},
],
next_page_params: {
token_contract_address_hash: '123',
token_type: 'ERC-1155',
},
};
...@@ -13,6 +13,8 @@ import AddressTokens from './AddressTokens'; ...@@ -13,6 +13,8 @@ import AddressTokens from './AddressTokens';
const ADDRESS_HASH = addressMock.withName.hash; const ADDRESS_HASH = addressMock.withName.hash;
const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH }); const API_URL_ADDRESS = buildApiUrl('address', { hash: ADDRESS_HASH });
const API_URL_TOKENS = buildApiUrl('address_tokens', { hash: ADDRESS_HASH }); const API_URL_TOKENS = buildApiUrl('address_tokens', { hash: ADDRESS_HASH });
const API_URL_NFT = buildApiUrl('address_nfts', { hash: ADDRESS_HASH });
const API_URL_COLLECTIONS = buildApiUrl('address_collections', { hash: ADDRESS_HASH });
const nextPageParams = { const nextPageParams = {
items_count: 50, items_count: 50,
...@@ -52,6 +54,14 @@ const test = base.extend({ ...@@ -52,6 +54,14 @@ const test = base.extend({
status: 200, status: 200,
body: JSON.stringify(response1155), body: JSON.stringify(response1155),
})); }));
await page.route(API_URL_NFT, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.nfts),
}));
await page.route(API_URL_COLLECTIONS, (route) => route.fulfill({
status: 200,
body: JSON.stringify(tokensMock.collections),
}));
use(page); use(page);
}, },
...@@ -76,10 +86,10 @@ test('erc20 +@dark-mode', async({ mount }) => { ...@@ -76,10 +86,10 @@ test('erc20 +@dark-mode', async({ mount }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc721 +@dark-mode', async({ mount }) => { test('collections +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc721' }, query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
isReady: true, isReady: true,
}, },
}; };
...@@ -95,10 +105,10 @@ test('erc721 +@dark-mode', async({ mount }) => { ...@@ -95,10 +105,10 @@ test('erc721 +@dark-mode', async({ mount }) => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc1155 +@dark-mode', async({ mount }) => { test('nfts +@dark-mode', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc1155' }, query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
isReady: true, isReady: true,
}, },
}; };
...@@ -111,6 +121,8 @@ test('erc1155 +@dark-mode', async({ mount }) => { ...@@ -111,6 +121,8 @@ test('erc1155 +@dark-mode', async({ mount }) => {
{ hooksConfig }, { hooksConfig },
); );
await component.getByText('List').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
...@@ -136,10 +148,10 @@ test.describe('mobile', () => { ...@@ -136,10 +148,10 @@ test.describe('mobile', () => {
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc721', async({ mount }) => { test('nfts', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc721' }, query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
isReady: true, isReady: true,
}, },
}; };
...@@ -152,13 +164,15 @@ test.describe('mobile', () => { ...@@ -152,13 +164,15 @@ test.describe('mobile', () => {
{ hooksConfig }, { hooksConfig },
); );
await component.getByLabel('list').click();
await expect(component).toHaveScreenshot(); await expect(component).toHaveScreenshot();
}); });
test('erc1155', async({ mount }) => { test('collections', async({ mount }) => {
const hooksConfig = { const hooksConfig = {
router: { router: {
query: { hash: ADDRESS_HASH, tab: 'tokens_erc1155' }, query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' },
isReady: true, isReady: true,
}, },
}; };
......
import { Box } from '@chakra-ui/react'; import { Box } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { AddressTokenBalance, AddressTokensBalancesSocketMessage, AddressTokensResponse } from 'types/api/address';
import type { TokenType } from 'types/api/token';
import type { PaginationParams } from 'ui/shared/pagination/types'; import type { PaginationParams } from 'ui/shared/pagination/types';
import { getResourceKey } from 'lib/api/useApiQuery'; import listIcon from 'icons/apps.svg';
import collectionIcon from 'icons/collection.svg';
import { useAppContext } from 'lib/contexts/app'; import { useAppContext } from 'lib/contexts/app';
import * as cookies from 'lib/cookies'; import * as cookies from 'lib/cookies';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address'; import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address';
import { generateListStub } from 'stubs/utils'; import { generateListStub } from 'stubs/utils';
import Pagination from 'ui/shared/pagination/Pagination'; import Pagination from 'ui/shared/pagination/Pagination';
...@@ -27,7 +22,7 @@ import AddressNFTs from './tokens/AddressNFTs'; ...@@ -27,7 +22,7 @@ import AddressNFTs from './tokens/AddressNFTs';
import ERC20Tokens from './tokens/ERC20Tokens'; import ERC20Tokens from './tokens/ERC20Tokens';
import TokenBalances from './tokens/TokenBalances'; import TokenBalances from './tokens/TokenBalances';
type TNftDisplayType = 'collections' | 'list'; type TNftDisplayType = 'collection' | 'list';
const TAB_LIST_PROPS = { const TAB_LIST_PROPS = {
marginBottom: 0, marginBottom: 0,
...@@ -41,12 +36,6 @@ const TAB_LIST_PROPS_MOBILE = { ...@@ -41,12 +36,6 @@ const TAB_LIST_PROPS_MOBILE = {
columnGap: 3, columnGap: 3,
}; };
const tokenBalanceItemIdentityFactory = (match: AddressTokenBalance) => (item: AddressTokenBalance) => ((
match.token.address === item.token.address &&
match.token_id === item.token_id &&
match.token_instance?.id === item.token_instance?.id
));
const AddressTokens = () => { const AddressTokens = () => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -54,7 +43,7 @@ const AddressTokens = () => { ...@@ -54,7 +43,7 @@ const AddressTokens = () => {
const scrollRef = React.useRef<HTMLDivElement>(null); const scrollRef = React.useRef<HTMLDivElement>(null);
const displayTypeCookie = cookies.get(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, useAppContext().cookies); const displayTypeCookie = cookies.get(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, useAppContext().cookies);
const [ nftDisplayType, setNftDisplayType ] = React.useState<TNftDisplayType>(displayTypeCookie === 'list' ? 'list' : 'collections'); const [ nftDisplayType, setNftDisplayType ] = React.useState<TNftDisplayType>(displayTypeCookie === 'list' ? 'list' : 'collection');
const tab = getQueryParamString(router.query.tab); const tab = getQueryParamString(router.query.tab);
const hash = getQueryParamString(router.query.hash); const hash = getQueryParamString(router.query.hash);
...@@ -76,7 +65,7 @@ const AddressTokens = () => { ...@@ -76,7 +65,7 @@ const AddressTokens = () => {
pathParams: { hash }, pathParams: { hash },
scrollRef, scrollRef,
options: { options: {
enabled: tab === 'tokens_nfts' && nftDisplayType === 'collections', enabled: tab === 'tokens_nfts' && nftDisplayType === 'collection',
refetchOnMount: false, refetchOnMount: false,
placeholderData: generateListStub<'address_collections'>(ADDRESS_COLLECTION, 10, { next_page_params: null }), placeholderData: generateListStub<'address_collections'>(ADDRESS_COLLECTION, 10, { next_page_params: null }),
}, },
...@@ -93,69 +82,6 @@ const AddressTokens = () => { ...@@ -93,69 +82,6 @@ const AddressTokens = () => {
}, },
}); });
const queryClient = useQueryClient();
const updateTokensData = React.useCallback((type: TokenType, payload: AddressTokensBalancesSocketMessage) => {
const queryKey = getResourceKey('address_tokens', { pathParams: { hash }, queryParams: { type } });
queryClient.setQueryData(queryKey, (prevData: AddressTokensResponse | undefined) => {
const items = prevData?.items.map((currentItem) => {
const updatedData = payload.token_balances.find(tokenBalanceItemIdentityFactory(currentItem));
return updatedData ?? currentItem;
}) || [];
const extraItems = prevData?.next_page_params ?
[] :
payload.token_balances.filter((socketItem) => !items.some(tokenBalanceItemIdentityFactory(socketItem)));
if (!prevData) {
return {
items: extraItems,
next_page_params: null,
};
}
return {
items: items.concat(extraItems),
next_page_params: prevData.next_page_params,
};
});
}, [ hash, queryClient ]);
const handleTokenBalancesErc20Message: SocketMessage.AddressTokenBalancesErc20['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-20', payload);
}, [ updateTokensData ]);
const handleTokenBalancesErc721Message: SocketMessage.AddressTokenBalancesErc721['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-721', payload);
}, [ updateTokensData ]);
const handleTokenBalancesErc1155Message: SocketMessage.AddressTokenBalancesErc1155['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-1155', payload);
}, [ updateTokensData ]);
const channel = useSocketChannel({
topic: `addresses:${ hash.toLowerCase() }`,
// !!!
isDisabled: erc20Query.isPlaceholderData || nftsQuery.isPlaceholderData || collectionsQuery.isPlaceholderData,
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_20',
handler: handleTokenBalancesErc20Message,
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_721',
handler: handleTokenBalancesErc721Message,
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_1155',
handler: handleTokenBalancesErc1155Message,
});
const handleNFTsDisplayTypeChange = React.useCallback((val: TNftDisplayType) => { const handleNFTsDisplayTypeChange = React.useCallback((val: TNftDisplayType) => {
cookies.set(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, val); cookies.set(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, val);
setNftDisplayType(val); setNftDisplayType(val);
...@@ -177,7 +103,10 @@ const AddressTokens = () => { ...@@ -177,7 +103,10 @@ const AddressTokens = () => {
onChange={ handleNFTsDisplayTypeChange } onChange={ handleNFTsDisplayTypeChange }
defaultValue={ nftDisplayType } defaultValue={ nftDisplayType }
name="type" name="type"
options={ [ { title: 'By collections', value: 'collections' }, { title: 'List', value: 'list' } ] } options={ [
{ title: 'By collection', value: 'collection', icon: collectionIcon, onlyIcon: isMobile },
{ title: 'List', value: 'list', icon: listIcon, onlyIcon: isMobile },
] }
/> />
); );
......
...@@ -2,10 +2,8 @@ import { Flex } from '@chakra-ui/react'; ...@@ -2,10 +2,8 @@ import { Flex } from '@chakra-ui/react';
import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import { test as base, expect, devices } from '@playwright/experimental-ct-react';
import React from 'react'; import React from 'react';
import * as coinBalanceMock from 'mocks/address/coinBalanceHistory';
import * as tokensMock from 'mocks/address/tokens'; import * as tokensMock from 'mocks/address/tokens';
import { tokenInfoERC20a } from 'mocks/tokens/tokenInfo'; import { tokenInfoERC20a } from 'mocks/tokens/tokenInfo';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp'; import TestApp from 'playwright/TestApp';
import buildApiUrl from 'playwright/utils/buildApiUrl'; import buildApiUrl from 'playwright/utils/buildApiUrl';
import MockAddressPage from 'ui/address/testUtils/MockAddressPage'; import MockAddressPage from 'ui/address/testUtils/MockAddressPage';
...@@ -175,76 +173,3 @@ base('long values', async({ mount, page }) => { ...@@ -175,76 +173,3 @@ base('long values', async({ mount, page }) => {
await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA }); await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA });
}); });
test.describe('socket', () => {
const testWithSocket = test.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
testWithSocket.describe.configure({ mode: 'serial' });
testWithSocket('new item after token balance update', async({ page, mount, createSocket }) => {
await mount(
<TestApp withSocket>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
...tokensMock.erc20List.items,
tokensMock.erc20d,
],
}),
}), { times: 1 });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1');
socketServer.sendMessage(socket, channel, 'token_balance', {
block_number: 1,
});
const button = page.getByRole('button', { name: /select/i });
const text = await button.innerText();
expect(text).toContain('10');
});
testWithSocket('new item after coin balance update', async({ page, mount, createSocket }) => {
await mount(
<TestApp withSocket>
<MockAddressPage>
<Flex>
<TokenSelect/>
</Flex>
</MockAddressPage>
</TestApp>,
{ hooksConfig },
);
await page.route(TOKENS_ERC20_API_URL, async(route) => route.fulfill({
status: 200,
body: JSON.stringify({
items: [
...tokensMock.erc20List.items,
tokensMock.erc20d,
],
}),
}), { times: 1 });
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'addresses:1');
socketServer.sendMessage(socket, channel, 'coin_balance', {
coin_balance: coinBalanceMock.base,
});
const button = page.getByRole('button', { name: /select/i });
const text = await button.innerText();
expect(text).toContain('10');
});
});
...@@ -5,7 +5,6 @@ import NextLink from 'next/link'; ...@@ -5,7 +5,6 @@ import NextLink from 'next/link';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { Address } from 'types/api/address'; import type { Address } from 'types/api/address';
import walletIcon from 'icons/wallet.svg'; import walletIcon from 'icons/wallet.svg';
...@@ -13,8 +12,6 @@ import { getResourceKey } from 'lib/api/useApiQuery'; ...@@ -13,8 +12,6 @@ import { getResourceKey } from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import * as mixpanel from 'lib/mixpanel/index'; import * as mixpanel from 'lib/mixpanel/index';
import getQueryParamString from 'lib/router/getQueryParamString'; import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import useFetchTokens from '../utils/useFetchTokens'; import useFetchTokens from '../utils/useFetchTokens';
import TokenSelectDesktop from './TokenSelectDesktop'; import TokenSelectDesktop from './TokenSelectDesktop';
...@@ -28,14 +25,13 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -28,14 +25,13 @@ const TokenSelect = ({ onClick }: Props) => {
const router = useRouter(); const router = useRouter();
const isMobile = useIsMobile(); const isMobile = useIsMobile();
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const [ blockNumber, setBlockNumber ] = React.useState<number>();
const addressHash = getQueryParamString(router.query.hash); const addressHash = getQueryParamString(router.query.hash);
const addressResourceKey = getResourceKey('address', { pathParams: { hash: addressHash } }); const addressResourceKey = getResourceKey('address', { pathParams: { hash: addressHash } });
const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey); const addressQueryData = queryClient.getQueryData<Address>(addressResourceKey);
const { data, isError, isPending, refetch } = useFetchTokens({ hash: addressQueryData?.hash }); const { data, isError, isPending } = useFetchTokens({ hash: addressQueryData?.hash });
const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } }); const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } });
const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey }); const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey });
...@@ -44,34 +40,6 @@ const TokenSelect = ({ onClick }: Props) => { ...@@ -44,34 +40,6 @@ const TokenSelect = ({ onClick }: Props) => {
onClick?.(); onClick?.();
}, [ onClick ]); }, [ onClick ]);
const handleTokenBalanceMessage: SocketMessage.AddressTokenBalance['handler'] = React.useCallback((payload) => {
if (payload.block_number !== blockNumber) {
refetch();
setBlockNumber(payload.block_number);
}
}, [ blockNumber, refetch ]);
const handleCoinBalanceMessage: SocketMessage.AddressCoinBalance['handler'] = React.useCallback((payload) => {
if (payload.coin_balance.block_number !== blockNumber) {
refetch();
setBlockNumber(payload.coin_balance.block_number);
}
}, [ blockNumber, refetch ]);
const channel = useSocketChannel({
topic: `addresses:${ addressQueryData?.hash.toLowerCase() }`,
isDisabled: !addressQueryData,
});
useSocketMessage({
channel,
event: 'coin_balance',
handler: handleCoinBalanceMessage,
});
useSocketMessage({
channel,
event: 'token_balance',
handler: handleTokenBalanceMessage,
});
if (isPending) { if (isPending) {
return ( return (
<Flex columnGap={ 3 }> <Flex columnGap={ 3 }>
......
...@@ -44,7 +44,7 @@ const AddressCollections = ({ collectionsQuery, address }: Props) => { ...@@ -44,7 +44,7 @@ const AddressCollections = ({ collectionsQuery, address }: Props) => {
const hasOverload = Number(item.amount) > item.token_instances.length; const hasOverload = Number(item.amount) > item.token_instances.length;
return ( return (
<Box key={ item.token.address + index } mb={ 6 }> <Box key={ item.token.address + index } mb={ 6 }>
<Flex mb={ 3 }> <Flex mb={ 3 } flexWrap="wrap">
<TokenEntity <TokenEntity
width="auto" width="auto"
noSymbol noSymbol
...@@ -53,11 +53,11 @@ const AddressCollections = ({ collectionsQuery, address }: Props) => { ...@@ -53,11 +53,11 @@ const AddressCollections = ({ collectionsQuery, address }: Props) => {
noCopy noCopy
fontWeight="600" fontWeight="600"
/> />
<Skeleton isLoaded={ !isPlaceholderData } > <Skeleton isLoaded={ !isPlaceholderData } mr={ 3 }>
<Text variant="secondary" whiteSpace="pre">{ ` - ${ Number(item.amount).toLocaleString() } item${ Number(item.amount) > 1 ? 's' : '' }` }</Text> <Text variant="secondary" whiteSpace="pre">{ ` - ${ Number(item.amount).toLocaleString() } item${ Number(item.amount) > 1 ? 's' : '' }` }</Text>
</Skeleton> </Skeleton>
{ hasOverload && ( { hasOverload && (
<LinkInternal href={ collectionUrl } ml={ 3 } isLoading={ isPlaceholderData }> <LinkInternal href={ collectionUrl } isLoading={ isPlaceholderData }>
<Skeleton isLoaded={ !isPlaceholderData }>View in collection</Skeleton> <Skeleton isLoaded={ !isPlaceholderData }>View in collection</Skeleton>
</LinkInternal> </LinkInternal>
) } ) }
......
...@@ -31,7 +31,7 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P ...@@ -31,7 +31,7 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P
/> />
</Link> </Link>
<Flex justifyContent="space-between" w="100%"> <Flex justifyContent="space-between" w="100%">
<Flex ml={ 1 }> <Flex ml={ 1 } overflow="hidden">
<Text whiteSpace="pre" variant="secondary">ID# </Text> <Text whiteSpace="pre" variant="secondary">ID# </Text>
<NftEntity hash={ token.address } id={ tokenInstance.id } isLoading={ isLoading } noIcon/> <NftEntity hash={ token.address } id={ tokenInstance.id } isLoading={ isLoading } noIcon/>
</Flex> </Flex>
......
import { useQueryClient } from '@tanstack/react-query';
import React from 'react'; import React from 'react';
import useApiQuery from 'lib/api/useApiQuery'; import type { SocketMessage } from 'lib/socket/types';
import type { AddressTokenBalance, AddressTokensBalancesSocketMessage, AddressTokensResponse } from 'types/api/address';
import type { TokenType } from 'types/api/token';
import { calculateUsdValue } from './tokenUtils'; import useApiQuery, { getResourceKey } from 'lib/api/useApiQuery';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { calculateUsdValue } from './tokenUtils';
interface Props { interface Props {
hash?: string; hash?: string;
} }
const tokenBalanceItemIdentityFactory = (match: AddressTokenBalance) => (item: AddressTokenBalance) => ((
match.token.address === item.token.address &&
match.token_id === item.token_id &&
match.token_instance?.id === item.token_instance?.id
));
export default function useFetchTokens({ hash }: Props) { export default function useFetchTokens({ hash }: Props) {
const erc20query = useApiQuery('address_tokens', { const erc20query = useApiQuery('address_tokens', {
pathParams: { hash }, pathParams: { hash },
...@@ -25,11 +37,67 @@ export default function useFetchTokens({ hash }: Props) { ...@@ -25,11 +37,67 @@ export default function useFetchTokens({ hash }: Props) {
queryOptions: { enabled: Boolean(hash), refetchOnMount: false }, queryOptions: { enabled: Boolean(hash), refetchOnMount: false },
}); });
const refetch = React.useCallback(() => { const queryClient = useQueryClient();
erc20query.refetch();
erc721query.refetch(); const updateTokensData = React.useCallback((type: TokenType, payload: AddressTokensBalancesSocketMessage) => {
erc1155query.refetch(); const queryKey = getResourceKey('address_tokens', { pathParams: { hash }, queryParams: { type } });
}, [ erc1155query, erc20query, erc721query ]);
queryClient.setQueryData(queryKey, (prevData: AddressTokensResponse | undefined) => {
const items = prevData?.items.map((currentItem) => {
const updatedData = payload.token_balances.find(tokenBalanceItemIdentityFactory(currentItem));
return updatedData ?? currentItem;
}) || [];
const extraItems = prevData?.next_page_params ?
[] :
payload.token_balances.filter((socketItem) => !items.some(tokenBalanceItemIdentityFactory(socketItem)));
if (!prevData) {
return {
items: extraItems,
next_page_params: null,
};
}
return {
items: items.concat(extraItems),
next_page_params: prevData.next_page_params,
};
});
}, [ hash, queryClient ]);
const handleTokenBalancesErc20Message: SocketMessage.AddressTokenBalancesErc20['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-20', payload);
}, [ updateTokensData ]);
const handleTokenBalancesErc721Message: SocketMessage.AddressTokenBalancesErc721['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-721', payload);
}, [ updateTokensData ]);
const handleTokenBalancesErc1155Message: SocketMessage.AddressTokenBalancesErc1155['handler'] = React.useCallback((payload) => {
updateTokensData('ERC-1155', payload);
}, [ updateTokensData ]);
const channel = useSocketChannel({
topic: `addresses:${ hash?.toLowerCase() }`,
isDisabled: Boolean(hash) && (erc20query.isPlaceholderData || erc721query.isPlaceholderData || erc1155query.isPlaceholderData),
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_20',
handler: handleTokenBalancesErc20Message,
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_721',
handler: handleTokenBalancesErc721Message,
});
useSocketMessage({
channel,
event: 'updated_token_balances_erc_1155',
handler: handleTokenBalancesErc1155Message,
});
const data = React.useMemo(() => { const data = React.useMemo(() => {
return { return {
...@@ -52,6 +120,5 @@ export default function useFetchTokens({ hash }: Props) { ...@@ -52,6 +120,5 @@ export default function useFetchTokens({ hash }: Props) {
isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending, isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending,
isError: erc20query.isError || erc721query.isError || erc1155query.isError, isError: erc20query.isError || erc721query.isError || erc1155query.isError,
data, data,
refetch,
}; };
} }
import { ButtonGroup, Button, Box, useRadio, useRadioGroup, useColorModeValue } from '@chakra-ui/react'; import { ButtonGroup, Button, Flex, Icon, useRadio, useRadioGroup, useColorModeValue } from '@chakra-ui/react';
import type { UseRadioProps } from '@chakra-ui/react'; import type { UseRadioProps } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
type RadioButtonProps = UseRadioProps & { type RadioItemProps = {
children: React.ReactNode; title: string;
icon?: React.FC<React.SVGAttributes<SVGElement>>;
onlyIcon: false | undefined;
} | {
title: string;
icon: React.FC<React.SVGAttributes<SVGElement>>;
onlyIcon: true;
} }
type RadioButtonProps = UseRadioProps & RadioItemProps;
const RadioButton = (props: RadioButtonProps) => { const RadioButton = (props: RadioButtonProps) => {
const { getInputProps, getRadioProps } = useRadio(props); const { getInputProps, getRadioProps } = useRadio(props);
const buttonColor = useColorModeValue('blue.50', 'gray.800'); const buttonColor = useColorModeValue('blue.50', 'gray.800');
...@@ -13,30 +21,52 @@ const RadioButton = (props: RadioButtonProps) => { ...@@ -13,30 +21,52 @@ const RadioButton = (props: RadioButtonProps) => {
const input = getInputProps(); const input = getInputProps();
const checkbox = getRadioProps(); const checkbox = getRadioProps();
const styleProps = {
flex: 1,
variant: 'outline',
fontWeight: 500,
cursor: props.isChecked ? 'initial' : 'pointer',
borderColor: buttonColor,
backgroundColor: props.isChecked ? buttonColor : 'none',
_hover: {
borderColor: buttonColor,
...(props.isChecked ? {} : { color: 'link_hovered' }),
},
_active: {
backgroundColor: 'none',
},
...(props.isChecked ? { color: 'text' } : {}),
};
if (props.onlyIcon) {
return (
<Button
as="label"
aria-label={ props.title }
{ ...styleProps }
>
<input { ...input }/>
<Flex
{ ...checkbox }
>
<Icon as={ props.icon } boxSize={ 5 }/>
</Flex>
</Button>
);
}
return ( return (
<Button <Button
as="label" as="label"
variant="outline" leftIcon={ props.icon ? <Icon as={ props.icon } boxSize={ 5 }/> : undefined }
fontWeight={ 500 } { ...styleProps }
cursor={ props.isChecked ? 'initial' : 'pointer' }
borderColor={ buttonColor }
_hover={{
borderColor: buttonColor,
...(props.isChecked ? {} : { color: 'link_hovered' }),
}}
_active={{
backgroundColor: 'none',
}}
backgroundColor={ props.isChecked ? buttonColor : 'none' }
{ ...(props.isChecked ? { color: 'text' } : {}) }
> >
<input { ...input }/> <input { ...input }/>
<Box <Flex
{ ...checkbox } { ...checkbox }
> >
{ props.children } { props.title }
</Box> </Flex>
</Button> </Button>
); );
}; };
...@@ -45,7 +75,7 @@ type RadioButtonGroupProps<T extends string> = { ...@@ -45,7 +75,7 @@ type RadioButtonGroupProps<T extends string> = {
onChange: (value: T) => void; onChange: (value: T) => void;
name: string; name: string;
defaultValue: string; defaultValue: string;
options: Array<{title: string; value: T}>; options: Array<{ value: T } & RadioItemProps>;
} }
const RadioButtonGroup = <T extends string>({ onChange, name, defaultValue, options }: RadioButtonGroupProps<T>) => { const RadioButtonGroup = <T extends string>({ onChange, name, defaultValue, options }: RadioButtonGroupProps<T>) => {
...@@ -54,10 +84,10 @@ const RadioButtonGroup = <T extends string>({ onChange, name, defaultValue, opti ...@@ -54,10 +84,10 @@ const RadioButtonGroup = <T extends string>({ onChange, name, defaultValue, opti
const group = getRootProps(); const group = getRootProps();
return ( return (
<ButtonGroup { ...group } isAttached size="sm"> <ButtonGroup { ...group } isAttached size="sm" display="grid" gridTemplateColumns="1fr 1fr">
{ options.map((option) => { { options.map((option) => {
const props = getRadioProps({ value: option.value }); const props = getRadioProps({ value: option.value });
return <RadioButton { ...props } key={ option.value }>{ option.title }</RadioButton>; return <RadioButton { ...props } key={ option.value } { ...option }/>;
}) } }) }
</ButtonGroup> </ButtonGroup>
); );
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment