diff --git a/icons/collection.svg b/icons/collection.svg new file mode 100644 index 0000000000..981040af5a --- /dev/null +++ b/icons/collection.svg @@ -0,0 +1,3 @@ + + + diff --git a/mocks/address/tokens.ts b/mocks/address/tokens.ts index 5f2299cee5..91abffd8c7 100644 --- a/mocks/address/tokens.ts +++ b/mocks/address/tokens.ts @@ -1,4 +1,4 @@ -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 tokenInstance from 'mocks/tokens/tokenInstance'; @@ -117,3 +117,49 @@ export const erc1155List = { 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', + }, +}; diff --git a/ui/address/AddressTokens.pw.tsx b/ui/address/AddressTokens.pw.tsx index 3cf3b34e93..49494a35b9 100644 --- a/ui/address/AddressTokens.pw.tsx +++ b/ui/address/AddressTokens.pw.tsx @@ -13,6 +13,8 @@ import AddressTokens from './AddressTokens'; const ADDRESS_HASH = addressMock.withName.hash; const API_URL_ADDRESS = buildApiUrl('address', { 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 = { items_count: 50, @@ -52,6 +54,14 @@ const test = base.extend({ status: 200, 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); }, @@ -76,10 +86,10 @@ test('erc20 +@dark-mode', async({ mount }) => { await expect(component).toHaveScreenshot(); }); -test('erc721 +@dark-mode', async({ mount }) => { +test('collections +@dark-mode', async({ mount }) => { const hooksConfig = { router: { - query: { hash: ADDRESS_HASH, tab: 'tokens_erc721' }, + query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' }, isReady: true, }, }; @@ -95,10 +105,10 @@ test('erc721 +@dark-mode', async({ mount }) => { await expect(component).toHaveScreenshot(); }); -test('erc1155 +@dark-mode', async({ mount }) => { +test('nfts +@dark-mode', async({ mount }) => { const hooksConfig = { router: { - query: { hash: ADDRESS_HASH, tab: 'tokens_erc1155' }, + query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' }, isReady: true, }, }; @@ -111,6 +121,8 @@ test('erc1155 +@dark-mode', async({ mount }) => { { hooksConfig }, ); + await component.getByText('List').click(); + await expect(component).toHaveScreenshot(); }); @@ -136,10 +148,10 @@ test.describe('mobile', () => { await expect(component).toHaveScreenshot(); }); - test('erc721', async({ mount }) => { + test('nfts', async({ mount }) => { const hooksConfig = { router: { - query: { hash: ADDRESS_HASH, tab: 'tokens_erc721' }, + query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' }, isReady: true, }, }; @@ -152,13 +164,15 @@ test.describe('mobile', () => { { hooksConfig }, ); + await component.getByLabel('list').click(); + await expect(component).toHaveScreenshot(); }); - test('erc1155', async({ mount }) => { + test('collections', async({ mount }) => { const hooksConfig = { router: { - query: { hash: ADDRESS_HASH, tab: 'tokens_erc1155' }, + query: { hash: ADDRESS_HASH, tab: 'tokens_nfts' }, isReady: true, }, }; diff --git a/ui/address/AddressTokens.tsx b/ui/address/AddressTokens.tsx index c3e445e3cc..046941f946 100644 --- a/ui/address/AddressTokens.tsx +++ b/ui/address/AddressTokens.tsx @@ -1,20 +1,15 @@ import { Box } from '@chakra-ui/react'; -import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; 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 { getResourceKey } from 'lib/api/useApiQuery'; +import listIcon from 'icons/apps.svg'; +import collectionIcon from 'icons/collection.svg'; import { useAppContext } from 'lib/contexts/app'; import * as cookies from 'lib/cookies'; import useIsMobile from 'lib/hooks/useIsMobile'; 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 { generateListStub } from 'stubs/utils'; import Pagination from 'ui/shared/pagination/Pagination'; @@ -27,7 +22,7 @@ import AddressNFTs from './tokens/AddressNFTs'; import ERC20Tokens from './tokens/ERC20Tokens'; import TokenBalances from './tokens/TokenBalances'; -type TNftDisplayType = 'collections' | 'list'; +type TNftDisplayType = 'collection' | 'list'; const TAB_LIST_PROPS = { marginBottom: 0, @@ -41,12 +36,6 @@ const TAB_LIST_PROPS_MOBILE = { 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 router = useRouter(); const isMobile = useIsMobile(); @@ -54,7 +43,7 @@ const AddressTokens = () => { const scrollRef = React.useRef(null); const displayTypeCookie = cookies.get(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, useAppContext().cookies); - const [ nftDisplayType, setNftDisplayType ] = React.useState(displayTypeCookie === 'list' ? 'list' : 'collections'); + const [ nftDisplayType, setNftDisplayType ] = React.useState(displayTypeCookie === 'list' ? 'list' : 'collection'); const tab = getQueryParamString(router.query.tab); const hash = getQueryParamString(router.query.hash); @@ -76,7 +65,7 @@ const AddressTokens = () => { pathParams: { hash }, scrollRef, options: { - enabled: tab === 'tokens_nfts' && nftDisplayType === 'collections', + enabled: tab === 'tokens_nfts' && nftDisplayType === 'collection', refetchOnMount: false, placeholderData: generateListStub<'address_collections'>(ADDRESS_COLLECTION, 10, { next_page_params: null }), }, @@ -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) => { cookies.set(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, val); setNftDisplayType(val); @@ -177,7 +103,10 @@ const AddressTokens = () => { onChange={ handleNFTsDisplayTypeChange } defaultValue={ nftDisplayType } 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 }, + ] } /> ); diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png new file mode 100644 index 0000000000..83db8a532e Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_collections-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc1155-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc1155-dark-mode-1.png deleted file mode 100644 index 8e21ba8758..0000000000 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc1155-dark-mode-1.png and /dev/null differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png index 5c9933e717..fd873438f9 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc20-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc721-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc721-dark-mode-1.png deleted file mode 100644 index 4eca88970b..0000000000 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_erc721-dark-mode-1.png and /dev/null differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png new file mode 100644 index 0000000000..e0c18ece19 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_dark-color-mode_nfts-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png new file mode 100644 index 0000000000..831cdeff85 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_collections-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc1155-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc1155-dark-mode-1.png deleted file mode 100644 index 7cbc7815c5..0000000000 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc1155-dark-mode-1.png and /dev/null differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png index 405d4f9ef4..14587bf801 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc20-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc721-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc721-dark-mode-1.png deleted file mode 100644 index ad6f89f6c1..0000000000 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_erc721-dark-mode-1.png and /dev/null differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png new file mode 100644 index 0000000000..58529aa9f9 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-collections-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc1155-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc1155-1.png deleted file mode 100644 index 6c1d0b61ac..0000000000 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc1155-1.png and /dev/null differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png index 4be344fc59..cebdaaf6b3 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc20-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc721-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc721-1.png deleted file mode 100644 index 9af951e2e9..0000000000 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-erc721-1.png and /dev/null differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png new file mode 100644 index 0000000000..95deb92652 Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_mobile-nfts-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png new file mode 100644 index 0000000000..205927b00c Binary files /dev/null and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_nfts-dark-mode-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png index 21adb1d539..8783664e35 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-1.png differ diff --git a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png index 1292dc8384..cbbc9e3d8b 100644 Binary files a/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png and b/ui/address/__screenshots__/AddressTokens.pw.tsx_default_update-balances-via-socket-2.png differ diff --git a/ui/address/tokenSelect/TokenSelect.pw.tsx b/ui/address/tokenSelect/TokenSelect.pw.tsx index 28e607dcdd..d2a810c981 100644 --- a/ui/address/tokenSelect/TokenSelect.pw.tsx +++ b/ui/address/tokenSelect/TokenSelect.pw.tsx @@ -2,10 +2,8 @@ import { Flex } from '@chakra-ui/react'; import { test as base, expect, devices } from '@playwright/experimental-ct-react'; import React from 'react'; -import * as coinBalanceMock from 'mocks/address/coinBalanceHistory'; import * as tokensMock from 'mocks/address/tokens'; import { tokenInfoERC20a } from 'mocks/tokens/tokenInfo'; -import * as socketServer from 'playwright/fixtures/socketServer'; import TestApp from 'playwright/TestApp'; import buildApiUrl from 'playwright/utils/buildApiUrl'; import MockAddressPage from 'ui/address/testUtils/MockAddressPage'; @@ -175,76 +173,3 @@ base('long values', async({ mount, page }) => { await expect(page).toHaveScreenshot({ clip: CLIPPING_AREA }); }); - -test.describe('socket', () => { - const testWithSocket = test.extend({ - createSocket: socketServer.createSocket, - }); - testWithSocket.describe.configure({ mode: 'serial' }); - - testWithSocket('new item after token balance update', async({ page, mount, createSocket }) => { - await mount( - - - - - - - , - { 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( - - - - - - - , - { 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'); - }); -}); diff --git a/ui/address/tokenSelect/TokenSelect.tsx b/ui/address/tokenSelect/TokenSelect.tsx index 9c54d9096e..18f5e5b43b 100644 --- a/ui/address/tokenSelect/TokenSelect.tsx +++ b/ui/address/tokenSelect/TokenSelect.tsx @@ -5,7 +5,6 @@ import NextLink from 'next/link'; import { useRouter } from 'next/router'; import React from 'react'; -import type { SocketMessage } from 'lib/socket/types'; import type { Address } from 'types/api/address'; import walletIcon from 'icons/wallet.svg'; @@ -13,8 +12,6 @@ import { getResourceKey } from 'lib/api/useApiQuery'; import useIsMobile from 'lib/hooks/useIsMobile'; import * as mixpanel from 'lib/mixpanel/index'; import getQueryParamString from 'lib/router/getQueryParamString'; -import useSocketChannel from 'lib/socket/useSocketChannel'; -import useSocketMessage from 'lib/socket/useSocketMessage'; import useFetchTokens from '../utils/useFetchTokens'; import TokenSelectDesktop from './TokenSelectDesktop'; @@ -28,14 +25,13 @@ const TokenSelect = ({ onClick }: Props) => { const router = useRouter(); const isMobile = useIsMobile(); const queryClient = useQueryClient(); - const [ blockNumber, setBlockNumber ] = React.useState(); const addressHash = getQueryParamString(router.query.hash); const addressResourceKey = getResourceKey('address', { pathParams: { hash: addressHash } }); const addressQueryData = queryClient.getQueryData
(addressResourceKey); - const { data, isError, isLoading, refetch } = useFetchTokens({ hash: addressQueryData?.hash }); + const { data, isError, isLoading } = useFetchTokens({ hash: addressQueryData?.hash }); const tokensResourceKey = getResourceKey('address_tokens', { pathParams: { hash: addressQueryData?.hash }, queryParams: { type: 'ERC-20' } }); const tokensIsFetching = useIsFetching({ queryKey: tokensResourceKey }); @@ -44,34 +40,6 @@ const TokenSelect = ({ onClick }: Props) => { 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 (isLoading) { return ( diff --git a/ui/address/tokens/AddressCollections.tsx b/ui/address/tokens/AddressCollections.tsx index 4bd542bd15..6ab437779b 100644 --- a/ui/address/tokens/AddressCollections.tsx +++ b/ui/address/tokens/AddressCollections.tsx @@ -44,7 +44,7 @@ const AddressCollections = ({ collectionsQuery, address }: Props) => { const hasOverload = Number(item.amount) > item.token_instances.length; return ( - + { noCopy fontWeight="600" /> - + { ` - ${ Number(item.amount).toLocaleString() } item${ Number(item.amount) > 1 ? 's' : '' }` } { hasOverload && ( - + View in collection ) } diff --git a/ui/address/tokens/NFTItem.tsx b/ui/address/tokens/NFTItem.tsx index c33038c323..50a33ab73b 100644 --- a/ui/address/tokens/NFTItem.tsx +++ b/ui/address/tokens/NFTItem.tsx @@ -31,7 +31,7 @@ const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: P /> - + ID# diff --git a/ui/address/utils/useFetchTokens.ts b/ui/address/utils/useFetchTokens.ts index 6a3ec6a960..bf4c5eecb8 100644 --- a/ui/address/utils/useFetchTokens.ts +++ b/ui/address/utils/useFetchTokens.ts @@ -1,13 +1,25 @@ +import { useQueryClient } from '@tanstack/react-query'; 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 { 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) { const erc20query = useApiQuery('address_tokens', { pathParams: { hash }, @@ -25,11 +37,67 @@ export default function useFetchTokens({ hash }: Props) { queryOptions: { enabled: Boolean(hash), refetchOnMount: false }, }); - const refetch = React.useCallback(() => { - erc20query.refetch(); - erc721query.refetch(); - erc1155query.refetch(); - }, [ erc1155query, erc20query, erc721query ]); + 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: 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(() => { return { @@ -52,6 +120,5 @@ export default function useFetchTokens({ hash }: Props) { isLoading: erc20query.isLoading || erc721query.isLoading || erc1155query.isLoading, isError: erc20query.isError || erc721query.isError || erc1155query.isError, data, - refetch, }; } diff --git a/ui/shared/RadioButtonGroup.tsx b/ui/shared/RadioButtonGroup.tsx index a89005c61a..518e58e579 100644 --- a/ui/shared/RadioButtonGroup.tsx +++ b/ui/shared/RadioButtonGroup.tsx @@ -1,11 +1,19 @@ -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 React from 'react'; -type RadioButtonProps = UseRadioProps & { - children: React.ReactNode; +type RadioItemProps = { + title: string; + icon?: React.FC>; + onlyIcon: false | undefined; +} | { + title: string; + icon: React.FC>; + onlyIcon: true; } +type RadioButtonProps = UseRadioProps & RadioItemProps; + const RadioButton = (props: RadioButtonProps) => { const { getInputProps, getRadioProps } = useRadio(props); const buttonColor = useColorModeValue('blue.50', 'gray.800'); @@ -13,30 +21,52 @@ const RadioButton = (props: RadioButtonProps) => { const input = getInputProps(); 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 ( + + ); + } + return ( ); }; @@ -45,7 +75,7 @@ type RadioButtonGroupProps = { onChange: (value: T) => void; name: string; defaultValue: string; - options: Array<{title: string; value: T}>; + options: Array<{ value: T } & RadioItemProps>; } const RadioButtonGroup = ({ onChange, name, defaultValue, options }: RadioButtonGroupProps) => { @@ -54,10 +84,10 @@ const RadioButtonGroup = ({ onChange, name, defaultValue, opti const group = getRootProps(); return ( - + { options.map((option) => { const props = getRadioProps({ value: option.value }); - return { option.title }; + return ; }) } );