Commit 99735f0f authored by isstuev's avatar isstuev

address txs socket

parent 77e0febf
......@@ -12,7 +12,8 @@ function getSocketParams(router: NextRouter) {
if (
router.pathname === ROUTES.txs.pattern &&
(router.query.tab === 'validated' || router.query.tab === undefined) &&
!router.query.block_number
!router.query.block_number &&
!router.query.page
) {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
......@@ -21,7 +22,12 @@ function getSocketParams(router: NextRouter) {
return { topic: 'transactions:new_transaction' as const, event: 'transaction' as const };
}
if (router.pathname === ROUTES.txs.pattern && router.query.tab === 'pending' && !router.query.block_number) {
if (
router.pathname === ROUTES.txs.pattern &&
router.query.tab === 'pending' &&
!router.query.block_number &&
!router.query.page
) {
return { topic: 'transactions:new_pending_transaction' as const, event: 'pending_transaction' as const };
}
......
......@@ -2,6 +2,7 @@ import type { Channel } from 'phoenix';
import type { AddressCoinBalanceHistoryItem } from 'types/api/address';
import type { NewBlockSocketResponse } from 'types/api/block';
import type { Transaction } from 'types/api/transaction';
export type SocketMessageParams = SocketMessage.NewBlock |
SocketMessage.BlocksIndexStatus |
......@@ -13,6 +14,8 @@ SocketMessage.AddressBalance |
SocketMessage.AddressCurrentCoinBalance |
SocketMessage.AddressTokenBalance |
SocketMessage.AddressCoinBalance |
SocketMessage.AddressTxs |
SocketMessage.AddressTxsPending |
SocketMessage.Unknown;
interface SocketMessageParamsGeneric<Event extends string | undefined, Payload extends object | unknown> {
......@@ -34,5 +37,7 @@ export namespace SocketMessage {
SocketMessageParamsGeneric<'current_coin_balance', { coin_balance: string; block_number: number; exchange_rate: string }>;
export type AddressTokenBalance = SocketMessageParamsGeneric<'token_balance', { block_number: number }>;
export type AddressCoinBalance = SocketMessageParamsGeneric<'coin_balance', { coin_balance: AddressCoinBalanceHistoryItem }>;
export type AddressTxs = SocketMessageParamsGeneric<'transaction', { transaction: Transaction }>;
export type AddressTxsPending = SocketMessageParamsGeneric<'pending_transaction', { transaction: Transaction }>;
export type Unknown = SocketMessageParamsGeneric<undefined, unknown>;
}
import { useQueryClient } from '@tanstack/react-query';
import castArray from 'lodash/castArray';
import { useRouter } from 'next/router';
import React from 'react';
import type { AddressFromToFilter } from 'types/api/address';
import type { SocketMessage } from 'lib/socket/types';
import type { AddressFromToFilter, AddressTransactionsResponse } from 'types/api/address';
import { AddressFromToFilterValues } from 'types/api/address';
import { getResourceKey } from 'lib/api/useApiQuery';
import getFilterValueFromQuery from 'lib/getFilterValueFromQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import useSocketChannel from 'lib/socket/useSocketChannel';
import useSocketMessage from 'lib/socket/useSocketMessage';
import ActionBar from 'ui/shared/ActionBar';
import Pagination from 'ui/shared/Pagination';
import TxsContent from 'ui/txs/TxsContent';
import AddressTxsFilter from './AddressTxsFilter';
const OVERLOAD_COUNT = 75;
const getFilterValue = (getFilterValueFromQuery<AddressFromToFilter>).bind(null, AddressFromToFilterValues);
const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}) => {
const router = useRouter();
const queryClient = useQueryClient();
const [ socketAlert, setSocketAlert ] = React.useState('');
const [ newItemsCount, setNewItemsCount ] = React.useState(0);
const isMobile = useIsMobile();
......@@ -37,6 +48,85 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
addressTxsQuery.onFilterChange({ filter: newVal });
}, [ addressTxsQuery ]);
const handleNewSocketMessage: SocketMessage.AddressTxs['handler'] = (payload) => {
setSocketAlert('');
const currentAddress = router.query.id?.toString();
if (addressTxsQuery.data?.items && addressTxsQuery.data.items.length >= OVERLOAD_COUNT) {
if (
!filterValue ||
(filterValue === 'from' && payload.transaction.from.hash === currentAddress) ||
(filterValue === 'to' && payload.transaction.to?.hash === currentAddress)
) {
setNewItemsCount(prev => prev + 1);
}
}
queryClient.setQueryData(
getResourceKey('address_txs', { pathParams: { id: router.query.id?.toString() }, queryParams: { filter: filterValue } }),
(prevData: AddressTransactionsResponse | undefined) => {
if (!prevData) {
return;
}
const currIndex = prevData.items.findIndex((tx) => tx.hash === payload.transaction.hash);
if (currIndex > -1) {
prevData.items[currIndex] = payload.transaction;
return prevData;
}
if (prevData.items.length >= OVERLOAD_COUNT) {
return prevData;
}
if (filterValue) {
if (
(filterValue === 'from' && payload.transaction.from.hash !== currentAddress) ||
(filterValue === 'to' && payload.transaction.to?.hash !== currentAddress)
) {
return prevData;
}
}
return {
...prevData,
items: [
payload.transaction,
...prevData.items,
],
};
});
};
const handleSocketClose = React.useCallback(() => {
setSocketAlert('Connection is lost. Please click here to load new transactions.');
}, []);
const handleSocketError = React.useCallback(() => {
setSocketAlert('An error has occurred while fetching new transactions. Please click here to refresh the page.');
}, []);
const channel = useSocketChannel({
topic: `addresses:${ (router.query.id as string).toLowerCase() }`,
onSocketClose: handleSocketClose,
onSocketError: handleSocketError,
isDisabled: addressTxsQuery.pagination.page !== 1,
});
useSocketMessage({
channel,
event: 'transaction',
handler: handleNewSocketMessage,
});
useSocketMessage({
channel,
event: 'pending_transaction',
handler: handleNewSocketMessage,
});
const filter = (
<AddressTxsFilter
defaultFilter={ filterValue }
......@@ -56,9 +146,11 @@ const AddressTxs = ({ scrollRef }: {scrollRef?: React.RefObject<HTMLDivElement>}
<TxsContent
filter={ filter }
query={ addressTxsQuery }
showSocketInfo={ false }
currentAddress={ typeof router.query.id === 'string' ? router.query.id : undefined }
enableTimeIncrement
showSocketInfo={ addressTxsQuery.pagination.page === 1 }
socketInfoAlert={ socketAlert }
socketInfoNum={ newItemsCount }
/>
</>
);
......
......@@ -3,6 +3,7 @@ import React from 'react';
import useApiQuery from 'lib/api/useApiQuery';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import link from 'lib/link/link';
import TxsNewItemNotice from 'ui/txs/TxsNewItemNotice';
......@@ -14,6 +15,8 @@ const LatestTransactions = () => {
const txsCount = isMobile ? 2 : 6;
const { data, isLoading, isError } = useApiQuery('homepage_txs');
const { num, socketAlert } = useNewTxsSocket();
let content;
if (isLoading) {
......@@ -33,7 +36,7 @@ const LatestTransactions = () => {
const txsUrl = link('txs');
content = (
<>
<TxsNewItemNotice borderBottomRadius={ 0 } url={ link('txs') }/>
<TxsNewItemNotice borderBottomRadius={ 0 } url={ link('txs') } num={ num } alert={ socketAlert }/>
<Box mb={{ base: 3, lg: 4 }}>
{ data.slice(0, txsCount).map((tx => <LatestTxsItem key={ tx.hash } tx={ tx }/>)) }
</Box>
......
......@@ -6,6 +6,7 @@ import type { RoutedTab } from 'ui/shared/RoutedTabs/types';
import appConfig from 'configs/app/config';
import useIsMobile from 'lib/hooks/useIsMobile';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
import useQueryWithPages from 'lib/hooks/useQueryWithPages';
import Page from 'ui/shared/Page/Page';
import PageTitle from 'ui/shared/Page/PageTitle';
......@@ -29,9 +30,20 @@ const Transactions = () => {
filters: { filter },
});
const { num, socketAlert } = useNewTxsSocket();
const isFirstPage = txsQuery.pagination.page === 1;
const tabs: Array<RoutedTab> = [
{ id: 'validated', title: verifiedTitle, component: <TxsContent query={ txsQuery }/> },
{ id: 'pending', title: 'Pending', component: <TxsContent query={ txsQuery } showBlockInfo={ false }/> },
{
id: 'validated',
title: verifiedTitle,
component: <TxsContent query={ txsQuery } showSocketInfo={ isFirstPage } socketInfoNum={ num } socketInfoAlert={ socketAlert }/> },
{
id: 'pending',
title: 'Pending',
component: <TxsContent query={ txsQuery } showBlockInfo={ false } showSocketInfo={ isFirstPage } socketInfoNum={ num } socketInfoAlert={ socketAlert }/>,
},
];
return (
......
......@@ -25,12 +25,23 @@ type Props = {
query: QueryResult;
showBlockInfo?: boolean;
showSocketInfo?: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
currentAddress?: string;
filter?: React.ReactNode;
enableTimeIncrement?: boolean;
}
const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true, currentAddress, enableTimeIncrement }: Props) => {
const TxsContent = ({
filter,
query,
showBlockInfo = true,
showSocketInfo = true,
socketInfoAlert,
socketInfoNum,
currentAddress,
enableTimeIncrement,
}: Props) => {
const { data, isLoading, isError, setSortByField, setSortByValue, sorting } = useTxsSort(query);
const isMobile = useIsMobile();
......@@ -64,7 +75,7 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
<Show below="lg" ssr={ false }>
<Box>
{ showSocketInfo && (
<TxsNewItemNotice url={ window.location.href }>
<TxsNewItemNotice url={ window.location.href } num={ socketInfoNum } alert={ socketInfoAlert }>
{ ({ content }) => <Box>{ content }</Box> }
</TxsNewItemNotice>
) }
......@@ -86,6 +97,8 @@ const TxsContent = ({ filter, query, showBlockInfo = true, showSocketInfo = true
sorting={ sorting }
showBlockInfo={ showBlockInfo }
showSocketInfo={ showSocketInfo }
socketInfoAlert={ socketInfoAlert }
socketInfoNum={ socketInfoNum }
top={ query.isPaginationVisible ? 80 : 0 }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
......
......@@ -25,6 +25,7 @@ import Address from 'ui/shared/address/Address';
import AddressIcon from 'ui/shared/address/AddressIcon';
import AddressLink from 'ui/shared/address/AddressLink';
import InOutTag from 'ui/shared/InOutTag';
import ListItemMobile from 'ui/shared/ListItemMobile';
import TxStatus from 'ui/shared/TxStatus';
import TxAdditionalInfo from 'ui/txs/TxAdditionalInfo';
import TxType from 'ui/txs/TxType';
......@@ -43,7 +44,6 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
const { isOpen, onOpen, onClose } = useDisclosure();
const iconColor = useColorModeValue('blue.600', 'blue.300');
const borderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
const dataTo = tx.to ? tx.to : tx.created_contract;
const isOut = Boolean(currentAddress && currentAddress === tx.from.hash);
......@@ -53,7 +53,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
return (
<>
<Box width="100%" borderBottom="1px solid" borderColor={ borderColor } _first={{ borderTop: '1px solid', borderColor }}>
<ListItemMobile display="block" width="100%" isAnimated key={ tx.hash }>
<Flex justifyContent="space-between" mt={ 4 }>
<HStack>
<TxType types={ tx.tx_types }/>
......@@ -139,7 +139,7 @@ const TxsListItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }:
<Text as="span">Fee { appConfig.network.currency.symbol } </Text>
<Text as="span" variant="secondary">{ getValueWithUnit(tx.fee.value).toFormat() }</Text>
</Box>
</Box>
</ListItemMobile>
<Modal isOpen={ isOpen } onClose={ onClose } size="full">
<ModalContent paddingTop={ 4 }>
<ModalCloseButton/>
......
import { test as base, expect } from '@playwright/experimental-ct-react';
import { test, expect } from '@playwright/experimental-ct-react';
import React from 'react';
import { ROUTES } from 'lib/link/routes';
import * as socketServer from 'playwright/fixtures/socketServer';
import TestApp from 'playwright/TestApp';
import TxsNewItemNotice from './TxsNewItemNotice';
......@@ -14,89 +13,35 @@ const hooksConfig = {
},
};
export const test = base.extend<socketServer.SocketServerFixture>({
createSocket: socketServer.createSocket,
});
test.describe.configure({ mode: 'serial' });
test('new item in validated txs list', async({ mount, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice url="/"/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await expect(component).toHaveScreenshot();
});
test.describe('dark mode', () => {
test.use({ colorScheme: 'dark' });
test('default view', async({ mount, createSocket }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice url="/"/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await expect(component).toHaveScreenshot();
});
});
test('2 new items in validated txs list', async({ mount, page, createSocket }) => {
test('2 new items in validated txs list +@dark-mode', async({ mount }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice url="/"/>
<TestApp>
<TxsNewItemNotice url="/" num={ 2 }/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
const channel = await socketServer.joinChannel(socket, 'transactions:new_transaction');
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
socketServer.sendMessage(socket, channel, 'transaction', { transaction: 1 });
await page.waitForSelector('text=2 more');
await expect(component).toHaveScreenshot();
});
test('connection loss', async({ mount, createSocket }) => {
test('connection loss', async({ mount }) => {
const component = await mount(
<TestApp withSocket>
<TxsNewItemNotice url="/"/>
<TestApp>
<TxsNewItemNotice url="/" alert="Connection is lost. Please reload the page."/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
await socketServer.joinChannel(socket, 'transactions:new_transaction');
socket.close();
await expect(component).toHaveScreenshot();
});
test('fetching', async({ mount, createSocket }) => {
test('fetching', async({ mount }) => {
const component = await mount(
<TestApp withSocket>
<TestApp>
<TxsNewItemNotice url="/"/>
</TestApp>,
{ hooksConfig },
);
const socket = await createSocket();
await socketServer.joinChannel(socket, 'transactions:new_transaction');
await expect(component).toHaveScreenshot();
});
......@@ -2,8 +2,6 @@ import { Alert, Link, Text, chakra, useTheme, useColorModeValue } from '@chakra-
import { transparentize } from '@chakra-ui/theme-tools';
import React from 'react';
import useNewTxsSocket from 'lib/hooks/useNewTxsSocket';
interface InjectedProps {
content: React.ReactNode;
}
......@@ -12,15 +10,16 @@ interface Props {
children?: (props: InjectedProps) => JSX.Element;
className?: string;
url: string;
alert?: string;
num?: number;
}
const TxsNewItemNotice = ({ children, className, url }: Props) => {
const { num, socketAlert } = useNewTxsSocket();
const TxsNewItemNotice = ({ children, className, url, num, alert }: Props) => {
const theme = useTheme();
const alertContent = (() => {
if (socketAlert) {
return socketAlert;
if (alert) {
return alert;
}
if (!num) {
......
import { Link, Table, Tbody, Tr, Th, Td, Icon } from '@chakra-ui/react';
import { AnimatePresence } from 'framer-motion';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
......@@ -18,11 +19,24 @@ type Props = {
top: number;
showBlockInfo: boolean;
showSocketInfo: boolean;
socketInfoAlert?: string;
socketInfoNum?: number;
currentAddress?: string;
enableTimeIncrement?: boolean;
}
const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, currentAddress, enableTimeIncrement }: Props) => {
const TxsTable = ({
txs,
sort,
sorting,
top,
showBlockInfo,
showSocketInfo,
socketInfoAlert,
socketInfoNum,
currentAddress,
enableTimeIncrement,
}: Props) => {
return (
<Table variant="simple" minWidth="950px" size="xs">
<TheadSticky top={ top }>
......@@ -53,19 +67,21 @@ const TxsTable = ({ txs, sort, sorting, top, showBlockInfo, showSocketInfo, curr
</TheadSticky>
<Tbody>
{ showSocketInfo && (
<TxsNewItemNotice borderRadius={ 0 } url={ window.location.href }>
<TxsNewItemNotice borderRadius={ 0 } url={ window.location.href } alert={ socketInfoAlert } num={ socketInfoNum }>
{ ({ content }) => <Tr><Td colSpan={ 10 } p={ 0 }>{ content }</Td></Tr> }
</TxsNewItemNotice>
) }
{ txs.map((item) => (
<TxsTableItem
key={ item.hash }
tx={ item }
showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
)) }
<AnimatePresence initial={ false }>
{ txs.map((item) => (
<TxsTableItem
key={ item.hash }
tx={ item }
showBlockInfo={ showBlockInfo }
currentAddress={ currentAddress }
enableTimeIncrement={ enableTimeIncrement }
/>
)) }
</AnimatePresence>
</Tbody>
</Table>
);
......
......@@ -15,6 +15,7 @@ import {
Show,
Hide,
} from '@chakra-ui/react';
import { motion } from 'framer-motion';
import React from 'react';
import type { Transaction } from 'types/api/transaction';
......@@ -65,7 +66,14 @@ const TxsTableItem = ({ tx, showBlockInfo, currentAddress, enableTimeIncrement }
const infoBorderColor = useColorModeValue('blackAlpha.200', 'whiteAlpha.200');
return (
<Tr>
<Tr
as={ motion.tr }
initial={{ opacity: 0, scale: 0.97 }}
animate={{ opacity: 1, scale: 1 }}
transitionDuration="normal"
transitionTimingFunction="linear"
key={ tx.hash }
>
<Td pl={ 4 }>
<Popover placement="right-start" openDelay={ 300 } isLazy>
{ ({ isOpen }) => (
......
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