From e9bda0c376d73fe315281282f29c49a5acf8d6ce Mon Sep 17 00:00:00 2001 From: He1DAr Date: Tue, 30 Jan 2024 16:17:28 -0500 Subject: [PATCH] feat: block page layout A --- src/app/PageClient.tsx | 11 +- src/app/_components/BlockList/Controls.tsx | 72 +-- .../LayoutA/BlockListWithControls.tsx | 71 +++ .../_components/BlockList/LayoutA/Blocks.tsx | 56 +++ .../BlockList/LayoutA/BurnBlock.tsx | 6 +- .../BlockList/LayoutA/NonPaginated.tsx | 54 +++ .../BlockList/LayoutA/Paginated.tsx | 118 +++++ .../BlockList/LayoutA/Provider.tsx | 24 + .../BlockList/LayoutA/UpdateBar.tsx | 64 +++ .../__tests__/BlockListWithControls.test.tsx | 65 +++ .../BlockListWithControls.test.tsx.snap | 426 ++++++++++++++++++ .../__snapshots__/useBlockList.test.tsx | 97 ++++ .../_components/BlockList/LayoutA/consts.ts | 1 + .../_components/BlockList/LayoutA/context.ts | 20 + .../_components/BlockList/LayoutA/index.tsx | 128 ------ .../BlockList/LayoutA/useBlockList.ts | 194 +++----- .../LayoutA/useBlockListWebSocket.ts | 185 ++++++++ .../BlockList/LayoutA/useInitialBlockList.ts | 34 ++ .../LayoutA/usePaginatedBlockList.ts | 83 ++++ .../BlockList/useStacksWebSocketClient.ts | 30 ++ .../BlockList/useSubscribeBlocks.ts | 36 ++ src/app/blocks/PageClient.tsx | 13 +- src/app/txid/[txId]/TxDetails/BlockHash.tsx | 2 +- src/app/txid/[txId]/TxDetails/Sender.tsx | 2 +- src/common/debug.ts | 59 +++ src/common/queries/useBlockListInfinite.ts | 1 - src/common/queries/useBlocksByBurnBlock.ts | 1 - src/common/queries/useBurnBlocks.ts | 1 - 28 files changed, 1528 insertions(+), 326 deletions(-) create mode 100644 src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx create mode 100644 src/app/_components/BlockList/LayoutA/Blocks.tsx create mode 100644 src/app/_components/BlockList/LayoutA/NonPaginated.tsx create mode 100644 src/app/_components/BlockList/LayoutA/Paginated.tsx create mode 100644 src/app/_components/BlockList/LayoutA/Provider.tsx create mode 100644 src/app/_components/BlockList/LayoutA/UpdateBar.tsx create mode 100644 src/app/_components/BlockList/LayoutA/__tests__/BlockListWithControls.test.tsx create mode 100644 src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap create mode 100644 src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/useBlockList.test.tsx create mode 100644 src/app/_components/BlockList/LayoutA/consts.ts create mode 100644 src/app/_components/BlockList/LayoutA/context.ts delete mode 100644 src/app/_components/BlockList/LayoutA/index.tsx create mode 100644 src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts create mode 100644 src/app/_components/BlockList/LayoutA/useInitialBlockList.ts create mode 100644 src/app/_components/BlockList/LayoutA/usePaginatedBlockList.ts create mode 100644 src/app/_components/BlockList/useStacksWebSocketClient.ts create mode 100644 src/app/_components/BlockList/useSubscribeBlocks.ts create mode 100644 src/common/debug.ts diff --git a/src/app/PageClient.tsx b/src/app/PageClient.tsx index d18b200bc..56dc377f2 100644 --- a/src/app/PageClient.tsx +++ b/src/app/PageClient.tsx @@ -11,10 +11,11 @@ import { SkeletonBlockList } from './_components/BlockList/SkeletonBlockList'; import { PageTitle } from './_components/PageTitle'; import { Stats } from './_components/Stats/Stats'; -const BLOCK_LIST_LAYOUT_A_LIMIT = 19; - -const BlocksListA = dynamic( - () => import('./_components/BlockList/LayoutA').then(mod => mod.BlockListLayoutA), +const NonPaginatedBlockListLayoutA = dynamic( + () => + import('./_components/BlockList/LayoutA/NonPaginated').then( + mod => mod.NonPaginatedBlockListLayoutA + ), { loading: () => , ssr: false, @@ -40,7 +41,7 @@ export default function Home() { {activeNetworkKey.indexOf('naka') !== -1 ? ( - + ) : ( )} diff --git a/src/app/_components/BlockList/Controls.tsx b/src/app/_components/BlockList/Controls.tsx index ba898f995..57ec6cb92 100644 --- a/src/app/_components/BlockList/Controls.tsx +++ b/src/app/_components/BlockList/Controls.tsx @@ -1,39 +1,27 @@ -import { useColorModeValue } from '@chakra-ui/react'; -import { keyframes } from '@emotion/react'; import React from 'react'; -import { TfiReload } from 'react-icons/tfi'; import { Flex } from '../../../ui/Flex'; import { FormControl } from '../../../ui/FormControl'; import { FormLabel } from '../../../ui/FormLabel'; -import { Icon } from '../../../ui/Icon'; import { Stack } from '../../../ui/Stack'; import { Switch, SwitchProps } from '../../../ui/Switch'; -import { Text } from '../../../ui/Text'; -import { TextLink } from '../../../ui/TextLink'; - -const spin = keyframes` - from { transform: rotate(0deg); } - to { transform: rotate(360deg); } -`; interface ControlsProps { groupByBtc: SwitchProps; liveUpdates: SwitchProps; - update?: { - isLoading: boolean; - onClick: () => void; - }; - latestBlocksCount: number; + horizontal?: boolean; } -export function Controls({ groupByBtc, liveUpdates, update, latestBlocksCount }: ControlsProps) { - const bgColor = useColorModeValue('purple.100', 'slate.900'); - const buttonColor = useColorModeValue('brand', 'purple.400'); - const textColor = useColorModeValue('slate.800', 'slate.400'); +export function Controls({ groupByBtc, liveUpdates, horizontal }: ControlsProps) { return ( <> - + @@ -67,48 +55,6 @@ export function Controls({ groupByBtc, liveUpdates, update, latestBlocksCount }: - - {update && ( - - - - {latestBlocksCount} - {' '} - new Stacks blocks have come in - - - - - - Update - - - - - )} ); } diff --git a/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx b/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx new file mode 100644 index 000000000..2f5b0a5d7 --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx @@ -0,0 +1,71 @@ +import React from 'react'; + +import { ListFooter } from '../../../../common/components/ListFooter'; +import { Section } from '../../../../common/components/Section'; +import { Box } from '../../../../ui/Box'; +import { Controls } from '../Controls'; +import { UIBlock } from '../types'; +import { Blocks } from './Blocks'; +import { UpdateBar } from './UpdateBar'; +import { useBlockListContext } from './context'; + +export function BlockListWithControls({ + blockList, + latestBlocksCount, + updateList, + enablePagination, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + horizontalControls, +}: { + blockList: UIBlock[]; + latestBlocksCount: number; + updateList: () => void; + enablePagination?: boolean; + isFetchingNextPage?: boolean; + hasNextPage?: boolean; + fetchNextPage?: () => void; + horizontalControls?: boolean; +}) { + const { fadeEffect, setFadeEffect, groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates } = + useBlockListContext(); + return ( +
+ + { + setGroupedByBtc(!groupedByBtc); + }, + isChecked: groupedByBtc, + isDisabled: true, + }} + liveUpdates={{ + onChange: () => setLiveUpdates(!liveUpdates), + isChecked: liveUpdates, + }} + horizontal={horizontalControls} + /> + {!liveUpdates && ( + + )} + + + {(!liveUpdates || !enablePagination) && ( + + )} + + +
+ ); +} diff --git a/src/app/_components/BlockList/LayoutA/Blocks.tsx b/src/app/_components/BlockList/LayoutA/Blocks.tsx new file mode 100644 index 000000000..191f82108 --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/Blocks.tsx @@ -0,0 +1,56 @@ +import React from 'react'; + +import { Icon } from '../../../../ui/Icon'; +import { Stack } from '../../../../ui/Stack'; +import { StxIcon } from '../../../../ui/icons'; +import { UIBlock, UIBlockType } from '../types'; +import { BlockCount } from './BlockCount'; +import { BurnBlock } from './BurnBlock'; +import { StxBlock } from './StxBlock'; +import { FADE_DURATION } from './consts'; + +export function Blocks({ blockList, fadeEffect }: { blockList: UIBlock[]; fadeEffect: boolean }) { + return ( + + {blockList.map((block, i) => { + switch (block.type) { + case UIBlockType.Block: + return ( + 0 && blockList[i - 1].type === UIBlockType.BurnBlock) ? ( + + ) : undefined + } + /> + ); + case UIBlockType.BurnBlock: + return ( + + ); + case UIBlockType.Count: + return ; + } + })} + + ); +} diff --git a/src/app/_components/BlockList/LayoutA/BurnBlock.tsx b/src/app/_components/BlockList/LayoutA/BurnBlock.tsx index 59af11d30..70e5d0b05 100644 --- a/src/app/_components/BlockList/LayoutA/BurnBlock.tsx +++ b/src/app/_components/BlockList/LayoutA/BurnBlock.tsx @@ -29,10 +29,12 @@ export const BurnBlock = memo(function ({ timestamp, height, hash }: ListItemPro justifyContent={'space-between'} alignItems={'center'} borderBottom={'1px'} - px={4} + pl={4} + pr={8} height={14} backgroundColor={bgColor} - marginX={'-6'} + mr={'-8'} + ml={'-10'} color={textColor} > diff --git a/src/app/_components/BlockList/LayoutA/NonPaginated.tsx b/src/app/_components/BlockList/LayoutA/NonPaginated.tsx new file mode 100644 index 000000000..16ce95a5b --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/NonPaginated.tsx @@ -0,0 +1,54 @@ +'use client'; + +import React, { useCallback, useState } from 'react'; + +import { ListFooter } from '../../../../common/components/ListFooter'; +import { Section } from '../../../../common/components/Section'; +import { Box } from '../../../../ui/Box'; +import { Icon } from '../../../../ui/Icon'; +import { Stack } from '../../../../ui/Stack'; +import { StxIcon } from '../../../../ui/icons'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { Controls } from '../Controls'; +import { UIBlockType } from '../types'; +import { BlockCount } from './BlockCount'; +import { BlockListWithControls } from './BlockListWithControls'; +import { BurnBlock } from './BurnBlock'; +import { BlockListProvider } from './Provider'; +import { StxBlock } from './StxBlock'; +import { UpdateBar } from './UpdateBar'; +import { useBlockListContext } from './context'; +import { useBlockList } from './useBlockList'; + +const LIST_LENGTH = 17; + +function NonPaginatedBlockListLayoutABase() { + const { blockList, updateList, latestBlocksCount } = useBlockList(LIST_LENGTH); + + return ( + + ); +} + +export function NonPaginatedBlockListLayoutA() { + return ( + + + + + + ); +} diff --git a/src/app/_components/BlockList/LayoutA/Paginated.tsx b/src/app/_components/BlockList/LayoutA/Paginated.tsx new file mode 100644 index 000000000..45b78aac9 --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/Paginated.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { Section } from '../../../../common/components/Section'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { UISingleBlock } from '../types'; +import { BlockListWithControls } from './BlockListWithControls'; +import { BlockListProvider } from './Provider'; +import { FADE_DURATION } from './consts'; +import { useBlockListContext } from './context'; +import { useBlockListWebSocket } from './useBlockListWebSocket'; +import { usePaginatedBlockList } from './usePaginatedBlockList'; + +function PaginatedBlockListLayoutABase() { + const { setFadeEffect, liveUpdates } = useBlockListContext(); + const [latestBlocksToShow, setLatestBlocksToShow] = useState([]); + + const { initialBlockList, initialBurnBlocks, updateList, hasNextPage, isFetchingNextPage } = + usePaginatedBlockList(); + + const initialBlockHashes = useMemo(() => { + return new Set(initialBlockList.map(block => block.hash)); + }, [initialBlockList]); + + const initialBurnBlockHashes = useMemo(() => { + return new Set(Object.keys(initialBurnBlocks)); + }, [initialBurnBlocks]); + + const { latestUIBlocks, latestBlocksCount, clearLatestBlocks } = useBlockListWebSocket( + initialBlockHashes, + initialBurnBlockHashes + ); + + const showLatestBlocks = useCallback(() => { + setLatestBlocksToShow(prevLatestBlocksToShow => { + return [...latestUIBlocks, ...prevLatestBlocksToShow]; + }); + clearLatestBlocks(); + }, [clearLatestBlocks, latestUIBlocks]); + + const blockList = useMemo( + () => [...latestBlocksToShow, ...initialBlockList], + [initialBlockList, latestBlocksToShow] + ); + + const showLatestBlocksWithFadeEffect = useCallback(() => { + setFadeEffect(true); + setTimeout(() => { + showLatestBlocks(); + setFadeEffect(false); + }, FADE_DURATION); + }, [setFadeEffect, showLatestBlocks]); + + const prevLiveUpdatesRef = useRef(liveUpdates); + const prevLatestBlocksCountRef = useRef(latestBlocksCount); + + useEffect(() => { + const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; + + const receivedLatestBlockWhileLiveUpdates = + liveUpdates && + latestBlocksCount > 0 && + prevLatestBlocksCountRef.current !== latestBlocksCount; + + if (liveUpdatesToggled) { + setFadeEffect(true); + setLatestBlocksToShow([]); + clearLatestBlocks(); + updateList().then(() => { + setFadeEffect(false); + }); + } else if (receivedLatestBlockWhileLiveUpdates) { + showLatestBlocksWithFadeEffect(); + } + + prevLiveUpdatesRef.current = liveUpdates; + prevLatestBlocksCountRef.current = latestBlocksCount; + }, [ + liveUpdates, + latestBlocksCount, + clearLatestBlocks, + updateList, + showLatestBlocksWithFadeEffect, + setFadeEffect, + ]); + + return ( + + ); +} + +export function PaginatedBlockListLayoutA() { + return ( + + + + + + ); +} diff --git a/src/app/_components/BlockList/LayoutA/Provider.tsx b/src/app/_components/BlockList/LayoutA/Provider.tsx new file mode 100644 index 000000000..1cfb1695e --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/Provider.tsx @@ -0,0 +1,24 @@ +import { ReactNode, useState } from 'react'; + +import { BlockListContext } from './context'; + +export function BlockListProvider({ children }: { children: ReactNode }) { + const [fadeEffect, setFadeEffect] = useState(false); + const [groupedByBtc, setGroupedByBtc] = useState(true); + const [liveUpdates, setLiveUpdates] = useState(false); + + return ( + + {children} + + ); +} diff --git a/src/app/_components/BlockList/LayoutA/UpdateBar.tsx b/src/app/_components/BlockList/LayoutA/UpdateBar.tsx new file mode 100644 index 000000000..385a4f132 --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/UpdateBar.tsx @@ -0,0 +1,64 @@ +import { useColorModeValue } from '@chakra-ui/react'; +import React from 'react'; +import { TfiReload } from 'react-icons/tfi'; + +import { Flex } from '../../../../ui/Flex'; +import { Icon } from '../../../../ui/Icon'; +import { Text } from '../../../../ui/Text'; +import { TextLink } from '../../../../ui/TextLink'; +import { FADE_DURATION } from './consts'; + +export function UpdateBar({ + latestBlocksCount, + onClick, + fadeEffect, +}: { + latestBlocksCount: number; + onClick: () => void; + fadeEffect: boolean; +}) { + const bgColor = useColorModeValue('purple.100', 'slate.900'); + const buttonColor = useColorModeValue('brand', 'purple.400'); + const textColor = useColorModeValue('slate.800', 'slate.400'); + return ( + + + + {latestBlocksCount} + {' '} + new Stacks blocks have come in + + + + + + Update + + + + + ); +} diff --git a/src/app/_components/BlockList/LayoutA/__tests__/BlockListWithControls.test.tsx b/src/app/_components/BlockList/LayoutA/__tests__/BlockListWithControls.test.tsx new file mode 100644 index 000000000..0c05b8fe1 --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/__tests__/BlockListWithControls.test.tsx @@ -0,0 +1,65 @@ +import { render } from '@testing-library/react'; + +import { UIBlock, UIBlockType } from '../../types'; +import { BlockListWithControls } from '../BlockListWithControls'; +import { BlockListProvider } from '../Provider'; + +describe('BlockListWithControls', () => { + it('renders correctly', () => { + const blockList: UIBlock[] = [ + { + type: UIBlockType.Block, + height: 1, + hash: 'hash1', + timestamp: Date.now(), + txsCount: 5, + }, + { + type: UIBlockType.Block, + height: 2, + hash: 'hash2', + timestamp: Date.now(), + txsCount: 10, + }, + { + type: UIBlockType.Block, + height: 3, + hash: 'hash3', + timestamp: Date.now(), + txsCount: 15, + }, + { + type: UIBlockType.BurnBlock, + height: 4, + hash: 'hash4', + timestamp: Date.now(), + txsCount: 30, + }, + { + type: UIBlockType.Count, + count: 25, + }, + ]; + const latestBlocksCount = 0; // replace with actual data + const updateList = jest.fn(); + const enablePagination = false; + const isFetchingNextPage = false; + const hasNextPage = false; + const fetchNextPage = jest.fn(); + + const { asFragment } = render( + + + + ); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap b/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap new file mode 100644 index 000000000..9977498ba --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/BlockListWithControls.test.tsx.snap @@ -0,0 +1,426 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`BlockListWithControls renders correctly 1`] = ` + +
+
+ + Recent Blocks + +
+
+
+
+
+
+ + +
+
+
+ + +
+
+
+ + + 0 + + new Stacks blocks have come in + + +
+ + + + + + Update + +
+
+
+
+
+
+
+
+ + + +
+ + + #1 + + +
+
+
+ hash1…hash1 +
+  ∙  +
+ 5 txn +
+  ∙  +
+ + in 54033 years + +
+
+
+
+
+
+ +
+
+ hash2…hash2 +
+  ∙  +
+ 10 txn +
+  ∙  +
+ + in 54033 years + +
+
+
+
+
+
+ +
+
+ hash3…hash3 +
+  ∙  +
+ 15 txn +
+  ∙  +
+ + in 54033 years + +
+
+
+
+
+
+ + + + + + + + + + + + #4 + + +
+
+
+ hash4…hash4 +
+  ∙  +
+ + in 54033 years + +
+
+
+ +
+
+
+
+
+ +`; diff --git a/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/useBlockList.test.tsx b/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/useBlockList.test.tsx new file mode 100644 index 000000000..7cc5ff13e --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/__tests__/__snapshots__/useBlockList.test.tsx @@ -0,0 +1,97 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook, waitFor } from '@testing-library/react'; + +import { useBlockList } from '../../useBlockList'; + +const LAST_BURN_BLOCK_STX_BLOCKS_COUNT = 50; + +jest.mock('../../context', () => ({ + useBlockListContext: jest.fn().mockImplementation(() => ({ + setFadeEffect: jest.fn(), + fadeEffect: false, + setLiveUpdates: jest.fn(), + liveUpdates: false, + setGroupedByBtc: jest.fn(), + groupedByBtc: false, + })), +})); + +jest.mock('../../useBlockListWebSocket', () => ({ + useBlockListWebSocket: jest.fn().mockImplementation(() => ({ + latestUIBlocks: [], + latestBlocksCount: 0, + clearLatestBlocks: jest.fn(), + latestBlock: undefined, + })), +})); + +jest.mock('../../useInitialBlockList', () => ({ + useInitialBlockList: jest.fn().mockImplementation(() => ({ + lastBurnBlock: { + burn_block_height: 1, + burn_block_hash: 'hash1', + burn_block_time: Date.now(), + stacks_blocks: Array(LAST_BURN_BLOCK_STX_BLOCKS_COUNT).map((_, i) => `block${i + 1}`), + }, + secondToLastBurnBlock: { + burn_block_height: 2, + burn_block_hash: 'hash2', + burn_block_time: Date.now(), + stacks_blocks: ['block4', 'block5', 'block6'], + }, + lastBurnBlockStxBlocks: [...Array(LAST_BURN_BLOCK_STX_BLOCKS_COUNT)].map((_, i) => ({ + height: i + 1, + hash: `block${i + 1}`, + burn_block_time: Date.now(), + tx_count: 10, + })), + secondToLastBlockStxBlocks: [ + { + height: 101, + hash: 'oldBlock1', + burn_block_time: Date.now(), + tx_count: 25, + }, + { + height: 102, + hash: 'oldBlock2', + burn_block_time: Date.now(), + tx_count: 25, + }, + { + height: 103, + hash: 'oldBlock3', + burn_block_time: Date.now(), + tx_count: 25, + }, + ], + })), +})); + +describe('useBlockList', () => { + it('returns a block list of the correct length', async () => { + const queryClient = new QueryClient(); + const wrapper = ({ children }: any) => ( + {children} + ); + + const length = 4; + + const { result } = renderHook(() => useBlockList(length), { wrapper }); + + // wait 5 seconds + await waitFor(() => { + expect(result.current.blockList).toHaveLength(length); + const countBlock = result.current.blockList.find(block => block.type === 'count') as any; + /* In this case, we have sufficient blocks in the last burn block to satisfy the length, + the list should look like this: + [ + { type: 'block', height: 1, ... }, + { type: 'block', height: 2, ... }, + { type: 'count', count: 48 } <- (50 - 2 visible blocks) + { type: 'burnBlock', ... }, + */ + expect(countBlock.count).toBe(LAST_BURN_BLOCK_STX_BLOCKS_COUNT - 2); + }); + }); +}); diff --git a/src/app/_components/BlockList/LayoutA/consts.ts b/src/app/_components/BlockList/LayoutA/consts.ts new file mode 100644 index 000000000..1168e566f --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/consts.ts @@ -0,0 +1 @@ +export const FADE_DURATION = 700; diff --git a/src/app/_components/BlockList/LayoutA/context.ts b/src/app/_components/BlockList/LayoutA/context.ts new file mode 100644 index 000000000..f87ac89eb --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/context.ts @@ -0,0 +1,20 @@ +import { Dispatch, SetStateAction, createContext, useContext } from 'react'; + +interface BlockListContextType { + fadeEffect: boolean; + setFadeEffect: Dispatch>; + groupedByBtc: boolean; + setGroupedByBtc: Dispatch>; + liveUpdates: boolean; + setLiveUpdates: Dispatch>; +} + +export const BlockListContext = createContext(undefined); + +export const useBlockListContext = () => { + const context = useContext(BlockListContext); + if (!context) { + throw new Error('useBlockListContext must be used within a BlockListContextProvider'); + } + return context; +}; diff --git a/src/app/_components/BlockList/LayoutA/index.tsx b/src/app/_components/BlockList/LayoutA/index.tsx deleted file mode 100644 index c0be94356..000000000 --- a/src/app/_components/BlockList/LayoutA/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'use client'; - -import React, { useCallback, useState } from 'react'; - -import { ListFooter } from '../../../../common/components/ListFooter'; -import { Section } from '../../../../common/components/Section'; -import { Box } from '../../../../ui/Box'; -import { Icon } from '../../../../ui/Icon'; -import { Stack } from '../../../../ui/Stack'; -import { StxIcon } from '../../../../ui/icons'; -import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { Controls } from '../Controls'; -import { UIBlockType } from '../types'; -import { BlockCount } from './BlockCount'; -import { BurnBlock } from './BurnBlock'; -import { StxBlock } from './StxBlock'; -import { useBlockList } from './useBlockList'; - -const LIST_LENGTH = 17; - -function BlockListLayoutABase({ limit }: { limit?: number }) { - const [groupedByBtc, setGroupedByBtc] = React.useState(true); - const [liveUpdates, setLiveUpdates] = React.useState(false); - const [fadeEffect, setFadeEffect] = useState(false); - - const { blockList, refetch, latestBlocks } = useBlockList( - LIST_LENGTH, - liveUpdates, - setFadeEffect - ); - - const reloadData = useCallback(async () => { - setFadeEffect(true); - await refetch(); - setFadeEffect(false); - }, [refetch]); - - return ( -
- - { - setGroupedByBtc(!groupedByBtc); - }, - isChecked: groupedByBtc, - }} - liveUpdates={{ - onChange: () => setLiveUpdates(!liveUpdates), - isChecked: liveUpdates, - }} - update={ - liveUpdates - ? undefined - : { - isLoading: false, - onClick: () => { - void reloadData(); - }, - } - } - latestBlocksCount={Object.keys(latestBlocks).length} - /> - - {blockList.map((block, i) => { - switch (block.type) { - case UIBlockType.Block: - return ( - 0 && blockList[i - 1].type === UIBlockType.BurnBlock) ? ( - - ) : undefined - } - /> - ); - case UIBlockType.BurnBlock: - return ( - - ); - case UIBlockType.Count: - return ; - } - })} - - - - - -
- ); -} - -export function BlockListLayoutA({ limit }: { limit?: number }) { - return ( - - - - ); -} diff --git a/src/app/_components/BlockList/LayoutA/useBlockList.ts b/src/app/_components/BlockList/LayoutA/useBlockList.ts index 73be868fb..ce4e532dd 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList.ts @@ -1,5 +1,5 @@ import { useQueryClient } from '@tanstack/react-query'; -import React, { useCallback, useEffect } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Subscription } from 'react-redux'; import { @@ -14,9 +14,10 @@ import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfi import { useSuspenseBlocksByBurnBlock } from '../../../../common/queries/useBlocksByBurnBlock'; import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocks'; import { UIBlock, UIBlockType } from '../types'; - -const BURN_BLOCK_LENGTH = 2; -const STX_BLOCK_LENGTH = 20; +import { FADE_DURATION } from './consts'; +import { useBlockListContext } from './context'; +import { useBlockListWebSocket } from './useBlockListWebSocket'; +import { useInitialBlockList } from './useInitialBlockList'; const createBurnBlockUIBlock = (burnBlock: BurnBlock): UIBlock => ({ type: UIBlockType.BurnBlock, @@ -61,144 +62,93 @@ const createUIBlockList = ( return blockList; }; -export function useBlockListWebSocket( - liveUpdates: boolean, - setFadeEffect: (value: boolean) => void -) { - const clientRef = React.useRef(null); - const subRef = React.useRef(null); - const activeNetworkUrl = useGlobalContext().activeNetworkKey; - const [latestBlocks, setLatestBlocks] = React.useState<{ [key: string]: NakamotoBlock }>({}); - const [latestBlock, setLatestBlock] = React.useState(); - - useEffect(() => { - const subscribe = async () => { - if (!clientRef.current) { - clientRef.current = await connectWebSocketClient( - activeNetworkUrl.replace('https://', 'wss://') - ); - } - - if (subRef.current?.unsubscribe) { - await subRef.current.unsubscribe(); - } - - subRef.current = await clientRef.current.subscribeBlocks((block: any) => { - function updateLatestBlocks() { - setLatestBlock(block); - setLatestBlocks(prevLatestBlocks => { - const updatedList = { ...prevLatestBlocks }; - updatedList[block.hash] = block; - return updatedList; - }); - } - - if (liveUpdates) { - setFadeEffect(true); - setTimeout(() => { - updateLatestBlocks(); - setFadeEffect(false); - }, 500); - } else { - updateLatestBlocks(); - } - }); - }; - - void subscribe(); - - return () => { - if (subRef.current?.unsubscribe) { - void subRef.current?.unsubscribe(); - } - }; - }, [liveUpdates]); - - const clearLatestBlocks = () => { - setLatestBlock(undefined); - setLatestBlocks({}); - }; - - return { latestBlocks, clearLatestBlocks, latestBlock }; -} - -export function useBlockListData() { - const burnBlocks = useSuspenseInfiniteQueryResult( - useSuspenseBurnBlocks(BURN_BLOCK_LENGTH), - BURN_BLOCK_LENGTH - ); - - const lastBurnBlock = burnBlocks[0]; - const secondToLastBurnBlock = burnBlocks[1]; - - const lastBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( - useSuspenseBlocksByBurnBlock(lastBurnBlock.burn_block_height, STX_BLOCK_LENGTH), - STX_BLOCK_LENGTH - ); - const secondToLastBlockStxBlocks = useSuspenseInfiniteQueryResult( - useSuspenseBlocksByBurnBlock(secondToLastBurnBlock.burn_block_height, STX_BLOCK_LENGTH), - STX_BLOCK_LENGTH - ); +export function useBlockList(length: number) { + const queryClient = useQueryClient(); + const { setFadeEffect, liveUpdates } = useBlockListContext(); - return { + const { lastBurnBlock, secondToLastBurnBlock, lastBurnBlockStxBlocks, secondToLastBlockStxBlocks, - }; -} + } = useInitialBlockList(); + + const initialBlockHashes = useMemo( + () => + new Set([ + ...lastBurnBlockStxBlocks.map(block => block.hash), + ...secondToLastBlockStxBlocks.map(block => block.hash), + ]), + [lastBurnBlockStxBlocks, secondToLastBlockStxBlocks] + ); -export function useBlockList( - length: number, - liveUpdates: boolean, - setFadeEffect: (value: boolean) => void -) { - const queryClient = useQueryClient(); - const [displayedBlocks, setDisplayedBlocks] = React.useState>({}); - const { latestBlocks, latestBlock, clearLatestBlocks } = useBlockListWebSocket( - liveUpdates, - setFadeEffect + const initialBurnBlockHashes = useMemo( + () => new Set([lastBurnBlock.burn_block_hash, secondToLastBurnBlock.burn_block_hash]), + [lastBurnBlock, secondToLastBurnBlock] ); - const refetch = useCallback( - function () { - clearLatestBlocks(); - return Promise.all([ + const { latestBlock, latestBlocksCount, clearLatestBlocks } = useBlockListWebSocket( + initialBlockHashes, + initialBurnBlockHashes + ); + + const updateList = useCallback( + async function () { + setFadeEffect(true); + await Promise.all([ queryClient.invalidateQueries({ queryKey: ['getBlocksByBurnBlock'] }), queryClient.invalidateQueries({ queryKey: ['burnBlocks'] }), ]); + clearLatestBlocks(); + setFadeEffect(false); }, - [queryClient] + [clearLatestBlocks, queryClient, setFadeEffect] ); + const prevLiveUpdatesRef = useRef(liveUpdates); + const prevLatestBlocksCountRef = useRef(latestBlocksCount); + useEffect(() => { - void refetch(); - }, [liveUpdates, refetch]); + const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; - const { - lastBurnBlock, - secondToLastBurnBlock, - lastBurnBlockStxBlocks, - secondToLastBlockStxBlocks, - } = useBlockListData(); + const receivedLatestBlockWhileLiveUpdates = + liveUpdates && + latestBlocksCount > 0 && + prevLatestBlocksCountRef.current !== latestBlocksCount; - if (liveUpdates) { - if (latestBlock && !displayedBlocks[latestBlock.hash]) { + if (liveUpdatesToggled) { + setFadeEffect(true); + clearLatestBlocks(); + updateList().then(() => { + setFadeEffect(false); + }); + } else if (receivedLatestBlockWhileLiveUpdates && latestBlock) { if (latestBlock.burn_block_height === lastBurnBlock.burn_block_height) { - lastBurnBlockStxBlocks.unshift(latestBlock); - lastBurnBlock.stacks_blocks.unshift(latestBlock.burn_block_hash); - setDisplayedBlocks(prevDisplayedBlocks => { - const updatedList = { ...prevDisplayedBlocks }; - updatedList[latestBlock.hash] = latestBlock.height; - return updatedList; - }); + setFadeEffect(true); + setTimeout(() => { + lastBurnBlockStxBlocks.unshift(latestBlock); + lastBurnBlock.stacks_blocks.unshift(latestBlock.hash); + setFadeEffect(false); + }, FADE_DURATION); } else { clearLatestBlocks(); - setDisplayedBlocks({}); - void refetch(); + void updateList(); } } - } + + prevLiveUpdatesRef.current = liveUpdates; + prevLatestBlocksCountRef.current = latestBlocksCount; + }, [ + liveUpdates, + latestBlocksCount, + clearLatestBlocks, + updateList, + setFadeEffect, + latestBlock, + lastBurnBlockStxBlocks, + lastBurnBlock.stacks_blocks, + lastBurnBlock.burn_block_height, + ]); let blockList = createUIBlockList(lastBurnBlock, lastBurnBlockStxBlocks, length); @@ -213,7 +163,7 @@ export function useBlockList( return { blockList, - latestBlocks, - refetch, + latestBlocksCount, + updateList, }; } diff --git a/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts new file mode 100644 index 000000000..64ada3c80 --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts @@ -0,0 +1,185 @@ +import { useCallback, useRef, useState } from 'react'; + +import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; +import { Block } from '@stacks/stacks-blockchain-api-types'; + +import { UIBlockType, UISingleBlock } from '../types'; +import { useSubscribeBlocks } from '../useSubscribeBlocks'; + +// interface ReducerState { +// latestBlockHashes: Set; +// } +// +// const initialState: ReducerState = { +// latestBlockHashes: new Set(), +// }; +// +// function reducer(state: ReducerState, action: any): ReducerState { +// const { latestBlockHashes } = state; +// if (action.type === 'add') { +// return { latestBlockHashes: new Set([...Array.from(latestBlockHashes), action.hash]) }; +// } +// if (action.type === 'clear') { +// return { latestBlockHashes: new Set() }; +// } +// return state; +// } + +// export function useBlockListWebSocket( +// fetchedBlockHashes: Set, +// burnBlocks: Record +// ) { +// const clientRef = React.useRef(null); +// const subscriptionRef = React.useRef(null); +// const activeNetworkUrl = useGlobalContext().activeNetworkKey; +// const [latestBlocks, setLatestBlocks] = React.useState([]); +// const [latestBlock, setLatestBlock] = React.useState(); +// const [latestBurnBlockHashes, setLatestBurnBlockHashes] = React.useState>(new Set()); +// +// const [state, dispatch] = useReducer(reducer, initialState); +// const { latestBlockHashes } = state; +// useEffect(() => { +// const subscribe = async () => { +// if (!clientRef.current) { +// clientRef.current = await connectWebSocketClient( +// activeNetworkUrl.replace('https://', 'wss://') +// ); +// } +// +// if (subscriptionRef.current?.unsubscribe) { +// await subscriptionRef.current.unsubscribe(); +// } +// +// subscriptionRef.current = await clientRef.current.subscribeBlocks((block: any) => { +// function updateLatestBlocks() { +// if (latestBlockHashes.has(block.hash) || fetchedBlockHashes.has(block.hash)) { +// return; +// } +// setLatestBlock(block); +// dispatch({ +// type: 'add', +// hash: block.hash, +// }); +// const isNewBurnBlock = +// !burnBlocks[block.burn_block_hash] && !latestBurnBlockHashes.has(block.burn_block_hash); +// if (isNewBurnBlock) { +// setLatestBurnBlockHashes(prevLatestBurnBlockHashes => { +// const newLatestBurnBlockHashes = new Set(prevLatestBurnBlockHashes); +// newLatestBurnBlockHashes.add(block.burn_block_hash); +// return newLatestBurnBlockHashes; +// }); +// setLatestBlocks(prevLatestBlocks => { +// return [ +// { +// type: UIBlockType.BurnBlock, +// height: block.burn_block_height, +// hash: block.burn_block_hash, +// timestamp: block.burn_block_time, +// }, +// ...prevLatestBlocks, +// ]; +// }); +// } +// setLatestBlocks(prevLatestBlocks => { +// return [ +// { +// type: UIBlockType.Block, +// height: block.height, +// hash: block.hash, +// timestamp: block.burn_block_time, +// txsCount: block.tx_count, +// }, +// ...prevLatestBlocks, +// ]; +// }); +// } +// +// updateLatestBlocks(); +// }); +// }; +// +// void subscribe(); +// +// return () => { +// if (subscriptionRef.current?.unsubscribe) { +// void subscriptionRef.current?.unsubscribe(); +// } +// }; +// }, [activeNetworkUrl, burnBlocks, fetchedBlockHashes, latestBlockHashes, latestBurnBlockHashes]); +// +// const clearLatestBlocks = () => { +// setLatestBlocks([]); +// dispatch({ type: 'clear' }); +// }; +// +// return { +// latestBlocks, +// latestBlock, +// latestBlocksCount: latestBlockHashes.size, +// clearLatestBlocks, +// }; +// } + +export function useBlockListWebSocket( + initialBlockHashes: Set, + initialBurnBlockHashes: Set +) { + const [latestBlocks, setLatestBlocks] = useState([]); + const [latestBlock, setLatestBlock] = useState(); + const latestBlockHashes = useRef(new Set()); + const latestBurnBlockHashes = useRef(new Set()); + + const handleBlock = useCallback( + (block: NakamotoBlock) => { + function updateLatestBlocks() { + if (latestBlockHashes.current.has(block.hash) || initialBlockHashes.has(block.hash)) { + return; + } + setLatestBlock(block); + latestBlockHashes.current.add(block.hash); + const isNewBurnBlock = + !initialBurnBlockHashes.has(block.burn_block_hash) && + !latestBurnBlockHashes.current.has(block.burn_block_hash); + if (isNewBurnBlock) { + latestBurnBlockHashes.current.add(block.burn_block_hash); + setLatestBlocks(prevLatestBlocks => [ + { + type: UIBlockType.BurnBlock, + height: block.burn_block_height, + hash: block.burn_block_hash, + timestamp: block.burn_block_time, + }, + ...prevLatestBlocks, + ]); + } + setLatestBlocks(prevLatestBlocks => [ + { + type: UIBlockType.Block, + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + txsCount: block.tx_count, + }, + ...prevLatestBlocks, + ]); + } + + updateLatestBlocks(); + }, + [initialBurnBlockHashes, initialBlockHashes] + ); + + useSubscribeBlocks(handleBlock); + + const clearLatestBlocks = () => { + setLatestBlocks([]); + latestBlockHashes.current = new Set(); + }; + + return { + latestUIBlocks: latestBlocks, + latestBlock, + latestBlocksCount: latestBlockHashes.current.size, + clearLatestBlocks, + }; +} diff --git a/src/app/_components/BlockList/LayoutA/useInitialBlockList.ts b/src/app/_components/BlockList/LayoutA/useInitialBlockList.ts new file mode 100644 index 000000000..07740d697 --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/useInitialBlockList.ts @@ -0,0 +1,34 @@ +import { BurnBlock } from '@stacks/blockchain-api-client'; + +import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; +import { useSuspenseBlocksByBurnBlock } from '../../../../common/queries/useBlocksByBurnBlock'; +import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocks'; + +const BURN_BLOCK_LENGTH = 2; +const STX_BLOCK_LENGTH = 20; + +export function useInitialBlockList() { + const burnBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBurnBlocks(BURN_BLOCK_LENGTH), + BURN_BLOCK_LENGTH + ); + + const lastBurnBlock = burnBlocks[0]; + const secondToLastBurnBlock = burnBlocks[1]; + + const lastBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(lastBurnBlock.burn_block_height, STX_BLOCK_LENGTH), + STX_BLOCK_LENGTH + ); + const secondToLastBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(secondToLastBurnBlock.burn_block_height, STX_BLOCK_LENGTH), + STX_BLOCK_LENGTH + ); + + return { + lastBurnBlock, + secondToLastBurnBlock, + lastBurnBlockStxBlocks, + secondToLastBlockStxBlocks, + }; +} diff --git a/src/app/_components/BlockList/LayoutA/usePaginatedBlockList.ts b/src/app/_components/BlockList/LayoutA/usePaginatedBlockList.ts new file mode 100644 index 000000000..3785880ce --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/usePaginatedBlockList.ts @@ -0,0 +1,83 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useMemo, useState } from 'react'; + +import { Block } from '@stacks/stacks-blockchain-api-types'; + +import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; +import { useSuspenseBlockListInfinite } from '../../../../common/queries/useBlockListInfinite'; +import { UIBlockType, UISingleBlock } from '../types'; +import { useBlockListContext } from './context'; + +export function usePaginatedBlockList() { + const queryClient = useQueryClient(); + const response = useSuspenseBlockListInfinite(); + const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + const blocks = useSuspenseInfiniteQueryResult(response); + + const initialBurnBlocks: Record = useMemo( + () => + blocks.reduce( + (acc, block) => { + if (!acc[block.burn_block_hash]) { + acc[block.burn_block_hash] = { + type: UIBlockType.BurnBlock, + height: block.burn_block_height, + hash: block.burn_block_hash, + timestamp: block.burn_block_time, + }; + } + return acc; + }, + {} as Record + ), + [blocks] + ); + + const stxBlocksGroupedByBurnBlock: Record = useMemo( + () => + blocks.reduce( + (acc, block) => { + if (!acc[block.burn_block_hash]) { + acc[block.burn_block_hash] = []; + } + acc[block.burn_block_hash].push({ + type: UIBlockType.Block, + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + txsCount: block.txs.length, + }); + return acc; + }, + {} as Record + ), + [blocks] + ); + + const initialBlockList = useMemo( + () => + Object.keys(stxBlocksGroupedByBurnBlock).reduce((acc, burnBlockHash) => { + const stxBlocks = stxBlocksGroupedByBurnBlock[burnBlockHash]; + const burnBlock = initialBurnBlocks[burnBlockHash]; + acc.push(...stxBlocks, burnBlock); + return acc; + }, [] as UISingleBlock[]), + [initialBurnBlocks, stxBlocksGroupedByBurnBlock] + ); + + const updateList = useCallback( + function () { + return queryClient.resetQueries({ queryKey: ['blockListInfinite'] }); + }, + [queryClient] + ); + + return { + initialBlockList, + initialBurnBlocks, + updateList, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + }; +} diff --git a/src/app/_components/BlockList/useStacksWebSocketClient.ts b/src/app/_components/BlockList/useStacksWebSocketClient.ts new file mode 100644 index 000000000..97b5437cd --- /dev/null +++ b/src/app/_components/BlockList/useStacksWebSocketClient.ts @@ -0,0 +1,30 @@ +import { useEffect, useState } from 'react'; + +import { StacksApiWebSocketClient, connectWebSocketClient } from '@stacks/blockchain-api-client'; + +import { useGlobalContext } from '../../../common/context/useAppContext'; + +export function useStacksWebSocketClient() { + const [webSocketClient, setWebSocketClient] = useState(); + const activeNetworkUrl = useGlobalContext().activeNetworkKey; + + useEffect(() => { + let client: StacksApiWebSocketClient; + const connect = async () => { + client = await connectWebSocketClient(activeNetworkUrl.replace('https://', 'wss://')); + // window.clients = window.clients || {}; + // window.clients[Date.now()] = client; + setWebSocketClient(client); + console.log('connecting'); + }; + + void connect(); + + return () => { + client?.webSocket?.close(); + console.log('closing', client?.webSocket?.close); + }; + }, [activeNetworkUrl]); + + return webSocketClient; +} diff --git a/src/app/_components/BlockList/useSubscribeBlocks.ts b/src/app/_components/BlockList/useSubscribeBlocks.ts new file mode 100644 index 000000000..50ea967b6 --- /dev/null +++ b/src/app/_components/BlockList/useSubscribeBlocks.ts @@ -0,0 +1,36 @@ +import { useEffect, useState } from 'react'; + +import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; +import { Block } from '@stacks/stacks-blockchain-api-types'; + +import { useStacksWebSocketClient } from './useStacksWebSocketClient'; + +interface Subscription { + unsubscribe(): Promise; +} + +export function useSubscribeBlocks(handleBlock: (block: NakamotoBlock) => any) { + const [subscription, setSubscription] = useState(); + const stacksWebSocketClient = useStacksWebSocketClient(); + useEffect(() => { + let subscription: Subscription; + const subscribe = async () => { + if (!stacksWebSocketClient) return; + console.log('subscribing'); + subscription = await stacksWebSocketClient?.subscribeBlocks((block: Block) => { + handleBlock({ + ...block, + parent_index_block_hash: '', + tx_count: 0, + }); + }); + setSubscription(subscription); + }; + void subscribe(); + return () => { + subscription?.unsubscribe(); + console.log('unsubscribe', subscription?.unsubscribe); + }; + }, [handleBlock, stacksWebSocketClient]); + return subscription; +} diff --git a/src/app/blocks/PageClient.tsx b/src/app/blocks/PageClient.tsx index 51b96d6ba..ef1a67331 100644 --- a/src/app/blocks/PageClient.tsx +++ b/src/app/blocks/PageClient.tsx @@ -5,6 +5,7 @@ import dynamic from 'next/dynamic'; import * as React from 'react'; import { SkeletonBlockList } from '../../common/components/loaders/skeleton-text'; +import { useGlobalContext } from '../../common/context/useAppContext'; import { PageTitle } from '../_components/PageTitle'; const BlocksList = dynamic(() => import('../_components/BlockList').then(mod => mod.BlocksList), { @@ -12,11 +13,21 @@ const BlocksList = dynamic(() => import('../_components/BlockList').then(mod => ssr: false, }); +const PaginatedBlockListLayoutA = dynamic( + () => + import('../_components/BlockList/LayoutA/Paginated').then(mod => mod.PaginatedBlockListLayoutA), + { + loading: () => , + ssr: false, + } +); + const BlocksPage: NextPage = () => { + const { activeNetworkKey } = useGlobalContext(); return ( <> Blocks - + {activeNetworkKey.indexOf('naka') !== -1 ? : } ); }; diff --git a/src/app/txid/[txId]/TxDetails/BlockHash.tsx b/src/app/txid/[txId]/TxDetails/BlockHash.tsx index 65e9f9457..e403c7bd9 100644 --- a/src/app/txid/[txId]/TxDetails/BlockHash.tsx +++ b/src/app/txid/[txId]/TxDetails/BlockHash.tsx @@ -22,7 +22,7 @@ export const BlockHash: FC<{ + {tx.block_hash} } diff --git a/src/app/txid/[txId]/TxDetails/Sender.tsx b/src/app/txid/[txId]/TxDetails/Sender.tsx index db91dc750..6ef345363 100644 --- a/src/app/txid/[txId]/TxDetails/Sender.tsx +++ b/src/app/txid/[txId]/TxDetails/Sender.tsx @@ -21,7 +21,7 @@ export const Sender: FC<{ tx: Transaction | MempoolTransaction }> = ({ tx }) => diff --git a/src/common/debug.ts b/src/common/debug.ts new file mode 100644 index 000000000..86b21fccd --- /dev/null +++ b/src/common/debug.ts @@ -0,0 +1,59 @@ +import { useCallback, useEffect, useRef } from 'react'; + +const usePrevious = (value: any, initialValue: any) => { + const ref = useRef(initialValue); + useEffect(() => { + ref.current = value; + }); + return ref.current; +}; + +const useEffectDebugger = (effectHook: any, dependencies: any, dependencyNames: any = []) => { + const previousDeps = usePrevious(dependencies, []); + + const changedDeps = dependencies.reduce((accum: any, dependency: any, index: any) => { + if (dependency !== previousDeps[index]) { + const keyName = dependencyNames[index] || index; + return { + ...accum, + [keyName]: { + before: previousDeps[index], + after: dependency, + }, + }; + } + + return accum; + }, {}); + + if (Object.keys(changedDeps).length) { + console.log('[use-effect-debugger] ', changedDeps); + } + + useEffect(effectHook, dependencies); +}; + +const useCallbackDebugger = (effectHook: any, dependencies: any, dependencyNames: any = []) => { + const previousDeps = usePrevious(dependencies, []); + + const changedDeps = dependencies.reduce((accum: any, dependency: any, index: any) => { + if (dependency !== previousDeps[index]) { + const keyName = dependencyNames[index] || index; + return { + ...accum, + [keyName]: { + before: previousDeps[index], + after: dependency, + }, + }; + } + + return accum; + }, {}); + + if (Object.keys(changedDeps).length) { + console.log('[use-effect-debugger] ', changedDeps); + } + + return useCallback(effectHook, dependencies); +}; diff --git a/src/common/queries/useBlockListInfinite.ts b/src/common/queries/useBlockListInfinite.ts index 96168f485..4fa49877e 100644 --- a/src/common/queries/useBlockListInfinite.ts +++ b/src/common/queries/useBlockListInfinite.ts @@ -32,6 +32,5 @@ export const useSuspenseBlockListInfinite = (limit = DEFAULT_LIST_LIMIT) => { staleTime: TWO_MINUTES, getNextPageParam, initialPageParam: 0, - refetchOnWindowFocus: true, }); }; diff --git a/src/common/queries/useBlocksByBurnBlock.ts b/src/common/queries/useBlocksByBurnBlock.ts index 7a104362d..0bb6efd92 100644 --- a/src/common/queries/useBlocksByBurnBlock.ts +++ b/src/common/queries/useBlocksByBurnBlock.ts @@ -53,7 +53,6 @@ export function useSuspenseBlocksByBurnBlock( getNextPageParam, initialPageParam: 0, staleTime: heightOrHash === 'latest' ? ONE_SECOND * 5 : TWO_MINUTES, - refetchOnWindowFocus: true, ...options, }); } diff --git a/src/common/queries/useBurnBlocks.ts b/src/common/queries/useBurnBlocks.ts index 087cb3f25..5bc1af720 100644 --- a/src/common/queries/useBurnBlocks.ts +++ b/src/common/queries/useBurnBlocks.ts @@ -48,7 +48,6 @@ export function useSuspenseBurnBlocks( getNextPageParam, initialPageParam: 0, staleTime: TWO_MINUTES, - refetchOnWindowFocus: true, ...options, }); }