Commit 2e4117d6 authored by tom's avatar tom

refactor sockets for transactions

parent 5e920bb5
......@@ -85,36 +85,6 @@ test.describe('socket', () => {
// test cases which use socket cannot run in parallel since the socket server always run on the same port
test.describe.configure({ mode: 'serial' });
test('without overload', async({ render, mockApiResponse, page, createSocket }) => {
await mockApiResponse(
'address_txs',
{ items: [ txMock.base ], next_page_params: DEFAULT_PAGINATION },
{ pathParams: { hash: CURRENT_ADDRESS } },
);
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs/>
</Box>,
{ hooksConfig },
{ withSocket: true },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, `addresses:${ CURRENT_ADDRESS.toLowerCase() }`);
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
const thirdRow = page.locator('tbody tr:nth-child(3)');
await thirdRow.waitFor();
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(4);
});
test('with update', async({ render, mockApiResponse, page, createSocket }) => {
await mockApiResponse(
'address_txs',
......@@ -136,13 +106,13 @@ test.describe('socket', () => {
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base, txMock.base2 ] });
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base ] });
const thirdRow = page.locator('tbody tr:nth-child(3)');
await thirdRow.waitFor();
const secondRow = page.locator('tbody tr:nth-child(2)');
await secondRow.waitFor();
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
expect(itemsCountNew).toBe(2);
});
test('with overload', async({ render, mockApiResponse, page, createSocket }) => {
......@@ -154,7 +124,7 @@ test.describe('socket', () => {
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs overloadCount={ 2 }/>
<AddressTxs/>
</Box>,
{ hooksConfig },
{ withSocket: true },
......@@ -205,10 +175,10 @@ test.describe('socket', () => {
const itemsCount = await page.locator('tbody tr').count();
expect(itemsCount).toBe(2);
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2, txMock.base4 ] });
socketServer.sendMessage(socket, channel, 'transaction', { transactions: [ txMock.base2 ] });
const thirdRow = page.locator('tbody tr:nth-child(3)');
await thirdRow.waitFor();
const secondRow = page.locator('tbody tr:nth-child(2)');
await secondRow.waitFor();
const itemsCountNew = await page.locator('tbody tr').count();
expect(itemsCountNew).toBe(3);
......@@ -229,7 +199,7 @@ test.describe('socket', () => {
await render(
<Box pt={{ base: '134px', lg: 6 }}>
<AddressTxs overloadCount={ 2 }/>
<AddressTxs/>
</Box>,
{ hooksConfig: hooksConfigWithFilter },
{ withSocket: true },
......
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import type { AddressFromToFilter } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import type { Transaction, TransactionsSortingField, TransactionsSortingValue, TransactionsSorting } from 'types/api/transaction';
import type { TransactionsSortingField, TransactionsSortingValue, TransactionsSorting } from 'types/api/transaction';
import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useIsMounted from 'lib/hooks/useIsMounted';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import { TX } from 'stubs/tx';
import { generateListStub } from 'stubs/utils';
import ActionBar, { ACTION_BAR_HEIGHT_DESKTOP } from 'ui/shared/ActionBar';
......@@ -21,45 +16,23 @@ import Pagination from 'ui/shared/pagination/Pagination';
import useQueryWithPages from 'ui/shared/pagination/useQueryWithPages';
import getSortParamsFromValue from 'ui/shared/sort/getSortParamsFromValue';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import { sortTxsFromSocket } from 'ui/txs/sortTxs';
import TxsWithAPISorting from 'ui/txs/TxsWithAPISorting';
import { SORT_OPTIONS } from 'ui/txs/useTxsSort';
import AddressCsvExportLink from './AddressCsvExportLink';
import AddressTxsFilter from './AddressTxsFilter';
const OVERLOAD_COUNT = 75;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, address?: string) => {
if (!filterValue) {
return true;
}
if (filterValue === 'from') {
return transaction.from.hash === address;
}
if (filterValue === 'to') {
return transaction.to?.hash === address;
}
};
type Props = {
shouldRender?: boolean;
isQueryEnabled?: boolean;
// for tests only
overloadCount?: number;
};
const AddressTxs = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQueryEnabled = true }: Props) => {
const AddressTxs = ({ shouldRender = true, isQueryEnabled = true }: Props) => {
const router = useRouter();
const queryClient = useQueryClient();
const isMounted = useIsMounted();
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const [ sort, setSort ] = React.useState<TransactionsSortingValue>(getSortValueFromQuery<TransactionsSortingValue>(router.query, SORT_OPTIONS) || 'default');
const isMobile = useIsMobile();
......@@ -90,76 +63,6 @@ const AddressTxs = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQue
addressTxsQuery.onFilterChange({ filter: newVal });
}, [ addressTxsQuery ]);
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = React.useCallback((payload) => {
setSocketAlert('');
queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: { filter: filterValue } }),
(prevData: AddressTransactionsResponse | undefined) => {
if (!prevData) {
return;
}
const newItems: Array<Transaction> = [];
let newCount = 0;
payload.transactions.forEach(tx => {
const currIndex = prevData.items.findIndex((item) => item.hash === tx.hash);
if (currIndex > -1) {
prevData.items[currIndex] = tx;
} else {
if (matchFilter(filterValue, tx, currentAddress)) {
if (newItems.length + prevData.items.length >= overloadCount) {
newCount++;
} else {
newItems.push(tx);
}
}
}
});
if (newCount > 0) {
setNewItemsCount(prev => prev + newCount);
}
return {
...prevData,
items: [
...newItems,
...prevData.items,
].sort(sortTxsFromSocket(sort)),
};
});
}, [ currentAddress, filterValue, overloadCount, queryClient, sort ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please refresh the page to load new transactions.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please refresh the page.');
}, []);
const channel = useSocketChannel({
topic: `addresses:${ currentAddress?.toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: addressTxsQuery.pagination.page !== 1 || addressTxsQuery.isPlaceholderData,
});
useSocketMessage({
channel,
event: 'transaction',
handler: handleNewSocketMessage,
});
useSocketMessage({
channel,
event: 'pending_transaction',
handler: handleNewSocketMessage,
});
if (!isMounted || !shouldRender) {
return null;
}
......@@ -197,9 +100,7 @@ const AddressTxs = ({ overloadCount = OVERLOAD_COUNT, shouldRender = true, isQue
query={ addressTxsQuery }
currentAddress={ typeof currentAddress === 'string' ? currentAddress : undefined }
enableTimeIncrement
showSocketInfo={ addressTxsQuery.pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
socketType="address_txs"
top={ ACTION_BAR_HEIGHT_DESKTOP }
sorting={ sort }
setSort={ setSort }
......
......@@ -6,10 +6,10 @@ import { route } from 'nextjs-routes';
import useApiQuery from 'lib/api/useApiQuery';
import { AddressHighlightProvider } from 'lib/contexts/addressHighlight';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import { TX } from 'stubs/tx';
import { Link } from 'toolkit/chakra/link';
import SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import useNewTxsSocket from 'ui/txs/socket/useTxsSocketTypeAll';
import LatestTxsItem from './LatestTxsItem';
import LatestTxsItemMobile from './LatestTxsItemMobile';
......@@ -23,7 +23,7 @@ const LatestTransactions = () => {
},
});
const { num, socketAlert } = useNewTxsSocket();
const { num, alertText } = useNewTxsSocket({ type: 'txs_home', isLoading: isPlaceholderData });
if (isError) {
return <Text mt={ 4 }>No data. Please reload the page.</Text>;
......@@ -33,7 +33,7 @@ const LatestTransactions = () => {
const txsUrl = route({ pathname: '/txs' });
return (
<>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ socketAlert } isLoading={ isPlaceholderData }/>
<SocketNewItemsNotice borderBottomRadius={ 0 } url={ txsUrl } num={ num } alert={ alertText } isLoading={ isPlaceholderData }/>
<Box mb={ 3 } display={{ base: 'block', lg: 'none' }}>
{ data.slice(0, txsCount).map(((tx, index) => (
<LatestTxsItemMobile
......
......@@ -85,7 +85,7 @@ const ArbitrumL2TxnBatch = () => {
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
component: <TxsWithFrontendSorting query={ batchTxsQuery } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
{
id: 'blocks',
......
......@@ -73,7 +73,7 @@ const BlockPageContent = () => {
component: (
<>
{ blockTxsQuery.isDegradedData && <ServiceDegradationWarning isLoading={ blockTxsQuery.isPlaceholderData } mb={ 6 }/> }
<TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>
<TxsWithFrontendSorting query={ blockTxsQuery } showBlockInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>
</>
),
},
......@@ -82,7 +82,7 @@ const BlockPageContent = () => {
id: 'blob_txs',
title: 'Blob txns',
component: (
<TxsWithFrontendSorting query={ blockBlobTxsQuery } showBlockInfo={ false } showSocketInfo={ false }/>
<TxsWithFrontendSorting query={ blockBlobTxsQuery } showBlockInfo={ false }/>
),
} : null,
config.features.beaconChain.isEnabled && Boolean(blockQuery.data?.withdrawals_count) ?
......
......@@ -31,10 +31,7 @@ const KettleTxs = () => {
<>
<PageTitle title="Computor transactions" withTextAd/>
<AddressEntity address={{ hash }} mb={ 6 }/>
<TxsWithFrontendSorting
query={ query }
showSocketInfo={ false }
/>
<TxsWithFrontendSorting query={ query }/>
</>
);
};
......
......@@ -83,7 +83,7 @@ const OptimisticL2TxnBatch = () => {
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
component: <TxsWithFrontendSorting query={ batchTxsQuery } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
{
id: 'blocks',
......
......@@ -90,7 +90,7 @@ const ScrollL2TxnBatch = () => {
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
component: <TxsWithFrontendSorting query={ batchTxsQuery } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
{
id: 'blocks',
......
......@@ -9,7 +9,6 @@ import { route } from 'nextjs-routes';
import config from 'configs/app';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import getNetworkValidationActionText from 'lib/networks/getNetworkValidationActionText';
import getQueryParamString from 'lib/router/getQueryParamString';
import { TX } from 'stubs/tx';
......@@ -91,8 +90,6 @@ const Transactions = () => {
},
});
const { num, socketAlert } = useNewTxsSocket();
const isAuth = useIsAuth();
const tabs: Array<TabItemRegular> = [
......@@ -102,9 +99,7 @@ const Transactions = () => {
component:
<TxsWithFrontendSorting
query={ txsValidatedQuery }
showSocketInfo={ txsValidatedQuery.pagination.page === 1 }
socketInfoNum={ num }
socketInfoAlert={ socketAlert }
socketType="txs_validated"
top={ TABS_HEIGHT }
/> },
{
......@@ -114,9 +109,7 @@ const Transactions = () => {
<TxsWithFrontendSorting
query={ txsPendingQuery }
showBlockInfo={ false }
showSocketInfo={ txsPendingQuery.pagination.page === 1 }
socketInfoNum={ num }
socketInfoAlert={ socketAlert }
socketType="txs_pending"
top={ TABS_HEIGHT }
/>
),
......@@ -127,9 +120,6 @@ const Transactions = () => {
component: (
<TxsWithFrontendSorting
query={ txsWithBlobsQuery }
showSocketInfo={ txsWithBlobsQuery.pagination.page === 1 }
socketInfoNum={ num }
socketInfoAlert={ socketAlert }
top={ TABS_HEIGHT }
/>
),
......
......@@ -47,7 +47,7 @@ const ZkEvmL2TxnBatch = () => {
const tabs: Array<TabItemRegular> = React.useMemo(() => ([
{ id: 'index', title: 'Details', component: <ZkEvmL2TxnBatchDetails query={ batchQuery }/> },
{ id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false }/> },
{ id: 'txs', title: 'Transactions', component: <TxsWithFrontendSorting query={ batchTxsQuery }/> },
].filter(Boolean)), [ batchQuery, batchTxsQuery ]);
const backLink = React.useMemo(() => {
......
......@@ -67,7 +67,7 @@ const ZkSyncL2TxnBatch = () => {
{
id: 'txs',
title: 'Transactions',
component: <TxsWithFrontendSorting query={ batchTxsQuery } showSocketInfo={ false } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
component: <TxsWithFrontendSorting query={ batchTxsQuery } top={ hasPagination ? TABS_HEIGHT : 0 }/>,
},
].filter(Boolean)), [ batchQuery, batchTxsQuery, hasPagination ]);
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { AddressFromToFilter } from 'types/api/address';
import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction';
......@@ -26,9 +27,7 @@ type Props = {
// eslint-disable-next-line max-len
query: QueryWithPagesResult<'txs_validated' | 'txs_pending'> | QueryWithPagesResult<'txs_watchlist'> | QueryWithPagesResult<'block_txs'> | QueryWithPagesResult<'zkevm_l2_txn_batch_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
currentAddress?: string;
filter?: React.ReactNode;
filterValue?: AddressFromToFilter;
......@@ -46,9 +45,7 @@ const TxsContent = ({
filter,
filterValue,
showBlockInfo = true,
showSocketInfo = true,
socketInfoAlert,
socketInfoNum,
socketType,
currentAddress,
enableTimeIncrement,
top,
......@@ -72,9 +69,7 @@ const TxsContent = ({
<Box hideFrom="lg">
<TxsList
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
socketType={ socketType }
isLoading={ isPlaceholderData }
enableTimeIncrement={ enableTimeIncrement }
currentAddress={ currentAddress }
......@@ -87,9 +82,7 @@ const TxsContent = ({
sort={ sort }
onSortToggle={ onSortToggle }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
socketType={ socketType }
top={ top || (query.pagination.isVisible ? ACTION_BAR_HEIGHT_DESKTOP : 0) }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
......
import { Box } from '@chakra-ui/react';
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { Transaction } from 'types/api/transaction';
import useInitialList from 'lib/hooks/useInitialList';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TxsSocketNotice from './socket/TxsSocketNotice';
import TxsListItem from './TxsListItem';
interface Props {
showBlockInfo: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
enableTimeIncrement?: boolean;
currentAddress?: string;
isLoading: boolean;
......@@ -30,14 +29,7 @@ const TxsList = (props: Props) => {
return (
<Box>
{ props.showSocketInfo && (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ props.socketInfoNum }
alert={ props.socketInfoAlert }
isLoading={ props.isLoading }
/>
) }
{ props.socketType && <TxsSocketNotice type={ props.socketType } place="list" isLoading={ props.isLoading }/> }
{ props.items.slice(0, renderedItemsNum).map((tx, index) => (
<TxsListItem
key={ tx.hash + (props.isLoading ? index : '') }
......
......@@ -15,7 +15,6 @@ test('base view +@dark-mode', async({ render }) => {
onSortToggle={ () => {} }
top={ 0 }
showBlockInfo
showSocketInfo={ false }
/>,
);
......@@ -36,7 +35,6 @@ test.describe('screen xl', () => {
onSortToggle={ () => {} }
top={ 0 }
showBlockInfo
showSocketInfo={ false }
/>,
);
......
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { Transaction, TransactionsSortingField, TransactionsSortingValue } from 'types/api/transaction';
import config from 'configs/app';
......@@ -8,8 +9,8 @@ import useInitialList from 'lib/hooks/useInitialList';
import useLazyRenderedList from 'lib/hooks/useLazyRenderedList';
import { currencyUnits } from 'lib/units';
import { TableBody, TableColumnHeader, TableColumnHeaderSortable, TableHeaderSticky, TableRoot, TableRow } from 'toolkit/chakra/table';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import TxsSocketNotice from './socket/TxsSocketNotice';
import TxsTableItem from './TxsTableItem';
type Props = {
......@@ -18,9 +19,7 @@ type Props = {
onSortToggle: (field: TransactionsSortingField) => void;
top: number;
showBlockInfo: boolean;
showSocketInfo: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
currentAddress?: string;
enableTimeIncrement?: boolean;
isLoading?: boolean;
......@@ -32,9 +31,7 @@ const TxsTable = ({
onSortToggle,
top,
showBlockInfo,
showSocketInfo,
socketInfoAlert,
socketInfoNum,
socketType,
currentAddress,
enableTimeIncrement,
isLoading,
......@@ -96,14 +93,7 @@ const TxsTable = ({
</TableRow>
</TableHeaderSticky>
<TableBody>
{ showSocketInfo && (
<SocketNewItemsNotice.Desktop
url={ window.location.href }
alert={ socketInfoAlert }
num={ socketInfoNum }
isLoading={ isLoading }
/>
) }
{ socketType && <TxsSocketNotice type={ socketType } place="table" isLoading={ isLoading }/> }
{ txs.slice(0, renderedItemsNum).map((item, index) => (
<TxsTableItem
key={ item.hash + (isLoading ? index : '') }
......
......@@ -10,7 +10,7 @@ type Props = {
const TxsWatchlist = ({ query }: Props) => {
useRedirectForInvalidAuthToken();
return <TxsWithFrontendSorting query={ query } showSocketInfo={ false } top={ 88 }/>;
return <TxsWithFrontendSorting query={ query } top={ 88 }/>;
};
export default TxsWatchlist;
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { AddressFromToFilter } from 'types/api/address';
import type { TransactionsSortingValue } from 'types/api/transaction';
......@@ -12,9 +13,7 @@ type Props = {
query: QueryWithPagesResult<'address_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
currentAddress?: string;
filter?: React.ReactNode;
filterValue?: AddressFromToFilter;
......@@ -29,9 +28,7 @@ const TxsWithAPISorting = ({
filterValue,
query,
showBlockInfo = true,
showSocketInfo = true,
socketInfoAlert,
socketInfoNum,
socketType,
currentAddress,
enableTimeIncrement,
top,
......@@ -49,9 +46,7 @@ const TxsWithAPISorting = ({
filter={ filter }
filterValue={ filterValue }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
socketType={ socketType }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
top={ top }
......
import React from 'react';
import type { TxsSocketType } from './socket/types';
import type { AddressFromToFilter } from 'types/api/address';
import type { QueryWithPagesResult } from 'ui/shared/pagination/useQueryWithPages';
......@@ -11,9 +12,7 @@ type Props = {
// eslint-disable-next-line max-len
query: QueryWithPagesResult<'txs_validated' | 'txs_pending'> | QueryWithPagesResult<'txs_watchlist'> | QueryWithPagesResult<'block_txs'> | QueryWithPagesResult<'zkevm_l2_txn_batch_txs'>;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
socketType?: TxsSocketType;
currentAddress?: string;
filter?: React.ReactNode;
filterValue?: AddressFromToFilter;
......@@ -26,9 +25,7 @@ const TxsWithFrontendSorting = ({
filterValue,
query,
showBlockInfo = true,
showSocketInfo = true,
socketInfoAlert,
socketInfoNum,
socketType,
currentAddress,
enableTimeIncrement,
top,
......@@ -40,9 +37,7 @@ const TxsWithFrontendSorting = ({
filter={ filter }
filterValue={ filterValue }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
socketType={ socketType }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
top={ top }
......
import React from 'react';
import type { TxsSocketNoticePlace, TxsSocketType } from './types';
import useIsMobile from 'lib/hooks/useIsMobile';
import TxsSocketNoticeTypeAddress from './TxsSocketNoticeTypeAddress';
import TxsSocketNoticeTypeAll from './TxsSocketNoticeTypeAll';
interface Props {
type: TxsSocketType;
place: TxsSocketNoticePlace;
isLoading?: boolean;
}
const TxsSocketNotice = ({ type, place, isLoading }: Props) => {
const isMobile = useIsMobile();
if ((isMobile && place === 'table') || (!isMobile && place === 'list')) {
return null;
}
switch (type) {
case 'txs_home':
case 'txs_validated':
case 'txs_pending': {
return <TxsSocketNoticeTypeAll type={ type } place={ place } isLoading={ isLoading }/>;
}
case 'address_txs': {
return <TxsSocketNoticeTypeAddress place={ place } isLoading={ isLoading }/>;
}
default:
return null;
}
};
export default React.memo(TxsSocketNotice);
import React from 'react';
import type { TxsSocketNoticePlace } from './types';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import useTxsSocketTypeAddress from './useTxsSocketTypeAddress';
interface Props {
place: TxsSocketNoticePlace;
isLoading?: boolean;
}
const TxsSocketNoticeTypeAddress = ({ place, isLoading }: Props) => {
const { num, alertText } = useTxsSocketTypeAddress({ isLoading });
if (num === undefined) {
return null;
}
if (place === 'table') {
return (
<SocketNewItemsNotice.Desktop
url={ window.location.href }
alert={ alertText }
num={ num }
isLoading={ isLoading }
/>
);
}
if (place === 'list') {
return (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ num }
alert={ alertText }
isLoading={ isLoading }
/>
);
}
};
export default React.memo(TxsSocketNoticeTypeAddress);
import React from 'react';
import type { TxsSocketNoticePlace, TxsSocketType } from './types';
import * as SocketNewItemsNotice from 'ui/shared/SocketNewItemsNotice';
import useNewTxsSocketTypeAll from './useTxsSocketTypeAll';
interface Props {
type: TxsSocketType;
place: TxsSocketNoticePlace;
isLoading?: boolean;
}
const TxsSocketNoticeTypeAll = ({ type, place, isLoading }: Props) => {
const { num, alertText } = useNewTxsSocketTypeAll({ type, isLoading });
if (num === undefined) {
return null;
}
if (place === 'table') {
return (
<SocketNewItemsNotice.Desktop
url={ window.location.href }
alert={ alertText }
num={ num }
isLoading={ isLoading }
/>
);
}
if (place === 'list') {
return (
<SocketNewItemsNotice.Mobile
url={ window.location.href }
num={ num }
alert={ alertText }
isLoading={ isLoading }
/>
);
}
};
export default React.memo(TxsSocketNoticeTypeAll);
export type TxsSocketType = 'txs_validated' | 'txs_pending' | 'txs_home' | 'address_txs';
export type TxsSocketNoticePlace = 'list' | 'table';
import { useQueryClient } from '@tanstack/react-query';
import { useRouter } from 'next/router';
import React from 'react';
import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import type { Transaction, TransactionsSortingValue } from 'types/api/transaction';
import config from 'configs/app';
import { getResourceKey } from 'lib/api/useApiQuery';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import getSortValueFromQuery from 'ui/shared/sort/getSortValueFromQuery';
import { sortTxsFromSocket } from '../sortTxs';
import { SORT_OPTIONS } from '../useTxsSort';
const matchFilter = (filterValue: AddressFromToFilter, transaction: Transaction, address?: string) => {
if (!filterValue) {
return true;
}
if (filterValue === 'from') {
return transaction.from.hash === address;
}
if (filterValue === 'to') {
return transaction.to?.hash === address;
}
};
const OVERLOAD_COUNT = config.app.isPw ? 2 : 75;
interface Params {
isLoading?: boolean;
}
export default function useTxsSocketTypeAddress({ isLoading }: Params) {
const [ alertText, setAlertText ] = React.useState('');
const [ num, setNum ] = React.useState(0);
const router = useRouter();
const queryClient = useQueryClient();
const currentAddress = getQueryParamString(router.query.hash);
const filterValue = getQueryParamString(router.query.filter);
const page = getQueryParamString(router.query.page);
const sort = getSortValueFromQuery<TransactionsSortingValue>(router.query, SORT_OPTIONS) || 'default';
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = React.useCallback((payload) => {
setAlertText('');
queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { hash: currentAddress }, queryParams: filterValue ? { filter: filterValue } : undefined }),
(prevData: AddressTransactionsResponse | undefined) => {
if (!prevData) {
return;
}
const newItems: Array<Transaction> = [];
let newCount = 0;
payload.transactions.forEach(tx => {
const currIndex = prevData.items.findIndex((item) => item.hash === tx.hash);
if (currIndex > -1) {
prevData.items[currIndex] = tx;
} else {
const isMatch = matchFilter(filterValue as AddressFromToFilter, tx, currentAddress);
if (isMatch) {
if (newItems.length + prevData.items.length >= OVERLOAD_COUNT) {
newCount++;
} else {
newItems.push(tx);
}
}
}
});
if (newCount > 0) {
setNum(prev => prev + newCount);
}
return {
...prevData,
items: [
...newItems,
...prevData.items,
].sort(sortTxsFromSocket(sort)),
};
});
}, [ currentAddress, filterValue, queryClient, sort ]);
const handleSocketClose = React.useCallback(() => {
setAlertText('Connection is lost. Please refresh the page to load new transactions.');
}, []);
const handleSocketError = React.useCallback(() => {
setAlertText('An error has occurred while fetching new transactions. Please refresh the page.');
}, []);
const isDisabled = Boolean((page && page !== '1') || isLoading);
const channel = useSocketChannel({
topic: `addresses:${ currentAddress?.toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled,
});
useSocketMessage({
channel,
event: 'transaction',
handler: handleNewSocketMessage,
});
useSocketMessage({
channel,
event: 'pending_transaction',
handler: handleNewSocketMessage,
});
if (isDisabled) {
return { };
}
return { num, alertText };
}
import type { NextRouter } from 'next/router';
import { useRouter } from 'next/router';
import React from 'react';
import type { TxsSocketType } from './types';
import useGradualIncrement from 'lib/hooks/useGradualIncrement';
import getQueryParamString from 'lib/router/getQueryParamString';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
function getSocketParams(router: NextRouter) {
if (
router.pathname === '/txs' &&
(router.query.tab === 'validated' || router.query.tab === undefined) &&
!router.query.block_number &&
!router.query.page
) {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
if (router.pathname === '/') {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
function getSocketParams(type: TxsSocketType, page: string) {
if (
router.pathname === '/txs' &&
router.query.tab === 'pending' &&
!router.query.block_number &&
!router.query.page
) {
return { topic: 'transactions:new_pending_transaction' as const, event: 'pending_transaction' as const };
switch (type) {
case 'txs_home': {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
case 'txs_validated': {
return !page || page === '1' ? { topic: 'transactions:new_transaction' as const, event: 'transaction' as const } : {};
}
case 'txs_pending': {
return !page || page === '1' ? { topic: 'transactions:new_pending_transaction' as const, event: 'pending_transaction' as const } : {};
}
default:
return {};
}
return {};
}
function assertIsNewTxResponse(response: unknown): response is { transaction: number } {
......@@ -40,12 +32,19 @@ function assertIsNewPendingTxResponse(response: unknown): response is { pending_
return typeof response === 'object' && response !== null && 'pending_transaction' in response;
}
export default function useNewTxsSocket() {
interface Params {
type: TxsSocketType;
isLoading?: boolean;
}
export default function useNewTxsSocketTypeAll({ type, isLoading }: Params) {
const router = useRouter();
const page = getQueryParamString(router.query.page);
const [ num, setNum ] = useGradualIncrement(0);
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ alertText, setAlertText ] = React.useState('');
const { topic, event } = getSocketParams(router);
const { topic, event } = getSocketParams(type, page);
const handleNewTxMessage = React.useCallback((response: { transaction: number } | { pending_transaction: number } | unknown) => {
if (assertIsNewTxResponse(response)) {
......@@ -57,18 +56,18 @@ export default function useNewTxsSocket() {
}, [ setNum ]);
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please reload the page.');
setAlertText('Connection is lost. Please reload the page.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please reload the page.');
setAlertText('An error has occurred while fetching new transactions. Please reload the page.');
}, []);
const channel = useSocketChannel({
topic,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: !topic,
isDisabled: !topic || Boolean(isLoading),
});
useSocketMessage({
......@@ -78,8 +77,8 @@ export default function useNewTxsSocket() {
});
if (!topic && !event) {
return {};
return { };
}
return { num, socketAlert };
return { num, alertText };
}
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