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/lib/api/resources.ts b/lib/api/resources.ts
index d317d6d5ac..c1ddac5826 100644
--- a/lib/api/resources.ts
+++ b/lib/api/resources.ts
@@ -26,6 +26,9 @@ import type {
AddressTokensFilter,
AddressTokensResponse,
AddressWithdrawalsResponse,
+ AddressNFTsResponse,
+ AddressCollectionsResponse,
+ AddressNFTTokensFilter,
} from 'types/api/address';
import type { AddressesResponse } from 'types/api/addresses';
import type { BlocksResponse, BlockTransactionsResponse, Block, BlockFilters, BlockWithdrawalsResponse } from 'types/api/block';
@@ -51,6 +54,7 @@ import type {
TokenInstance,
TokenInstanceTransfersCount,
TokenVerifiedInfo,
+ TokenInventoryFilters,
} from 'types/api/token';
import type { TokensResponse, TokensFilters, TokensSorting, TokenInstanceTransferResponse, TokensBridgedFilters } from 'types/api/tokens';
import type { TokenTransferResponse, TokenTransferFilters } from 'types/api/tokenTransfer';
@@ -305,6 +309,16 @@ export const RESOURCES = {
pathParams: [ 'hash' as const ],
filterFields: [ 'type' as const ],
},
+ address_nfts: {
+ path: '/api/v2/addresses/:hash/nft',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'type' as const ],
+ },
+ address_collections: {
+ path: '/api/v2/addresses/:hash/nft/collections',
+ pathParams: [ 'hash' as const ],
+ filterFields: [ 'type' as const ],
+ },
address_withdrawals: {
path: '/api/v2/addresses/:hash/withdrawals',
pathParams: [ 'hash' as const ],
@@ -384,7 +398,7 @@ export const RESOURCES = {
token_inventory: {
path: '/api/v2/tokens/:hash/instances',
pathParams: [ 'hash' as const ],
- filterFields: [],
+ filterFields: [ 'holder_address_hash' as const ],
},
tokens: {
path: '/api/v2/tokens',
@@ -580,7 +594,7 @@ export type PaginatedResources = 'blocks' | 'block_txs' |
'addresses' |
'address_txs' | 'address_internal_txs' | 'address_token_transfers' | 'address_blocks_validated' | 'address_coin_balance' |
'search' |
-'address_logs' | 'address_tokens' |
+'address_logs' | 'address_tokens' | 'address_nfts' | 'address_collections' |
'token_transfers' | 'token_holders' | 'token_inventory' | 'tokens' | 'tokens_bridged' |
'token_instance_transfers' | 'token_instance_holders' |
'verified_contracts' |
@@ -642,6 +656,8 @@ Q extends 'address_coin_balance' ? AddressCoinBalanceHistoryResponse :
Q extends 'address_coin_balance_chart' ? AddressCoinBalanceHistoryChart :
Q extends 'address_logs' ? LogsResponseAddress :
Q extends 'address_tokens' ? AddressTokensResponse :
+Q extends 'address_nfts' ? AddressNFTsResponse :
+Q extends 'address_collections' ? AddressCollectionsResponse :
Q extends 'address_withdrawals' ? AddressWithdrawalsResponse :
Q extends 'token' ? TokenInfo :
Q extends 'token_verified_info' ? TokenVerifiedInfo :
@@ -695,7 +711,10 @@ Q extends 'token_transfers' ? TokenTransferFilters :
Q extends 'address_txs' | 'address_internal_txs' ? AddressTxsFilters :
Q extends 'address_token_transfers' ? AddressTokenTransferFilters :
Q extends 'address_tokens' ? AddressTokensFilter :
+Q extends 'address_nfts' ? AddressNFTTokensFilter :
+Q extends 'address_collections' ? AddressNFTTokensFilter :
Q extends 'search' ? SearchResultFilters :
+Q extends 'token_inventory' ? TokenInventoryFilters :
Q extends 'tokens' ? TokensFilters :
Q extends 'tokens_bridged' ? TokensBridgedFilters :
Q extends 'verified_contracts' ? VerifiedContractsFilters :
diff --git a/lib/cookies.ts b/lib/cookies.ts
index 9634ebfc15..df61547187 100644
--- a/lib/cookies.ts
+++ b/lib/cookies.ts
@@ -12,6 +12,7 @@ export enum NAMES {
INDEXING_ALERT='indexing_alert',
ADBLOCK_DETECTED='adblock_detected',
MIXPANEL_DEBUG='_mixpanel_debug',
+ ADDRESS_NFT_DISPLAY_TYPE='address_nft_display_type'
}
export function get(name?: NAMES | undefined | null, serverCookie?: string) {
diff --git a/lib/token/tokenTypes.ts b/lib/token/tokenTypes.ts
index c8f258b4fb..5246fc2418 100644
--- a/lib/token/tokenTypes.ts
+++ b/lib/token/tokenTypes.ts
@@ -1,9 +1,14 @@
-import type { TokenType } from 'types/api/token';
+import type { NFTTokenType, TokenType } from 'types/api/token';
-export const TOKEN_TYPES: Array<{ title: string; id: TokenType }> = [
- { title: 'ERC-20', id: 'ERC-20' },
+export const NFT_TOKEN_TYPES: Array<{ title: string; id: NFTTokenType }> = [
{ title: 'ERC-721', id: 'ERC-721' },
{ title: 'ERC-1155', id: 'ERC-1155' },
];
+export const TOKEN_TYPES: Array<{ title: string; id: TokenType }> = [
+ { title: 'ERC-20', id: 'ERC-20' },
+ ...NFT_TOKEN_TYPES,
+];
+
+export const NFT_TOKEN_TYPE_IDS = NFT_TOKEN_TYPES.map(i => i.id);
export const TOKEN_TYPE_IDS = TOKEN_TYPES.map(i => i.id);
diff --git a/mocks/address/tokens.ts b/mocks/address/tokens.ts
index 5f2299cee5..c8c7386134 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,51 @@ export const erc1155List = {
erc1155b,
],
};
+
+export const nfts: AddressNFTsResponse = {
+ items: [
+ {
+ ...tokenInstance.base,
+ token: tokens.tokenInfoERC1155a,
+ token_type: 'ERC-1155',
+ value: '11',
+ },
+ {
+ ...tokenInstance.unique,
+ token: tokens.tokenInfoERC721a,
+ 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/stubs/address.ts b/stubs/address.ts
index f4c3ec2f52..c2e0ffd7c6 100644
--- a/stubs/address.ts
+++ b/stubs/address.ts
@@ -1,4 +1,12 @@
-import type { Address, AddressCoinBalanceHistoryItem, AddressCounters, AddressTabsCounters, AddressTokenBalance } from 'types/api/address';
+import type {
+ Address,
+ AddressCoinBalanceHistoryItem,
+ AddressCollection,
+ AddressCounters,
+ AddressNFT,
+ AddressTabsCounters,
+ AddressTokenBalance,
+} from 'types/api/address';
import type { AddressesItem } from 'types/api/addresses';
import { ADDRESS_HASH } from './addressParams';
@@ -80,16 +88,22 @@ export const ADDRESS_TOKEN_BALANCE_ERC_20: AddressTokenBalance = {
value: '1000000000000000000000000',
};
-export const ADDRESS_TOKEN_BALANCE_ERC_721: AddressTokenBalance = {
+export const ADDRESS_NFT_721: AddressNFT = {
+ token_type: 'ERC-721',
token: TOKEN_INFO_ERC_721,
- token_id: null,
- token_instance: null,
- value: '176',
+ value: '1',
+ ...TOKEN_INSTANCE,
+};
+
+export const ADDRESS_NFT_1155: AddressNFT = {
+ token_type: 'ERC-1155',
+ token: TOKEN_INFO_ERC_1155,
+ value: '10',
+ ...TOKEN_INSTANCE,
};
-export const ADDRESS_TOKEN_BALANCE_ERC_1155: AddressTokenBalance = {
+export const ADDRESS_COLLECTION: AddressCollection = {
token: TOKEN_INFO_ERC_1155,
- token_id: '188882',
- token_instance: TOKEN_INSTANCE,
- value: '176',
+ amount: '4',
+ token_instances: Array(4).fill(TOKEN_INSTANCE),
};
diff --git a/types/api/address.ts b/types/api/address.ts
index 10dc6aa757..7edd55659e 100644
--- a/types/api/address.ts
+++ b/types/api/address.ts
@@ -3,7 +3,7 @@ import type { Transaction } from 'types/api/transaction';
import type { UserTags } from './addressParams';
import type { Block } from './block';
import type { InternalTransaction } from './internalTransaction';
-import type { TokenInfo, TokenInstance, TokenType } from './token';
+import type { NFTTokenType, TokenInfo, TokenInstance, TokenType } from './token';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address extends UserTags {
@@ -49,17 +49,47 @@ export interface AddressTokenBalance {
token_instance: TokenInstance | null;
}
+export type AddressNFT = TokenInstance & {
+ token: TokenInfo;
+ token_type: Omit;
+ value: string;
+}
+
+export type AddressCollection = {
+ token: TokenInfo;
+ amount: string;
+ token_instances: Array>;
+}
+
export interface AddressTokensResponse {
items: Array;
next_page_params: {
items_count: number;
- token_name: 'string' | null;
+ token_name: string | null;
token_type: TokenType;
value: number;
fiat_value: string | null;
} | null;
}
+export interface AddressNFTsResponse {
+ items: Array;
+ next_page_params: {
+ items_count: number;
+ token_id: string;
+ token_type: TokenType;
+ token_contract_address_hash: string;
+ } | null;
+}
+
+export interface AddressCollectionsResponse {
+ items: Array;
+ next_page_params: {
+ token_contract_address_hash: string;
+ token_type: TokenType;
+ } | null;
+}
+
export interface AddressTokensBalancesSocketMessage {
overflow: boolean;
token_balances: Array;
@@ -97,6 +127,10 @@ export type AddressTokensFilter = {
type: TokenType;
}
+export type AddressNFTTokensFilter = {
+ type: Array | undefined;
+}
+
export interface AddressCoinBalanceHistoryItem {
block_number: number;
block_timestamp: string;
diff --git a/types/api/token.ts b/types/api/token.ts
index 681dde2075..417c74aab6 100644
--- a/types/api/token.ts
+++ b/types/api/token.ts
@@ -1,7 +1,8 @@
import type { TokenInfoApplication } from './account';
import type { AddressParam } from './addressParams';
-export type TokenType = 'ERC-20' | 'ERC-721' | 'ERC-1155';
+export type NFTTokenType = 'ERC-721' | 'ERC-1155';
+export type TokenType = 'ERC-20' | NFTTokenType;
export interface TokenInfo {
address: string;
@@ -77,3 +78,7 @@ export type TokenInventoryPagination = {
}
export type TokenVerifiedInfo = Omit;
+
+export type TokenInventoryFilters = {
+ holder_address_hash?: string;
+}
diff --git a/ui/address/AddressTokenTransfers.tsx b/ui/address/AddressTokenTransfers.tsx
index 0355618015..0ebba17f8e 100644
--- a/ui/address/AddressTokenTransfers.tsx
+++ b/ui/address/AddressTokenTransfers.tsx
@@ -1,4 +1,4 @@
-import { Flex, Hide, Icon, Show, Text, Tooltip, useColorModeValue } from '@chakra-ui/react';
+import { Flex, Hide, Show, Text } from '@chakra-ui/react';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
@@ -9,7 +9,6 @@ import type { AddressFromToFilter, AddressTokenTransferResponse } from 'types/ap
import type { TokenType } from 'types/api/token';
import type { TokenTransfer } from 'types/api/tokenTransfer';
-import crossIcon from 'icons/cross.svg';
import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
@@ -26,6 +25,7 @@ import * as TokenEntity from 'ui/shared/entities/token/TokenEntity';
import HashStringShorten from 'ui/shared/HashStringShorten';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
+import ResetIconButton from 'ui/shared/ResetIconButton';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TokenTransferFilter from 'ui/shared/TokenTransfer/TokenTransferFilter';
import TokenTransferList from 'ui/shared/TokenTransfer/TokenTransferList';
@@ -115,9 +115,6 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
onFilterChange({});
}, [ onFilterChange ]);
- const resetTokenIconColor = useColorModeValue('blue.600', 'blue.300');
- const resetTokenIconHoverColor = useColorModeValue('blue.400', 'blue.200');
-
const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => {
setSocketAlert('');
@@ -235,19 +232,7 @@ const AddressTokenTransfers = ({ scrollRef, overloadCount = OVERLOAD_COUNT }: Pr
{ isMobile ? : tokenFilter }
-
-
-
-
-
+
);
diff --git a/ui/address/AddressTokens.pw.tsx b/ui/address/AddressTokens.pw.tsx
index 3cf3b34e93..7ee5556986 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 }) + '?type=';
+const API_URL_COLLECTIONS = buildApiUrl('address_collections', { hash: ADDRESS_HASH }) + '?type=';
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 1d693bd4d6..6aae805c47 100644
--- a/ui/address/AddressTokens.tsx
+++ b/ui/address/AddressTokens.tsx
@@ -1,47 +1,46 @@
-import { Box } from '@chakra-ui/react';
-import { useQueryClient } from '@tanstack/react-query';
+import { Box, HStack } from '@chakra-ui/react';
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 { NFTTokenType } 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 getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery';
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_1155, ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_TOKEN_BALANCE_ERC_721 } from 'stubs/address';
+import { NFT_TOKEN_TYPE_IDS } from 'lib/token/tokenTypes';
+import { ADDRESS_TOKEN_BALANCE_ERC_20, ADDRESS_NFT_1155, ADDRESS_COLLECTION } from 'stubs/address';
import { generateListStub } from 'stubs/utils';
-import { tokenTabsByType } from 'ui/pages/Address';
+import PopoverFilter from 'ui/shared/filters/PopoverFilter';
+import TokenTypeFilter from 'ui/shared/filters/TokenTypeFilter';
import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
+import RadioButtonGroup from 'ui/shared/radioButtonGroup/RadioButtonGroup';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
-import ERC1155Tokens from './tokens/ERC1155Tokens';
+import AddressCollections from './tokens/AddressCollections';
+import AddressNFTs from './tokens/AddressNFTs';
import ERC20Tokens from './tokens/ERC20Tokens';
-import ERC721Tokens from './tokens/ERC721Tokens';
import TokenBalances from './tokens/TokenBalances';
+type TNftDisplayType = 'collection' | 'list';
+
const TAB_LIST_PROPS = {
- marginBottom: 0,
+ my: 3,
py: 5,
- marginTop: 3,
columnGap: 3,
};
const TAB_LIST_PROPS_MOBILE = {
- mt: 8,
+ my: 8,
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 getTokenFilterValue = (getFilterValuesFromQuery).bind(null, NFT_TOKEN_TYPE_IDS);
const AddressTokens = () => {
const router = useRouter();
@@ -49,6 +48,10 @@ 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' : 'collection');
+ const [ tokenTypes, setTokenTypes ] = React.useState | undefined>(getTokenFilterValue(router.query.type) || []);
+
const tab = getQueryParamString(router.query.tab);
const hash = getQueryParamString(router.query.hash);
@@ -58,111 +61,100 @@ const AddressTokens = () => {
filters: { type: 'ERC-20' },
scrollRef,
options: {
+ enabled: !tab || tab === 'tokens_erc20',
refetchOnMount: false,
placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_20, 10, { next_page_params: null }),
},
});
- const erc721Query = useQueryWithPages({
- resourceName: 'address_tokens',
+ const collectionsQuery = useQueryWithPages({
+ resourceName: 'address_collections',
pathParams: { hash },
- filters: { type: 'ERC-721' },
scrollRef,
options: {
- refetchOnMount: false,
- placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_721, 10, { next_page_params: null }),
+ enabled: tab === 'tokens_nfts' && nftDisplayType === 'collection',
+ placeholderData: generateListStub<'address_collections'>(ADDRESS_COLLECTION, 10, { next_page_params: null }),
},
+ filters: { type: tokenTypes },
});
- const erc1155Query = useQueryWithPages({
- resourceName: 'address_tokens',
+ const nftsQuery = useQueryWithPages({
+ resourceName: 'address_nfts',
pathParams: { hash },
- filters: { type: 'ERC-1155' },
scrollRef,
options: {
- refetchOnMount: false,
- placeholderData: generateListStub<'address_tokens'>(ADDRESS_TOKEN_BALANCE_ERC_1155, 10, { next_page_params: null }),
+ enabled: tab === 'tokens_nfts' && nftDisplayType === 'list',
+ placeholderData: generateListStub<'address_nfts'>(ADDRESS_NFT_1155, 10, { next_page_params: null }),
},
+ filters: { type: tokenTypes },
});
- 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 || erc721Query.isPlaceholderData || erc1155Query.isPlaceholderData,
- });
+ const handleNFTsDisplayTypeChange = React.useCallback((val: TNftDisplayType) => {
+ cookies.set(cookies.NAMES.ADDRESS_NFT_DISPLAY_TYPE, val);
+ setNftDisplayType(val);
+ }, []);
+
+ const handleTokenTypesChange = React.useCallback((value: Array) => {
+ nftsQuery.onFilterChange({ type: value });
+ collectionsQuery.onFilterChange({ type: value });
+ setTokenTypes(value);
+ }, [ nftsQuery, collectionsQuery ]);
+
+ const nftTypeFilter = (
+ 0 } contentProps={{ w: '200px' }} appliedFiltersNum={ tokenTypes?.length }>
+ nftOnly onChange={ handleTokenTypesChange } defaultValue={ tokenTypes }/>
+
+ );
- 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 hasActiveFilters = Boolean(tokenTypes?.length);
const tabs = [
- { id: tokenTabsByType['ERC-20'], title: 'ERC-20', component: },
- { id: tokenTabsByType['ERC-721'], title: 'ERC-721', component: },
- { id: tokenTabsByType['ERC-1155'], title: 'ERC-1155', component: },
+ { id: 'tokens_erc20', title: 'ERC-20', component: },
+ {
+ id: 'tokens_nfts',
+ title: 'NFTs',
+ component: nftDisplayType === 'list' ?
+ :
+ ,
+ },
];
+ const nftDisplayTypeRadio = (
+
+ onChange={ handleNFTsDisplayTypeChange }
+ defaultValue={ nftDisplayType }
+ name="type"
+ options={ [
+ { title: 'By collection', value: 'collection', icon: collectionIcon, onlyIcon: isMobile },
+ { title: 'List', value: 'list', icon: listIcon, onlyIcon: isMobile },
+ ] }
+ />
+ );
+
let pagination: PaginationParams | undefined;
- if (tab === tokenTabsByType['ERC-1155']) {
- pagination = erc1155Query.pagination;
- } else if (tab === tokenTabsByType['ERC-721']) {
- pagination = erc721Query.pagination;
+ if (tab === 'tokens_nfts') {
+ pagination = nftDisplayType === 'list' ? nftsQuery.pagination : collectionsQuery.pagination;
} else {
pagination = erc20Query.pagination;
}
+ const hasNftData =
+ (!nftsQuery.isPlaceholderData && nftsQuery.data?.items.length) ||
+ (!collectionsQuery.isPlaceholderData && collectionsQuery.data?.items.length);
+
+ const isNftTab = tab !== 'tokens' && tab !== 'tokens_erc20';
+
+ const rightSlot = (
+ <>
+
+ { isNftTab && (hasNftData || hasActiveFilters) && nftDisplayTypeRadio }
+ { isNftTab && (hasNftData || hasActiveFilters) && nftTypeFilter }
+
+ { pagination.isVisible && !isMobile && }
+ >
+ );
+
return (
<>
@@ -174,7 +166,8 @@ const AddressTokens = () => {
colorScheme="gray"
size="sm"
tabListProps={ isMobile ? TAB_LIST_PROPS_MOBILE : TAB_LIST_PROPS }
- rightSlot={ pagination.isVisible && !isMobile ? : null }
+ rightSlot={ rightSlot }
+ rightSlotProps={ tab !== 'tokens_erc20' && !isMobile ? { flexGrow: 1, display: 'flex', justifyContent: 'space-between', ml: 8 } : {} }
stickyEnabled={ !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..2cfbdaf3a7
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..988d310f09 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..ff3f91748f
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..185fa05a5c
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..5cb20256d2 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..d2aacbe810
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..9325e2a5b4 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..cb01e73aa2
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..f4ca1946ec
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..fab130576d 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..4ada42569e 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 11b358e3f7..ca03f06879 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, 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 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 (isPending) {
return (
diff --git a/ui/address/tokens/AddressCollections.tsx b/ui/address/tokens/AddressCollections.tsx
new file mode 100644
index 0000000000..96b8326116
--- /dev/null
+++ b/ui/address/tokens/AddressCollections.tsx
@@ -0,0 +1,116 @@
+import { Box, Flex, Text, Grid, HStack, Skeleton } from '@chakra-ui/react';
+import React from 'react';
+
+import { route } from 'nextjs-routes';
+
+import useIsMobile from 'lib/hooks/useIsMobile';
+import { apos } from 'lib/html-entities';
+import ActionBar from 'ui/shared/ActionBar';
+import DataListDisplay from 'ui/shared/DataListDisplay';
+import TokenEntity from 'ui/shared/entities/token/TokenEntity';
+import LinkInternal from 'ui/shared/LinkInternal';
+import NftFallback from 'ui/shared/nft/NftFallback';
+import Pagination from 'ui/shared/pagination/Pagination';
+import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
+
+import NFTItem from './NFTItem';
+import NFTItemContainer from './NFTItemContainer';
+
+type Props = {
+ collectionsQuery: QueryWithPagesResult<'address_collections'>;
+ address: string;
+ hasActiveFilters: boolean;
+}
+
+const AddressCollections = ({ collectionsQuery, address, hasActiveFilters }: Props) => {
+ const isMobile = useIsMobile();
+
+ const { isError, isPlaceholderData, data, pagination } = collectionsQuery;
+
+ const actionBar = isMobile && pagination.isVisible && (
+
+
+
+ );
+
+ const content = data?.items ? data?.items.map((item, index) => {
+ const collectionUrl = route({
+ pathname: '/token/[hash]',
+ query: {
+ hash: item.token.address,
+ tab: 'inventory',
+ holder_address_hash: address,
+ scroll_to_tabs: 'true',
+ },
+ });
+ const hasOverload = Number(item.amount) > item.token_instances.length;
+ return (
+
+
+
+
+ { ` - ${ Number(item.amount).toLocaleString() } item${ Number(item.amount) > 1 ? 's' : '' }` }
+
+
+ View in collection
+
+
+
+ { item.token_instances.map((instance, index) => {
+ const key = item.token.address + '_' + (instance.id && !isPlaceholderData ? `id_${ instance.id }` : `index_${ index }`);
+
+ return (
+
+ );
+ }) }
+ { hasOverload && (
+
+
+
+
+
+
+
+ View all NFTs
+
+
+ ) }
+
+
+ );
+ }) : null;
+
+ return (
+
+ );
+};
+
+export default AddressCollections;
diff --git a/ui/address/tokens/ERC1155Tokens.tsx b/ui/address/tokens/AddressNFTs.tsx
similarity index 72%
rename from ui/address/tokens/ERC1155Tokens.tsx
rename to ui/address/tokens/AddressNFTs.tsx
index 245fbec635..1c559e8d08 100644
--- a/ui/address/tokens/ERC1155Tokens.tsx
+++ b/ui/address/tokens/AddressNFTs.tsx
@@ -2,6 +2,7 @@ import { Grid } from '@chakra-ui/react';
import React from 'react';
import useIsMobile from 'lib/hooks/useIsMobile';
+import { apos } from 'lib/html-entities';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
import Pagination from 'ui/shared/pagination/Pagination';
@@ -10,10 +11,11 @@ import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPage
import NFTItem from './NFTItem';
type Props = {
- tokensQuery: QueryWithPagesResult<'address_tokens'>;
+ tokensQuery: QueryWithPagesResult<'address_nfts'>;
+ hasActiveFilters: boolean;
}
-const ERC1155Tokens = ({ tokensQuery }: Props) => {
+const AddressNFTs = ({ tokensQuery, hasActiveFilters }: Props) => {
const isMobile = useIsMobile();
const { isError, isPlaceholderData, data, pagination } = tokensQuery;
@@ -32,13 +34,14 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
gridTemplateColumns={{ base: 'repeat(2, calc((100% - 12px)/2))', lg: 'repeat(auto-fill, minmax(210px, 1fr))' }}
>
{ data.items.map((item, index) => {
- const key = item.token.address + '_' + (item.token_instance?.id && !isPlaceholderData ? `id_${ item.token_instance?.id }` : `index_${ index }`);
+ const key = item.token.address + '_' + (item.id && !isPlaceholderData ? `id_${ item.id }` : `index_${ index }`);
return (
);
}) }
@@ -52,8 +55,12 @@ const ERC1155Tokens = ({ tokensQuery }: Props) => {
emptyText="There are no tokens of selected type."
content={ content }
actionBar={ actionBar }
+ filterProps={{
+ emptyFilteredText: `Couldn${ apos }t find any token that matches your query.`,
+ hasActiveFilters,
+ }}
/>
);
};
-export default ERC1155Tokens;
+export default AddressNFTs;
diff --git a/ui/address/tokens/ERC721Tokens.tsx b/ui/address/tokens/ERC721Tokens.tsx
deleted file mode 100644
index 8877703844..0000000000
--- a/ui/address/tokens/ERC721Tokens.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import { Show, Hide } from '@chakra-ui/react';
-import React from 'react';
-
-import useIsMobile from 'lib/hooks/useIsMobile';
-import ActionBar from 'ui/shared/ActionBar';
-import DataListDisplay from 'ui/shared/DataListDisplay';
-import Pagination from 'ui/shared/pagination/Pagination';
-import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
-
-import ERC721TokensListItem from './ERC721TokensListItem';
-import ERC721TokensTable from './ERC721TokensTable';
-
-type Props = {
- tokensQuery: QueryWithPagesResult<'address_tokens'>;
-}
-
-const ERC721Tokens = ({ tokensQuery }: Props) => {
- const isMobile = useIsMobile();
-
- const { isError, isPlaceholderData, data, pagination } = tokensQuery;
-
- const actionBar = isMobile && pagination.isVisible && (
-
-
-
- );
-
- const content = data?.items ? (
- <>
-
- { data.items.map((item, index) => (
-
- )) }>
- ) : null;
-
- return (
-
- );
-
-};
-
-export default ERC721Tokens;
diff --git a/ui/address/tokens/ERC721TokensListItem.tsx b/ui/address/tokens/ERC721TokensListItem.tsx
deleted file mode 100644
index 66cf9e3911..0000000000
--- a/ui/address/tokens/ERC721TokensListItem.tsx
+++ /dev/null
@@ -1,48 +0,0 @@
-import { Flex, HStack, Skeleton } from '@chakra-ui/react';
-import { useRouter } from 'next/router';
-import React from 'react';
-
-import type { AddressTokenBalance } from 'types/api/address';
-
-import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
-import AddressEntity from 'ui/shared/entities/address/AddressEntity';
-import TokenEntityWithAddressFilter from 'ui/shared/entities/token/TokenEntityWithAddressFilter';
-import ListItemMobile from 'ui/shared/ListItemMobile/ListItemMobile';
-
-type Props = AddressTokenBalance & { isLoading: boolean};
-
-const ERC721TokensListItem = ({ token, value, isLoading }: Props) => {
- const router = useRouter();
-
- const hash = router.query.hash?.toString() || '';
-
- return (
-
-
-
-
-
-
-
- Quantity
-
- { value }
-
-
-
- );
-};
-
-export default ERC721TokensListItem;
diff --git a/ui/address/tokens/ERC721TokensTable.tsx b/ui/address/tokens/ERC721TokensTable.tsx
deleted file mode 100644
index 9490e2cb58..0000000000
--- a/ui/address/tokens/ERC721TokensTable.tsx
+++ /dev/null
@@ -1,35 +0,0 @@
-import { Table, Tbody, Tr, Th } from '@chakra-ui/react';
-import React from 'react';
-
-import type { AddressTokenBalance } from 'types/api/address';
-
-import { default as Thead } from 'ui/shared/TheadSticky';
-
-import ERC721TokensTableItem from './ERC721TokensTableItem';
-
-interface Props {
- data: Array;
- top: number;
- isLoading: boolean;
-}
-
-const ERC721TokensTable = ({ data, top, isLoading }: Props) => {
- return (
-
-
-
- Asset |
- Contract address |
- Quantity |
-
-
-
- { data.map((item, index) => (
-
- )) }
-
-
- );
-};
-
-export default ERC721TokensTable;
diff --git a/ui/address/tokens/ERC721TokensTableItem.tsx b/ui/address/tokens/ERC721TokensTableItem.tsx
deleted file mode 100644
index fdb8c04611..0000000000
--- a/ui/address/tokens/ERC721TokensTableItem.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import { Tr, Td, Flex, Skeleton } from '@chakra-ui/react';
-import { useRouter } from 'next/router';
-import React from 'react';
-
-import type { AddressTokenBalance } from 'types/api/address';
-
-import AddressAddToWallet from 'ui/shared/address/AddressAddToWallet';
-import AddressEntity from 'ui/shared/entities/address/AddressEntity';
-import TokenEntityWithAddressFilter from 'ui/shared/entities/token/TokenEntityWithAddressFilter';
-
-type Props = AddressTokenBalance & { isLoading: boolean};
-
-const ERC721TokensTableItem = ({
- token,
- value,
- isLoading,
-}: Props) => {
- const router = useRouter();
-
- const hash = router.query.hash?.toString() || '';
-
- return (
-
-
-
- |
-
-
-
-
-
- |
-
-
- { value }
-
- |
-
- );
-};
-
-export default React.memo(ERC721TokensTableItem);
diff --git a/ui/address/tokens/NFTItem.tsx b/ui/address/tokens/NFTItem.tsx
index b4439d7f66..50a33ab73b 100644
--- a/ui/address/tokens/NFTItem.tsx
+++ b/ui/address/tokens/NFTItem.tsx
@@ -1,7 +1,7 @@
-import { Box, Flex, Text, Link, useColorModeValue } from '@chakra-ui/react';
+import { Tag, Flex, Text, Link, Skeleton, LightMode } from '@chakra-ui/react';
import React from 'react';
-import type { AddressTokenBalance } from 'types/api/address';
+import type { AddressNFT } from 'types/api/address';
import { route } from 'nextjs-routes';
@@ -9,22 +9,20 @@ import NftEntity from 'ui/shared/entities/nft/NftEntity';
import TokenEntity from 'ui/shared/entities/token/TokenEntity';
import NftMedia from 'ui/shared/nft/NftMedia';
-type Props = AddressTokenBalance & { isLoading: boolean };
+import NFTItemContainer from './NFTItemContainer';
-const NFTItem = ({ token, token_id: tokenId, token_instance: tokenInstance, isLoading }: Props) => {
- const tokenInstanceLink = tokenId ? route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenId } }) : undefined;
+type Props = AddressNFT & { isLoading: boolean; withTokenLink?: boolean };
+
+const NFTItem = ({ token, value, isLoading, withTokenLink, ...tokenInstance }: Props) => {
+ const tokenInstanceLink = tokenInstance.id ?
+ route({ pathname: '/token/[hash]/instance/[id]', query: { hash: token.address, id: tokenInstance.id } }) :
+ undefined;
return (
-
+
+
+ { token.type }
+
- { tokenId && (
-
+
+
ID#
-
+
+
+ { Number(value) > 1 && Qty { value } }
+
+
+ { withTokenLink && (
+
) }
-
-
+
);
};
diff --git a/ui/address/tokens/NFTItemContainer.tsx b/ui/address/tokens/NFTItemContainer.tsx
new file mode 100644
index 0000000000..fc3d906d3e
--- /dev/null
+++ b/ui/address/tokens/NFTItemContainer.tsx
@@ -0,0 +1,27 @@
+import { Box, useColorModeValue, chakra } from '@chakra-ui/react';
+import React from 'react';
+
+type Props = {
+ children: React.ReactNode;
+ className?: string;
+};
+
+const NFTItemContainer = ({ children, className }: Props) => {
+ return (
+
+ { children }
+
+ );
+};
+
+export default chakra(NFTItemContainer);
diff --git a/ui/address/utils/useFetchTokens.ts b/ui/address/utils/useFetchTokens.ts
index 9bc003373a..9b5bcda583 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) {
isPending: erc20query.isPending || erc721query.isPending || erc1155query.isPending,
isError: erc20query.isError || erc721query.isError || erc1155query.isError,
data,
- refetch,
};
}
diff --git a/ui/pages/Address.tsx b/ui/pages/Address.tsx
index 51c58d493e..eb4b95dc36 100644
--- a/ui/pages/Address.tsx
+++ b/ui/pages/Address.tsx
@@ -2,7 +2,6 @@ import { Box, Flex, HStack, Icon } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import React from 'react';
-import type { TokenType } from 'types/api/token';
import type { RoutedTab } from 'ui/shared/Tabs/types';
import config from 'configs/app';
@@ -36,13 +35,7 @@ import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/Tabs/RoutedTabs';
import TabsSkeleton from 'ui/shared/Tabs/TabsSkeleton';
-export const tokenTabsByType: Record = {
- 'ERC-20': 'tokens_erc20',
- 'ERC-721': 'tokens_erc721',
- 'ERC-1155': 'tokens_erc1155',
-} as const;
-
-const TOKEN_TABS = Object.values(tokenTabsByType);
+const TOKEN_TABS = [ 'tokens_erc20', 'tokens_nfts', 'tokens_nfts_collection', 'tokens_nfts_list' ];
const AddressPageContent = () => {
const router = useRouter();
diff --git a/ui/pages/Token.tsx b/ui/pages/Token.tsx
index 18e9f2dd05..20a3cf5dfd 100644
--- a/ui/pages/Token.tsx
+++ b/ui/pages/Token.tsx
@@ -56,6 +56,7 @@ const TokenPageContent = () => {
const hashString = getQueryParamString(router.query.hash);
const tab = getQueryParamString(router.query.tab);
+ const ownerFilter = getQueryParamString(router.query.holder_address_hash) || undefined;
const queryClient = useQueryClient();
@@ -140,6 +141,7 @@ const TokenPageContent = () => {
const inventoryQuery = useQueryWithPages({
resourceName: 'token_inventory',
pathParams: { hash: hashString },
+ filters: ownerFilter ? { holder_address_hash: ownerFilter } : {},
scrollRef,
options: {
enabled: Boolean(
@@ -150,7 +152,7 @@ const TokenPageContent = () => {
tab === 'inventory'
),
),
- placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE, 50, { next_page_params: null }),
+ placeholderData: generateListStub<'token_inventory'>(tokenStubs.TOKEN_INSTANCE, 50, { next_page_params: { unique_token: 1 } }),
},
});
@@ -173,9 +175,11 @@ const TokenPageContent = () => {
const contractTabs = useContractTabs(contractQuery.data);
const tabs: Array = [
- (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ?
- { id: 'inventory', title: 'Inventory', component: } :
- undefined,
+ (tokenQuery.data?.type === 'ERC-1155' || tokenQuery.data?.type === 'ERC-721') ? {
+ id: 'inventory',
+ title: 'Inventory',
+ component: ,
+ } : undefined,
{ id: 'token_transfers', title: 'Token transfers', component: },
{ id: 'holders', title: 'Holders', component: },
contractQuery.data?.is_contract ? {
diff --git a/ui/pages/Tokens.tsx b/ui/pages/Tokens.tsx
index 2442470b37..1cac311216 100644
--- a/ui/pages/Tokens.tsx
+++ b/ui/pages/Tokens.tsx
@@ -105,7 +105,7 @@ const Tokens = () => {
) : (
0 } contentProps={{ w: '200px' }} appliedFiltersNum={ tokenTypes?.length }>
-
+ onChange={ handleTokenTypesChange } defaultValue={ tokenTypes } nftOnly={ false }/>
);
diff --git a/ui/shared/ResetIconButton.tsx b/ui/shared/ResetIconButton.tsx
new file mode 100644
index 0000000000..53519eb1d1
--- /dev/null
+++ b/ui/shared/ResetIconButton.tsx
@@ -0,0 +1,31 @@
+import { Tooltip, Flex, Icon, useColorModeValue } from '@chakra-ui/react';
+import React from 'react';
+
+import crossIcon from 'icons/cross.svg';
+
+type Props = {
+ onClick: () => void;
+}
+
+const ResetIconButton = ({ onClick }: Props) => {
+ const resetTokenIconColor = useColorModeValue('blue.600', 'blue.300');
+ const resetTokenIconHoverColor = useColorModeValue('blue.400', 'blue.200');
+
+ return (
+
+
+
+
+
+ );
+};
+
+export default ResetIconButton;
diff --git a/ui/shared/TokenTransfer/TokenTransferFilter.tsx b/ui/shared/TokenTransfer/TokenTransferFilter.tsx
index c6bc95b713..137c545b34 100644
--- a/ui/shared/TokenTransfer/TokenTransferFilter.tsx
+++ b/ui/shared/TokenTransfer/TokenTransferFilter.tsx
@@ -56,7 +56,7 @@ const TokenTransferFilter = ({
>
) }
Type
-
+ onChange={ onTypeFilterChange } defaultValue={ defaultTypeFilters } nftOnly={ false }/>
);
};
diff --git a/ui/shared/filters/TokenTypeFilter.tsx b/ui/shared/filters/TokenTypeFilter.tsx
index 3aec39fa48..9d80814284 100644
--- a/ui/shared/filters/TokenTypeFilter.tsx
+++ b/ui/shared/filters/TokenTypeFilter.tsx
@@ -1,16 +1,16 @@
import { CheckboxGroup, Checkbox, Text, Flex, Link, useCheckboxGroup } from '@chakra-ui/react';
import React from 'react';
-import type { TokenType } from 'types/api/token';
+import type { NFTTokenType, TokenType } from 'types/api/token';
-import { TOKEN_TYPES } from 'lib/token/tokenTypes';
+import { NFT_TOKEN_TYPES, TOKEN_TYPES } from 'lib/token/tokenTypes';
-type Props = {
- onChange: (nextValue: Array) => void;
- defaultValue?: Array;
+type Props = {
+ onChange: (nextValue: Array) => void;
+ defaultValue?: Array;
+ nftOnly: T extends NFTTokenType ? true : false;
}
-
-const TokenTypeFilter = ({ onChange, defaultValue }: Props) => {
+const TokenTypeFilter = ({ nftOnly, onChange, defaultValue }: Props) => {
const { value, setValue } = useCheckboxGroup({ defaultValue });
const handleReset = React.useCallback(() => {
@@ -21,7 +21,7 @@ const TokenTypeFilter = ({ onChange, defaultValue }: Props) => {
onChange([]);
}, [ onChange, setValue, value.length ]);
- const handleChange = React.useCallback((nextValue: Array) => {
+ const handleChange = React.useCallback((nextValue: Array) => {
setValue(nextValue);
onChange(nextValue);
}, [ onChange, setValue ]);
@@ -32,6 +32,7 @@ const TokenTypeFilter = ({ onChange, defaultValue }: Props) => {
Type
0 ? 'pointer' : 'unset' }
color={ value.length > 0 ? 'link' : 'text_secondary' }
_hover={{
color: value.length > 0 ? 'link_hovered' : 'text_secondary',
@@ -41,7 +42,7 @@ const TokenTypeFilter = ({ onChange, defaultValue }: Props) => {
- { TOKEN_TYPES.map(({ title, id }) => (
+ { (nftOnly ? NFT_TOKEN_TYPES : TOKEN_TYPES).map(({ title, id }) => (
{ title }
diff --git a/ui/shared/radioButtonGroup/RadioButtonGroup.pw.tsx b/ui/shared/radioButtonGroup/RadioButtonGroup.pw.tsx
new file mode 100644
index 0000000000..01e60fc43b
--- /dev/null
+++ b/ui/shared/radioButtonGroup/RadioButtonGroup.pw.tsx
@@ -0,0 +1,19 @@
+import { Box } from '@chakra-ui/react';
+import { test, expect } from '@playwright/experimental-ct-react';
+import React from 'react';
+
+import TestApp from 'playwright/TestApp';
+
+import RadioButtonGroupTest from './specs/RadioButtonGroupTest';
+
+test('radio button group', async({ mount }) => {
+ const component = await mount(
+
+
+
+
+ ,
+ );
+
+ await expect(component).toHaveScreenshot();
+});
diff --git a/ui/shared/radioButtonGroup/RadioButtonGroup.tsx b/ui/shared/radioButtonGroup/RadioButtonGroup.tsx
new file mode 100644
index 0000000000..27a8d70fa3
--- /dev/null
+++ b/ui/shared/radioButtonGroup/RadioButtonGroup.tsx
@@ -0,0 +1,97 @@
+import { ButtonGroup, Button, Flex, Icon, useRadio, useRadioGroup, useColorModeValue } from '@chakra-ui/react';
+import type { UseRadioProps } from '@chakra-ui/react';
+import React from 'react';
+
+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');
+ const checkedTextColor = useColorModeValue('blue.700', 'gray.50');
+
+ 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: checkedTextColor } : {}),
+ };
+
+ if (props.onlyIcon) {
+ return (
+
+ );
+ }
+
+ return (
+ : undefined }
+ { ...styleProps }
+ >
+
+
+ { props.title }
+
+
+ );
+};
+
+type RadioButtonGroupProps = {
+ onChange: (value: T) => void;
+ name: string;
+ defaultValue: string;
+ options: Array<{ value: T } & RadioItemProps>;
+}
+
+const RadioButtonGroup = ({ onChange, name, defaultValue, options }: RadioButtonGroupProps) => {
+ const { getRootProps, getRadioProps } = useRadioGroup({ name, defaultValue, onChange });
+
+ const group = getRootProps();
+
+ return (
+
+ { options.map((option) => {
+ const props = getRadioProps({ value: option.value });
+ return ;
+ }) }
+
+ );
+};
+
+export default RadioButtonGroup;
diff --git a/ui/shared/radioButtonGroup/__screenshots__/RadioButtonGroup.pw.tsx_default_radio-button-group-1.png b/ui/shared/radioButtonGroup/__screenshots__/RadioButtonGroup.pw.tsx_default_radio-button-group-1.png
new file mode 100644
index 0000000000..212b0b3e89
Binary files /dev/null and b/ui/shared/radioButtonGroup/__screenshots__/RadioButtonGroup.pw.tsx_default_radio-button-group-1.png differ
diff --git a/ui/shared/radioButtonGroup/specs/RadioButtonGroupTest.tsx b/ui/shared/radioButtonGroup/specs/RadioButtonGroupTest.tsx
new file mode 100644
index 0000000000..e62adedcc2
--- /dev/null
+++ b/ui/shared/radioButtonGroup/specs/RadioButtonGroupTest.tsx
@@ -0,0 +1,32 @@
+import React from 'react';
+
+import RadioButtonGroup from '../RadioButtonGroup';
+
+const TestIcon = ({ className }: {className?: string}) => {
+ return (
+
+ );
+};
+
+type Test = 'v1' | 'v2' | 'v3';
+
+const RadioButtonGroupTest = () => {
+ return (
+
+ // eslint-disable-next-line react/jsx-no-bind
+ onChange={ () => {} }
+ defaultValue="v1"
+ name="test"
+ options={ [
+ { value: 'v1', title: 'test option 1', icon: TestIcon, onlyIcon: false },
+ { value: 'v2', title: 'test 2', onlyIcon: false },
+ { value: 'v2', title: 'test 2', icon: TestIcon, onlyIcon: true },
+ ] }
+ />
+ );
+};
+
+export default RadioButtonGroupTest;
diff --git a/ui/token/TokenInventory.tsx b/ui/token/TokenInventory.tsx
index 2a99a5e828..50153b466c 100644
--- a/ui/token/TokenInventory.tsx
+++ b/ui/token/TokenInventory.tsx
@@ -1,4 +1,4 @@
-import { Grid } from '@chakra-ui/react';
+import { Flex, Grid, Text } from '@chakra-ui/react';
import type { UseQueryResult } from '@tanstack/react-query';
import React from 'react';
@@ -8,23 +8,50 @@ import type { ResourceError } from 'lib/api/resources';
import useIsMobile from 'lib/hooks/useIsMobile';
import ActionBar from 'ui/shared/ActionBar';
import DataListDisplay from 'ui/shared/DataListDisplay';
+import AddressEntity from 'ui/shared/entities/address/AddressEntity';
import Pagination from 'ui/shared/pagination/Pagination';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
+import ResetIconButton from 'ui/shared/ResetIconButton';
import TokenInventoryItem from './TokenInventoryItem';
type Props = {
inventoryQuery: QueryWithPagesResult<'token_inventory'>;
tokenQuery: UseQueryResult>;
+ ownerFilter?: string;
}
-const TokenInventory = ({ inventoryQuery, tokenQuery }: Props) => {
+const TokenInventory = ({ inventoryQuery, tokenQuery, ownerFilter }: Props) => {
const isMobile = useIsMobile();
- const actionBar = isMobile && inventoryQuery.pagination.isVisible && (
-
-
-
+ const resetOwnerFilter = React.useCallback(() => {
+ inventoryQuery.onFilterChange({});
+ }, [ inventoryQuery ]);
+
+ const isActionBarHidden = !ownerFilter && !inventoryQuery.data?.items.length;
+
+ const ownerFilterComponent = ownerFilter && (
+
+ Filtered by owner
+
+
+
+
+
+ );
+
+ const actionBar = !isActionBarHidden && (
+ <>
+ { ownerFilterComponent }
+
+ { isMobile && }
+
+ >
);
const items = inventoryQuery.data?.items;