Commit 6fc40192 authored by tom's avatar tom

Merge branch 'address-coin-balance' of github.com:blockscout/frontend into address-blocks-validated

parents bc0dfb37 01cf9e1b
import type { MetaMaskInpageProvider } from '@metamask/providers';
declare global {
interface Window {
ethereum: MetaMaskInpageProvider;
}
}
...@@ -3,9 +3,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; ...@@ -3,9 +3,9 @@ import { useQuery, useQueryClient } from '@tanstack/react-query';
import { pick, omit } from 'lodash'; import { pick, omit } from 'lodash';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React, { useCallback } from 'react'; import React, { useCallback } from 'react';
import { animateScroll } from 'react-scroll'; import { animateScroll, scroller } from 'react-scroll';
import { PAGINATION_FIELDS } from 'types/api/pagination'; import { PAGINATION_FIELDS, PAGINATION_FILTERS_FIELDS } from 'types/api/pagination';
import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination'; import type { PaginationParams, PaginatedResponse, PaginatedQueryKeys, PaginationFilters } from 'types/api/pagination';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
...@@ -16,9 +16,17 @@ interface Params<QueryName extends PaginatedQueryKeys> { ...@@ -16,9 +16,17 @@ interface Params<QueryName extends PaginatedQueryKeys> {
queryIds?: Array<string>; queryIds?: Array<string>;
filters?: PaginationFilters<QueryName>; filters?: PaginationFilters<QueryName>;
options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>; options?: Omit<UseQueryOptions<unknown, unknown, PaginatedResponse<QueryName>>, 'queryKey' | 'queryFn'>;
scroll?: { elem: string; offset: number };
} }
export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({ queryName, filters, options, apiPath, queryIds }: Params<QueryName>) { export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>({
queryName,
filters,
options,
apiPath,
queryIds,
scroll,
}: Params<QueryName>) {
const paginationFields = PAGINATION_FIELDS[queryName]; const paginationFields = PAGINATION_FIELDS[queryName];
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const router = useRouter(); const router = useRouter();
...@@ -31,6 +39,10 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -31,6 +39,10 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
const queryKey = [ queryName, ...(queryIds || []), { page, filters } ]; const queryKey = [ queryName, ...(queryIds || []), { page, filters } ];
const scrollToTop = useCallback(() => {
scroll ? scroller.scrollTo(scroll.elem, { offset: scroll.offset }) : animateScroll.scrollToTop({ duration: 0 });
}, [ scroll ]);
const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>( const queryResult = useQuery<unknown, unknown, PaginatedResponse<QueryName>>(
queryKey, queryKey,
async() => { async() => {
...@@ -65,10 +77,10 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -65,10 +77,10 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
animateScroll.scrollToTop({ duration: 0 }); scrollToTop();
setPage(prev => prev + 1); setPage(prev => prev + 1);
}); });
}, [ data?.next_page_params, page, pageParams.length, router ]); }, [ data?.next_page_params, page, pageParams.length, router, scrollToTop ]);
const onPrevPageClick = useCallback(() => { const onPrevPageClick = useCallback(() => {
// returning to the first page // returning to the first page
...@@ -85,17 +97,18 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -85,17 +97,18 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
router.query = nextPageQuery; router.query = nextPageQuery;
router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true }) router.push({ pathname: router.pathname, query: nextPageQuery }, undefined, { shallow: true })
.then(() => { .then(() => {
animateScroll.scrollToTop({ duration: 0 }); scrollToTop();
setPage(prev => prev - 1); setPage(prev => prev - 1);
page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] }); page === 2 && queryClient.removeQueries({ queryKey: [ queryName ] });
}); });
}, [ router, page, paginationFields, pageParams, queryClient, queryName ]); }, [ router, page, paginationFields, pageParams, queryClient, scrollToTop, queryName ]);
const resetPage = useCallback(() => { const resetPage = useCallback(() => {
queryClient.removeQueries({ queryKey: [ queryName ] }); queryClient.removeQueries({ queryKey: [ queryName ] });
router.push({ pathname: router.pathname, query: omit(router.query, paginationFields, 'page') }, undefined, { shallow: true }).then(() => { router.push({ pathname: router.pathname, query: omit(router.query, paginationFields, 'page') }, undefined, { shallow: true }).then(() => {
animateScroll.scrollToTop({ duration: 0 }); queryClient.removeQueries({ queryKey: [ queryName ] });
scrollToTop();
setPage(1); setPage(1);
setPageParams([ ]); setPageParams([ ]);
canGoBackwards.current = true; canGoBackwards.current = true;
...@@ -105,7 +118,30 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -105,7 +118,30 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' }); queryClient.removeQueries({ queryKey: [ queryName ], type: 'inactive' });
}, 100); }, 100);
}); });
}, [ queryClient, queryName, router, paginationFields ]); }, [ queryClient, queryName, router, paginationFields, scrollToTop ]);
const onFilterChange = useCallback((newFilters: PaginationFilters<QueryName> | undefined) => {
const newQuery = omit(router.query, PAGINATION_FIELDS[queryName], 'page', PAGINATION_FILTERS_FIELDS[queryName]);
if (newFilters) {
Object.entries(newFilters).forEach(([ key, value ]) => {
if (value) {
newQuery[key] = Array.isArray(value) ? value.join(',') : (value || '');
}
});
}
router.push(
{
pathname: router.pathname,
query: newQuery,
},
undefined,
{ shallow: true },
).then(() => {
setPage(1);
setPageParams([ ]);
scrollToTop();
});
}, [ queryName, router, scrollToTop, setPageParams, setPage ]);
const hasPaginationParams = Object.keys(currPageParams).length > 0; const hasPaginationParams = Object.keys(currPageParams).length > 0;
const nextPageParams = data?.next_page_params; const nextPageParams = data?.next_page_params;
...@@ -135,5 +171,5 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>( ...@@ -135,5 +171,5 @@ export default function useQueryWithPages<QueryName extends PaginatedQueryKeys>(
}, 0); }, 0);
}, []); }, []);
return { ...queryResult, pagination }; return { ...queryResult, pagination, onFilterChange };
} }
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/token-transfers${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
import type { NextApiRequest } from 'next';
import getSearchParams from 'lib/api/getSearchParams';
import handler from 'lib/api/handler';
const getUrl = (req: NextApiRequest) => {
const searchParamsStr = getSearchParams(req);
return `/v2/addresses/${ req.query.id }/transactions${ searchParamsStr ? '?' + searchParamsStr : '' }`;
};
const requestHandler = handler(getUrl, [ 'GET' ]);
export default requestHandler;
...@@ -16,6 +16,6 @@ ...@@ -16,6 +16,6 @@
"incremental": true, "incremental": true,
"baseUrl": ".", "baseUrl": ".",
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "additional.d.ts", "decs.d.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", "decs.d.ts", "global.d.ts"],
"exclude": ["node_modules", "node_modules_linux"], "exclude": ["node_modules", "node_modules_linux"],
} }
import type { Transaction } from 'types/api/transaction';
import type { AddressTag, WatchlistName } from './addressParams'; import type { AddressTag, WatchlistName } from './addressParams';
import type { Block } from './block'; import type { Block } from './block';
import type { TokenInfo } from './tokenInfo'; import type { TokenInfo, TokenType } from './tokenInfo';
import type { TokenTransfer, TokenTransferPagination } from './tokenTransfer';
export interface Address { export interface Address {
block_number_balance_updated_at: number | null; block_number_balance_updated_at: number | null;
...@@ -33,6 +36,31 @@ export interface AddressTokenBalance { ...@@ -33,6 +36,31 @@ export interface AddressTokenBalance {
value: string; value: string;
} }
export interface AddressTransactionsResponse {
items: Array<Transaction>;
next_page_params: {
block_number: number;
index: number;
items_count: number;
} | null;
}
type AddressFromToFilter = 'from' | 'to' | undefined;
export type AddressTxsFilters = {
filter: AddressFromToFilter;
}
export interface AddressTokenTransferResponse {
items: Array<TokenTransfer>;
next_page_params: TokenTransferPagination | null;
}
export type AddressTokenTransferFilters = {
filter: AddressFromToFilter;
type: TokenType;
}
export interface AddressCoinBalanceHistoryItem { export interface AddressCoinBalanceHistoryItem {
block_number: number; block_number: number;
block_timestamp: string; block_timestamp: string;
......
import type { AddressCoinBalanceHistoryResponse, AddressBlocksValidatedResponse } from 'types/api/address'; import type {
AddressTransactionsResponse,
AddressTokenTransferResponse,
AddressTxsFilters,
AddressTokenTransferFilters,
AddressCoinBalanceHistoryResponse,
AddressBlocksValidatedResponse,
} from 'types/api/address';
import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block'; import type { BlocksResponse, BlockTransactionsResponse, BlockFilters } from 'types/api/block';
import type { InternalTransactionsResponse } from 'types/api/internalTransaction'; import type { InternalTransactionsResponse } from 'types/api/internalTransaction';
import type { LogsResponse } from 'types/api/log'; import type { LogsResponse } from 'types/api/log';
...@@ -9,6 +16,8 @@ import { QueryKeys } from 'types/client/queries'; ...@@ -9,6 +16,8 @@ import { QueryKeys } from 'types/client/queries';
import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull'; import type { KeysOfObjectOrNull } from 'types/utils/KeysOfObjectOrNull';
export type PaginatedQueryKeys = export type PaginatedQueryKeys =
QueryKeys.addressTxs |
QueryKeys.addressTokenTransfers |
QueryKeys.blocks | QueryKeys.blocks |
QueryKeys.blocksReorgs | QueryKeys.blocksReorgs |
QueryKeys.blocksUncles | QueryKeys.blocksUncles |
...@@ -22,23 +31,27 @@ export type PaginatedQueryKeys = ...@@ -22,23 +31,27 @@ export type PaginatedQueryKeys =
QueryKeys.addressBlocksValidated; QueryKeys.addressBlocksValidated;
export type PaginatedResponse<Q extends PaginatedQueryKeys> = export type PaginatedResponse<Q extends PaginatedQueryKeys> =
Q extends (QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles) ? BlocksResponse : Q extends QueryKeys.addressTxs ? AddressTransactionsResponse :
Q extends QueryKeys.blockTxs ? BlockTransactionsResponse : Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferResponse :
Q extends QueryKeys.txsValidate ? TransactionsResponseValidated : Q extends (QueryKeys.blocks | QueryKeys.blocksReorgs | QueryKeys.blocksUncles) ? BlocksResponse :
Q extends QueryKeys.txsPending ? TransactionsResponsePending : Q extends QueryKeys.blockTxs ? BlockTransactionsResponse :
Q extends QueryKeys.txInternals ? InternalTransactionsResponse : Q extends QueryKeys.txsValidate ? TransactionsResponseValidated :
Q extends QueryKeys.txLogs ? LogsResponse : Q extends QueryKeys.txsPending ? TransactionsResponsePending :
Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse : Q extends QueryKeys.txInternals ? InternalTransactionsResponse :
Q extends QueryKeys.addressCoinBalanceHistory ? AddressCoinBalanceHistoryResponse : Q extends QueryKeys.txLogs ? LogsResponse :
Q extends QueryKeys.addressBlocksValidated ? AddressBlocksValidatedResponse : Q extends QueryKeys.txTokenTransfers ? TokenTransferResponse :
never Q extends QueryKeys.addressCoinBalanceHistory ? AddressCoinBalanceHistoryResponse :
Q extends QueryKeys.addressBlocksValidated ? AddressBlocksValidatedResponse :
never
export type PaginationFilters<Q extends PaginatedQueryKeys> = export type PaginationFilters<Q extends PaginatedQueryKeys> =
Q extends QueryKeys.blocks ? BlockFilters : Q extends QueryKeys.addressTxs ? AddressTxsFilters :
Q extends QueryKeys.txsValidate ? TTxsFilters : Q extends QueryKeys.addressTokenTransfers ? AddressTokenTransferFilters :
Q extends QueryKeys.txsPending ? TTxsFilters : Q extends QueryKeys.blocks ? BlockFilters :
Q extends QueryKeys.txTokenTransfers ? TokenTransferFilters : Q extends QueryKeys.txsValidate ? TTxsFilters :
never Q extends QueryKeys.txsPending ? TTxsFilters :
Q extends QueryKeys.txTokenTransfers ? TokenTransferFilters :
never
export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params']; export type PaginationParams<Q extends PaginatedQueryKeys> = PaginatedResponse<Q>['next_page_params'];
...@@ -47,6 +60,8 @@ type PaginationFields = { ...@@ -47,6 +60,8 @@ type PaginationFields = {
} }
export const PAGINATION_FIELDS: PaginationFields = { export const PAGINATION_FIELDS: PaginationFields = {
[QueryKeys.addressTxs]: [ 'block_number', 'items_count', 'index' ],
[QueryKeys.addressTokenTransfers]: [ 'block_number', 'items_count', 'index', 'transaction_hash' ],
[QueryKeys.blocks]: [ 'block_number', 'items_count' ], [QueryKeys.blocks]: [ 'block_number', 'items_count' ],
[QueryKeys.blocksReorgs]: [ 'block_number', 'items_count' ], [QueryKeys.blocksReorgs]: [ 'block_number', 'items_count' ],
[QueryKeys.blocksUncles]: [ 'block_number', 'items_count' ], [QueryKeys.blocksUncles]: [ 'block_number', 'items_count' ],
...@@ -59,3 +74,23 @@ export const PAGINATION_FIELDS: PaginationFields = { ...@@ -59,3 +74,23 @@ export const PAGINATION_FIELDS: PaginationFields = {
[QueryKeys.addressCoinBalanceHistory]: [ 'items_count', 'block_number' ], [QueryKeys.addressCoinBalanceHistory]: [ 'items_count', 'block_number' ],
[QueryKeys.addressBlocksValidated]: [ 'items_count', 'block_number' ], [QueryKeys.addressBlocksValidated]: [ 'items_count', 'block_number' ],
}; };
type PaginationFiltersFields = {
[K in PaginatedQueryKeys]: Array<KeysOfObjectOrNull<PaginationFilters<K>>>
}
export const PAGINATION_FILTERS_FIELDS: PaginationFiltersFields = {
[QueryKeys.addressTxs]: [ 'filter' ],
[QueryKeys.addressTokenTransfers]: [ 'filter', 'type' ],
[QueryKeys.addressCoinBalanceHistory]: [],
[QueryKeys.addressBlocksValidated]: [],
[QueryKeys.blocks]: [ 'type' ],
[QueryKeys.txsValidate]: [ 'filter', 'type', 'method' ],
[QueryKeys.txsPending]: [ 'filter', 'type', 'method' ],
[QueryKeys.txTokenTransfers]: [ 'type' ],
[QueryKeys.blocksReorgs]: [],
[QueryKeys.blocksUncles]: [],
[QueryKeys.blockTxs]: [],
[QueryKeys.txInternals]: [],
[QueryKeys.txLogs]: [],
};
...@@ -38,14 +38,16 @@ interface TokenTransferBase { ...@@ -38,14 +38,16 @@ interface TokenTransferBase {
to: AddressParam; to: AddressParam;
} }
export type TokenTransferPagination = {
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
}
export interface TokenTransferResponse { export interface TokenTransferResponse {
items: Array<TokenTransfer>; items: Array<TokenTransfer>;
next_page_params: { next_page_params: TokenTransferPagination | null;
block_number: number;
index: number;
items_count: number;
transaction_hash: string;
} | null;
} }
export interface TokenTransferFilters { export interface TokenTransferFilters {
......
import type { AddressParam } from './addressParams';
export type TransactionReward = {
types: Array<string>;
emission_reward: string;
block_hash: string;
from: AddressParam;
to: AddressParam;
}
...@@ -26,5 +26,7 @@ export enum QueryKeys { ...@@ -26,5 +26,7 @@ export enum QueryKeys {
addressTokenBalances='address-token-balances', addressTokenBalances='address-token-balances',
addressCoinBalanceHistory='address-coin-balance-history', addressCoinBalanceHistory='address-coin-balance-history',
addressCoinBalanceHistoryByDay='address-coin-balance-history-by-day', addressCoinBalanceHistoryByDay='address-coin-balance-history-by-day',
addressTxs='addressTxs',
addressTokenTransfers='addressTokenTransfers',
addressBlocksValidated='address-blocks-validated', addressBlocksValidated='address-blocks-validated',
} }
...@@ -10,7 +10,6 @@ import { QueryKeys } from 'types/client/queries'; ...@@ -10,7 +10,6 @@ import { QueryKeys } from 'types/client/queries';
import appConfig from 'configs/app/config'; import appConfig from 'configs/app/config';
import blockIcon from 'icons/block.svg'; import blockIcon from 'icons/block.svg';
import metamaskIcon from 'icons/metamask.svg';
import useFetch from 'lib/hooks/useFetch'; import useFetch from 'lib/hooks/useFetch';
import useIsMobile from 'lib/hooks/useIsMobile'; import useIsMobile from 'lib/hooks/useIsMobile';
import link from 'lib/link/link'; import link from 'lib/link/link';
...@@ -22,6 +21,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem'; ...@@ -22,6 +21,7 @@ import DetailsInfoItem from 'ui/shared/DetailsInfoItem';
import ExternalLink from 'ui/shared/ExternalLink'; import ExternalLink from 'ui/shared/ExternalLink';
import HashStringShorten from 'ui/shared/HashStringShorten'; import HashStringShorten from 'ui/shared/HashStringShorten';
import AddressAddToMetaMask from './details/AddressAddToMetaMask';
import AddressBalance from './details/AddressBalance'; import AddressBalance from './details/AddressBalance';
import AddressDetailsSkeleton from './details/AddressDetailsSkeleton'; import AddressDetailsSkeleton from './details/AddressDetailsSkeleton';
import AddressFavoriteButton from './details/AddressFavoriteButton'; import AddressFavoriteButton from './details/AddressFavoriteButton';
...@@ -73,7 +73,7 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -73,7 +73,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
{ isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash } { isMobile ? <HashStringShorten hash={ addressQuery.data.hash }/> : addressQuery.data.hash }
</Text> </Text>
<CopyToClipboard text={ addressQuery.data.hash }/> <CopyToClipboard text={ addressQuery.data.hash }/>
<Icon as={ metamaskIcon } boxSize={ 6 } ml={ 2 }/> { addressQuery.data.is_contract && addressQuery.data.token && <AddressAddToMetaMask ml={ 2 } token={ addressQuery.data.token }/> }
<AddressFavoriteButton hash={ addressQuery.data.hash } isAdded={ Boolean(addressQuery.data.watchlist_names?.length) } ml={ 3 }/> <AddressFavoriteButton hash={ addressQuery.data.hash } isAdded={ Boolean(addressQuery.data.watchlist_names?.length) } ml={ 3 }/>
<AddressQrCode hash={ addressQuery.data.hash } ml={ 2 }/> <AddressQrCode hash={ addressQuery.data.hash } ml={ 2 }/>
</Flex> </Flex>
...@@ -89,7 +89,7 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -89,7 +89,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
<Grid <Grid
mt={ 8 } mt={ 8 }
columnGap={ 8 } columnGap={ 8 }
rowGap={{ base: 3, lg: 3 }} rowGap={{ base: 1, lg: 3 }}
templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden" templateColumns={{ base: 'minmax(0, 1fr)', lg: 'auto minmax(0, 1fr)' }} overflow="hidden"
> >
<AddressNameInfo data={ addressQuery.data }/> <AddressNameInfo data={ addressQuery.data }/>
...@@ -108,7 +108,7 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -108,7 +108,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
title="Tokens" title="Tokens"
hint="All tokens in the account and total value." hint="All tokens in the account and total value."
alignSelf="center" alignSelf="center"
py="2px" py={ 0 }
> >
<TokenSelect/> <TokenSelect/>
</DetailsInfoItem> </DetailsInfoItem>
...@@ -143,7 +143,7 @@ const AddressDetails = ({ addressQuery }: Props) => { ...@@ -143,7 +143,7 @@ const AddressDetails = ({ addressQuery }: Props) => {
title="Last balance update" title="Last balance update"
hint="Block number in which the address was updated." hint="Block number in which the address was updated."
alignSelf="center" alignSelf="center"
py={{ base: 0, lg: 1 }} py={{ base: '2px', lg: 1 }}
> >
<Link <Link
href={ link('block', { id: String(addressQuery.data.block_number_balance_updated_at) }) } href={ link('block', { id: String(addressQuery.data.block_number_balance_updated_at) }) }
......
import { Box } from '@chakra-ui/react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { base as txMock } from 'mocks/txs/tx';
import TestApp from 'playwright/TestApp';
import AddressTxs from './AddressTxs';
const API_URL = '/node-api/addresses/0xd789a607CEac2f0E14867de4EB15b15C9FFB5859/transactions';
const hooksConfig = {
router: {
query: { id: '0xd789a607CEac2f0E14867de4EB15b15C9FFB5859' },
},
};
test('address txs +@mobile +@desktop-xl', async({ mount, page }) => {
await page.route(API_URL, (route) => route.fulfill({
status: 200,
body: JSON.stringify({ items: [ txMock, txMock ], next_page_params: { block: 1 } }),
}));
const component = await mount(
<TestApp>
<Box h={{ base: '134px', lg: 6 }}/>
<AddressTxs/>
</TestApp>,
{ hooksConfig },
);
await page.waitForResponse(API_URL),
await expect(component).toHaveScreenshot();
});
import { useRouter } from 'next/router';
import React from 'react';
import { Element } from 'react-scroll';
import { QueryKeys } from 'types/client/queries';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination';
import TxsContent from 'ui/txs/TxsContent';
import AddressTxsFilter from './AddressTxsFilter';
const FILTER_VALUES = [ 'from', 'to' ] as const;
type FilterType = typeof FILTER_VALUES[number];
const getFilterValue = (val: string | Array<string> | undefined): FilterType | undefined => {
if (typeof val === 'string' && FILTER_VALUES.includes(val as FilterType)) {
return val as FilterType;
}
};
const SCROLL_ELEM = 'address-txs';
const SCROLL_OFFSET = -100;
const AddressTxs = () => {
const router = useRouter();
const isMobile = useIsMobile();
const [ filterValue, setFilterValue ] = React.useState<'from' | 'to' | undefined>(getFilterValue(router.query.filter));
const addressTxsQuery = useQueryWithPages({
apiPath: `/node-api/addresses/${ router.query.id }/transactions`,
queryName: QueryKeys.addressTxs,
filters: { filter: filterValue },
scroll: { elem: SCROLL_ELEM, offset: SCROLL_OFFSET },
});
const handleFilterChange = React.useCallback((val: string | Array<string>) => {
const newVal = getFilterValue(val);
setFilterValue(newVal);
addressTxsQuery.onFilterChange({ filter: newVal });
}, [ addressTxsQuery ]);
const isPaginatorHidden =
!addressTxsQuery.isLoading &&
!addressTxsQuery.isError &&
addressTxsQuery.pagination.page === 1 &&
!addressTxsQuery.pagination.hasNextPage;
const filter = (
<AddressTxsFilter
defaultFilter={ filterValue }
onFilterChange={ handleFilterChange }
isActive={ Boolean(filterValue) }
/>
);
return (
<Element name={ SCROLL_ELEM }>
{ !isMobile && (
<ActionBar mt={ -6 }>
{ filter }
{ !isPaginatorHidden && <Pagination { ...addressTxsQuery.pagination }/> }
</ActionBar>
) }
<TxsContent
filter={ filter }
query={ addressTxsQuery }
showSocketInfo={ false }
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
/>
</Element>
);
};
export default AddressTxs;
import {
Menu,
MenuButton,
MenuList,
MenuOptionGroup,
MenuItemOption,
useDisclosure,
} from '@chakra-ui/react';
import React from 'react';
import FilterButton from 'ui/shared/FilterButton';
interface Props {
isActive: boolean;
defaultFilter: 'from' | 'to' | undefined;
onFilterChange: (nextValue: string | Array<string>) => void;
}
const AddressTxsFilter = ({ onFilterChange, defaultFilter, isActive }: Props) => {
const { isOpen, onToggle } = useDisclosure();
return (
<Menu>
<MenuButton>
<FilterButton
isActive={ isOpen || isActive }
onClick={ onToggle }
/>
</MenuButton>
<MenuList zIndex={ 2 }>
<MenuOptionGroup defaultValue={ defaultFilter || 'all' } title="Address" type="radio" onChange={ onFilterChange }>
<MenuItemOption value="all">All</MenuItemOption>
<MenuItemOption value="from">From</MenuItemOption>
<MenuItemOption value="to">To</MenuItemOption>
</MenuOptionGroup>
</MenuList>
</Menu>
);
};
export default React.memo(AddressTxsFilter);
import { Box, chakra, Icon, Tooltip } from '@chakra-ui/react';
import React from 'react';
import type { TokenInfo } from 'types/api/tokenInfo';
import metamaskIcon from 'icons/metamask.svg';
import useToast from 'lib/hooks/useToast';
interface Props {
className?: string;
token: TokenInfo;
}
const AddressAddToMetaMask = ({ className, token }: Props) => {
const toast = useToast();
const handleClick = React.useCallback(async() => {
try {
const wasAdded = await window.ethereum.request({
method: 'wallet_watchAsset',
params: {
type: 'ERC20', // Initially only supports ERC20, but eventually more!
options: {
address: token.address,
symbol: token.symbol,
decimals: Number(token.decimals) || 18,
},
},
});
if (wasAdded) {
toast({
position: 'top-right',
title: 'Success',
description: 'Successfully added token to MetaMask',
status: 'success',
variant: 'subtle',
isClosable: true,
});
}
} catch (error) {
toast({
position: 'top-right',
title: 'Error',
description: (error as Error)?.message || 'Something went wrong',
status: 'error',
variant: 'subtle',
isClosable: true,
});
}
}, [ toast, token ]);
if (token.type !== 'ERC-20' || !('ethereum' in window)) {
return null;
}
return (
<Tooltip label="Add token to MetaMask">
<Box className={ className } display="inline-flex" cursor="pointer" onClick={ handleClick }>
<Icon as={ metamaskIcon } boxSize={ 6 }/>
</Box>
</Tooltip>
);
};
export default React.memo(chakra(AddressAddToMetaMask));
...@@ -8,7 +8,7 @@ const AddressDetailsSkeleton = () => { ...@@ -8,7 +8,7 @@ const AddressDetailsSkeleton = () => {
<Box> <Box>
<Flex align="center"> <Flex align="center">
<SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/> <SkeletonCircle boxSize={ 6 } flexShrink={ 0 }/>
<Skeleton h={ 6 } w="420px" ml={ 2 }/> <Skeleton h={ 6 } w={{ base: '100px', lg: '420px' }} ml={ 2 }/>
<Skeleton h={ 6 } w="24px" ml={ 2 } flexShrink={ 0 }/> <Skeleton h={ 6 } w="24px" ml={ 2 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/> <Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/>
<Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/> <Skeleton h={ 8 } w="48px" ml={ 3 } flexShrink={ 0 }/>
......
import { Box, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react'; import { Box, Flex, Icon, IconButton, Skeleton, Tooltip } from '@chakra-ui/react';
import { useQuery, useQueryClient, useIsFetching } from '@tanstack/react-query'; import { useQuery, useQueryClient, useIsFetching } from '@tanstack/react-query';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import React from 'react'; import React from 'react';
...@@ -71,7 +71,7 @@ const TokenSelect = () => { ...@@ -71,7 +71,7 @@ const TokenSelect = () => {
} }
return ( return (
<> <Flex columnGap={ 3 } mt={{ base: '6px', lg: 0 }}>
{ isMobile ? { isMobile ?
<TokenSelectMobile data={ data } isLoading={ balancesIsFetching === 1 }/> : <TokenSelectMobile data={ data } isLoading={ balancesIsFetching === 1 }/> :
<TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/> <TokenSelectDesktop data={ data } isLoading={ balancesIsFetching === 1 }/>
...@@ -83,11 +83,10 @@ const TokenSelect = () => { ...@@ -83,11 +83,10 @@ const TokenSelect = () => {
size="sm" size="sm"
pl="6px" pl="6px"
pr="6px" pr="6px"
ml={ 3 }
icon={ <Icon as={ walletIcon } boxSize={ 5 }/> } icon={ <Icon as={ walletIcon } boxSize={ 5 }/> }
/> />
</Tooltip> </Tooltip>
</> </Flex>
); );
}; };
......
...@@ -82,10 +82,10 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -82,10 +82,10 @@ const BlocksContent = ({ type, query }: Props) => {
if (query.isLoading) { if (query.isLoading) {
return ( return (
<> <>
<Show below="lg" key="skeleton-mobile"> <Show below="lg" key="skeleton-mobile" ssr={ false }>
<BlocksSkeletonMobile/> <BlocksSkeletonMobile/>
</Show> </Show>
<Hide below="lg" key="skeleton-desktop"> <Hide below="lg" key="skeleton-desktop" ssr={ false }>
<SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/> <SkeletonTable columns={ [ '125px', '120px', '21%', '64px', '35%', '22%', '22%' ] }/>
</Hide> </Hide>
</> </>
...@@ -103,8 +103,8 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -103,8 +103,8 @@ const BlocksContent = ({ type, query }: Props) => {
return ( return (
<> <>
{ socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> } { socketAlert && <Alert status="warning" mb={ 6 } as="a" href={ window.document.location.href }>{ socketAlert }</Alert> }
<Show below="lg" key="content-mobile"><BlocksList data={ query.data.items }/></Show> <Show below="lg" key="content-mobile" ssr={ false }><BlocksList data={ query.data.items }/></Show>
<Hide below="lg" key="content-desktop"><BlocksTable data={ query.data.items } top={ 80 } page={ 1 }/></Hide> <Hide below="lg" key="content-desktop" ssr={ false }><BlocksTable data={ query.data.items } top={ 80 } page={ 1 }/></Hide>
</> </>
); );
...@@ -115,7 +115,7 @@ const BlocksContent = ({ type, query }: Props) => { ...@@ -115,7 +115,7 @@ const BlocksContent = ({ type, query }: Props) => {
return ( return (
<> <>
{ isMobile && !isPaginatorHidden && ( { isMobile && !isPaginatorHidden && (
<ActionBar> <ActionBar mt={ -6 }>
<Pagination ml="auto" { ...query.pagination }/> <Pagination ml="auto" { ...query.pagination }/>
</ActionBar> </ActionBar>
) } ) }
......
...@@ -11,6 +11,7 @@ import useFetch from 'lib/hooks/useFetch'; ...@@ -11,6 +11,7 @@ import useFetch from 'lib/hooks/useFetch';
import AddressBlocksValidated from 'ui/address/AddressBlocksValidated'; import AddressBlocksValidated from 'ui/address/AddressBlocksValidated';
import AddressCoinBalance from 'ui/address/AddressCoinBalance'; import AddressCoinBalance from 'ui/address/AddressCoinBalance';
import AddressDetails from 'ui/address/AddressDetails'; import AddressDetails from 'ui/address/AddressDetails';
import AddressTxs from 'ui/address/AddressTxs';
import Page from 'ui/shared/Page/Page'; import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle'; import PageTitle from 'ui/shared/Page/PageTitle';
import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs'; import RoutedTabs from 'ui/shared/RoutedTabs/RoutedTabs';
...@@ -34,7 +35,7 @@ const AddressPageContent = () => { ...@@ -34,7 +35,7 @@ const AddressPageContent = () => {
].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>); ].map((tag) => <Tag key={ tag.label }>{ tag.display_name }</Tag>);
const tabs: Array<RoutedTab> = [ const tabs: Array<RoutedTab> = [
{ id: 'txs', title: 'Transactions', component: null }, { id: 'txs', title: 'Transactions', component: <AddressTxs/> },
{ id: 'token_transfers', title: 'Token transfers', component: null }, { id: 'token_transfers', title: 'Token transfers', component: null },
{ id: 'tokens', title: 'Tokens', component: null }, { id: 'tokens', title: 'Tokens', component: null },
{ id: 'internal_txn', title: 'Internal txn', component: null }, { id: 'internal_txn', title: 'Internal txn', component: null },
......
...@@ -2,16 +2,28 @@ import { Tag, chakra } from '@chakra-ui/react'; ...@@ -2,16 +2,28 @@ import { Tag, chakra } from '@chakra-ui/react';
import React from 'react'; import React from 'react';
interface Props { interface Props {
baseAddress: string; isIn: boolean;
addressFrom: string; isOut: boolean;
className?: string; className?: string;
} }
const InOutTag = ({ baseAddress, addressFrom, className }: Props) => { const InOutTag = ({ isIn, isOut, className }: Props) => {
const isOut = addressFrom === baseAddress; if (!isIn && !isOut) {
return null;
}
const colorScheme = isOut ? 'orange' : 'green'; const colorScheme = isOut ? 'orange' : 'green';
return <Tag className={ className } colorScheme={ colorScheme }>{ isOut ? 'OUT' : 'IN' }</Tag>; return (
<Tag
className={ className }
colorScheme={ colorScheme }
display="flex"
justifyContent="center"
>
{ isOut ? 'OUT' : 'IN' }
</Tag>
);
}; };
export default React.memo(chakra(InOutTag)); export default React.memo(chakra(InOutTag));
...@@ -53,7 +53,7 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd ...@@ -53,7 +53,7 @@ const TokenTransferListItem = ({ token, total, tx_hash: txHash, from, to, baseAd
<AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/> <AddressLink ml={ 2 } fontWeight="500" hash={ from.hash }/>
</Address> </Address>
{ baseAddress ? { baseAddress ?
<InOutTag baseAddress={ baseAddress } addressFrom={ from.hash } w="50px" textAlign="center"/> : <InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center"/> :
<Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/> <Icon as={ eastArrowIcon } boxSize={ 6 } color="gray.500"/>
} }
<Address width={ addressWidth }> <Address width={ addressWidth }>
......
...@@ -59,7 +59,7 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA ...@@ -59,7 +59,7 @@ const TokenTransferTableItem = ({ token, total, tx_hash: txHash, from, to, baseA
</Td> </Td>
{ baseAddress && ( { baseAddress && (
<Td px={ 0 }> <Td px={ 0 }>
<InOutTag baseAddress={ baseAddress } addressFrom={ from.hash } w="50px" textAlign="center" mt="3px"/> <InOutTag isIn={ baseAddress === to.hash } isOut={ baseAddress === from.hash } w="50px" textAlign="center" mt="3px"/>
</Td> </Td>
) } ) }
<Td> <Td>
......
...@@ -18,10 +18,9 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => ...@@ -18,10 +18,9 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
const borderColor = useToken('colors', 'blue.200'); const borderColor = useToken('colors', 'blue.200');
const ref = React.useRef(null); const ref = React.useRef(null);
const isPressed = React.useRef(false); const isActive = React.useRef(false);
const startX = React.useRef<number>(); const startX = React.useRef<number>();
const endX = React.useRef<number>(); const endX = React.useRef<number>();
const startIndex = React.useRef<number>(0);
const getIndexByX = React.useCallback((x: number) => { const getIndexByX = React.useCallback((x: number) => {
const xDate = scale.invert(x); const xDate = scale.invert(x);
...@@ -51,20 +50,33 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => ...@@ -51,20 +50,33 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
.attr('width', Math.abs(diffX)); .attr('width', Math.abs(diffX));
}, []); }, []);
const handelMouseUp = React.useCallback(() => { const handleSelect = React.useCallback((x0: number, x1: number) => {
isPressed.current = false; const index0 = getIndexByX(x0);
const index1 = getIndexByX(x1);
if (Math.abs(index0 - index1) > SELECTION_THRESHOLD) {
onSelect([ Math.min(index0, index1), Math.max(index0, index1) ]);
}
}, [ getIndexByX, onSelect ]);
const cleanUp = React.useCallback(() => {
isActive.current = false;
startX.current = undefined; startX.current = undefined;
endX.current = undefined;
d3.select(ref.current).attr('opacity', 0); d3.select(ref.current).attr('opacity', 0);
}, [ ]);
if (!endX.current) { const handelMouseUp = React.useCallback(() => {
if (!isActive.current) {
return; return;
} }
const index = getIndexByX(endX.current); if (startX.current && endX.current) {
if (Math.abs(index - startIndex.current) > SELECTION_THRESHOLD) { handleSelect(startX.current, endX.current);
onSelect([ Math.min(index, startIndex.current), Math.max(index, startIndex.current) ]);
} }
}, [ getIndexByX, onSelect ]);
cleanUp();
}, [ cleanUp, handleSelect ]);
React.useEffect(() => { React.useEffect(() => {
if (!anchorEl) { if (!anchorEl) {
...@@ -76,20 +88,34 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => ...@@ -76,20 +88,34 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
anchorD3 anchorD3
.on('mousedown.selectionX', (event: MouseEvent) => { .on('mousedown.selectionX', (event: MouseEvent) => {
const [ x ] = d3.pointer(event, anchorEl); const [ x ] = d3.pointer(event, anchorEl);
isPressed.current = true; isActive.current = true;
startX.current = x; startX.current = x;
const index = getIndexByX(x);
startIndex.current = index;
}) })
.on('mouseup.selectionX', handelMouseUp)
.on('mousemove.selectionX', (event: MouseEvent) => { .on('mousemove.selectionX', (event: MouseEvent) => {
if (isPressed.current) { if (isActive.current) {
const [ x ] = d3.pointer(event, anchorEl); const [ x ] = d3.pointer(event, anchorEl);
startX.current && drawSelection(startX.current, x); startX.current && drawSelection(startX.current, x);
endX.current = x; endX.current = x;
} }
}); })
.on('mouseup.selectionX', handelMouseUp)
.on('touchstart.selectionX', (event: TouchEvent) => {
const pointers = d3.pointers(event, anchorEl);
isActive.current = pointers.length === 2;
})
.on('touchmove.selectionX', (event: TouchEvent) => {
if (isActive.current) {
const pointers = d3.pointers(event, anchorEl);
if (pointers.length === 2 && Math.abs(pointers[0][0] - pointers[1][0]) > 5) {
drawSelection(pointers[0][0], pointers[1][0]);
startX.current = pointers[0][0];
endX.current = pointers[1][0];
}
}
})
.on('touchend.selectionX', handelMouseUp);
d3.select('body').on('mouseup.selectionX', function(event) { d3.select('body').on('mouseup.selectionX', function(event) {
const isOutside = startX.current !== undefined && event.target !== anchorD3.node(); const isOutside = startX.current !== undefined && event.target !== anchorD3.node();
...@@ -99,10 +125,10 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) => ...@@ -99,10 +125,10 @@ const ChartSelectionX = ({ anchorEl, height, scale, data, onSelect }: Props) =>
}); });
return () => { return () => {
anchorD3.on('mousedown.selectionX mouseup.selectionX mousemove.selectionX', null); anchorD3.on('.selectionX', null);
d3.select('body').on('mouseup.selectionX', null); d3.select('body').on('.selectionX', null);
}; };
}, [ anchorEl, drawSelection, getIndexByX, handelMouseUp ]); }, [ anchorEl, cleanUp, drawSelection, getIndexByX, handelMouseUp, handleSelect ]);
return ( return (
<g className="ChartSelectionX" ref={ ref } opacity={ 0 }> <g className="ChartSelectionX" ref={ ref } opacity={ 0 }>
......
...@@ -32,6 +32,8 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...@@ -32,6 +32,8 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
const bgColor = useToken('colors', 'blackAlpha.900'); const bgColor = useToken('colors', 'blackAlpha.900');
const ref = React.useRef(null); const ref = React.useRef(null);
const trackerId = React.useRef<number>();
const isVisible = React.useRef(false);
const drawLine = React.useCallback( const drawLine = React.useCallback(
(x: number) => { (x: number) => {
...@@ -129,67 +131,78 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl, ...@@ -129,67 +131,78 @@ const ChartTooltip = ({ chartId, xScale, yScale, width, height, data, anchorEl,
}, [ drawPoints, drawLine, drawContent ]); }, [ drawPoints, drawLine, drawContent ]);
const showContent = React.useCallback(() => { const showContent = React.useCallback(() => {
d3.select(ref.current).attr('opacity', 1); if (!isVisible.current) {
d3.select(ref.current) d3.select(ref.current).attr('opacity', 1);
.selectAll('.ChartTooltip__point') d3.select(ref.current)
.attr('opacity', 1); .selectAll('.ChartTooltip__point')
.attr('opacity', 1);
isVisible.current = true;
}
}, []); }, []);
const hideContent = React.useCallback(() => { const hideContent = React.useCallback(() => {
d3.select(ref.current).attr('opacity', 0); d3.select(ref.current).attr('opacity', 0);
isVisible.current = false;
}, []); }, []);
const createPointerTracker = React.useCallback((event: PointerEvent, isSubsequentCall?: boolean) => { const createPointerTracker = React.useCallback((event: PointerEvent, isSubsequentCall?: boolean) => {
let isShown = false;
let isPressed = event.pointerType === 'mouse' && event.type === 'pointerdown' && !isSubsequentCall; let isPressed = event.pointerType === 'mouse' && event.type === 'pointerdown' && !isSubsequentCall;
if (isPressed) { if (isPressed) {
hideContent(); hideContent();
} }
trackPointer(event, { return trackPointer(event, {
move: (pointer) => { move: (pointer) => {
if (!pointer.point || isPressed) { if (!pointer.point || isPressed) {
return; return;
} }
draw(pointer); draw(pointer);
if (!isShown) { showContent();
showContent();
isShown = true;
}
}, },
out: () => { out: () => {
hideContent(); hideContent();
isShown = false; trackerId.current = undefined;
}, },
end: (tracker) => { end: () => {
hideContent(); hideContent();
const isOutside = tracker.sourceEvent?.offsetX && width && (tracker.sourceEvent.offsetX > width || tracker.sourceEvent.offsetX < 0); trackerId.current = undefined;
if (!isOutside && isPressed) {
window.setTimeout(() => {
createPointerTracker(event, true);
}, 0);
}
isShown = false;
isPressed = false; isPressed = false;
}, },
}); });
}, [ draw, hideContent, showContent, width ]); }, [ draw, hideContent, showContent ]);
React.useEffect(() => { React.useEffect(() => {
const anchorD3 = d3.select(anchorEl); const anchorD3 = d3.select(anchorEl);
let isMultiTouch = false; // disabling creation of new tracker in multi touch mode
anchorD3 anchorD3
.on('touchmove.tooltip', (event: PointerEvent) => event.preventDefault()) // prevent scrolling .on('touchmove.tooltip', (event: TouchEvent) => event.preventDefault()) // prevent scrolling
.on(`touchstart.tooltip`, (event: TouchEvent) => {
isMultiTouch = event.touches.length > 1;
})
.on(`touchend.tooltip`, (event: TouchEvent) => {
if (isMultiTouch && event.touches.length === 0) {
isMultiTouch = false;
}
})
.on('pointerenter.tooltip pointerdown.tooltip', (event: PointerEvent) => { .on('pointerenter.tooltip pointerdown.tooltip', (event: PointerEvent) => {
createPointerTracker(event); if (!isMultiTouch) {
trackerId.current = createPointerTracker(event);
}
})
.on('pointermove.tooltip', (event: PointerEvent) => {
if (event.pointerType === 'mouse' && !isMultiTouch && trackerId.current === undefined) {
trackerId.current = createPointerTracker(event);
}
}); });
return () => { return () => {
anchorD3.on('touchmove.tooltip pointerenter.tooltip pointerdown.tooltip', null); anchorD3.on('touchmove.tooltip pointerenter.tooltip pointerdown.tooltip', null);
trackerId.current && anchorD3.on(
[ 'pointerup', 'pointercancel', 'lostpointercapture', 'pointermove', 'pointerout' ].map((event) => `${ event }.${ trackerId.current }`).join(' '),
null,
);
}; };
}, [ anchorEl, createPointerTracker, draw, hideContent, showContent ]); }, [ anchorEl, createPointerTracker, draw, hideContent, showContent ]);
......
...@@ -14,7 +14,7 @@ export interface PointerOptions { ...@@ -14,7 +14,7 @@ export interface PointerOptions {
end?: (tracker: Pointer) => void; end?: (tracker: Pointer) => void;
} }
export function trackPointer(event: PointerEvent, { start, move, out, end }: PointerOptions) { export function trackPointer(event: PointerEvent, { start, move, out, end }: PointerOptions): number {
const tracker: Pointer = { const tracker: Pointer = {
id: event.pointerId, id: event.pointerId,
point: null, point: null,
...@@ -26,16 +26,27 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi ...@@ -26,16 +26,27 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi
tracker.point = d3.pointer(event, target); tracker.point = d3.pointer(event, target);
target.setPointerCapture(id); target.setPointerCapture(id);
const untrack = (sourceEvent: PointerEvent) => {
tracker.sourceEvent = sourceEvent;
d3.select(target).on(`.${ id }`, null);
target.releasePointerCapture(id);
end?.(tracker);
};
d3.select(target) d3.select(target)
.on(`touchstart.${ id }`, (sourceEvent: PointerEvent) => {
const target = sourceEvent.target as Element;
const touches = d3.pointers(sourceEvent, target);
// disable current tracker when entering multi touch mode
touches.length > 1 && untrack(sourceEvent);
})
.on(`pointerup.${ id } pointercancel.${ id } lostpointercapture.${ id }`, (sourceEvent: PointerEvent) => { .on(`pointerup.${ id } pointercancel.${ id } lostpointercapture.${ id }`, (sourceEvent: PointerEvent) => {
if (sourceEvent.pointerId !== id) { if (sourceEvent.pointerId !== id) {
return; return;
} }
tracker.sourceEvent = sourceEvent; untrack(sourceEvent);
d3.select(target).on(`.${ id }`, null);
target.releasePointerCapture(id);
end?.(tracker);
}) })
.on(`pointermove.${ id }`, (sourceEvent) => { .on(`pointermove.${ id }`, (sourceEvent) => {
if (sourceEvent.pointerId !== id) { if (sourceEvent.pointerId !== id) {
...@@ -57,5 +68,5 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi ...@@ -57,5 +68,5 @@ export function trackPointer(event: PointerEvent, { start, move, out, end }: Poi
start?.(tracker); start?.(tracker);
return [ 'pointerup', 'pointercancel', 'lostpointercapture', 'pointermove', 'pointerout' ].map((event) => `${ event }.${ id }`); return id;
} }
...@@ -24,9 +24,11 @@ type Props = { ...@@ -24,9 +24,11 @@ type Props = {
query: QueryResult; query: QueryResult;
showBlockInfo?: boolean; showBlockInfo?: boolean;
showSocketInfo?: boolean; showSocketInfo?: boolean;
currentAddress?: string;
filter?: React.ReactNode;
} }
const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Props) => { const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress }: Props) => {
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query); const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage; const isPaginatorHidden = !isLoading && !isError && query.pagination.page === 1 && !query.pagination.hasNextPage;
const isMobile = useIsMobile(); const isMobile = useIsMobile();
...@@ -60,7 +62,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop ...@@ -60,7 +62,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop
{ ({ content }) => <Box>{ content }</Box> } { ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice> </TxsNewItemNotice>
) } ) }
{ txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo }/>) } { txs.map(tx => <TxsListItem tx={ tx } key={ tx.hash } showBlockInfo={ showBlockInfo } currentAddress={ currentAddress }/>) }
</Box> </Box>
</Show> </Show>
<Hide below="lg" ssr={ false }> <Hide below="lg" ssr={ false }>
...@@ -71,6 +73,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop ...@@ -71,6 +73,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo } showSocketInfo={ showSocketInfo }
top={ isPaginatorHidden ? 0 : 80 } top={ isPaginatorHidden ? 0 : 80 }
currentAddress={ currentAddress }
/> />
</Hide> </Hide>
</> </>
...@@ -86,6 +89,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop ...@@ -86,6 +89,7 @@ const TxsContent = ({ query, showBlockInfo = true, showSocketInfo = true }: Prop
setSorting={ setSortByValue } setSorting={ setSortByValue }
paginationProps={ query.pagination } paginationProps={ query.pagination }
showPagination={ !isPaginatorHidden } showPagination={ !isPaginatorHidden }
filterComponent={ filter }
/> />
) } ) }
{ content } { content }
......
...@@ -17,18 +17,14 @@ type Props = { ...@@ -17,18 +17,14 @@ type Props = {
paginationProps: PaginationProps; paginationProps: PaginationProps;
className?: string; className?: string;
showPagination?: boolean; showPagination?: boolean;
filterComponent?: React.ReactNode;
} }
const TxsHeaderMobile = ({ sorting, setSorting, paginationProps, className, showPagination = true }: Props) => { const TxsHeaderMobile = ({ filterComponent, sorting, setSorting, paginationProps, className, showPagination = true }: Props) => {
return ( return (
<ActionBar className={ className }> <ActionBar className={ className }>
<HStack> <HStack>
{ /* api is not implemented */ } { filterComponent }
{ /* <TxsFilters
filters={ filters }
onFiltersChange={ setFilters }
appliedFiltersNum={ 0 }
/> */ }
<TxsSorting <TxsSorting
isActive={ Boolean(sorting) } isActive={ Boolean(sorting) }
setSorting={ setSorting } setSorting={ setSorting }
......
...@@ -24,17 +24,30 @@ import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton'; ...@@ -24,17 +24,30 @@ import AdditionalInfoButton from 'ui/shared/AdditionalInfoButton';
import Address from 'ui/shared/address/Address'; import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType'; import TxType from 'ui/txs/TxType';
const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boolean}) => { type Props = {
tx: Transaction;
showBlockInfo: boolean;
currentAddress?: string;
}
const TAG_WIDTH = 48;
const ARROW_WIDTH = 24;
const TxsListItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
const { isOpen, onOpen, onClose } = useDisclosure(); const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300'); const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200'); const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataTo = tx.to ? tx.to : tx.created_contract; const dataTo = tx.to ? tx.to : tx.created_contract;
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash);
return ( return (
<> <>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}> <Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
...@@ -83,7 +96,7 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo ...@@ -83,7 +96,7 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo
</Box> </Box>
) } ) }
<Flex alignItems="center" height={ 6 } mt={ 6 }> <Flex alignItems="center" height={ 6 } mt={ 6 }>
<Address width="calc((100%-40px)/2)"> <Address width={ `calc((100%-${ currentAddress ? TAG_WIDTH : ARROW_WIDTH + 8 }px)/2)` }>
<AddressIcon hash={ tx.from.hash }/> <AddressIcon hash={ tx.from.hash }/>
<AddressLink <AddressLink
hash={ tx.from.hash } hash={ tx.from.hash }
...@@ -92,12 +105,15 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo ...@@ -92,12 +105,15 @@ const TxsListItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boo
ml={ 2 } ml={ 2 }
/> />
</Address> </Address>
<Icon { (isIn || isOut) ?
as={ rightArrowIcon } <InOutTag isIn={ isIn } isOut={ isOut } width="48px" mr={ 2 }/> : (
boxSize={ 6 } <Icon
mx={ 2 } as={ rightArrowIcon }
color="gray.500" boxSize={ 6 }
/> mx={ 2 }
color="gray.500"
/>
) }
<Address width="calc((100%-40px)/2)"> <Address width="calc((100%-40px)/2)">
<AddressIcon hash={ dataTo.hash }/> <AddressIcon hash={ dataTo.hash }/>
<AddressLink <AddressLink
......
...@@ -18,9 +18,10 @@ type Props = { ...@@ -18,9 +18,10 @@ type Props = {
top: number; top: number;
showBlockInfo: boolean; showBlockInfo: boolean;
showSocketInfo: boolean; showSocketInfo: boolean;
currentAddress?: string;
} }
const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo }: Props) => { const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, currentAddress }: Props) => {
return ( return (
<Table variant="simple" minWidth="950px" size="xs"> <Table variant="simple" minWidth="950px" size="xs">
<TheadSticky top={ top }> <TheadSticky top={ top }>
...@@ -31,7 +32,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo }: Pr ...@@ -31,7 +32,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo }: Pr
<Th width="15%">Method</Th> <Th width="15%">Method</Th>
{ showBlockInfo && <Th width="11%">Block</Th> } { showBlockInfo && <Th width="11%">Block</Th> }
<Th width={{ xl: '128px', base: '66px' }}>From</Th> <Th width={{ xl: '128px', base: '66px' }}>From</Th>
<Th width={{ xl: '36px', base: '0' }}></Th> <Th width={{ xl: currentAddress ? '48px' : '36px', base: '0' }}></Th>
<Th width={{ xl: '128px', base: '66px' }}>To</Th> <Th width={{ xl: '128px', base: '66px' }}>To</Th>
<Th width="18%" isNumeric> <Th width="18%" isNumeric>
<Link onClick={ sort('val') } display="flex" justifyContent="end"> <Link onClick={ sort('val') } display="flex" justifyContent="end">
...@@ -60,6 +61,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo }: Pr ...@@ -60,6 +61,7 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo }: Pr
key={ item.hash } key={ item.hash }
tx={ item } tx={ item }
showBlockInfo={ showBlockInfo } showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress }
/> />
)) } )) }
</Tbody> </Tbody>
......
...@@ -28,13 +28,23 @@ import Address from 'ui/shared/address/Address'; ...@@ -28,13 +28,23 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon'; import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink'; import AddressLink from 'ui/shared/address/AddressLink';
import CurrencyValue from 'ui/shared/CurrencyValue'; import CurrencyValue from 'ui/shared/CurrencyValue';
import InOutTag from 'ui/shared/InOutTag';
import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip'; import TruncatedTextTooltip from 'ui/shared/TruncatedTextTooltip';
import TxStatus from 'ui/shared/TxStatus'; import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo'; import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from './TxType'; import TxType from './TxType';
const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: boolean }) => { type Props = {
tx: Transaction;
showBlockInfo: boolean;
currentAddress?: string;
}
const TxsTableItem = ({ tx, showBlockInfo, currentAddress }: Props) => {
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
const isIn = Boolean(currentAddress && currentAddress === tx.to?.hash);
const addressFrom = ( const addressFrom = (
<Address> <Address>
<Tooltip label={ tx.from.implementation_name }> <Tooltip label={ tx.from.implementation_name }>
...@@ -110,8 +120,11 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo ...@@ -110,8 +120,11 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
<Td> <Td>
{ addressFrom } { addressFrom }
</Td> </Td>
<Td> <Td px={ 0 }>
<Icon as={ rightArrowIcon } boxSize={ 6 } mr={ 2 } color="gray.500"/> { (isIn || isOut) ?
<InOutTag isIn={ isIn } isOut={ isOut } width="48px" mr={ 2 }/> :
<Icon as={ rightArrowIcon } boxSize={ 6 } mx="6px" color="gray.500"/>
}
</Td> </Td>
<Td> <Td>
{ addressTo } { addressTo }
...@@ -121,14 +134,17 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo ...@@ -121,14 +134,17 @@ const TxsTableItem = ({ tx, showBlockInfo }: {tx: Transaction; showBlockInfo: bo
<Td colSpan={ 3 }> <Td colSpan={ 3 }>
<Box> <Box>
{ addressFrom } { addressFrom }
<Icon { (isIn || isOut) ?
as={ rightArrowIcon } <InOutTag isIn={ isIn } isOut={ isOut } width="48px" my={ 2 }/> : (
boxSize={ 6 } <Icon
mt={ 2 } as={ rightArrowIcon }
mb={ 1 } boxSize={ 6 }
color="gray.500" mt={ 2 }
transform="rotate(90deg)" mb={ 1 }
/> color="gray.500"
transform="rotate(90deg)"
/>
) }
{ addressTo } { addressTo }
</Box> </Box>
</Td> </Td>
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment