diff --git a/ui/address/AddressAdvancedFilterLink.tsx b/ui/address/AddressAdvancedFilterLink.tsx new file mode 100644 index 0000000000..95d2da0dd3 --- /dev/null +++ b/ui/address/AddressAdvancedFilterLink.tsx @@ -0,0 +1,50 @@ +import { chakra } from '@chakra-ui/react'; +import React from 'react'; + +import type { AddressFromToFilter } from 'types/api/address'; +import { ADVANCED_FILTER_TYPES } from 'types/api/advancedFilter'; +import type { TokenType } from 'types/api/token'; + +import { route } from 'nextjs-routes'; + +import config from 'configs/app'; +import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; +import { Link } from 'toolkit/chakra/link'; +import IconSvg from 'ui/shared/IconSvg'; + +interface Props { + isLoading?: boolean; + address: string; + typeFilter: Array; + directionFilter: AddressFromToFilter; +} + +const AddressAdvancedFilterLink = ({ isLoading, address, typeFilter, directionFilter }: Props) => { + const isInitialLoading = useIsInitialLoading(isLoading); + + if (!config.features.advancedFilter.isEnabled) { + return null; + } + + const queryParams = { + to_address_hashes_to_include: !directionFilter || directionFilter === 'to' ? [ address ] : undefined, + from_address_hashes_to_include: !directionFilter || directionFilter === 'from' ? [ address ] : undefined, + transaction_types: typeFilter.length > 0 ? typeFilter : ADVANCED_FILTER_TYPES.filter((type) => type !== 'coin_transfer'), + }; + + return ( + + + Advanced filter + + ); +}; + +export default React.memo(AddressAdvancedFilterLink); diff --git a/ui/address/AddressCsvExportLink.tsx b/ui/address/AddressCsvExportLink.tsx index a60e4cf961..0a90412c50 100644 --- a/ui/address/AddressCsvExportLink.tsx +++ b/ui/address/AddressCsvExportLink.tsx @@ -1,4 +1,4 @@ -import { chakra, Flex } from '@chakra-ui/react'; +import { chakra } from '@chakra-ui/react'; import React from 'react'; import type { CsvExportParams } from 'types/client/address'; @@ -9,7 +9,6 @@ import config from 'configs/app'; import useIsInitialLoading from 'lib/hooks/useIsInitialLoading'; import useIsMobile from 'lib/hooks/useIsMobile'; import { Link } from 'toolkit/chakra/link'; -import { Skeleton } from 'toolkit/chakra/skeleton'; import { Tooltip } from 'toolkit/chakra/tooltip'; import IconSvg from 'ui/shared/IconSvg'; @@ -28,15 +27,6 @@ const AddressCsvExportLink = ({ className, address, params, isLoading }: Props) return null; } - if (isInitialLoading) { - return ( - - - - - ); - } - return ( - + Download CSV diff --git a/ui/address/AddressTokenTransfers.pw.tsx b/ui/address/AddressTokenTransfers.pw.tsx index 2c7ccac9c1..9a08f5dec1 100644 --- a/ui/address/AddressTokenTransfers.pw.tsx +++ b/ui/address/AddressTokenTransfers.pw.tsx @@ -8,10 +8,9 @@ import { test, expect, devices } from 'playwright/lib'; import AddressTokenTransfers from './AddressTokenTransfers'; const CURRENT_ADDRESS = '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859'; -const TOKEN_HASH = '0x1189a607CEac2f0E14867de4EB15b15C9FFB5859'; const hooksConfig = { router: { - query: { hash: CURRENT_ADDRESS, token: TOKEN_HASH }, + query: { hash: CURRENT_ADDRESS }, }, }; @@ -28,10 +27,10 @@ const tokenTransfersWoPagination = { next_page_params: null, }; -test('with token filter and pagination', async({ render, mockApiResponse }) => { +test('with pagination', async({ render, mockApiResponse }) => { await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, { pathParams: { hash: CURRENT_ADDRESS }, - queryParams: { token: TOKEN_HASH }, + queryParams: { type: [] }, }); const component = await render( @@ -42,10 +41,10 @@ test('with token filter and pagination', async({ render, mockApiResponse }) => { await expect(component).toHaveScreenshot(); }); -test('with token filter and no pagination', async({ render, mockApiResponse }) => { +test('without pagination', async({ render, mockApiResponse }) => { await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, { pathParams: { hash: CURRENT_ADDRESS }, - queryParams: { token: TOKEN_HASH }, + queryParams: { type: [] }, }); const component = await render( @@ -59,10 +58,10 @@ test('with token filter and no pagination', async({ render, mockApiResponse }) = test.describe('mobile', () => { test.use({ viewport: devices['iPhone 13 Pro'].viewport }); - test('with token filter and pagination', async({ render, mockApiResponse }) => { + test('with pagination', async({ render, mockApiResponse }) => { await mockApiResponse('address_token_transfers', tokenTransfersWithPagination, { pathParams: { hash: CURRENT_ADDRESS }, - queryParams: { token: TOKEN_HASH }, + queryParams: { type: [] }, }); const component = await render( @@ -73,10 +72,10 @@ test.describe('mobile', () => { await expect(component).toHaveScreenshot(); }); - test('with token filter and no pagination', async({ render, mockApiResponse }) => { + test('without pagination', async({ render, mockApiResponse }) => { await mockApiResponse('address_token_transfers', tokenTransfersWoPagination, { pathParams: { hash: CURRENT_ADDRESS }, - queryParams: { token: TOKEN_HASH }, + queryParams: { type: [] }, }); const component = await render( diff --git a/ui/address/AddressTokenTransfers.tsx b/ui/address/AddressTokenTransfers.tsx index bdccc6d2b9..ce96208279 100644 --- a/ui/address/AddressTokenTransfers.tsx +++ b/ui/address/AddressTokenTransfers.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, Text } from '@chakra-ui/react'; +import { Box, Flex } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/router'; import React from 'react'; @@ -12,7 +12,6 @@ import type { TokenTransfer } from 'types/api/tokenTransfer'; import { getResourceKey } from 'lib/api/useApiQuery'; import getFilterValueFromQuery from 'lib/getFilterValueFromQuery'; import getFilterValuesFromQuery from 'lib/getFilterValuesFromQuery'; -import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMounted from 'lib/hooks/useIsMounted'; import { apos } from 'lib/html-entities'; import getQueryParamString from 'lib/router/getQueryParamString'; @@ -22,16 +21,14 @@ import { TOKEN_TYPE_IDS } from 'lib/token/tokenTypes'; import { getTokenTransfersStub } from 'stubs/token'; import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar'; import DataListDisplay from 'ui/shared/DataListDisplay'; -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'; import TokenTransferTable from 'ui/shared/TokenTransfer/TokenTransferTable'; +import AddressAdvancedFilterLink from './AddressAdvancedFilterLink'; import AddressCsvExportLink from './AddressCsvExportLink'; type Filters = { @@ -72,7 +69,6 @@ type Props = { const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => { const router = useRouter(); const queryClient = useQueryClient(); - const isMobile = useIsMobile(); const isMounted = useIsMounted(); const currentAddress = getQueryParamString(router.query.hash); @@ -80,8 +76,6 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender = const [ socketAlert, setSocketAlert ] = React.useState(''); const [ newItemsCount, setNewItemsCount ] = React.useState(0); - const tokenFilter = getQueryParamString(router.query.token) || undefined; - const [ filters, setFilters ] = React.useState( { type: getTokenFilterValue(router.query.type) || [], @@ -92,7 +86,7 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender = const { isError, isPlaceholderData, data, pagination, onFilterChange } = useQueryWithPages({ resourceName: 'address_token_transfers', pathParams: { hash: currentAddress }, - filters: tokenFilter ? { token: tokenFilter } : filters, + filters, options: { enabled: isQueryEnabled, placeholderData: getTokenTransfersStub(undefined, { @@ -114,10 +108,6 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender = setFilters((prevState) => ({ ...prevState, filter: filterVal })); }, [ filters, onFilterChange ]); - const resetTokenFilter = React.useCallback(() => { - onFilterChange({}); - }, [ onFilterChange ]); - const handleNewSocketMessage: SocketMessage.AddressTokenTransfer['handler'] = (payload) => { setSocketAlert(''); @@ -173,7 +163,7 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender = topic: `addresses:${ currentAddress.toLowerCase() }`, onSocketClose: handleSocketClose, onSocketError: handleSocketError, - isDisabled: pagination.page !== 1 || Boolean(tokenFilter), + isDisabled: pagination.page !== 1, }); useSocketMessage({ @@ -182,20 +172,12 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender = handler: handleNewSocketMessage, }); - const tokenData = React.useMemo(() => ({ - address: tokenFilter || '', - name: '', - icon_url: '', - symbol: '', - type: 'ERC-20' as const, - }), [ tokenFilter ]); - if (!isMounted || !shouldRender) { return null; } const numActiveFilters = (filters.type?.length || 0) + (filters.filter ? 1 : 0); - const isActionBarHidden = !tokenFilter && !numActiveFilters && !data?.items.length && !currentAddress; + const isActionBarHidden = !numActiveFilters && !data?.items.length && !currentAddress; const content = data?.items ? ( <> @@ -206,14 +188,14 @@ const AddressTokenTransfers = ({ overloadCount = OVERLOAD_COUNT, shouldRender = showTxInfo top={ isActionBarHidden ? 0 : ACTION_BAR_HEIGHT_DESKTOP } enableTimeIncrement - showSocketInfo={ pagination.page === 1 && !tokenFilter } + showSocketInfo={ pagination.page === 1 } socketInfoAlert={ socketAlert } socketInfoNum={ newItemsCount } isLoading={ isPlaceholderData } /> - { pagination.page === 1 && !tokenFilter && ( + { pagination.page === 1 && ( ) : null; - const tokenFilterComponent = tokenFilter && ( - - Filtered by token - - - { isMobile ? : tokenFilter } - + const actionBar = !isActionBarHidden ? ( + + + + + - - ); - - const actionBar = ( - <> - { isMobile && tokenFilterComponent } - { !isActionBarHidden && ( - - { !isMobile && tokenFilterComponent } - { !tokenFilter && ( - - ) } - { currentAddress && ( - - ) } - - - ) } - - ); + + + ) : null; return ( Reset diff --git a/ui/pages/Token.pw.tsx b/ui/pages/Token.pw.tsx index 72e86a827a..e508b4c60f 100644 --- a/ui/pages/Token.pw.tsx +++ b/ui/pages/Token.pw.tsx @@ -34,12 +34,13 @@ test.beforeEach(async({ mockApiResponse, mockTextAd }) => { }); test('base view', async({ render, page, createSocket }) => { + test.slow(); const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); - await component.getByText('100 ARIA').waitFor({ state: 'visible' }); + await component.getByText('100 ARIA').waitFor({ state: 'visible', timeout: 10_000 }); await expect(component).toHaveScreenshot({ mask: [ page.locator(pwConfig.adsBannerSelector) ], @@ -98,11 +99,12 @@ test.describe('mobile', () => { test.use({ viewport: devices['iPhone 13 Pro'].viewport }); test('base view', async({ render, page, createSocket }) => { + test.slow(); const component = await render(, { hooksConfig }, { withSocket: true }); const socket = await createSocket(); const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); - await component.getByText('100 ARIA').waitFor({ state: 'visible' }); + await component.getByText('100 ARIA').waitFor({ state: 'visible', timeout: 10_000 }); await expect(component).toHaveScreenshot({ mask: [ page.locator(pwConfig.adsBannerSelector) ], @@ -111,6 +113,7 @@ test.describe('mobile', () => { }); test('with verified info', async({ render, page, createSocket, mockApiResponse, mockAssetResponse }) => { + test.slow(); await mockApiResponse('token_verified_info', verifiedAddressesMocks.TOKEN_INFO_APPLICATION.APPROVED, { pathParams: { chainId, hash } }); await mockAssetResponse(tokenInfo.icon_url as string, './playwright/mocks/image_s.jpg'); @@ -118,7 +121,7 @@ test.describe('mobile', () => { const socket = await createSocket(); const channel = await socketServer.joinChannel(socket, `tokens:${ hash }`); socketServer.sendMessage(socket, channel, 'total_supply', { total_supply: 10 ** 20 }); - await component.getByText('100 ARIA').waitFor({ state: 'visible' }); + await component.getByText('100 ARIA').waitFor({ state: 'visible', timeout: 10_000 }); await expect(component).toHaveScreenshot({ mask: [ page.locator(pwConfig.adsBannerSelector) ], diff --git a/ui/pages/__screenshots__/NameDomains.pw.tsx_default_filters-1.png b/ui/pages/__screenshots__/NameDomains.pw.tsx_default_filters-1.png index 3ad5714660..ae930b66d1 100644 Binary files a/ui/pages/__screenshots__/NameDomains.pw.tsx_default_filters-1.png and b/ui/pages/__screenshots__/NameDomains.pw.tsx_default_filters-1.png differ diff --git a/ui/shared/address/AddressFromTo.tsx b/ui/shared/address/AddressFromTo.tsx index 7d33b6bdf8..0d2779e483 100644 --- a/ui/shared/address/AddressFromTo.tsx +++ b/ui/shared/address/AddressFromTo.tsx @@ -21,11 +21,12 @@ interface Props { className?: string; isLoading?: boolean; tokenHash?: string; + tokenSymbol?: string; truncation?: EntityProps['truncation']; noIcon?: boolean; } -const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading, tokenHash = '', noIcon }: Props) => { +const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading, tokenHash = '', tokenSymbol = '', noIcon }: Props) => { const mode = useBreakpointValue( { base: (typeof modeProp === 'object' && 'base' in modeProp ? modeProp.base : modeProp), @@ -34,7 +35,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading }, ) ?? 'long'; - const Entity = tokenHash ? AddressEntityWithTokenFilter : AddressEntity; + const Entity = tokenHash && tokenSymbol ? AddressEntityWithTokenFilter : AddressEntity; if (mode === 'compact') { return ( @@ -52,6 +53,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading noCopy={ current === from.hash } noIcon={ noIcon } tokenHash={ tokenHash } + tokenSymbol={ tokenSymbol } truncation="constant" maxW="calc(100% - 28px)" w="min-content" @@ -65,6 +67,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading noCopy={ current === to.hash } noIcon={ noIcon } tokenHash={ tokenHash } + tokenSymbol={ tokenSymbol } truncation="constant" maxW="calc(100% - 28px)" w="min-content" @@ -87,6 +90,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading noCopy={ isOutgoing } noIcon={ noIcon } tokenHash={ tokenHash } + tokenSymbol={ tokenSymbol } truncation="constant" mr={ isOutgoing ? 4 : 2 } /> @@ -102,6 +106,7 @@ const AddressFromTo = ({ from, to, current, mode: modeProp, className, isLoading noCopy={ current === to.hash } noIcon={ noIcon } tokenHash={ tokenHash } + tokenSymbol={ tokenSymbol } truncation="constant" ml={ 3 } /> diff --git a/ui/shared/entities/address/AddressEntityWithTokenFilter.tsx b/ui/shared/entities/address/AddressEntityWithTokenFilter.tsx index 1ec363d532..44f859621b 100644 --- a/ui/shared/entities/address/AddressEntityWithTokenFilter.tsx +++ b/ui/shared/entities/address/AddressEntityWithTokenFilter.tsx @@ -3,21 +3,29 @@ import React from 'react'; import { route } from 'nextjs-routes'; +import config from 'configs/app'; + import * as AddressEntity from './AddressEntity'; interface Props extends AddressEntity.EntityProps { tokenHash: string; + tokenSymbol: string; } const AddressEntityWithTokenFilter = (props: Props) => { + + if (!config.features.advancedFilter.isEnabled) { + return ; + } + const defaultHref = route({ - pathname: '/address/[hash]', + pathname: '/advanced-filter', query: { ...props.query, - hash: props.address.hash, - tab: 'token_transfers', - token: props.tokenHash, - scroll_to_tabs: 'true', + to_address_hashes_to_include: [ props.address.hash ], + from_address_hashes_to_include: [ props.address.hash ], + token_contract_address_hashes_to_include: [ props.tokenHash ], + token_contract_symbols_to_include: [ props.tokenSymbol ], }, }); diff --git a/ui/shared/entities/token/TokenEntityWithAddressFilter.tsx b/ui/shared/entities/token/TokenEntityWithAddressFilter.tsx index 6045a35838..9ccc20c4d1 100644 --- a/ui/shared/entities/token/TokenEntityWithAddressFilter.tsx +++ b/ui/shared/entities/token/TokenEntityWithAddressFilter.tsx @@ -3,6 +3,8 @@ import React from 'react'; import { route } from 'nextjs-routes'; +import config from 'configs/app'; + import * as TokenEntity from './TokenEntity'; interface Props extends TokenEntity.EntityProps { @@ -10,14 +12,19 @@ interface Props extends TokenEntity.EntityProps { } const TokenEntityWithAddressFilter = (props: Props) => { + + if (!config.features.advancedFilter.isEnabled) { + return ; + } + const defaultHref = route({ - pathname: '/address/[hash]', + pathname: '/advanced-filter', query: { ...props.query, - hash: props.addressHash, - tab: 'token_transfers', - token: props.token.address, - scroll_to_tabs: 'true', + to_address_hashes_to_include: [ props.addressHash ], + from_address_hashes_to_include: [ props.addressHash ], + token_contract_address_hashes_to_include: [ props.token.address ], + token_contract_symbols_to_include: [ props.token.symbol ?? '' ], }, }); diff --git a/ui/shared/filters/TokenTypeFilter.tsx b/ui/shared/filters/TokenTypeFilter.tsx index e6c2e92cbc..897732ce4c 100644 --- a/ui/shared/filters/TokenTypeFilter.tsx +++ b/ui/shared/filters/TokenTypeFilter.tsx @@ -30,12 +30,13 @@ const TokenTypeFilter = ({ nftOnly, onChange return ( <> - + Type diff --git a/ui/token/TokenTransfer/TokenTransferListItem.tsx b/ui/token/TokenTransfer/TokenTransferListItem.tsx index 31e6c42470..86261984d5 100644 --- a/ui/token/TokenTransfer/TokenTransferListItem.tsx +++ b/ui/token/TokenTransfer/TokenTransferListItem.tsx @@ -62,6 +62,7 @@ const TokenTransferListItem = ({ to={ to } isLoading={ isLoading } tokenHash={ token?.address } + tokenSymbol={ token?.symbol ?? undefined } w="100%" fontWeight="500" /> diff --git a/ui/token/TokenTransfer/TokenTransferTableItem.tsx b/ui/token/TokenTransfer/TokenTransferTableItem.tsx index 927e80fae8..4145ac2f71 100644 --- a/ui/token/TokenTransfer/TokenTransferTableItem.tsx +++ b/ui/token/TokenTransfer/TokenTransferTableItem.tsx @@ -73,6 +73,7 @@ const TokenTransferTableItem = ({ mt="5px" mode={{ lg: 'compact', xl: 'long' }} tokenHash={ token?.address } + tokenSymbol={ token?.symbol ?? undefined } /> { (token && NFT_TOKEN_TYPE_IDS.includes(token.type)) && ( diff --git a/ui/tokens/TokensBridgedChainsFilter.tsx b/ui/tokens/TokensBridgedChainsFilter.tsx index 977dc163b2..2e557634ec 100644 --- a/ui/tokens/TokensBridgedChainsFilter.tsx +++ b/ui/tokens/TokensBridgedChainsFilter.tsx @@ -34,12 +34,13 @@ const TokensBridgedChainsFilter = ({ onChange, defaultValue }: Props) => { return ( <> - + Show bridged tokens from