From d2a34627ddaa992ba62d4202995fb231344dd953 Mon Sep 17 00:00:00 2001 From: He1DAr Date: Tue, 19 Mar 2024 12:59:54 -0400 Subject: [PATCH 01/70] feat: grouped by btc block list view --- src/app/PageClient.tsx | 11 + .../BlockList/BlockAndMicroblocksItem.tsx | 2 +- .../_components/BlockList/BlockListItem.tsx | 2 +- .../GroupedByBurnBlock/BlocksGroup.tsx | 222 ++++++++++++++++++ .../GroupedByBurnBlock/NonPaginated.tsx | 78 ++++++ .../_components/BlockList/LayoutA/Blocks.tsx | 1 + .../BlockList/LayoutA/BurnBlock.tsx | 6 +- .../BlockList/LayoutA/StxBlock.tsx | 20 +- src/app/_components/Footer.tsx | 2 +- src/app/_components/ListHeader.tsx | 23 ++ src/app/_components/NavBar/MobileNav.tsx | 4 + src/app/_components/NetworkModeToast.tsx | 2 +- .../_components/Stats/SkeletonStatSection.tsx | 2 +- src/app/_components/Stats/StatSection.tsx | 6 +- src/app/_components/Stats/Stats.tsx | 6 +- src/app/blocks/PageClient.tsx | 12 +- src/app/sandbox/layout/RightPanel.tsx | 2 +- src/app/sandbox/transfer/PageClient.tsx | 2 +- src/app/transactions/MempoolFeeStats.tsx | 16 +- src/app/txid/[txId]/Events.tsx | 4 +- src/common/components/BtcStxBlockLinks.tsx | 8 +- src/common/components/FilterMenu.tsx | 12 +- src/common/components/TwoColumnsListItem.tsx | 4 +- src/common/components/TxIcon.tsx | 6 +- .../components/modals/unlocking-schedule.tsx | 2 +- .../txs-list/ListItem/MempoolTxListItem.tsx | 2 +- .../ListItem/MempoolTxListItemMini.tsx | 2 +- src/features/txs-list/ListItem/TxListItem.tsx | 2 +- .../ListItem/TxWithTransferListItem.tsx | 2 +- .../txsFilterAndSort/FilterButton.tsx | 21 +- .../txsFilterAndSort/TransactionMessages.tsx | 2 +- src/ui/theme/colors.ts | 2 +- src/ui/theme/componentTheme/Checkbox.ts | 4 +- src/ui/theme/componentTheme/Input.ts | 2 +- src/ui/theme/componentTheme/Menu.ts | 14 +- src/ui/theme/theme.ts | 57 +++-- 36 files changed, 470 insertions(+), 95 deletions(-) create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx create mode 100644 src/app/_components/ListHeader.tsx diff --git a/src/app/PageClient.tsx b/src/app/PageClient.tsx index 6e0a77195..bcc32b976 100644 --- a/src/app/PageClient.tsx +++ b/src/app/PageClient.tsx @@ -22,6 +22,17 @@ const NonPaginatedBlockListLayoutA = dynamic( } ); +const NonPaginatedBlockListGroupedByBurnBlock = dynamic( + () => + import('./_components/BlockList/GroupedByBurnBlock/NonPaginated').then( + mod => mod.NonPaginatedBlockListGroupedByBurnBlock + ), + { + loading: () => , + ssr: false, + } +); + const BlocksList = dynamic(() => import('./_components/BlockList').then(mod => mod.BlocksList), { loading: () => , ssr: false, diff --git a/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx b/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx index 11bf1a714..ea7fdcfdb 100644 --- a/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx +++ b/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx @@ -15,7 +15,7 @@ export const BlockAndMicroblocksItem: React.FC<{ block: Block }> = ({ block }) = return ( diff --git a/src/app/_components/BlockList/BlockListItem.tsx b/src/app/_components/BlockList/BlockListItem.tsx index 0a19fdc8e..62b372559 100644 --- a/src/app/_components/BlockList/BlockListItem.tsx +++ b/src/app/_components/BlockList/BlockListItem.tsx @@ -30,7 +30,7 @@ export const BlockListItem: React.FC<{ block: Block } & FlexProps> = React.memo( ), subtitle: ( - + {addSepBetweenStrings([ `${block?.microblocks_accepted?.length || 0} ${pluralize( 'microblock', diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx new file mode 100644 index 000000000..cce1e82ae --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx @@ -0,0 +1,222 @@ +import { useColorModeValue } from '@chakra-ui/react'; +import { hash } from '@noble/hashes/_assert'; +import * as React from 'react'; +import { ReactNode, useEffect, useRef, useState } from 'react'; +import { PiArrowElbowLeftDown } from 'react-icons/pi'; + +import { BurnBlock } from '@stacks/blockchain-api-client'; + +import { Circle } from '../../../../common/components/Circle'; +import { BlockLink } from '../../../../common/components/ExplorerLinks'; +import { Timestamp } from '../../../../common/components/Timestamp'; +import { truncateMiddle } from '../../../../common/utils/utils'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { Grid } from '../../../../ui/Grid'; +import { HStack } from '../../../../ui/HStack'; +import { Icon } from '../../../../ui/Icon'; +import { Text } from '../../../../ui/Text'; +import { BitcoinIcon, StxIcon } from '../../../../ui/icons'; +import { Caption } from '../../../../ui/typography'; +import { ListHeader } from '../../ListHeader'; +import { StxBlock } from '../LayoutA/StxBlock'; +import { UIBlockType, UISingleBlock } from '../types'; + +interface BlocksGroupProps { + burnBlock: UISingleBlock; + stxBlocks: UISingleBlock[]; +} + +const GroupHeader = () => { + const borderColor = useColorModeValue('slate.300', 'slate.800'); + + return ( + <> + + Block height + + + Block hash + + + Transactions + + + Timestamp + + + ); +}; + +const BlockItem = ({ block, icon }: { block: UISingleBlock; icon?: ReactNode }) => { + const textColor = useColorModeValue('slate.900', 'slate.50'); + const secondaryTextColor = useColorModeValue('slate.700', 'slate.600'); + const borderColor = useColorModeValue('slate.300', 'slate.800'); + return ( + <> + + {icon} + + + #{block.height} + + + + + + {block.hash} + + + + 100 + + + + + + ); +}; + +function ScrollableDiv({ children }: { children: ReactNode }) { + const [hasHorizontalScroll, setHasHorizontalScroll] = useState(false); + const divRef = useRef(null); + + useEffect(() => { + const checkForScroll = () => { + if (divRef.current) { + const { scrollWidth, clientWidth } = divRef.current; + if (scrollWidth > clientWidth) { + setHasHorizontalScroll(true); + } else { + setHasHorizontalScroll(false); + } + } + }; + checkForScroll(); + window.addEventListener('resize', checkForScroll); + return () => window.removeEventListener('resize', checkForScroll); + }, []); + + return ( + + {children} + + ); +} + +export function BlocksGroup({ burnBlock, stxBlocks }: BlocksGroupProps) { + return ( + + + + + + {burnBlock.height} + + ∙} gap={1}> + + {truncateMiddle(burnBlock.hash, 6)} + + + + + + + + {stxBlocks.map((stxBlock, i) => ( + <> + + + + ) : ( + + ) + } + /> + {i < stxBlocks.length - 1 && } + + ))} + + + + ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx new file mode 100644 index 000000000..13393b685 --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx @@ -0,0 +1,78 @@ +'use client'; + +import { Block } from 'bitcoinjs-lib'; +import React from 'react'; + +import { Section } from '../../../../common/components/Section'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { ListHeader } from '../../ListHeader'; +import { BlockListProvider } from '../LayoutA/Provider'; +import { UIBlockType } from '../types'; +import { BlocksGroup } from './BlocksGroup'; + +const LIST_LENGTH = 17; + +function NonPaginatedBlockListGroupedByBurnBlockBase() { + const blockList = [ + { + type: UIBlockType.Block, + height: 10001, + hash: '0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + }, + { + type: UIBlockType.Block, + height: 10002, + hash: '0xrerqreqwjdhgjhdgj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + }, + { + type: UIBlockType.Block, + height: 10003, + hash: '0xbxvcbxvcbvxcbvxc0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + }, + { + type: UIBlockType.Block, + height: 10004, + hash: '0xjhjhfhgjhdjdhjhhj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + }, + ]; + + const burnBlock = { + height: 332141, + hash: '0xhfgjdkhbafgkjhdafjkhdsafjkhflkjdsahfjkhdsafhdsafdsaf', + timestamp: 0, + }; + + return ( +
+ + + +
+ ); +} + +export function NonPaginatedBlockListGroupedByBurnBlock() { + return ( + + + + + + ); +} diff --git a/src/app/_components/BlockList/LayoutA/Blocks.tsx b/src/app/_components/BlockList/LayoutA/Blocks.tsx index 4c90e0286..b7ce92a50 100644 --- a/src/app/_components/BlockList/LayoutA/Blocks.tsx +++ b/src/app/_components/BlockList/LayoutA/Blocks.tsx @@ -44,6 +44,7 @@ export function Blocks({ ) : undefined } + hasBorder={i < blockList.length && blockList[i + 1].type === UIBlockType.Block} /> ); case UIBlockType.BurnBlock: diff --git a/src/app/_components/BlockList/LayoutA/BurnBlock.tsx b/src/app/_components/BlockList/LayoutA/BurnBlock.tsx index 0201467ae..bb780759d 100644 --- a/src/app/_components/BlockList/LayoutA/BurnBlock.tsx +++ b/src/app/_components/BlockList/LayoutA/BurnBlock.tsx @@ -36,7 +36,7 @@ export const BurnBlock = memo(function ({ timestamp, height, hash, ...flexProps mr={'-8'} ml={'-10'} color={textColor} - {...flexProps} + className={'burn-block'} > #{height} diff --git a/src/app/_components/BlockList/LayoutA/StxBlock.tsx b/src/app/_components/BlockList/LayoutA/StxBlock.tsx index 24355305e..6029a1939 100644 --- a/src/app/_components/BlockList/LayoutA/StxBlock.tsx +++ b/src/app/_components/BlockList/LayoutA/StxBlock.tsx @@ -17,9 +17,17 @@ interface ListItemProps { timestamp: number; txsCount?: number; icon?: ReactNode; + hasBorder?: boolean; } -export const StxBlock = memo(function ({ timestamp, height, hash, txsCount, icon }: ListItemProps) { +export const StxBlock = memo(function ({ + timestamp, + height, + hash, + txsCount, + icon, + hasBorder, +}: ListItemProps) { const textColor = useColorModeValue('slate.900', 'slate.50'); const secondaryTextColor = useColorModeValue('slate.700', 'slate.600'); const borderColor = useColorModeValue('slate.300', 'slate.800'); @@ -29,20 +37,14 @@ export const StxBlock = memo(function ({ timestamp, height, hash, txsCount, icon borderLeft={icon ? undefined : '1px'} borderColor={borderColor} position="relative" - __css={{ - '>div': { - borderTop: '1px', - }, - '&:first-child >div': { - borderTop: 'none', - }, - }} + className={'stx-block'} > { flexDirection={['column', 'column', 'row']} alignItems={['center', 'center', 'unset']} textAlign={['center', 'center', 'unset']} - borderTop="1px solid var(--stacks-colors-border)" + borderTop="1px solid var(--stacks-colors-borderPrimary)" px={'unset'} > diff --git a/src/app/_components/ListHeader.tsx b/src/app/_components/ListHeader.tsx new file mode 100644 index 000000000..5fafb7f88 --- /dev/null +++ b/src/app/_components/ListHeader.tsx @@ -0,0 +1,23 @@ +import { useColorModeValue } from '@chakra-ui/react'; +import { ReactNode } from 'react'; + +import { Text, TextProps } from '../../ui/Text'; + +export function ListHeader({ children, ...textProps }: { children: ReactNode } & TextProps) { + const color = useColorModeValue('slate.700', 'slate.250'); + const bg = useColorModeValue('slate.150', 'slate.850'); + return ( + + {children} + + ); +} diff --git a/src/app/_components/NavBar/MobileNav.tsx b/src/app/_components/NavBar/MobileNav.tsx index 25287274c..04819c27a 100644 --- a/src/app/_components/NavBar/MobileNav.tsx +++ b/src/app/_components/NavBar/MobileNav.tsx @@ -1,4 +1,8 @@ +<<<<<<< HEAD import { FC, useEffect } from 'react'; +======= +import React, { FC } from 'react'; +>>>>>>> e74175d (feat: grouped by btc block list view) import { PiX } from 'react-icons/pi'; import { useAppSelector } from '../../../common/state/hooks'; diff --git a/src/app/_components/NetworkModeToast.tsx b/src/app/_components/NetworkModeToast.tsx index d8070d8a9..b34c5f6d5 100644 --- a/src/app/_components/NetworkModeToast.tsx +++ b/src/app/_components/NetworkModeToast.tsx @@ -12,7 +12,7 @@ export const NetworkModeToast: React.FC = () => { width: '540px', height: '68px', fontSize: '16px', - color: 'bg', + color: 'surface', backgroundColor: 'invert', }, }} diff --git a/src/app/_components/Stats/SkeletonStatSection.tsx b/src/app/_components/Stats/SkeletonStatSection.tsx index 1c76d978a..eac4c2f8a 100644 --- a/src/app/_components/Stats/SkeletonStatSection.tsx +++ b/src/app/_components/Stats/SkeletonStatSection.tsx @@ -30,7 +30,7 @@ export const SkeletonStatSection: FC = props => ( } - borderColor={'border'} + borderColor={'borderPrimary'} {...props} /> ); diff --git a/src/app/_components/Stats/StatSection.tsx b/src/app/_components/Stats/StatSection.tsx index 9be7544fc..b8e1b1467 100644 --- a/src/app/_components/Stats/StatSection.tsx +++ b/src/app/_components/Stats/StatSection.tsx @@ -21,7 +21,7 @@ export const StatSection: FC< p={5} height={32} justifyContent={'center'} - borderColor={'border'} + borderColor={'borderPrimary'} {...rest} > @@ -37,11 +37,11 @@ export const StatSection: FC< > {bodyMainText} - + {bodySecondaryText} - + {caption} diff --git a/src/app/_components/Stats/Stats.tsx b/src/app/_components/Stats/Stats.tsx index bcd7a9dd7..1589da94c 100644 --- a/src/app/_components/Stats/Stats.tsx +++ b/src/app/_components/Stats/Stats.tsx @@ -38,11 +38,11 @@ export function Stats(props: FlexProps) { return ( null}> - - + + diff --git a/src/app/blocks/PageClient.tsx b/src/app/blocks/PageClient.tsx index 8302e1a42..9cac4d86f 100644 --- a/src/app/blocks/PageClient.tsx +++ b/src/app/blocks/PageClient.tsx @@ -2,7 +2,6 @@ import type { NextPage } from 'next'; import dynamic from 'next/dynamic'; -import * as React from 'react'; import { SkeletonBlockList } from '../../common/components/loaders/skeleton-text'; import { useGlobalContext } from '../../common/context/useAppContext'; @@ -22,6 +21,17 @@ const PaginatedBlockListLayoutA = dynamic( } ); +const NonPaginatedBlockListGroupedByBurnBlock = dynamic( + () => + import('../_components/BlockList/GroupedByBurnBlock/NonPaginated').then( + mod => mod.NonPaginatedBlockListGroupedByBurnBlock + ), + { + loading: () => , + ssr: false, + } +); + const BlocksPage: NextPage = () => { const { activeNetworkKey } = useGlobalContext(); return ( diff --git a/src/app/sandbox/layout/RightPanel.tsx b/src/app/sandbox/layout/RightPanel.tsx index cf448cf8c..ed49558e1 100644 --- a/src/app/sandbox/layout/RightPanel.tsx +++ b/src/app/sandbox/layout/RightPanel.tsx @@ -34,7 +34,7 @@ export function RightPanel() { height={'full'} > {balance ? ( - + Account balance diff --git a/src/app/sandbox/transfer/PageClient.tsx b/src/app/sandbox/transfer/PageClient.tsx index 9e059e404..0e45508de 100644 --- a/src/app/sandbox/transfer/PageClient.tsx +++ b/src/app/sandbox/transfer/PageClient.tsx @@ -122,7 +122,7 @@ const PageClient: NextPage = () => { flexGrow={1} /> {mempoolFeeAll / MICROSTACKS_IN_STACKS} STX - + {getUsdValue(mempoolFeeAll, stxPrice, true)} @@ -104,7 +104,7 @@ function MempoolFeePriorityCard({ mempoolFeeTokenTransfer / MICROSTACKS_IN_STACKS } STX`} > - + - + - + return ( { style={{ whiteSpace: 'pre-wrap' }} bg={'transparent'} fontSize={'xs'} - color={'secondaryText'} + color={'textSubdued'} > {handleContractLogHex(event.contract_log.value.repr, event.contract_log.value.hex)} diff --git a/src/common/components/BtcStxBlockLinks.tsx b/src/common/components/BtcStxBlockLinks.tsx index a2414bec2..9aa4d9aeb 100644 --- a/src/common/components/BtcStxBlockLinks.tsx +++ b/src/common/components/BtcStxBlockLinks.tsx @@ -29,20 +29,20 @@ export const BtcStxBlockLinks: FC = ({ return ( - + #{stxBlockHeight} {btcBlockHeight && ( - - + + #{btcBlockHeight} diff --git a/src/common/components/FilterMenu.tsx b/src/common/components/FilterMenu.tsx index a3053e2b7..e4a512831 100644 --- a/src/common/components/FilterMenu.tsx +++ b/src/common/components/FilterMenu.tsx @@ -43,19 +43,19 @@ export function FilterMenu({ filterLabel, menuItems, leftIcon }: FilterMenuProps rightIcon={} leftIcon={ leftIcon ? ( - + ) : null } fontSize={'sm'} - bg="bg" + bg="surface" fontWeight={'semibold'} border={'1px'} borderColor={borderColor} - _hover={{ color: 'text', backgroundColor: 'border' }} - _active={{ color: 'text', backgroundColor: 'border' }} - _focus={{ color: 'text', backgroundColor: 'border' }} + _hover={{ color: 'text', backgroundColor: 'borderPrimary' }} + _active={{ color: 'text', backgroundColor: 'borderPrimary' }} + _focus={{ color: 'text', backgroundColor: 'borderPrimary' }} > - + Show:{' '} diff --git a/src/common/components/TwoColumnsListItem.tsx b/src/common/components/TwoColumnsListItem.tsx index ec013366e..fab873b42 100644 --- a/src/common/components/TwoColumnsListItem.tsx +++ b/src/common/components/TwoColumnsListItem.tsx @@ -68,7 +68,7 @@ export const TwoColsListItem: FC = memo( ) : null} {leftContent.subtitle !== undefined ? ( - + {leftContent.subtitle} ) : null} @@ -88,7 +88,7 @@ export const TwoColsListItem: FC = memo( {rightContent.title} ) : null} {rightContent.subtitle !== undefined ? ( - + {rightContent.subtitle} ) : null} diff --git a/src/common/components/TxIcon.tsx b/src/common/components/TxIcon.tsx index b8b31b9a0..05312f08e 100644 --- a/src/common/components/TxIcon.tsx +++ b/src/common/components/TxIcon.tsx @@ -119,7 +119,7 @@ const StatusBubble: React.FC<{ txStatus?: TxStatus }> = ({ txStatus }) => { position="absolute" bottom={'0px'} right={'0px'} - bg="bg" + bg="surface" transform="translate(35%, 35%)" border={'1px'} rounded={'full'} @@ -131,7 +131,7 @@ const StatusBubble: React.FC<{ txStatus?: TxStatus }> = ({ txStatus }) => { height={`${statusBubbleIconSize}px`} width={`${statusBubbleIconSize}px`} color={color} - bg={'bg'} + bg={'surface'} /> ); @@ -164,7 +164,7 @@ export const TxIcon: FC< position="relative" bottom={'0px'} right={'0px'} - bg="bg" + bg="surface" border={'1px'} rounded={'full'} alignItems={'center'} diff --git a/src/common/components/modals/unlocking-schedule.tsx b/src/common/components/modals/unlocking-schedule.tsx index f157ff028..3df891536 100644 --- a/src/common/components/modals/unlocking-schedule.tsx +++ b/src/common/components/modals/unlocking-schedule.tsx @@ -180,7 +180,7 @@ const Table: React.FC<{ balance?: AddressBalanceResponse; stacksTipHeight?: numb Received ) : ( - + Locked )} diff --git a/src/features/txs-list/ListItem/MempoolTxListItem.tsx b/src/features/txs-list/ListItem/MempoolTxListItem.tsx index f11e7e68b..028cc13dc 100644 --- a/src/features/txs-list/ListItem/MempoolTxListItem.tsx +++ b/src/features/txs-list/ListItem/MempoolTxListItem.tsx @@ -92,7 +92,7 @@ export const MempoolTxListItem: FC = memo(({ tx, ...res as="span" gap="1.5" flexWrap="nowrap" - color={'secondaryText'} + color={'textSubdued'} divider={∙} direction={['column', 'column', 'row', 'row']} justifyContent={['center', 'center', 'flex-end', 'flex-end']} diff --git a/src/features/txs-list/ListItem/MempoolTxListItemMini.tsx b/src/features/txs-list/ListItem/MempoolTxListItemMini.tsx index 13668eb47..9d661de17 100644 --- a/src/features/txs-list/ListItem/MempoolTxListItemMini.tsx +++ b/src/features/txs-list/ListItem/MempoolTxListItemMini.tsx @@ -38,7 +38,7 @@ export const MempoolTxListItemMini: FC = memo(({ tx }) alignItems="center" flexWrap="wrap" divider={∙} - color={'secondaryText'} + color={'textSubdued'} > {getTransactionTypeLabel(tx.tx_type)} diff --git a/src/features/txs-list/ListItem/TxListItem.tsx b/src/features/txs-list/ListItem/TxListItem.tsx index 4f5ea8502..fa5b9e4e7 100644 --- a/src/features/txs-list/ListItem/TxListItem.tsx +++ b/src/features/txs-list/ListItem/TxListItem.tsx @@ -84,7 +84,7 @@ const RightSubtitle: FC<{ tx: Transaction }> = memo(({ tx }) => { as="span" gap="1.5" flexWrap="nowrap" - color={'secondaryText'} + color={'textSubdued'} divider={∙} direction={['column', 'column', 'row', 'row']} justifyContent={['center', 'center', 'flex-end', 'flex-end']} diff --git a/src/features/txs-list/ListItem/TxWithTransferListItem.tsx b/src/features/txs-list/ListItem/TxWithTransferListItem.tsx index 361a724c7..09a9ec457 100644 --- a/src/features/txs-list/ListItem/TxWithTransferListItem.tsx +++ b/src/features/txs-list/ListItem/TxWithTransferListItem.tsx @@ -33,7 +33,7 @@ const LeftSubtitle: FC<{ burnCount: number; }> = memo(({ tx, transferCount, mintCount, burnCount }) => ( + { } - bg="bg" - color="secondaryText" + bg="surface" + color="textSubdued" border={'1px'} borderColor={borderColor} fontWeight={'semibold'} fontSize={'sm'} - _hover={{ color: 'text', backgroundColor: 'border' }} - _active={{ color: 'text', backgroundColor: 'border' }} - _focus={{ color: 'text', backgroundColor: 'border' }} + _hover={{ color: 'text', backgroundColor: 'borderPrimary' }} + _active={{ color: 'text', backgroundColor: 'borderPrimary' }} + _focus={{ color: 'text', backgroundColor: 'borderPrimary' }} > Filters {selectedFilters.length > 0 && `(${selectedFilters.length})`} - + } diff --git a/src/features/txsFilterAndSort/TransactionMessages.tsx b/src/features/txsFilterAndSort/TransactionMessages.tsx index f39d7c146..9ec1defd3 100644 --- a/src/features/txsFilterAndSort/TransactionMessages.tsx +++ b/src/features/txsFilterAndSort/TransactionMessages.tsx @@ -27,7 +27,7 @@ const MessageBase = ({ alignItems="center" height="100%" width="100%" - color="secondaryText" + color="textSubdued" > diff --git a/src/ui/theme/colors.ts b/src/ui/theme/colors.ts index daa7343d3..db2cc8b65 100644 --- a/src/ui/theme/colors.ts +++ b/src/ui/theme/colors.ts @@ -10,7 +10,7 @@ export const COLORS = { 400: '#CDCED6', 500: '#B9BBC6', 600: '#8B8D98', - 700: '#80838D', + 700: '#737680', 800: '#60646C', 850: '#3E4248', 900: '#1C2024', diff --git a/src/ui/theme/componentTheme/Checkbox.ts b/src/ui/theme/componentTheme/Checkbox.ts index 4c8526097..036289075 100644 --- a/src/ui/theme/componentTheme/Checkbox.ts +++ b/src/ui/theme/componentTheme/Checkbox.ts @@ -11,7 +11,7 @@ export const checkboxTheme = multiStyleConfigHelpers.defineMultiStyleConfig({ variants: { outline: multiStyleConfigHelpers.definePartsStyle(props => ({ control: { - bg: 'bg', + bg: 'surface', borderColor: mode(`slate.300`, `slate.700`)(props), _checked: { bg: mode(`purple.600`, `purple.400`)(props), @@ -22,7 +22,7 @@ export const checkboxTheme = multiStyleConfigHelpers.defineMultiStyleConfig({ }, }, _hover: { - bg: 'bg', + bg: 'surface', borderColor: mode(`slate.300`, `slate.700`)(props), }, }, diff --git a/src/ui/theme/componentTheme/Input.ts b/src/ui/theme/componentTheme/Input.ts index 75300c900..34bf3995a 100644 --- a/src/ui/theme/componentTheme/Input.ts +++ b/src/ui/theme/componentTheme/Input.ts @@ -10,7 +10,7 @@ export const inputTheme = multiStyleConfigHelpers.defineMultiStyleConfig({ outline: multiStyleConfigHelpers.definePartsStyle(props => ({ field: { fontSize: 'sm', - borderColor: 'border', + borderColor: 'borderPrimary', _placeholder: { color: mode(`slate.600`, `slate.500`)(props), }, diff --git a/src/ui/theme/componentTheme/Menu.ts b/src/ui/theme/componentTheme/Menu.ts index cda1f37d8..d275b03b6 100644 --- a/src/ui/theme/componentTheme/Menu.ts +++ b/src/ui/theme/componentTheme/Menu.ts @@ -11,28 +11,28 @@ const baseStyle = definePartsStyle(props => ({ // this will style the MenuButton component button: { _hover: { - bg: 'dropdownBgHover', + bg: 'hoverBackground', }, _active: { - bg: 'dropdownBgHover', + bg: 'hoverBackground', }, }, // this will style the MenuList component list: { - border: 'border', - bg: 'bg', + border: 'borderPrimary', + bg: 'surface', }, // this will style the MenuItem and MenuItemOption components item: { padding: '2 3', borderRadius: '10px', color: 'text', - bg: 'bg', + bg: 'surface', _hover: { - bg: 'dropdownBgHover', + bg: 'hoverBackground', }, _active: { - bg: 'dropdownBgHover', + bg: 'hoverBackground', }, }, })); diff --git a/src/ui/theme/theme.ts b/src/ui/theme/theme.ts index a9b3e3f08..c6008808e 100644 --- a/src/ui/theme/theme.ts +++ b/src/ui/theme/theme.ts @@ -24,38 +24,57 @@ export const theme = extendTheme({ semanticTokens: { colors: { brand: 'purple.600', - border: { - default: 'slate.150', + borderPrimary: { + default: 'slate.250', _dark: 'slate.850', }, - borderDark: { - default: 'slate.300', - _dark: 'slate.700', + borderSecondary: { + default: 'slate.150', + _dark: 'slate.900', + }, + error: { + default: 'red.600', + _dark: 'red.500', }, - error: 'red.600', - bg: { + success: { + default: 'green.600', + _dark: 'green.500', + }, + surface: { default: 'white', _dark: 'black', }, - dropdownBgHover: { + surfaceHighlight: { default: 'slate.150', - _dark: 'slate.850', - }, - invert: { - default: 'black', - _dark: 'white', + _dark: 'slate.900', }, text: { default: 'slate.900', _dark: 'slate.50', }, - secondaryText: { + textSubdued: { default: 'slate.700', _dark: 'slate.600', }, - buttonHoverBg: { - default: 'slate.100', - _dark: 'slate.900', + interactive: { + default: 'purple.600', + _dark: 'purple.400', + }, + hoverBackground: { + default: 'slate.150', + _dark: 'slate.850', + }, + icon: { + default: 'slate.900', + _dark: 'slate.50', + }, + iconSubdued: { + default: 'slate.700', + _dark: 'slate.600', + }, + invert: { + default: 'black', + _dark: 'white', }, }, }, @@ -81,8 +100,8 @@ export const theme = extendTheme({ 4.5: '1.125rem', }, borders: { - '1px': '1px solid var(--stacks-colors-border)', - dark_1px: '1px solid var(--stacks-colors-borderDark)', + '1px': '1px solid var(--stacks-colors-borderPrimary)', + dark_1px: '1px solid var(--stacks-colors-borderSecondary)', }, components: { Switch: switchTheme, From 674a208a1620dc61c5a8bf94d332dad3dd7964bf Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Thu, 21 Mar 2024 19:15:40 -0500 Subject: [PATCH 02/70] feat(blocklist): created new hk to fetch data for all block views --- .../AnimatedBlockAndMicroblocksItem.tsx | 5 +- .../_components/BlockList/BlockListItem.tsx | 2 +- .../GroupedByBurnBlock/BlocksGroup.tsx | 6 +- .../GroupedByBurnBlock/NonPaginated.tsx | 5 - .../LayoutA/BlockListWithControls.tsx | 2 +- .../BlockList/LayoutA/NonPaginated.tsx | 15 +- .../LayoutA/use-stacks-api-socket-client.ts | 56 ++++ .../BlockList/LayoutA/useBlockList copy.ts | 269 ++++++++++++++++++ .../BlockList/LayoutA/useBlockList.ts | 18 +- .../LayoutA/useBlockListWebSocket.ts | 1 - src/app/_components/BlockList/index.tsx | 19 +- 11 files changed, 350 insertions(+), 48 deletions(-) create mode 100644 src/app/_components/BlockList/LayoutA/use-stacks-api-socket-client.ts create mode 100644 src/app/_components/BlockList/LayoutA/useBlockList copy.ts diff --git a/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx b/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx index 1e57a6037..8121baa5d 100644 --- a/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx +++ b/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx @@ -1,12 +1,9 @@ 'use client'; -import { ScaleFade, SlideFade } from '@chakra-ui/react'; -import { FC, memo, useEffect, useState } from 'react'; +import { FC, useEffect, useState } from 'react'; import { Box } from '../../../ui/Box'; -import { Button } from '../../../ui/Button'; import { Collapse } from '../../../ui/Collapse'; -import { useDisclosure } from '../../../ui/hooks/useDisclosure'; import { BlockAndMicroblocksItem } from './BlockAndMicroblocksItem'; import { EnhancedBlock } from './types'; diff --git a/src/app/_components/BlockList/BlockListItem.tsx b/src/app/_components/BlockList/BlockListItem.tsx index 62b372559..b5bc770eb 100644 --- a/src/app/_components/BlockList/BlockListItem.tsx +++ b/src/app/_components/BlockList/BlockListItem.tsx @@ -7,7 +7,7 @@ import { BtcStxBlockLinks } from '../../../common/components/BtcStxBlockLinks'; import { TwoColsListItem } from '../../../common/components/TwoColumnsListItem'; import { addSepBetweenStrings, toRelativeTime, truncateMiddle } from '../../../common/utils/utils'; import { Flex, FlexProps } from '../../../ui/Flex'; -import { Caption, Text } from '../../../ui/typography'; +import { Caption } from '../../../ui/typography'; export const BlockListItem: React.FC<{ block: Block } & FlexProps> = React.memo( ({ block, ...rest }) => { diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx index cce1e82ae..be7e4c6be 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx @@ -1,11 +1,8 @@ import { useColorModeValue } from '@chakra-ui/react'; import { hash } from '@noble/hashes/_assert'; -import * as React from 'react'; import { ReactNode, useEffect, useRef, useState } from 'react'; import { PiArrowElbowLeftDown } from 'react-icons/pi'; -import { BurnBlock } from '@stacks/blockchain-api-client'; - import { Circle } from '../../../../common/components/Circle'; import { BlockLink } from '../../../../common/components/ExplorerLinks'; import { Timestamp } from '../../../../common/components/Timestamp'; @@ -19,8 +16,7 @@ import { Text } from '../../../../ui/Text'; import { BitcoinIcon, StxIcon } from '../../../../ui/icons'; import { Caption } from '../../../../ui/typography'; import { ListHeader } from '../../ListHeader'; -import { StxBlock } from '../LayoutA/StxBlock'; -import { UIBlockType, UISingleBlock } from '../types'; +import { UISingleBlock } from '../types'; interface BlocksGroupProps { burnBlock: UISingleBlock; diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx index 13393b685..8f1bd9da4 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx @@ -1,13 +1,8 @@ 'use client'; -import { Block } from 'bitcoinjs-lib'; -import React from 'react'; - import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; -import { Flex } from '../../../../ui/Flex'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { ListHeader } from '../../ListHeader'; import { BlockListProvider } from '../LayoutA/Provider'; import { UIBlockType } from '../types'; import { BlocksGroup } from './BlocksGroup'; diff --git a/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx b/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx index 8cb8cc67b..61b47a591 100644 --- a/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx +++ b/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import { ListFooter } from '../../../../common/components/ListFooter'; import { Section } from '../../../../common/components/Section'; diff --git a/src/app/_components/BlockList/LayoutA/NonPaginated.tsx b/src/app/_components/BlockList/LayoutA/NonPaginated.tsx index 16ce95a5b..b8d55f040 100644 --- a/src/app/_components/BlockList/LayoutA/NonPaginated.tsx +++ b/src/app/_components/BlockList/LayoutA/NonPaginated.tsx @@ -1,29 +1,16 @@ '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); + console.log({ blockList }); return ( void) => void; + disconnect: () => void; +} { + const socketClient = useRef(null); + const socketUrlTracker = useRef(null); + const isSocketClientConnecting = useRef(false); + const activeNetwork = useGlobalContext().activeNetwork; + + const connect = useCallback( + async (handleOnConnect?: (sc?: StacksApiSocketClient) => void) => { + if (socketClient.current?.socket.connected || isSocketClientConnecting.current) { + return; + } + try { + isSocketClientConnecting.current = true; + const socketUrl = `https://api.${activeNetwork.mode}.hiro.so/`; + socketUrlTracker.current = socketUrl; + const connection = StacksApiSocketClient.connect({ url: socketUrl }); + socketClient.current = connection; + socketClient.current.socket.on('connect', () => { + handleOnConnect?.(socketClient.current || undefined); + isSocketClientConnecting.current = false; + }); + socketClient.current.socket.on('disconnect', () => { + isSocketClientConnecting.current = false; + }); + socketClient.current.socket.on('connect_error', error => { + isSocketClientConnecting.current = false; + }); + } catch (error) { + isSocketClientConnecting.current = false; + } + }, + [activeNetwork.mode] + ); + + const disconnect = useCallback(() => { + if (socketClient.current?.socket.connected) { + socketClient.current.socket.close(); + } + }, []); + + return { + connection: socketClient.current, + connect, + disconnect, + }; +} diff --git a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts new file mode 100644 index 000000000..2a6c2d19c --- /dev/null +++ b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts @@ -0,0 +1,269 @@ +import { useGlobalContext } from '@/common/context/useAppContext'; +import { useSuspenseInfiniteQueryResult } from '@/common/hooks/useInfiniteQueryResult'; +import { useSuspenseBlockListInfinite } from '@/common/queries/useBlockListInfinite'; +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { BurnBlock, StacksApiSocketClient } from '@stacks/blockchain-api-client'; +import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; +import { Block } from '@stacks/stacks-blockchain-api-types'; + +import { EnhancedBlock, UIBlock, UIBlockType } from '../types'; +import { FADE_DURATION } from './consts'; +import { useBlockListContext } from './context'; +import { useStacksApiSocketClient } from './use-stacks-api-socket-client'; +import { useBlockListWebSocket } from './useBlockListWebSocket'; +import { useInitialBlockList } from './useInitialBlockList'; + +('use client'); + +interface Subscription { + unsubscribe(): Promise; +} + +const createBurnBlockUIBlock = (burnBlock: BurnBlock): UIBlock => ({ + type: UIBlockType.BurnBlock, + height: burnBlock.burn_block_height, + hash: burnBlock.burn_block_hash, + timestamp: burnBlock.burn_block_time, +}); + +const createBlockUIBlock = (block: NakamotoBlock): UIBlock => ({ + type: UIBlockType.Block, + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + txsCount: block.tx_count, +}); + +const createCountUIBlock = (count: number): UIBlock => ({ + type: UIBlockType.Count, + count, +}); + +const createUIBlockList = ( + burnBlock: BurnBlock, + stxBlocks: NakamotoBlock[], + length: number +): UIBlock[] => { + const blockList: UIBlock[] = [createBurnBlockUIBlock(burnBlock)]; + if (length <= 1) { + return blockList; + } + const hasCount = burnBlock.stacks_blocks.length > length - 1; + const stxBlocksToShow = stxBlocks.slice(0, length - 1 - (hasCount ? 1 : 0)); + + if (hasCount) { + blockList.unshift(createCountUIBlock(burnBlock.stacks_blocks.length - stxBlocksToShow.length)); + } + + stxBlocksToShow.reverse().forEach(block => { + blockList.unshift(createBlockUIBlock(block)); + }); + + return blockList; +}; + +export function useBlockList2(limit: number) { + const [isLive, setIsLive] = useState(false); + const [groupByBtcBlock, setGroupByBtcBlock] = useState(false); + + const [initialBlocks, setInitialBlocks] = useState([]); + const [latestBlocks, setLatestBlocks] = useState([]); + + const activeNetwork = useGlobalContext().activeNetwork; + + const response = useSuspenseBlockListInfinite(); // queryKey: ['blockListInfinite', limit] + const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + const blocks = useSuspenseInfiniteQueryResult(response, limit); + + // const { data: blocks, isFetchingNextPage, fetchNextPage, hasNextPage } = useSuspenseBlockListInfinite(); + + const queryClient = useQueryClient(); + + console.log('BlockList/index', { blocks }); + + useEffect(() => { + setInitialBlocks(blocks); + }, [blocks]); + + const { connect: stacksApiSocketConnect, disconnect: stacksApiSocketDisconnect } = + useStacksApiSocketClient(); + + useEffect(() => { + if (isLive) { + void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); + stacksApiSocketConnect((socketClient: StacksApiSocketClient | undefined) => { + socketClient?.subscribeBlocks((block: any) => { + setLatestBlocks(prevLatestBlocks => [ + { ...block, microblock_tx_count: {}, animate: true }, + ...prevLatestBlocks, + ]); + }); + }); + } else { + stacksApiSocketDisconnect(); + } + }, [isLive, activeNetwork.url, stacksApiSocketConnect, stacksApiSocketDisconnect, queryClient]); + + // useEffect(() => { + // if (!isLive) return; + // void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); + // let sub: { + // unsubscribe?: () => Promise; + // }; + // const subscribe = async () => { + // const client = await connectWebSocketClient(activeNetwork.url.replace('https://', 'wss://')); // TODO: Save this as ref so that when the live toggle is switched off, we can close the connection. Return subscribe and unsunscribe functions from the hook + // sub = await client.subscribeBlocks((block: any) => { + // setLatestBlocks(prevLatestBlocks => [ + // { ...block, microblock_tx_count: {}, animate: true }, + // ...prevLatestBlocks, + // ]); + // }); + // }; + // void subscribe(); + // return () => { + // if (sub?.unsubscribe) { + // void sub.unsubscribe(); + // } + // }; + // }, [activeNetwork.url, isLive, queryClient]); + + // const allBlocks = useMemo(() => { + // return [...latestBlocks, ...initialBlocks] + // .sort((a, b) => (b.height || 0) - (a.height || 0)) + // .reduce((acc: EnhancedBlock[], block, index) => { + // if (!acc.some(b => b.height === block.height)) { + // acc.push({ ...block, destroy: index >= (limit || DEFAULT_LIST_LIMIT) }); + // } + // return acc; + // }, []); + // }, [initialBlocks, latestBlocks, limit]); + + // // whats happening here? + // const removeOldBlock = useCallback((block: EnhancedBlock) => { + // setInitialBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); + // setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); + // }, []); + + if (groupByBtcBlock) { + // TODO: group by btc block + } + + return { + setIsLive, + setGroupByBtcBlock, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + blocks, + }; +} + +export function useBlockList1() { + const queryClient = useQueryClient(); + const { setIsUpdateListLoading, liveUpdates } = useBlockListContext(); + + const { + lastBurnBlock, + secondToLastBurnBlock, + lastBurnBlockStxBlocks, + secondToLastBlockStxBlocks, + } = useInitialBlockList(); + + const initialBlockHashes = useMemo( + () => + new Set([ + ...lastBurnBlockStxBlocks.map(block => block.hash), + ...secondToLastBlockStxBlocks.map(block => block.hash), + ]), + [lastBurnBlockStxBlocks, secondToLastBlockStxBlocks] + ); + + // Initial burn block hashes are used to filter out blocks that were already added to the list + const initialBurnBlockHashes = useMemo( + () => new Set([lastBurnBlock.burn_block_hash, secondToLastBurnBlock.burn_block_hash]), + [lastBurnBlock, secondToLastBurnBlock] + ); + + const { latestBlock, latestBlocksCount, clearLatestBlocks } = useBlockListWebSocket( + initialBlockHashes, + initialBurnBlockHashes + ); + + const updateList = useCallback( + async function () { + setIsUpdateListLoading(true); + await Promise.all([ + queryClient.invalidateQueries({ queryKey: ['getBlocksByBurnBlock'] }), // TODO: make these constants + queryClient.invalidateQueries({ queryKey: ['burnBlocks'] }), + ]); + clearLatestBlocks(); + setIsUpdateListLoading(false); + }, + [clearLatestBlocks, queryClient, setIsUpdateListLoading] + ); + + const prevLiveUpdatesRef = useRef(liveUpdates); + const prevLatestBlocksCountRef = useRef(latestBlocksCount); + + useEffect(() => { + const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; + + const receivedLatestBlockWhileLiveUpdates = + liveUpdates && + latestBlocksCount > 0 && + prevLatestBlocksCountRef.current !== latestBlocksCount; + + if (liveUpdatesToggled) { + setIsUpdateListLoading(true); + clearLatestBlocks(); + updateList().then(() => { + setIsUpdateListLoading(false); + }); + } else if (receivedLatestBlockWhileLiveUpdates && latestBlock) { + // If latest block belongs to the last burn block, add it to the list, otherwise trigger an update. + if (latestBlock.burn_block_height === lastBurnBlock.burn_block_height) { + setIsUpdateListLoading(true); + setTimeout(() => { + lastBurnBlockStxBlocks.unshift(latestBlock); + lastBurnBlock.stacks_blocks.unshift(latestBlock.hash); + setIsUpdateListLoading(false); + }, FADE_DURATION); + } else { + clearLatestBlocks(); + void updateList(); + } + } + + prevLiveUpdatesRef.current = liveUpdates; + prevLatestBlocksCountRef.current = latestBlocksCount; + }, [ + liveUpdates, + latestBlocksCount, + clearLatestBlocks, + updateList, + setIsUpdateListLoading, + latestBlock, + lastBurnBlockStxBlocks, + lastBurnBlock.stacks_blocks, + lastBurnBlock.burn_block_height, + ]); + + let blockList = createUIBlockList(lastBurnBlock, lastBurnBlockStxBlocks, length); + + if (blockList.length < length) { + const secondToLastBlockList = createUIBlockList( + secondToLastBurnBlock, + secondToLastBlockStxBlocks, + length - blockList.length + ); + blockList = blockList.concat(secondToLastBlockList); + } + + return { + blockList, + latestBlocksCount, + updateList, + }; +} diff --git a/src/app/_components/BlockList/LayoutA/useBlockList.ts b/src/app/_components/BlockList/LayoutA/useBlockList.ts index 5d147ffc6..b50474458 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList.ts @@ -1,18 +1,9 @@ import { useQueryClient } from '@tanstack/react-query'; -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { Subscription } from 'react-redux'; - -import { - BurnBlock, - StacksApiWebSocketClient, - connectWebSocketClient, -} from '@stacks/blockchain-api-client'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; + +import { BurnBlock } from '@stacks/blockchain-api-client'; import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; -import { useGlobalContext } from '../../../../common/context/useAppContext'; -import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; -import { useSuspenseBlocksByBurnBlock } from '../../../../common/queries/useBlocksByBurnBlock'; -import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocks'; import { UIBlock, UIBlockType } from '../types'; import { FADE_DURATION } from './consts'; import { useBlockListContext } from './context'; @@ -82,6 +73,7 @@ export function useBlockList(length: number) { [lastBurnBlockStxBlocks, secondToLastBlockStxBlocks] ); + // Initial burn block hashes are used to filter out blocks that were already added to the list const initialBurnBlockHashes = useMemo( () => new Set([lastBurnBlock.burn_block_hash, secondToLastBurnBlock.burn_block_hash]), [lastBurnBlock, secondToLastBurnBlock] @@ -96,7 +88,7 @@ export function useBlockList(length: number) { async function () { setIsUpdateListLoading(true); await Promise.all([ - queryClient.invalidateQueries({ queryKey: ['getBlocksByBurnBlock'] }), + queryClient.invalidateQueries({ queryKey: ['getBlocksByBurnBlock'] }), // TODO: make these constants queryClient.invalidateQueries({ queryKey: ['burnBlocks'] }), ]); clearLatestBlocks(); diff --git a/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts index 99d4284a4..8ff0e5668 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts @@ -1,7 +1,6 @@ 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'; diff --git a/src/app/_components/BlockList/index.tsx b/src/app/_components/BlockList/index.tsx index c49783359..eed02a16a 100644 --- a/src/app/_components/BlockList/index.tsx +++ b/src/app/_components/BlockList/index.tsx @@ -23,7 +23,9 @@ import { Switch } from '../../../ui/Switch'; import { ExplorerErrorBoundary } from '../ErrorBoundary'; import { AnimatedBlockAndMicroblocksItem } from './AnimatedBlockAndMicroblocksItem'; import { BlockAndMicroblocksItem } from './BlockAndMicroblocksItem'; +import { useBlockList } from './LayoutA/useBlockList'; import { EnhancedBlock } from './types'; +import { BlockListProvider } from './LayoutA/Provider'; function BlocksListBase({ limit, @@ -34,11 +36,16 @@ function BlocksListBase({ const [initialBlocks, setInitialBlocks] = useState([]); const [latestBlocks, setLatestBlocks] = useState([]); const activeNetwork = useGlobalContext().activeNetwork; - const response = useSuspenseBlockListInfinite(); + + const response = useSuspenseBlockListInfinite(); // queryKey: ['blockListInfinite', limit] const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + const blocks = useSuspenseInfiniteQueryResult(response, limit); + const queryClient = useQueryClient(); - const blocks = useSuspenseInfiniteQueryResult(response, limit); + console.log('BlockList/index', { blocks }); + const { blockList, updateList, latestBlocksCount } = useBlockList(17); + console.log('BlockList/index', { blockList }); const labelColor = useColorModeValue('slate.600', 'slate.400'); @@ -53,7 +60,7 @@ function BlocksListBase({ unsubscribe?: () => Promise; }; const subscribe = async () => { - const client = await connectWebSocketClient(activeNetwork.url.replace('https://', 'wss://')); + const client = await connectWebSocketClient(activeNetwork.url.replace('https://', 'wss://')); // TODO: Save this as ref so that when the live toggle is switched off, we can close the connection. Return subscribe and unsunscribe functions from the hook sub = await client.subscribeBlocks((block: any) => { setLatestBlocks(prevLatestBlocks => [ { ...block, microblock_tx_count: {}, animate: true }, @@ -80,6 +87,8 @@ function BlocksListBase({ }, []); }, [initialBlocks, latestBlocks, limit]); + + // whats happening here? const removeOldBlock = useCallback((block: EnhancedBlock) => { setInitialBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); @@ -148,7 +157,9 @@ export function BlocksList({ limit }: { limit?: number }) { }} tryAgainButton > - + + + ); } From 3d4651e3f8782efc8730a83b2fe9eb1539796145 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 22 Mar 2024 11:20:26 -0500 Subject: [PATCH 03/70] feat(grouped-by-btc-block-data-fetching): work in progress --- .../BlockList/LayoutA/useBlockList copy.ts | 61 +++++++++++++++++-- src/app/_components/BlockList/index.tsx | 9 ++- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts index 2a6c2d19c..44f9e3f65 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts @@ -14,8 +14,7 @@ import { useBlockListContext } from './context'; import { useStacksApiSocketClient } from './use-stacks-api-socket-client'; import { useBlockListWebSocket } from './useBlockListWebSocket'; import { useInitialBlockList } from './useInitialBlockList'; - -('use client'); +import { group } from 'console'; interface Subscription { unsubscribe(): Promise; @@ -64,6 +63,27 @@ const createUIBlockList = ( return blockList; }; +interface blocksGroupedByParentHash { + [parentHash: string]: Block[]; +} + + + +function groupBlocksByParentHash(blocks: Block[]): Record { + const groupedBlocks: Record = {}; + + blocks.forEach(block => { + const parentHash = block.parent_block_hash; + if (!groupedBlocks[parentHash]) { + groupedBlocks[parentHash] = [block]; + } else { + groupedBlocks[parentHash].push(block); + } + }); + + return groupedBlocks; +} + export function useBlockList2(limit: number) { const [isLive, setIsLive] = useState(false); const [groupByBtcBlock, setGroupByBtcBlock] = useState(false); @@ -74,15 +94,16 @@ export function useBlockList2(limit: number) { const activeNetwork = useGlobalContext().activeNetwork; const response = useSuspenseBlockListInfinite(); // queryKey: ['blockListInfinite', limit] + console.log('useBlockList copy', { response }); + const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; const blocks = useSuspenseInfiniteQueryResult(response, limit); + console.log('useBlockList copy', { blocks }); // const { data: blocks, isFetchingNextPage, fetchNextPage, hasNextPage } = useSuspenseBlockListInfinite(); const queryClient = useQueryClient(); - console.log('BlockList/index', { blocks }); - useEffect(() => { setInitialBlocks(blocks); }, [blocks]); @@ -95,7 +116,7 @@ export function useBlockList2(limit: number) { void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); stacksApiSocketConnect((socketClient: StacksApiSocketClient | undefined) => { socketClient?.subscribeBlocks((block: any) => { - setLatestBlocks(prevLatestBlocks => [ + setLatestBlocks(prevLatestBlocks => [ // TODO: or I could just push this onto the blocks array { ...block, microblock_tx_count: {}, animate: true }, ...prevLatestBlocks, ]); @@ -146,8 +167,35 @@ export function useBlockList2(limit: number) { // setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); // }, []); + // const allBlocks = useMemo(() => { + // console.log('useBlockList copy', { initialBlocks, latestBlocks }); + + // const blocks = [...latestBlocks, ...initialBlocks].sort( + // (a, b) => (b.height || 0) - (a.height || 0) + // ); // desc sort by height + // // .reduce((acc: EnhancedBlock[], block, index) => { + // // if (!acc.some(b => b.height === block.height)) { + // // acc.push({ ...block, destroy: index >= (limit || DEFAULT_LIST_LIMIT) }); + // // } + // // return acc; + // // }, []); + // console.log('useBlockList copy', { allBlocks: blocks }); + // }, [initialBlocks, latestBlocks]); + + // whats happening here? + const removeOldBlock = useCallback((block: EnhancedBlock) => { + setInitialBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); + setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); + }, []); + + let blocksResult = [...latestBlocks, ...initialBlocks].sort( + (a, b) => (b.height || 0) - (a.height || 0) + ); // desc + if (groupByBtcBlock) { // TODO: group by btc block + blocksResult = groupBlocksByParentHash(blocksResult); + } return { @@ -156,7 +204,8 @@ export function useBlockList2(limit: number) { isFetchingNextPage, fetchNextPage, hasNextPage, - blocks, + blocks: allBlocks, + removeOldBlock, }; } diff --git a/src/app/_components/BlockList/index.tsx b/src/app/_components/BlockList/index.tsx index eed02a16a..18af3c28c 100644 --- a/src/app/_components/BlockList/index.tsx +++ b/src/app/_components/BlockList/index.tsx @@ -23,9 +23,9 @@ import { Switch } from '../../../ui/Switch'; import { ExplorerErrorBoundary } from '../ErrorBoundary'; import { AnimatedBlockAndMicroblocksItem } from './AnimatedBlockAndMicroblocksItem'; import { BlockAndMicroblocksItem } from './BlockAndMicroblocksItem'; -import { useBlockList } from './LayoutA/useBlockList'; -import { EnhancedBlock } from './types'; import { BlockListProvider } from './LayoutA/Provider'; +import { useBlockList2 } from './LayoutA/useBlockList copy'; +import { EnhancedBlock } from './types'; function BlocksListBase({ limit, @@ -44,8 +44,8 @@ function BlocksListBase({ const queryClient = useQueryClient(); console.log('BlockList/index', { blocks }); - const { blockList, updateList, latestBlocksCount } = useBlockList(17); - console.log('BlockList/index', { blockList }); + const { blocks: blocksFromUseBlockList } = useBlockList2(17); + console.log('BlockList/index', { blocksFromUseBlockList }); const labelColor = useColorModeValue('slate.600', 'slate.400'); @@ -87,7 +87,6 @@ function BlocksListBase({ }, []); }, [initialBlocks, latestBlocks, limit]); - // whats happening here? const removeOldBlock = useCallback((block: EnhancedBlock) => { setInitialBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); From 614e9ef6a234f0dfec80f020115cbc6352390c6c Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 22 Mar 2024 13:19:19 -0500 Subject: [PATCH 04/70] feat(blocklist): added grp by btc functionality --- .../BlockList/LayoutA/useBlockList copy.ts | 260 +++++++++--------- src/app/_components/BlockList/index.tsx | 25 +- 2 files changed, 153 insertions(+), 132 deletions(-) diff --git a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts index 44f9e3f65..f588a7923 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts @@ -2,19 +2,14 @@ import { useGlobalContext } from '@/common/context/useAppContext'; import { useSuspenseInfiniteQueryResult } from '@/common/hooks/useInfiniteQueryResult'; import { useSuspenseBlockListInfinite } from '@/common/queries/useBlockListInfinite'; import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { BurnBlock, StacksApiSocketClient } from '@stacks/blockchain-api-client'; import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; import { Block } from '@stacks/stacks-blockchain-api-types'; import { EnhancedBlock, UIBlock, UIBlockType } from '../types'; -import { FADE_DURATION } from './consts'; -import { useBlockListContext } from './context'; import { useStacksApiSocketClient } from './use-stacks-api-socket-client'; -import { useBlockListWebSocket } from './useBlockListWebSocket'; -import { useInitialBlockList } from './useInitialBlockList'; -import { group } from 'console'; interface Subscription { unsubscribe(): Promise; @@ -63,12 +58,10 @@ const createUIBlockList = ( return blockList; }; -interface blocksGroupedByParentHash { - [parentHash: string]: Block[]; +interface BlocksGroupedByParentHash { + [parentHash: string]: EnhancedBlock[]; } - - function groupBlocksByParentHash(blocks: Block[]): Record { const groupedBlocks: Record = {}; @@ -84,9 +77,19 @@ function groupBlocksByParentHash(blocks: Block[]): Record { return groupedBlocks; } -export function useBlockList2(limit: number) { +export function useBlockList2(limit: number): { + setIsLive: (value: React.SetStateAction) => void; + isLive: boolean; + setIsGroupedByBtcBlock: (value: React.SetStateAction) => void; + isGroupedByBtcBlock: boolean; + isFetchingNextPage: boolean; + fetchNextPage: () => void; + hasNextPage: boolean; + blocks: EnhancedBlock[] | BlocksGroupedByParentHash; + removeOldBlock: (block: EnhancedBlock) => void; +} { const [isLive, setIsLive] = useState(false); - const [groupByBtcBlock, setGroupByBtcBlock] = useState(false); + const [isGroupedByBtcBlock, setIsGroupedByBtcBlock] = useState(false); const [initialBlocks, setInitialBlocks] = useState([]); const [latestBlocks, setLatestBlocks] = useState([]); @@ -116,7 +119,8 @@ export function useBlockList2(limit: number) { void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); stacksApiSocketConnect((socketClient: StacksApiSocketClient | undefined) => { socketClient?.subscribeBlocks((block: any) => { - setLatestBlocks(prevLatestBlocks => [ // TODO: or I could just push this onto the blocks array + setLatestBlocks(prevLatestBlocks => [ + // TODO: or I could just push this onto the blocks array { ...block, microblock_tx_count: {}, animate: true }, ...prevLatestBlocks, ]); @@ -188,131 +192,133 @@ export function useBlockList2(limit: number) { setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); }, []); - let blocksResult = [...latestBlocks, ...initialBlocks].sort( + let formattedBlocks = [...latestBlocks, ...initialBlocks].sort( (a, b) => (b.height || 0) - (a.height || 0) - ); // desc + ); // desc - if (groupByBtcBlock) { + let blocksGroupedByParentHash: BlocksGroupedByParentHash = {}; + if (isGroupedByBtcBlock) { // TODO: group by btc block - blocksResult = groupBlocksByParentHash(blocksResult); - + blocksGroupedByParentHash = groupBlocksByParentHash(blocks); } return { setIsLive, - setGroupByBtcBlock, + isLive, + setIsGroupedByBtcBlock, + isGroupedByBtcBlock, isFetchingNextPage, fetchNextPage, hasNextPage, - blocks: allBlocks, + blocks: isGroupedByBtcBlock ? blocksGroupedByParentHash : formattedBlocks, removeOldBlock, }; } -export function useBlockList1() { - const queryClient = useQueryClient(); - const { setIsUpdateListLoading, liveUpdates } = useBlockListContext(); - - const { - lastBurnBlock, - secondToLastBurnBlock, - lastBurnBlockStxBlocks, - secondToLastBlockStxBlocks, - } = useInitialBlockList(); - - const initialBlockHashes = useMemo( - () => - new Set([ - ...lastBurnBlockStxBlocks.map(block => block.hash), - ...secondToLastBlockStxBlocks.map(block => block.hash), - ]), - [lastBurnBlockStxBlocks, secondToLastBlockStxBlocks] - ); - - // Initial burn block hashes are used to filter out blocks that were already added to the list - const initialBurnBlockHashes = useMemo( - () => new Set([lastBurnBlock.burn_block_hash, secondToLastBurnBlock.burn_block_hash]), - [lastBurnBlock, secondToLastBurnBlock] - ); - - const { latestBlock, latestBlocksCount, clearLatestBlocks } = useBlockListWebSocket( - initialBlockHashes, - initialBurnBlockHashes - ); - - const updateList = useCallback( - async function () { - setIsUpdateListLoading(true); - await Promise.all([ - queryClient.invalidateQueries({ queryKey: ['getBlocksByBurnBlock'] }), // TODO: make these constants - queryClient.invalidateQueries({ queryKey: ['burnBlocks'] }), - ]); - clearLatestBlocks(); - setIsUpdateListLoading(false); - }, - [clearLatestBlocks, queryClient, setIsUpdateListLoading] - ); - - const prevLiveUpdatesRef = useRef(liveUpdates); - const prevLatestBlocksCountRef = useRef(latestBlocksCount); - - useEffect(() => { - const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; - - const receivedLatestBlockWhileLiveUpdates = - liveUpdates && - latestBlocksCount > 0 && - prevLatestBlocksCountRef.current !== latestBlocksCount; - - if (liveUpdatesToggled) { - setIsUpdateListLoading(true); - clearLatestBlocks(); - updateList().then(() => { - setIsUpdateListLoading(false); - }); - } else if (receivedLatestBlockWhileLiveUpdates && latestBlock) { - // If latest block belongs to the last burn block, add it to the list, otherwise trigger an update. - if (latestBlock.burn_block_height === lastBurnBlock.burn_block_height) { - setIsUpdateListLoading(true); - setTimeout(() => { - lastBurnBlockStxBlocks.unshift(latestBlock); - lastBurnBlock.stacks_blocks.unshift(latestBlock.hash); - setIsUpdateListLoading(false); - }, FADE_DURATION); - } else { - clearLatestBlocks(); - void updateList(); - } - } - - prevLiveUpdatesRef.current = liveUpdates; - prevLatestBlocksCountRef.current = latestBlocksCount; - }, [ - liveUpdates, - latestBlocksCount, - clearLatestBlocks, - updateList, - setIsUpdateListLoading, - latestBlock, - lastBurnBlockStxBlocks, - lastBurnBlock.stacks_blocks, - lastBurnBlock.burn_block_height, - ]); - - let blockList = createUIBlockList(lastBurnBlock, lastBurnBlockStxBlocks, length); - - if (blockList.length < length) { - const secondToLastBlockList = createUIBlockList( - secondToLastBurnBlock, - secondToLastBlockStxBlocks, - length - blockList.length - ); - blockList = blockList.concat(secondToLastBlockList); - } - - return { - blockList, - latestBlocksCount, - updateList, - }; -} +// export function useBlockList1() { +// const queryClient = useQueryClient(); +// const { setIsUpdateListLoading, liveUpdates } = useBlockListContext(); + +// const { +// lastBurnBlock, +// secondToLastBurnBlock, +// lastBurnBlockStxBlocks, +// secondToLastBlockStxBlocks, +// } = useInitialBlockList(); + +// const initialBlockHashes = useMemo( +// () => +// new Set([ +// ...lastBurnBlockStxBlocks.map(block => block.hash), +// ...secondToLastBlockStxBlocks.map(block => block.hash), +// ]), +// [lastBurnBlockStxBlocks, secondToLastBlockStxBlocks] +// ); + +// // Initial burn block hashes are used to filter out blocks that were already added to the list +// const initialBurnBlockHashes = useMemo( +// () => new Set([lastBurnBlock.burn_block_hash, secondToLastBurnBlock.burn_block_hash]), +// [lastBurnBlock, secondToLastBurnBlock] +// ); + +// const { latestBlock, latestBlocksCount, clearLatestBlocks } = useBlockListWebSocket( +// initialBlockHashes, +// initialBurnBlockHashes +// ); + +// const updateList = useCallback( +// async function () { +// setIsUpdateListLoading(true); +// await Promise.all([ +// queryClient.invalidateQueries({ queryKey: ['getBlocksByBurnBlock'] }), // TODO: make these constants +// queryClient.invalidateQueries({ queryKey: ['burnBlocks'] }), +// ]); +// clearLatestBlocks(); +// setIsUpdateListLoading(false); +// }, +// [clearLatestBlocks, queryClient, setIsUpdateListLoading] +// ); + +// const prevLiveUpdatesRef = useRef(liveUpdates); +// const prevLatestBlocksCountRef = useRef(latestBlocksCount); + +// useEffect(() => { +// const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; + +// const receivedLatestBlockWhileLiveUpdates = +// liveUpdates && +// latestBlocksCount > 0 && +// prevLatestBlocksCountRef.current !== latestBlocksCount; + +// if (liveUpdatesToggled) { +// setIsUpdateListLoading(true); +// clearLatestBlocks(); +// updateList().then(() => { +// setIsUpdateListLoading(false); +// }); +// } else if (receivedLatestBlockWhileLiveUpdates && latestBlock) { +// // If latest block belongs to the last burn block, add it to the list, otherwise trigger an update. +// if (latestBlock.burn_block_height === lastBurnBlock.burn_block_height) { +// setIsUpdateListLoading(true); +// setTimeout(() => { +// lastBurnBlockStxBlocks.unshift(latestBlock); +// lastBurnBlock.stacks_blocks.unshift(latestBlock.hash); +// setIsUpdateListLoading(false); +// }, FADE_DURATION); +// } else { +// clearLatestBlocks(); +// void updateList(); +// } +// } + +// prevLiveUpdatesRef.current = liveUpdates; +// prevLatestBlocksCountRef.current = latestBlocksCount; +// }, [ +// liveUpdates, +// latestBlocksCount, +// clearLatestBlocks, +// updateList, +// setIsUpdateListLoading, +// latestBlock, +// lastBurnBlockStxBlocks, +// lastBurnBlock.stacks_blocks, +// lastBurnBlock.burn_block_height, +// ]); + +// let blockList = createUIBlockList(lastBurnBlock, lastBurnBlockStxBlocks, length); + +// if (blockList.length < length) { +// const secondToLastBlockList = createUIBlockList( +// secondToLastBurnBlock, +// secondToLastBlockStxBlocks, +// length - blockList.length +// ); +// blockList = blockList.concat(secondToLastBlockList); +// } + +// return { +// blockList, +// latestBlocksCount, +// updateList, +// }; +// } diff --git a/src/app/_components/BlockList/index.tsx b/src/app/_components/BlockList/index.tsx index 18af3c28c..47dad6314 100644 --- a/src/app/_components/BlockList/index.tsx +++ b/src/app/_components/BlockList/index.tsx @@ -2,7 +2,7 @@ import { useColorModeValue } from '@chakra-ui/react'; import { useQueryClient } from '@tanstack/react-query'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { connectWebSocketClient } from '@stacks/blockchain-api-client'; import { Block } from '@stacks/stacks-blockchain-api-types'; @@ -32,7 +32,8 @@ function BlocksListBase({ }: { limit?: number; } & FlexProps) { - const [isLive, setIsLive] = React.useState(false); + // const [isLive, setIsLive] = useState(false); + const [initialBlocks, setInitialBlocks] = useState([]); const [latestBlocks, setLatestBlocks] = useState([]); const activeNetwork = useGlobalContext().activeNetwork; @@ -44,8 +45,14 @@ function BlocksListBase({ const queryClient = useQueryClient(); console.log('BlockList/index', { blocks }); - const { blocks: blocksFromUseBlockList } = useBlockList2(17); - console.log('BlockList/index', { blocksFromUseBlockList }); + const { + blocks: blocksFromUseBlockList, + setIsGroupedByBtcBlock, + isGroupedByBtcBlock, + isLive, + setIsLive, + } = useBlockList2(17); + console.log('BlockList/index', { blocksFromUseBlockList, isGroupedByBtcBlock, isLive }); const labelColor = useColorModeValue('slate.600', 'slate.400'); @@ -106,13 +113,21 @@ function BlocksListBase({ topRight={ - live view + Live Updates setIsLive(!isLive)} /> + + Group by Bitcoin block + + setIsGroupedByBtcBlock(!isGroupedByBtcBlock)} + /> } > From 43132c7bf914d04269b6f4a8e7fd496ed996fba3 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 22 Mar 2024 19:01:54 -0500 Subject: [PATCH 05/70] feat(grouped-by-btc-block-data-fetching): work in progress --- .../GroupedByBurnBlock/NonPaginated.tsx | 2 + .../LayoutA/use-stacks-api-socket-client.ts | 4 + .../BlockList/LayoutA/useBlockList copy.ts | 242 ++++-------------- src/app/_components/BlockList/index.tsx | 211 ++++++++------- 4 files changed, 172 insertions(+), 287 deletions(-) diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx index 8f1bd9da4..badfd4d4e 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx @@ -10,6 +10,8 @@ import { BlocksGroup } from './BlocksGroup'; const LIST_LENGTH = 17; function NonPaginatedBlockListGroupedByBurnBlockBase() { + + const blockList = [ { type: UIBlockType.Block, diff --git a/src/app/_components/BlockList/LayoutA/use-stacks-api-socket-client.ts b/src/app/_components/BlockList/LayoutA/use-stacks-api-socket-client.ts index 903105da2..81c67e348 100644 --- a/src/app/_components/BlockList/LayoutA/use-stacks-api-socket-client.ts +++ b/src/app/_components/BlockList/LayoutA/use-stacks-api-socket-client.ts @@ -26,13 +26,16 @@ export function useStacksApiSocketClient(): { const connection = StacksApiSocketClient.connect({ url: socketUrl }); socketClient.current = connection; socketClient.current.socket.on('connect', () => { + console.log('Connected to socket. About to run handleOnConnect') handleOnConnect?.(socketClient.current || undefined); isSocketClientConnecting.current = false; }); socketClient.current.socket.on('disconnect', () => { + console.log('Disconnected from socket') isSocketClientConnecting.current = false; }); socketClient.current.socket.on('connect_error', error => { + console.error('Socket connection error', error); isSocketClientConnecting.current = false; }); } catch (error) { @@ -44,6 +47,7 @@ export function useStacksApiSocketClient(): { const disconnect = useCallback(() => { if (socketClient.current?.socket.connected) { + console.log('Disconnecting from socket') socketClient.current.socket.close(); } }, []); diff --git a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts index f588a7923..46755bfe5 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts @@ -4,16 +4,15 @@ import { useSuspenseBlockListInfinite } from '@/common/queries/useBlockListInfin import { useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useState } from 'react'; -import { BurnBlock, StacksApiSocketClient } from '@stacks/blockchain-api-client'; +import { BurnBlock, connectWebSocketClient } from '@stacks/blockchain-api-client'; import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; import { Block } from '@stacks/stacks-blockchain-api-types'; import { EnhancedBlock, UIBlock, UIBlockType } from '../types'; -import { useStacksApiSocketClient } from './use-stacks-api-socket-client'; -interface Subscription { - unsubscribe(): Promise; -} +// interface Subscription { +// unsubscribe(): Promise; +// } const createBurnBlockUIBlock = (burnBlock: BurnBlock): UIBlock => ({ type: UIBlockType.BurnBlock, @@ -59,25 +58,25 @@ const createUIBlockList = ( }; interface BlocksGroupedByParentHash { - [parentHash: string]: EnhancedBlock[]; + [btcBlockHeight: string]: EnhancedBlock[]; } -function groupBlocksByParentHash(blocks: Block[]): Record { +function groupBlocksByBtcBlock(blocks: Block[]): Record { const groupedBlocks: Record = {}; blocks.forEach(block => { - const parentHash = block.parent_block_hash; - if (!groupedBlocks[parentHash]) { - groupedBlocks[parentHash] = [block]; + const btcBlockNum = block.burn_block_height; + if (!groupedBlocks[btcBlockNum]) { + groupedBlocks[btcBlockNum] = [block]; } else { - groupedBlocks[parentHash].push(block); + groupedBlocks[btcBlockNum].push(block); } }); return groupedBlocks; } -export function useBlockList2(limit: number): { +export function useBlockList2(limit?: number): { setIsLive: (value: React.SetStateAction) => void; isLive: boolean; setIsGroupedByBtcBlock: (value: React.SetStateAction) => void; @@ -86,6 +85,7 @@ export function useBlockList2(limit: number): { fetchNextPage: () => void; hasNextPage: boolean; blocks: EnhancedBlock[] | BlocksGroupedByParentHash; + blocksGroupedByBtcBlock: BlocksGroupedByParentHash; removeOldBlock: (block: EnhancedBlock) => void; } { const [isLive, setIsLive] = useState(false); @@ -111,49 +111,48 @@ export function useBlockList2(limit: number): { setInitialBlocks(blocks); }, [blocks]); - const { connect: stacksApiSocketConnect, disconnect: stacksApiSocketDisconnect } = - useStacksApiSocketClient(); + // const { connect: stacksApiSocketConnect, disconnect: stacksApiSocketDisconnect } = + // useStacksApiSocketClient(); useEffect(() => { - if (isLive) { - void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); - stacksApiSocketConnect((socketClient: StacksApiSocketClient | undefined) => { - socketClient?.subscribeBlocks((block: any) => { - setLatestBlocks(prevLatestBlocks => [ - // TODO: or I could just push this onto the blocks array - { ...block, microblock_tx_count: {}, animate: true }, - ...prevLatestBlocks, - ]); - }); + // if (isLive) { + // void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); + // stacksApiSocketConnect((socketClient: StacksApiSocketClient | undefined) => { + // console.log('socketClient?.subscribeBlocks...'); + // socketClient?.subscribeBlocks((block: any) => { + // console.log('new block received', block); + // setLatestBlocks(prevLatestBlocks => [ + // // TODO: or I could just push this onto the blocks array + // { ...block, microblock_tx_count: {}, animate: true }, + // ...prevLatestBlocks, + // ]); + // }); + // }); + // } else { + // stacksApiSocketDisconnect(); + // } + if (!isLive) return; + void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); + let sub: { + unsubscribe?: () => Promise; + }; + const subscribe = async () => { + const client = await connectWebSocketClient(activeNetwork.url.replace('https://', 'wss://')); // TODO: Save this as ref so that when the live toggle is switched off, we can close the connection. Return subscribe and unsunscribe functions from the hook + sub = await client.subscribeBlocks((block: any) => { + setLatestBlocks(prevLatestBlocks => [ + { ...block, microblock_tx_count: {}, animate: true }, + ...prevLatestBlocks, + ]); }); - } else { - stacksApiSocketDisconnect(); - } - }, [isLive, activeNetwork.url, stacksApiSocketConnect, stacksApiSocketDisconnect, queryClient]); - - // useEffect(() => { - // if (!isLive) return; - // void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); - // let sub: { - // unsubscribe?: () => Promise; - // }; - // const subscribe = async () => { - // const client = await connectWebSocketClient(activeNetwork.url.replace('https://', 'wss://')); // TODO: Save this as ref so that when the live toggle is switched off, we can close the connection. Return subscribe and unsunscribe functions from the hook - // sub = await client.subscribeBlocks((block: any) => { - // setLatestBlocks(prevLatestBlocks => [ - // { ...block, microblock_tx_count: {}, animate: true }, - // ...prevLatestBlocks, - // ]); - // }); - // }; - // void subscribe(); - // return () => { - // if (sub?.unsubscribe) { - // void sub.unsubscribe(); - // } - // }; - // }, [activeNetwork.url, isLive, queryClient]); - + }; + void subscribe(); + return () => { + if (sub?.unsubscribe) { + void sub.unsubscribe(); + } + }; + }, [isLive, activeNetwork.url, queryClient]); + // const allBlocks = useMemo(() => { // return [...latestBlocks, ...initialBlocks] // .sort((a, b) => (b.height || 0) - (a.height || 0)) @@ -165,28 +164,6 @@ export function useBlockList2(limit: number): { // }, []); // }, [initialBlocks, latestBlocks, limit]); - // // whats happening here? - // const removeOldBlock = useCallback((block: EnhancedBlock) => { - // setInitialBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); - // setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); - // }, []); - - // const allBlocks = useMemo(() => { - // console.log('useBlockList copy', { initialBlocks, latestBlocks }); - - // const blocks = [...latestBlocks, ...initialBlocks].sort( - // (a, b) => (b.height || 0) - (a.height || 0) - // ); // desc sort by height - // // .reduce((acc: EnhancedBlock[], block, index) => { - // // if (!acc.some(b => b.height === block.height)) { - // // acc.push({ ...block, destroy: index >= (limit || DEFAULT_LIST_LIMIT) }); - // // } - // // return acc; - // // }, []); - // console.log('useBlockList copy', { allBlocks: blocks }); - // }, [initialBlocks, latestBlocks]); - - // whats happening here? const removeOldBlock = useCallback((block: EnhancedBlock) => { setInitialBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); @@ -196,11 +173,7 @@ export function useBlockList2(limit: number): { (a, b) => (b.height || 0) - (a.height || 0) ); // desc - let blocksGroupedByParentHash: BlocksGroupedByParentHash = {}; - if (isGroupedByBtcBlock) { - // TODO: group by btc block - blocksGroupedByParentHash = groupBlocksByParentHash(blocks); - } + const blocksGroupedByParentHash = groupBlocksByBtcBlock(blocks); return { setIsLive, @@ -210,115 +183,8 @@ export function useBlockList2(limit: number): { isFetchingNextPage, fetchNextPage, hasNextPage, - blocks: isGroupedByBtcBlock ? blocksGroupedByParentHash : formattedBlocks, + blocks: formattedBlocks, + blocksGroupedByBtcBlock: blocksGroupedByParentHash, removeOldBlock, }; } - -// export function useBlockList1() { -// const queryClient = useQueryClient(); -// const { setIsUpdateListLoading, liveUpdates } = useBlockListContext(); - -// const { -// lastBurnBlock, -// secondToLastBurnBlock, -// lastBurnBlockStxBlocks, -// secondToLastBlockStxBlocks, -// } = useInitialBlockList(); - -// const initialBlockHashes = useMemo( -// () => -// new Set([ -// ...lastBurnBlockStxBlocks.map(block => block.hash), -// ...secondToLastBlockStxBlocks.map(block => block.hash), -// ]), -// [lastBurnBlockStxBlocks, secondToLastBlockStxBlocks] -// ); - -// // Initial burn block hashes are used to filter out blocks that were already added to the list -// const initialBurnBlockHashes = useMemo( -// () => new Set([lastBurnBlock.burn_block_hash, secondToLastBurnBlock.burn_block_hash]), -// [lastBurnBlock, secondToLastBurnBlock] -// ); - -// const { latestBlock, latestBlocksCount, clearLatestBlocks } = useBlockListWebSocket( -// initialBlockHashes, -// initialBurnBlockHashes -// ); - -// const updateList = useCallback( -// async function () { -// setIsUpdateListLoading(true); -// await Promise.all([ -// queryClient.invalidateQueries({ queryKey: ['getBlocksByBurnBlock'] }), // TODO: make these constants -// queryClient.invalidateQueries({ queryKey: ['burnBlocks'] }), -// ]); -// clearLatestBlocks(); -// setIsUpdateListLoading(false); -// }, -// [clearLatestBlocks, queryClient, setIsUpdateListLoading] -// ); - -// const prevLiveUpdatesRef = useRef(liveUpdates); -// const prevLatestBlocksCountRef = useRef(latestBlocksCount); - -// useEffect(() => { -// const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; - -// const receivedLatestBlockWhileLiveUpdates = -// liveUpdates && -// latestBlocksCount > 0 && -// prevLatestBlocksCountRef.current !== latestBlocksCount; - -// if (liveUpdatesToggled) { -// setIsUpdateListLoading(true); -// clearLatestBlocks(); -// updateList().then(() => { -// setIsUpdateListLoading(false); -// }); -// } else if (receivedLatestBlockWhileLiveUpdates && latestBlock) { -// // If latest block belongs to the last burn block, add it to the list, otherwise trigger an update. -// if (latestBlock.burn_block_height === lastBurnBlock.burn_block_height) { -// setIsUpdateListLoading(true); -// setTimeout(() => { -// lastBurnBlockStxBlocks.unshift(latestBlock); -// lastBurnBlock.stacks_blocks.unshift(latestBlock.hash); -// setIsUpdateListLoading(false); -// }, FADE_DURATION); -// } else { -// clearLatestBlocks(); -// void updateList(); -// } -// } - -// prevLiveUpdatesRef.current = liveUpdates; -// prevLatestBlocksCountRef.current = latestBlocksCount; -// }, [ -// liveUpdates, -// latestBlocksCount, -// clearLatestBlocks, -// updateList, -// setIsUpdateListLoading, -// latestBlock, -// lastBurnBlockStxBlocks, -// lastBurnBlock.stacks_blocks, -// lastBurnBlock.burn_block_height, -// ]); - -// let blockList = createUIBlockList(lastBurnBlock, lastBurnBlockStxBlocks, length); - -// if (blockList.length < length) { -// const secondToLastBlockList = createUIBlockList( -// secondToLastBurnBlock, -// secondToLastBlockStxBlocks, -// length - blockList.length -// ); -// blockList = blockList.concat(secondToLastBlockList); -// } - -// return { -// blockList, -// latestBlocksCount, -// updateList, -// }; -// } diff --git a/src/app/_components/BlockList/index.tsx b/src/app/_components/BlockList/index.tsx index 47dad6314..d686cc17e 100644 --- a/src/app/_components/BlockList/index.tsx +++ b/src/app/_components/BlockList/index.tsx @@ -1,31 +1,42 @@ 'use client'; -import { useColorModeValue } from '@chakra-ui/react'; -import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo, useState } from 'react'; - -import { connectWebSocketClient } from '@stacks/blockchain-api-client'; -import { Block } from '@stacks/stacks-blockchain-api-types'; - import { ListFooter } from '../../../common/components/ListFooter'; import { Section } from '../../../common/components/Section'; import { SkeletonBlockList } from '../../../common/components/loaders/skeleton-text'; -import { DEFAULT_LIST_LIMIT } from '../../../common/constants/constants'; -import { useGlobalContext } from '../../../common/context/useAppContext'; -import { useSuspenseInfiniteQueryResult } from '../../../common/hooks/useInfiniteQueryResult'; -import { useSuspenseBlockListInfinite } from '../../../common/queries/useBlockListInfinite'; import { Accordion } from '../../../ui/Accordion'; import { Box } from '../../../ui/Box'; -import { FlexProps } from '../../../ui/Flex'; +import { Flex, FlexProps } from '../../../ui/Flex'; import { FormControl } from '../../../ui/FormControl'; import { FormLabel } from '../../../ui/FormLabel'; import { Switch } from '../../../ui/Switch'; import { ExplorerErrorBoundary } from '../ErrorBoundary'; import { AnimatedBlockAndMicroblocksItem } from './AnimatedBlockAndMicroblocksItem'; import { BlockAndMicroblocksItem } from './BlockAndMicroblocksItem'; +import { BlocksGroup } from './GroupedByBurnBlock/BlocksGroup'; import { BlockListProvider } from './LayoutA/Provider'; import { useBlockList2 } from './LayoutA/useBlockList copy'; -import { EnhancedBlock } from './types'; +import { EnhancedBlock, UIBlockType, UISingleBlock } from './types'; + +function BtcBlock({ + burnBlock, + blockList, +}: { + burnBlock: UISingleBlock; + blockList: UISingleBlock[]; +}) { + return ( +
+ + + +
+ ); +} function BlocksListBase({ limit, @@ -33,116 +44,118 @@ function BlocksListBase({ limit?: number; } & FlexProps) { // const [isLive, setIsLive] = useState(false); + // const labelColor = useColorModeValue('slate.600', 'slate.400'); // TODO: get rid of this - const [initialBlocks, setInitialBlocks] = useState([]); - const [latestBlocks, setLatestBlocks] = useState([]); - const activeNetwork = useGlobalContext().activeNetwork; + // const [initialBlocks, setInitialBlocks] = useState([]); + // const [latestBlocks, setLatestBlocks] = useState([]); + // const activeNetwork = useGlobalContext().activeNetwork; - const response = useSuspenseBlockListInfinite(); // queryKey: ['blockListInfinite', limit] - const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; - const blocks = useSuspenseInfiniteQueryResult(response, limit); + // const response = useSuspenseBlockListInfinite(); // queryKey: ['blockListInfinite', limit] + // const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + // const blocks = useSuspenseInfiniteQueryResult(response, limit); - const queryClient = useQueryClient(); + // const queryClient = useQueryClient(); - console.log('BlockList/index', { blocks }); + // console.log('BlockList/index', { blocks }); const { - blocks: blocksFromUseBlockList, + blocks, + blocksGroupedByBtcBlock: blocksGroupedByParentHash, setIsGroupedByBtcBlock, isGroupedByBtcBlock, isLive, setIsLive, - } = useBlockList2(17); - console.log('BlockList/index', { blocksFromUseBlockList, isGroupedByBtcBlock, isLive }); - - const labelColor = useColorModeValue('slate.600', 'slate.400'); + removeOldBlock, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + } = useBlockList2(limit); + console.log('BlockList/index', { blocks, isGroupedByBtcBlock, isLive }); - useEffect(() => { - setInitialBlocks(blocks); - }, [blocks]); + - useEffect(() => { - if (!isLive) return; - void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); - let sub: { - unsubscribe?: () => Promise; - }; - const subscribe = async () => { - const client = await connectWebSocketClient(activeNetwork.url.replace('https://', 'wss://')); // TODO: Save this as ref so that when the live toggle is switched off, we can close the connection. Return subscribe and unsunscribe functions from the hook - sub = await client.subscribeBlocks((block: any) => { - setLatestBlocks(prevLatestBlocks => [ - { ...block, microblock_tx_count: {}, animate: true }, - ...prevLatestBlocks, - ]); - }); - }; - void subscribe(); - return () => { - if (sub?.unsubscribe) { - void sub.unsubscribe(); - } - }; - }, [activeNetwork.url, isLive, queryClient]); - - const allBlocks = useMemo(() => { - return [...latestBlocks, ...initialBlocks] - .sort((a, b) => (b.height || 0) - (a.height || 0)) - .reduce((acc: EnhancedBlock[], block, index) => { - if (!acc.some(b => b.height === block.height)) { - acc.push({ ...block, destroy: index >= (limit || DEFAULT_LIST_LIMIT) }); - } - return acc; - }, []); - }, [initialBlocks, latestBlocks, limit]); - - // whats happening here? - const removeOldBlock = useCallback((block: EnhancedBlock) => { - setInitialBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); - setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); - }, []); - - if (!allBlocks?.length) return ; + if ( + (isGroupedByBtcBlock && Object.keys(blocksGroupedByParentHash).length === 0) || + !blocks?.length + ) + return ; return (
- - Live Updates - - setIsLive(!isLive)} - /> - - Group by Bitcoin block - - setIsGroupedByBtcBlock(!isGroupedByBtcBlock)} - /> + + + + Group by Bitcoin block + + setIsGroupedByBtcBlock(!isGroupedByBtcBlock)} + /> + + + + Live Updates + + setIsLive(!isLive)} + /> + + } > - + - {allBlocks?.map(block => - isLive ? ( - removeOldBlock(block)} - /> - ) : ( - + {!isGroupedByBtcBlock ? ( + (blocks as EnhancedBlock[])?.map(block => + isLive ? ( + removeOldBlock(block)} + /> + ) : ( + + ) ) + ) : ( + + {Object.entries(blocksGroupedByParentHash).map(([burnBlockHeight, stxBlocks]) => { + const stxBlock = blocksGroupedByParentHash[burnBlockHeight][0]; + const burnBlock: UISingleBlock = { + type: UIBlockType.BurnBlock, + height: stxBlock.burn_block_height, + hash: stxBlock.burn_block_hash, + timestamp: stxBlock.burn_block_time, + }; + return ( + + ({ + type: UIBlockType.Block, + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + txsCount: block.txs.length, + }) as UISingleBlock + )} + /> + ); + })} + )} {!isLive && ( @@ -154,7 +167,7 @@ function BlocksListBase({ label={'blocks'} /> )} - +
); } From 1c4f039dd535ef13aeb612fb8ac411c53d7e5ee9 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 22 Mar 2024 19:04:57 -0500 Subject: [PATCH 06/70] feat(grouped-by-btc-block-data-fetching): work in progress --- src/app/_components/BlockList/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/_components/BlockList/index.tsx b/src/app/_components/BlockList/index.tsx index d686cc17e..eb879244b 100644 --- a/src/app/_components/BlockList/index.tsx +++ b/src/app/_components/BlockList/index.tsx @@ -68,7 +68,7 @@ function BlocksListBase({ isFetchingNextPage, fetchNextPage, hasNextPage, - } = useBlockList2(limit); + } = useBlockList2(); console.log('BlockList/index', { blocks, isGroupedByBtcBlock, isLive }); From f891390c1bd25d9cca3e412b6137b53534f5b87f Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 22 Mar 2024 19:20:43 -0500 Subject: [PATCH 07/70] feat(grouped-by-btc-block-data-fetching): work in progress --- .../GroupedByBurnBlock/BlocksGroup.tsx | 6 ++-- src/app/_components/BlockList/index.tsx | 34 +++++-------------- 2 files changed, 13 insertions(+), 27 deletions(-) diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx index be7e4c6be..73f12ca7a 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx @@ -21,6 +21,7 @@ import { UISingleBlock } from '../types'; interface BlocksGroupProps { burnBlock: UISingleBlock; stxBlocks: UISingleBlock[]; + index?: number; } const GroupHeader = () => { @@ -145,9 +146,9 @@ function ScrollableDiv({ children }: { children: ReactNode }) { ); } -export function BlocksGroup({ burnBlock, stxBlocks }: BlocksGroupProps) { +export function BlocksGroup({ burnBlock, stxBlocks, index }: BlocksGroupProps) { return ( - + @@ -159,6 +160,7 @@ export function BlocksGroup({ burnBlock, stxBlocks }: BlocksGroupProps) { {truncateMiddle(burnBlock.hash, 6)}
+ {index}
diff --git a/src/app/_components/BlockList/index.tsx b/src/app/_components/BlockList/index.tsx index eb879244b..7dffe2818 100644 --- a/src/app/_components/BlockList/index.tsx +++ b/src/app/_components/BlockList/index.tsx @@ -20,9 +20,11 @@ import { EnhancedBlock, UIBlockType, UISingleBlock } from './types'; function BtcBlock({ burnBlock, blockList, + index }: { burnBlock: UISingleBlock; blockList: UISingleBlock[]; + index?: number; }) { return (
@@ -30,6 +32,7 @@ function BtcBlock({ @@ -43,23 +46,9 @@ function BlocksListBase({ }: { limit?: number; } & FlexProps) { - // const [isLive, setIsLive] = useState(false); - // const labelColor = useColorModeValue('slate.600', 'slate.400'); // TODO: get rid of this - - // const [initialBlocks, setInitialBlocks] = useState([]); - // const [latestBlocks, setLatestBlocks] = useState([]); - // const activeNetwork = useGlobalContext().activeNetwork; - - // const response = useSuspenseBlockListInfinite(); // queryKey: ['blockListInfinite', limit] - // const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; - // const blocks = useSuspenseInfiniteQueryResult(response, limit); - - // const queryClient = useQueryClient(); - - // console.log('BlockList/index', { blocks }); const { blocks, - blocksGroupedByBtcBlock: blocksGroupedByParentHash, + blocksGroupedByBtcBlock, setIsGroupedByBtcBlock, isGroupedByBtcBlock, isLive, @@ -69,19 +58,13 @@ function BlocksListBase({ fetchNextPage, hasNextPage, } = useBlockList2(); - console.log('BlockList/index', { blocks, isGroupedByBtcBlock, isLive }); - - + console.log('BlockList/index', { blocks, blocksGroupedByBtcBlock, blocksGroupedByBtcBlockNum: Object.keys(blocksGroupedByBtcBlock).length, isGroupedByBtcBlock, isLive }); - if ( - (isGroupedByBtcBlock && Object.keys(blocksGroupedByParentHash).length === 0) || - !blocks?.length - ) + if ((isGroupedByBtcBlock && Object.keys(blocksGroupedByBtcBlock).length === 0) || !blocks?.length) return ; return (
- {Object.entries(blocksGroupedByParentHash).map(([burnBlockHeight, stxBlocks]) => { - const stxBlock = blocksGroupedByParentHash[burnBlockHeight][0]; + {Object.entries(blocksGroupedByBtcBlock).map(([burnBlockHeight, stxBlocks], index) => { + const stxBlock = blocksGroupedByBtcBlock[burnBlockHeight][0]; const burnBlock: UISingleBlock = { type: UIBlockType.BurnBlock, height: stxBlock.burn_block_height, @@ -141,6 +124,7 @@ function BlocksListBase({ return ( From 1ef130010d45feaf0d5ff88061ae73b72d797e46 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Tue, 26 Mar 2024 16:45:54 -0500 Subject: [PATCH 08/70] feat(blocklist): home+blocks page data fetching working --- src/app/PageClient.tsx | 6 +- .../GroupedByBurnBlock/BlocksGroup.tsx | 33 ++- .../BlocksPageBlockListGroupedByBtcBlock.tsx | 133 ++++++++++++ .../HomePageBlockListGroupedByBtcBlock.tsx | 114 ++++++++++ .../GroupedByBurnBlock/NonPaginated.tsx | 10 +- ...seBlockListGroupedByBtcBlockBlocksPage.tsx | 159 ++++++++++++++ .../useBlockListGroupedByBtcBlockHomePage.tsx | 176 +++++++++++++++ .../useBlockListWebSocket.tsx | 72 +++++++ ...seInitialBlockListGroupedByBtcHomePage.tsx | 41 ++++ .../_components/BlockList/LayoutA/Blocks.tsx | 8 +- .../BlockList/LayoutA/NonPaginated.tsx | 1 - .../BlockList/LayoutA/StxBlock.tsx | 2 + .../__tests__/BlockListWithControls.test.tsx | 6 +- .../BlockList/LayoutA/useBlockList copy.ts | 4 +- .../BlockList/LayoutA/useBlockList.ts | 9 +- .../LayoutA/useBlockListWebSocket.ts | 7 +- .../LayoutA/usePaginatedBlockList.ts | 5 +- src/app/_components/BlockList/index.tsx | 202 ++++++++---------- src/app/_components/BlockList/types.ts | 4 +- src/app/blocks/PageClient.tsx | 11 + src/common/queries/useBlockListInfinite.ts | 7 +- src/common/queries/useBlocksByBurnBlock.ts | 8 +- src/common/queries/useBurnBlocks.ts | 7 +- 23 files changed, 864 insertions(+), 161 deletions(-) create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx diff --git a/src/app/PageClient.tsx b/src/app/PageClient.tsx index bcc32b976..1264f48de 100644 --- a/src/app/PageClient.tsx +++ b/src/app/PageClient.tsx @@ -22,10 +22,10 @@ const NonPaginatedBlockListLayoutA = dynamic( } ); -const NonPaginatedBlockListGroupedByBurnBlock = dynamic( +const HomePageBlockListGroupedByBtcBlock = dynamic( () => - import('./_components/BlockList/GroupedByBurnBlock/NonPaginated').then( - mod => mod.NonPaginatedBlockListGroupedByBurnBlock + import('./_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock').then( + mod => mod.HomePageBlockListGroupedByBtcBlock ), { loading: () => , diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx index 73f12ca7a..745737383 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx @@ -16,13 +16,9 @@ import { Text } from '../../../../ui/Text'; import { BitcoinIcon, StxIcon } from '../../../../ui/icons'; import { Caption } from '../../../../ui/typography'; import { ListHeader } from '../../ListHeader'; +import { BlockCount } from '../LayoutA/BlockCount'; import { UISingleBlock } from '../types'; - -interface BlocksGroupProps { - burnBlock: UISingleBlock; - stxBlocks: UISingleBlock[]; - index?: number; -} +import { BurnBlock } from '../LayoutA/BurnBlock'; const GroupHeader = () => { const borderColor = useColorModeValue('slate.300', 'slate.800'); @@ -113,6 +109,7 @@ const BlockItem = ({ block, icon }: { block: UISingleBlock; icon?: ReactNode }) ); }; + function ScrollableDiv({ children }: { children: ReactNode }) { const [hasHorizontalScroll, setHasHorizontalScroll] = useState(false); const divRef = useRef(null); @@ -146,7 +143,25 @@ function ScrollableDiv({ children }: { children: ReactNode }) { ); } -export function BlocksGroup({ burnBlock, stxBlocks, index }: BlocksGroupProps) { +export interface BlocksGroupProps { + burnBlock: UISingleBlock; + stxBlocks: UISingleBlock[]; + stxBlocksDisplayLimit?: number; +} + +export function BlocksGroup({ + burnBlock, + stxBlocks, + stxBlocksDisplayLimit = stxBlocks.length, +}: BlocksGroupProps) { + const stxBlocksNotDisplayed = burnBlock.txsCount ? burnBlock.txsCount - (stxBlocksDisplayLimit || 0) : 0; + // console.log({ + // burnBlockHeight: burnBlock.height, + // burnBlock, + // stxBlocks, + // stxBlocksDisplayLimit, + // stxBlocksNotDisplayed + // }) return ( @@ -160,13 +175,12 @@ export function BlocksGroup({ burnBlock, stxBlocks, index }: BlocksGroupProps) { {truncateMiddle(burnBlock.hash, 6)} - {index} - {stxBlocks.map((stxBlock, i) => ( + {stxBlocks.slice(0, stxBlocksDisplayLimit).map((stxBlock, i) => ( <> + {stxBlocksNotDisplayed > 0 ? : null} ); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx new file mode 100644 index 000000000..46c37ab7e --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx @@ -0,0 +1,133 @@ +'use client'; + +import { ListFooter } from '@/common/components/ListFooter'; +import { useCallback, useRef } from 'react'; + +import { Section } from '../../../../common/components/Section'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { Controls } from '../Controls'; +import { BlockListProvider } from '../LayoutA/Provider'; +import { UpdateBar } from '../LayoutA/UpdateBar'; +import { useBlockListContext } from '../LayoutA/context'; +import { BlocksGroup } from './BlocksGroup'; +import { useBlockListGroupedByBtcBlockBlocksPage } from './useBlockListGroupedByBtcBlockBlocksPage'; + +function BlocksPageBlockListGroupedByBtcBlockBase() { + const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates, isUpdateListLoading } = + useBlockListContext(); + const { + blockList, + updateBlockList, + latestBlocksCount, + isFetchingNextPage, + hasNextPage, + fetchNextPage, + } = useBlockListGroupedByBtcBlockBlocksPage(10); + + // const blockList = [ + // { + // type: UIBlockType.StxBlock, + // height: 10001, + // hash: '0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + // }, + // { + // type: UIBlockType.StxBlock, + // height: 10002, + // hash: '0xrerqreqwjdhgjhdgj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + // }, + // { + // type: UIBlockType.StxBlock, + // height: 10003, + // hash: '0xbxvcbxvcbvxcbvxc0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + // }, + // { + // type: UIBlockType.StxBlock, + // height: 10004, + // hash: '0xjhjhfhgjhdjdhjhhj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + // }, + // ]; + + // const burnBlock = { + // height: 332141, + // hash: '0xhfgjdkhbafgkjhdafjkhdsafjkhflkjdsahfjkhdsafhdsafdsaf', + // timestamp: 0, + // }; + const lastClickTimeRef = useRef(0); + const toggleLiveUpdates = useCallback(() => { + const now = Date.now(); + if (now - lastClickTimeRef.current > 2000) { + lastClickTimeRef.current = now; + setLiveUpdates(!liveUpdates); + } + }, [liveUpdates, setLiveUpdates]); + + const enablePagination = true; + + return ( +
+ + { + setGroupedByBtc(!groupedByBtc); + }, + isChecked: groupedByBtc, + isDisabled: true, + }} + liveUpdates={{ + onChange: toggleLiveUpdates, + isChecked: liveUpdates, + }} + // horizontal={horizontalControls} + /> + {!liveUpdates && ( + + )} + + {blockList.map(block => ( + + ))} + + + {(!liveUpdates || !enablePagination) && ( + + )} + + +
+ ); +} + +export function BlocksPageBlockListGroupedByBtcBlock() { + return ( + + + + + + ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx new file mode 100644 index 000000000..cd9beb5d2 --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { useCallback, useRef } from 'react'; + +import { Section } from '../../../../common/components/Section'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { Controls } from '../Controls'; +import { BlockListProvider } from '../LayoutA/Provider'; +import { UpdateBar } from '../LayoutA/UpdateBar'; +import { useBlockListContext } from '../LayoutA/context'; +import { BlocksGroup } from './BlocksGroup'; +import { useBlockListGroupedByBtcBlockHomePage } from './useBlockListGroupedByBtcBlockHomePage'; + +// const LIST_LENGTH = 17; + +function HomePageBlockListGroupedByBtcBlockBase() { + const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates, isUpdateListLoading } = + useBlockListContext(); + const { blockList, updateBlockList, latestBlocksCount } = useBlockListGroupedByBtcBlockHomePage(); + + // const blockList = [ + // { + // type: UIBlockType.StxBlock, + // height: 10001, + // hash: '0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + // }, + // { + // type: UIBlockType.StxBlock, + // height: 10002, + // hash: '0xrerqreqwjdhgjhdgj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + // }, + // { + // type: UIBlockType.StxBlock, + // height: 10003, + // hash: '0xbxvcbxvcbvxcbvxc0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + // }, + // { + // type: UIBlockType.StxBlock, + // height: 10004, + // hash: '0xjhjhfhgjhdjdhjhhj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', + // }, + // ]; + + // const burnBlock = { + // height: 332141, + // hash: '0xhfgjdkhbafgkjhdafjkhdsafjkhflkjdsahfjkhdsafhdsafdsaf', + // timestamp: 0, + // }; + const lastClickTimeRef = useRef(0); + const toggleLiveUpdates = useCallback(() => { + const now = Date.now(); + if (now - lastClickTimeRef.current > 2000) { + lastClickTimeRef.current = now; + setLiveUpdates(!liveUpdates); + } + }, [liveUpdates, setLiveUpdates]); + return ( +
+ + { + setGroupedByBtc(!groupedByBtc); + }, + isChecked: groupedByBtc, + isDisabled: true, + }} + liveUpdates={{ + onChange: toggleLiveUpdates, + isChecked: liveUpdates, + }} + // horizontal={horizontalControls} + /> + {!liveUpdates && ( + + )} + + {blockList.map(block => ( + + ))} + + +
+ ); +} + +export function HomePageBlockListGroupedByBtcBlock() { + return ( + + + + + + ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx index badfd4d4e..9be910c64 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx @@ -10,26 +10,24 @@ import { BlocksGroup } from './BlocksGroup'; const LIST_LENGTH = 17; function NonPaginatedBlockListGroupedByBurnBlockBase() { - - const blockList = [ { - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: 10001, hash: '0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', }, { - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: 10002, hash: '0xrerqreqwjdhgjhdgj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', }, { - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: 10003, hash: '0xbxvcbxvcbvxcbvxc0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', }, { - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: 10004, hash: '0xjhjhfhgjhdjdhjhhj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', }, diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx new file mode 100644 index 000000000..fe60b5c0c --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx @@ -0,0 +1,159 @@ +import { GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY } from '@/common/queries/useBlocksByBurnBlock'; +import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocks'; +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; + +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'; +import { FADE_DURATION } from '../LayoutA/consts'; +import { useBlockListContext } from '../LayoutA/context'; +import { UIBlockType, UISingleBlock } from '../types'; +import { BlocksGroupProps } from './BlocksGroup'; +import { useBlockListWebSocket } from './useBlockListWebSocket'; + +const STX_BLOCK_LENGTH = 10; +const BURN_BLOCK_LENGTH = 10; + +export function useBlockListGroupedByBtcBlockBlocksPage(blockListLimit: number) { + const queryClient = useQueryClient(); + const { setIsUpdateListLoading: setIsBlockListUpdateLoading, liveUpdates: isLiveUpdateEnabled } = + useBlockListContext(); + + const response = useSuspenseBurnBlocks(BURN_BLOCK_LENGTH); + const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + const burnBlocks = useSuspenseInfiniteQueryResult(response); + + const latestBurnBlock = useMemo(() => burnBlocks[0], [burnBlocks]); + + const latestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(latestBurnBlock.burn_block_height, STX_BLOCK_LENGTH), + STX_BLOCK_LENGTH + ); + + const stxBlockHashes = useMemo( + () => new Set([...latestBurnBlockStxBlocks.map(block => block.hash)]), + [latestBurnBlockStxBlocks] + ); + const burnBlockHashes = useMemo( + () => + new Set([ + ...burnBlocks.map(block => block.burn_block_hash), + ]), + [burnBlocks] + ); + + const { + latestBlock: latestStxBlock, + latestBlocksCount: latestStxBlocksWaitingToBeLoaded, + clearLatestBlocks: clearLatestStxBlocksFromWebSocket, + } = useBlockListWebSocket(stxBlockHashes, burnBlockHashes); // TODO: fix this + + const updateBlockList = useCallback( + async function () { + setIsBlockListUpdateLoading(true); + await Promise.all([ + // invalidates queries so that they can be refetched + queryClient.invalidateQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), + queryClient.invalidateQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), + ]); + clearLatestStxBlocksFromWebSocket(); // clears blocks from socket connection + setIsBlockListUpdateLoading(false); + }, + [clearLatestStxBlocksFromWebSocket, queryClient, setIsBlockListUpdateLoading] + ); + + const prevIsLiveUpdateEnabledRef = useRef(isLiveUpdateEnabled); + const prevLatestBlocksCountRef = useRef(latestStxBlocksWaitingToBeLoaded); + + useEffect(() => { + const liveUpdatesJustToggled = prevIsLiveUpdateEnabledRef.current !== isLiveUpdateEnabled; + + // If live updates are enabled and oe or more new blocks have been received, verified by checking the latest block count and the previous latest block count, then + // add the latest block to the list of blocks + const receivedLatestBlockWhileLiveUpdates = + isLiveUpdateEnabled && + latestStxBlocksWaitingToBeLoaded > 0 && // data coming from the websocket + prevLatestBlocksCountRef.current !== latestStxBlocksWaitingToBeLoaded; + + // If live updates have just been toggled, then refetch/update the block list + if (liveUpdatesJustToggled) { + setIsBlockListUpdateLoading(true); + clearLatestStxBlocksFromWebSocket(); + updateBlockList().then(() => { + setIsBlockListUpdateLoading(false); + }); + } else if (receivedLatestBlockWhileLiveUpdates && latestStxBlock) { + // If latest stx block belongs to the latest burn block, add it to the latest burn block list of stx blocks + if (latestStxBlock.burn_block_height === latestBurnBlock.burn_block_height) { + setIsBlockListUpdateLoading(true); + setTimeout(() => { + latestBurnBlockStxBlocks.unshift(latestStxBlock); + latestBurnBlock.stacks_blocks.unshift(latestStxBlock.hash); + setIsBlockListUpdateLoading(false); + }, FADE_DURATION); + } else { + // Otherwise, we have a new burn block, and in this situation, adding a new burn block is the equivalent of refetching/updating the block list + clearLatestStxBlocksFromWebSocket(); + void updateBlockList(); + } + } + + prevIsLiveUpdateEnabledRef.current = isLiveUpdateEnabled; + prevLatestBlocksCountRef.current = latestStxBlocksWaitingToBeLoaded; + }, [ + latestStxBlock, + latestStxBlocksWaitingToBeLoaded, + isLiveUpdateEnabled, + latestBurnBlock, + latestBurnBlockStxBlocks, + latestBurnBlock.stacks_blocks, + latestBurnBlock.burn_block_height, + clearLatestStxBlocksFromWebSocket, + updateBlockList, + setIsBlockListUpdateLoading, + ]); + + const restOfBlockList: BlocksGroupProps[] = burnBlocks.slice(1).map(burnBlock => ({ + burnBlock: { + type: UIBlockType.BurnBlock, + height: burnBlock.burn_block_height, + hash: burnBlock.burn_block_hash, + timestamp: burnBlock.burn_block_time, + txsCount: burnBlock.stacks_blocks.length, + }, + stxBlocks: [] as UISingleBlock[], + stxBlocksDisplayLimit: 0, + })); + + const blockList: BlocksGroupProps[] = [ + { + burnBlock: { + type: UIBlockType.BurnBlock, + height: latestBurnBlock.burn_block_height, + hash: latestBurnBlock.burn_block_hash, + timestamp: latestBurnBlock.burn_block_time, + txsCount: latestBurnBlock.stacks_blocks.length, + }, + stxBlocks: latestBurnBlockStxBlocks.map(block => ({ + type: UIBlockType.StxBlock, + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + })), + stxBlocksDisplayLimit: blockListLimit, + }, + ...restOfBlockList, + ]; + + return { + blockList, + updateBlockList, + latestBlocksCount: latestStxBlocksWaitingToBeLoaded, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + }; +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx new file mode 100644 index 000000000..e2419d0c2 --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx @@ -0,0 +1,176 @@ +import { GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY } from '@/common/queries/useBlocksByBurnBlock'; +import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocks'; +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect, useMemo, useRef } from 'react'; + +import { FADE_DURATION } from '../LayoutA/consts'; +import { useBlockListContext } from '../LayoutA/context'; +import { UIBlockType } from '../types'; +import { BlocksGroupProps } from './BlocksGroup'; +import { useBlockListWebSocket } from './useBlockListWebSocket'; +import { useInitialBlockListGroupedByBtcBlockHomePage } from './useInitialBlockListGroupedByBtcHomePage'; + +export function useBlockListGroupedByBtcBlockHomePage() { + const queryClient = useQueryClient(); + const { setIsUpdateListLoading: setIsBlockListUpdateLoading, liveUpdates: isLiveUpdateEnabled } = + useBlockListContext(); + + const { + latestBurnBlock, + latestBurnBlockStxBlocks, + secondLatestBurnBlock, + secondLatestBurnBlockStxBlocks, + thirdLatestBurnBlock, + thirdLatestBurnBlockStxBlocks, + } = useInitialBlockListGroupedByBtcBlockHomePage(); + + const initialStxBlockHashes = useMemo( + () => + new Set([ + ...latestBurnBlockStxBlocks.map(block => block.hash), + ...secondLatestBurnBlockStxBlocks.map(block => block.hash), + ...thirdLatestBurnBlockStxBlocks.map(block => block.hash), + ]), + [latestBurnBlockStxBlocks, secondLatestBurnBlockStxBlocks, thirdLatestBurnBlockStxBlocks] + ); + const initialBurnBlockHashes = useMemo( + () => + new Set([ + latestBurnBlock.burn_block_hash, + secondLatestBurnBlock.burn_block_hash, + thirdLatestBurnBlock.burn_block_hash, + ]), + [latestBurnBlock, secondLatestBurnBlock, thirdLatestBurnBlock] + ); + + const { + latestBlock: latestStxBlock, + latestBlocksCount: latestStxBlocksWaitingToBeLoaded, + clearLatestBlocks: clearLatestStxBlocksFromWebSocket, + } = useBlockListWebSocket(initialStxBlockHashes, initialBurnBlockHashes); // TODO: fix this + + const updateBlockList = useCallback( + async function () { + setIsBlockListUpdateLoading(true); + await Promise.all([ + // invalidates queries so that they can be refetched + queryClient.invalidateQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), + queryClient.invalidateQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), + ]); + clearLatestStxBlocksFromWebSocket(); // clears blocks from socket connection + setIsBlockListUpdateLoading(false); + }, + [clearLatestStxBlocksFromWebSocket, queryClient, setIsBlockListUpdateLoading] + ); + + const prevIsLiveUpdateEnabledRef = useRef(isLiveUpdateEnabled); + const prevLatestBlocksCountRef = useRef(latestStxBlocksWaitingToBeLoaded); + + useEffect(() => { + const liveUpdatesJustToggled = prevIsLiveUpdateEnabledRef.current !== isLiveUpdateEnabled; + + // If live updates are enabled and oe or more new blocks have been received, verified by checking the latest block count and the previous latest block count, then + // add the latest block to the list of blocks + const receivedLatestBlockWhileLiveUpdates = + isLiveUpdateEnabled && + latestStxBlocksWaitingToBeLoaded > 0 && // data coming from the websocket + prevLatestBlocksCountRef.current !== latestStxBlocksWaitingToBeLoaded; + + // If live updates have just been toggled, then refetch/update the block list + if (liveUpdatesJustToggled) { + setIsBlockListUpdateLoading(true); + clearLatestStxBlocksFromWebSocket(); + updateBlockList().then(() => { + setIsBlockListUpdateLoading(false); + }); + } else if (receivedLatestBlockWhileLiveUpdates && latestStxBlock) { + // If latest stx block belongs to the latest burn block, add it to the latest burn block list of stx blocks + if (latestStxBlock.burn_block_height === latestBurnBlock.burn_block_height) { + setIsBlockListUpdateLoading(true); + setTimeout(() => { + // latestBurnBlockStxBlocks.pop(); + // latestBurnBlock.stacks_blocks.pop(); + latestBurnBlockStxBlocks.unshift(latestStxBlock); + latestBurnBlock.stacks_blocks.unshift(latestStxBlock.hash); + setIsBlockListUpdateLoading(false); + }, FADE_DURATION); + } else { + // Otherwise, we have a new burn block, and in this situation, adding a new burn block is the equivalent of refetching/updating the block list + clearLatestStxBlocksFromWebSocket(); + void updateBlockList(); + } + } + + prevIsLiveUpdateEnabledRef.current = isLiveUpdateEnabled; + prevLatestBlocksCountRef.current = latestStxBlocksWaitingToBeLoaded; + }, [ + latestStxBlock, + latestStxBlocksWaitingToBeLoaded, + isLiveUpdateEnabled, + latestBurnBlock, + latestBurnBlockStxBlocks, + latestBurnBlock.stacks_blocks, + latestBurnBlock.burn_block_height, + clearLatestStxBlocksFromWebSocket, + updateBlockList, + setIsBlockListUpdateLoading, + ]); + + // all btc block groups are rendered the same + const blockList: BlocksGroupProps[] = [ + { + burnBlock: { + type: UIBlockType.BurnBlock, + height: latestBurnBlock.burn_block_height, + hash: latestBurnBlock.burn_block_hash, + timestamp: latestBurnBlock.burn_block_time, + txsCount: latestBurnBlock.stacks_blocks.length, + }, + stxBlocks: latestBurnBlockStxBlocks.map(block => ({ + type: UIBlockType.StxBlock, + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + })), + stxBlocksDisplayLimit: 3, + }, + { + burnBlock: { + type: UIBlockType.BurnBlock, + height: secondLatestBurnBlock.burn_block_height, + hash: secondLatestBurnBlock.burn_block_hash, + timestamp: secondLatestBurnBlock.burn_block_time, + txsCount: secondLatestBurnBlock.stacks_blocks.length, + }, + stxBlocks: secondLatestBurnBlockStxBlocks.map(block => ({ + type: UIBlockType.StxBlock, + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + })), + stxBlocksDisplayLimit: 3, + }, + { + burnBlock: { + type: UIBlockType.BurnBlock, + height: thirdLatestBurnBlock.burn_block_height, + hash: thirdLatestBurnBlock.burn_block_hash, + timestamp: thirdLatestBurnBlock.burn_block_time, + txsCount: thirdLatestBurnBlock.stacks_blocks.length, + }, + stxBlocks: thirdLatestBurnBlockStxBlocks.map(block => ({ + type: UIBlockType.StxBlock, + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + })), + stxBlocksDisplayLimit: 3, + }, + ]; + + return { + blockList, + updateBlockList, + latestBlocksCount: latestStxBlocksWaitingToBeLoaded, + }; +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx new file mode 100644 index 000000000..0528d3e09 --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx @@ -0,0 +1,72 @@ +import { useCallback, useRef, useState } from 'react'; + +import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; + +import { UIBlockType, UISingleBlock } from '../types'; +import { useSubscribeBlocks } from '../useSubscribeBlocks'; + +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 the block is already in the list, don't add it again + if (latestBlockHashes.current.has(block.hash) || initialBlockHashes.has(block.hash)) { + return; + } + // Otherwise, add it to the list + 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.StxBlock, + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + txsCount: block.tx_count, + }, + ...prevLatestBlocks, + ]); + } + + updateLatestBlocks(); + }, + [initialBurnBlockHashes, initialBlockHashes] + ); + + useSubscribeBlocks(handleBlock); + + const clearLatestBlocks = () => { + setLatestBlocks([]); + }; + + return { + latestUIBlocks: latestBlocks, + latestBlock, + latestBlocksCount: latestBlocks.filter(block => block.type === UIBlockType.StxBlock).length, + clearLatestBlocks, + }; +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx new file mode 100644 index 000000000..06cdaeb3b --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx @@ -0,0 +1,41 @@ +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 = 3; +const STX_BLOCK_LENGTH = 3; + +export function useInitialBlockListGroupedByBtcBlockHomePage() { + const burnBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBurnBlocks(BURN_BLOCK_LENGTH), + BURN_BLOCK_LENGTH + ); + + const latestBurnBlock = burnBlocks[0]; + const secondLatestBurnBlock = burnBlocks[1]; + const thirdLatestBurnBlock = burnBlocks[2]; + + const latestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(latestBurnBlock.burn_block_height, STX_BLOCK_LENGTH), + STX_BLOCK_LENGTH + ); + const secondLatestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(secondLatestBurnBlock.burn_block_height, STX_BLOCK_LENGTH), + STX_BLOCK_LENGTH + ); + const thirdLatestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(thirdLatestBurnBlock.burn_block_height, STX_BLOCK_LENGTH), + STX_BLOCK_LENGTH + ); + + return { + latestBurnBlock, + latestBurnBlockStxBlocks, + secondLatestBurnBlock, + secondLatestBurnBlockStxBlocks, + thirdLatestBurnBlock, + thirdLatestBurnBlockStxBlocks, + }; +} diff --git a/src/app/_components/BlockList/LayoutA/Blocks.tsx b/src/app/_components/BlockList/LayoutA/Blocks.tsx index b7ce92a50..c505e34fa 100644 --- a/src/app/_components/BlockList/LayoutA/Blocks.tsx +++ b/src/app/_components/BlockList/LayoutA/Blocks.tsx @@ -1,5 +1,3 @@ -import React from 'react'; - import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; import { StxIcon } from '../../../../ui/icons'; @@ -29,9 +27,9 @@ export function Blocks({ > {blockList.map((block, i) => { switch (block.type) { - case UIBlockType.Block: + case UIBlockType.StxBlock: const isFirstStxBlockInBurnBlock = - i === 0 || (i > 0 && blockList[i - 1].type === UIBlockType.BurnBlock); + i === 0 || (i > 0 && blockList[i - 1].type === UIBlockType.BurnBlock); // what is this check for? - (i > 0 && blockList[i - 1].type === UIBlockType.BurnBlock. It's to make sure to skip Burn Blocks that dont have any stxx txs. Stacks tx should be first return ( ) : undefined } - hasBorder={i < blockList.length && blockList[i + 1].type === UIBlockType.Block} + hasBorder={i < blockList.length && blockList[i + 1].type === UIBlockType.StxBlock} /> ); case UIBlockType.BurnBlock: diff --git a/src/app/_components/BlockList/LayoutA/NonPaginated.tsx b/src/app/_components/BlockList/LayoutA/NonPaginated.tsx index b8d55f040..ee05ee7ed 100644 --- a/src/app/_components/BlockList/LayoutA/NonPaginated.tsx +++ b/src/app/_components/BlockList/LayoutA/NonPaginated.tsx @@ -10,7 +10,6 @@ const LIST_LENGTH = 17; function NonPaginatedBlockListLayoutABase() { const { blockList, updateList, latestBlocksCount } = useBlockList(LIST_LENGTH); - console.log({ blockList }); return ( { it('renders correctly', () => { const blockList: UIBlock[] = [ { - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: 1, hash: 'hash1', timestamp: date, txsCount: 5, }, { - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: 2, hash: 'hash2', timestamp: date, txsCount: 10, }, { - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: 3, hash: 'hash3', timestamp: date, diff --git a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts index 46755bfe5..d9e364631 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts @@ -22,7 +22,7 @@ const createBurnBlockUIBlock = (burnBlock: BurnBlock): UIBlock => ({ }); const createBlockUIBlock = (block: NakamotoBlock): UIBlock => ({ - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: block.height, hash: block.hash, timestamp: block.burn_block_time, @@ -152,7 +152,7 @@ export function useBlockList2(limit?: number): { } }; }, [isLive, activeNetwork.url, queryClient]); - + // const allBlocks = useMemo(() => { // return [...latestBlocks, ...initialBlocks] // .sort((a, b) => (b.height || 0) - (a.height || 0)) diff --git a/src/app/_components/BlockList/LayoutA/useBlockList.ts b/src/app/_components/BlockList/LayoutA/useBlockList.ts index b50474458..739b74122 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList.ts @@ -18,7 +18,7 @@ const createBurnBlockUIBlock = (burnBlock: BurnBlock): UIBlock => ({ }); const createBlockUIBlock = (block: NakamotoBlock): UIBlock => ({ - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: block.height, hash: block.hash, timestamp: block.burn_block_time, @@ -134,17 +134,18 @@ export function useBlockList(length: number) { }, [ liveUpdates, latestBlocksCount, - clearLatestBlocks, - updateList, - setIsUpdateListLoading, latestBlock, lastBurnBlockStxBlocks, lastBurnBlock.stacks_blocks, lastBurnBlock.burn_block_height, + clearLatestBlocks, + updateList, + setIsUpdateListLoading, ]); let blockList = createUIBlockList(lastBurnBlock, lastBurnBlockStxBlocks, length); + // If the list is not long enough, give the secondLatestBurnBlock and its associated stx blocks the same UI treatment as the latestBurnBlock if (blockList.length < length) { const secondToLastBlockList = createUIBlockList( secondToLastBurnBlock, diff --git a/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts index 8ff0e5668..0528d3e09 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts @@ -17,11 +17,14 @@ export function useBlockListWebSocket( const handleBlock = useCallback( (block: NakamotoBlock) => { function updateLatestBlocks() { + // If the block is already in the list, don't add it again if (latestBlockHashes.current.has(block.hash) || initialBlockHashes.has(block.hash)) { return; } + // Otherwise, add it to the list setLatestBlock(block); latestBlockHashes.current.add(block.hash); + const isNewBurnBlock = !initialBurnBlockHashes.has(block.burn_block_hash) && !latestBurnBlockHashes.current.has(block.burn_block_hash); @@ -39,7 +42,7 @@ export function useBlockListWebSocket( } setLatestBlocks(prevLatestBlocks => [ { - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: block.height, hash: block.hash, timestamp: block.burn_block_time, @@ -63,7 +66,7 @@ export function useBlockListWebSocket( return { latestUIBlocks: latestBlocks, latestBlock, - latestBlocksCount: latestBlocks.filter(block => block.type === UIBlockType.Block).length, + latestBlocksCount: latestBlocks.filter(block => block.type === UIBlockType.StxBlock).length, clearLatestBlocks, }; } diff --git a/src/app/_components/BlockList/LayoutA/usePaginatedBlockList.ts b/src/app/_components/BlockList/LayoutA/usePaginatedBlockList.ts index 3785880ce..0cbbeed7d 100644 --- a/src/app/_components/BlockList/LayoutA/usePaginatedBlockList.ts +++ b/src/app/_components/BlockList/LayoutA/usePaginatedBlockList.ts @@ -1,12 +1,11 @@ import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useMemo, useState } from 'react'; +import { useCallback, useMemo } 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(); @@ -41,7 +40,7 @@ export function usePaginatedBlockList() { acc[block.burn_block_hash] = []; } acc[block.burn_block_hash].push({ - type: UIBlockType.Block, + type: UIBlockType.StxBlock, height: block.height, hash: block.hash, timestamp: block.burn_block_time, diff --git a/src/app/_components/BlockList/index.tsx b/src/app/_components/BlockList/index.tsx index 7dffe2818..c49783359 100644 --- a/src/app/_components/BlockList/index.tsx +++ b/src/app/_components/BlockList/index.tsx @@ -1,145 +1,125 @@ 'use client'; +import { useColorModeValue } from '@chakra-ui/react'; +import { useQueryClient } from '@tanstack/react-query'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; + +import { connectWebSocketClient } from '@stacks/blockchain-api-client'; +import { Block } from '@stacks/stacks-blockchain-api-types'; + import { ListFooter } from '../../../common/components/ListFooter'; import { Section } from '../../../common/components/Section'; import { SkeletonBlockList } from '../../../common/components/loaders/skeleton-text'; +import { DEFAULT_LIST_LIMIT } from '../../../common/constants/constants'; +import { useGlobalContext } from '../../../common/context/useAppContext'; +import { useSuspenseInfiniteQueryResult } from '../../../common/hooks/useInfiniteQueryResult'; +import { useSuspenseBlockListInfinite } from '../../../common/queries/useBlockListInfinite'; import { Accordion } from '../../../ui/Accordion'; import { Box } from '../../../ui/Box'; -import { Flex, FlexProps } from '../../../ui/Flex'; +import { FlexProps } from '../../../ui/Flex'; import { FormControl } from '../../../ui/FormControl'; import { FormLabel } from '../../../ui/FormLabel'; import { Switch } from '../../../ui/Switch'; import { ExplorerErrorBoundary } from '../ErrorBoundary'; import { AnimatedBlockAndMicroblocksItem } from './AnimatedBlockAndMicroblocksItem'; import { BlockAndMicroblocksItem } from './BlockAndMicroblocksItem'; -import { BlocksGroup } from './GroupedByBurnBlock/BlocksGroup'; -import { BlockListProvider } from './LayoutA/Provider'; -import { useBlockList2 } from './LayoutA/useBlockList copy'; -import { EnhancedBlock, UIBlockType, UISingleBlock } from './types'; - -function BtcBlock({ - burnBlock, - blockList, - index -}: { - burnBlock: UISingleBlock; - blockList: UISingleBlock[]; - index?: number; -}) { - return ( -
- - - -
- ); -} +import { EnhancedBlock } from './types'; function BlocksListBase({ limit, }: { limit?: number; } & FlexProps) { - const { - blocks, - blocksGroupedByBtcBlock, - setIsGroupedByBtcBlock, - isGroupedByBtcBlock, - isLive, - setIsLive, - removeOldBlock, - isFetchingNextPage, - fetchNextPage, - hasNextPage, - } = useBlockList2(); - console.log('BlockList/index', { blocks, blocksGroupedByBtcBlock, blocksGroupedByBtcBlockNum: Object.keys(blocksGroupedByBtcBlock).length, isGroupedByBtcBlock, isLive }); + const [isLive, setIsLive] = React.useState(false); + const [initialBlocks, setInitialBlocks] = useState([]); + const [latestBlocks, setLatestBlocks] = useState([]); + const activeNetwork = useGlobalContext().activeNetwork; + const response = useSuspenseBlockListInfinite(); + const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + const queryClient = useQueryClient(); + + const blocks = useSuspenseInfiniteQueryResult(response, limit); + + const labelColor = useColorModeValue('slate.600', 'slate.400'); - if ((isGroupedByBtcBlock && Object.keys(blocksGroupedByBtcBlock).length === 0) || !blocks?.length) - return ; + useEffect(() => { + setInitialBlocks(blocks); + }, [blocks]); + + useEffect(() => { + if (!isLive) return; + void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); + let sub: { + unsubscribe?: () => Promise; + }; + const subscribe = async () => { + const client = await connectWebSocketClient(activeNetwork.url.replace('https://', 'wss://')); + sub = await client.subscribeBlocks((block: any) => { + setLatestBlocks(prevLatestBlocks => [ + { ...block, microblock_tx_count: {}, animate: true }, + ...prevLatestBlocks, + ]); + }); + }; + void subscribe(); + return () => { + if (sub?.unsubscribe) { + void sub.unsubscribe(); + } + }; + }, [activeNetwork.url, isLive, queryClient]); + + const allBlocks = useMemo(() => { + return [...latestBlocks, ...initialBlocks] + .sort((a, b) => (b.height || 0) - (a.height || 0)) + .reduce((acc: EnhancedBlock[], block, index) => { + if (!acc.some(b => b.height === block.height)) { + acc.push({ ...block, destroy: index >= (limit || DEFAULT_LIST_LIMIT) }); + } + return acc; + }, []); + }, [initialBlocks, latestBlocks, limit]); + + const removeOldBlock = useCallback((block: EnhancedBlock) => { + setInitialBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); + setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); + }, []); + + if (!allBlocks?.length) return ; return (
- - - - Group by Bitcoin block - - setIsGroupedByBtcBlock(!isGroupedByBtcBlock)} - /> - - - - Live Updates - - setIsLive(!isLive)} - /> - - + + live view + + setIsLive(!isLive)} + /> } > - + - {!isGroupedByBtcBlock ? ( - (blocks as EnhancedBlock[])?.map(block => - isLive ? ( - removeOldBlock(block)} - /> - ) : ( - - ) + {allBlocks?.map(block => + isLive ? ( + removeOldBlock(block)} + /> + ) : ( + ) - ) : ( - - {Object.entries(blocksGroupedByBtcBlock).map(([burnBlockHeight, stxBlocks], index) => { - const stxBlock = blocksGroupedByBtcBlock[burnBlockHeight][0]; - const burnBlock: UISingleBlock = { - type: UIBlockType.BurnBlock, - height: stxBlock.burn_block_height, - hash: stxBlock.burn_block_hash, - timestamp: stxBlock.burn_block_time, - }; - return ( - - ({ - type: UIBlockType.Block, - height: block.height, - hash: block.hash, - timestamp: block.burn_block_time, - txsCount: block.txs.length, - }) as UISingleBlock - )} - /> - ); - })} - )} {!isLive && ( @@ -151,7 +131,7 @@ function BlocksListBase({ label={'blocks'} /> )} - +
); } @@ -168,9 +148,7 @@ export function BlocksList({ limit }: { limit?: number }) { }} tryAgainButton > - - - + ); } diff --git a/src/app/_components/BlockList/types.ts b/src/app/_components/BlockList/types.ts index 69f30d17b..1195f6ea6 100644 --- a/src/app/_components/BlockList/types.ts +++ b/src/app/_components/BlockList/types.ts @@ -3,13 +3,13 @@ import { Block } from '@stacks/stacks-blockchain-api-types'; export type EnhancedBlock = Block & { destroy?: boolean; animate?: boolean }; export enum UIBlockType { - Block = 'block', + StxBlock = 'stx-block', BurnBlock = 'burn_block', Count = 'count', } export interface UISingleBlock { - type: UIBlockType.Block | UIBlockType.BurnBlock; + type: UIBlockType.StxBlock | UIBlockType.BurnBlock; height: number | string; hash: string; timestamp: number; diff --git a/src/app/blocks/PageClient.tsx b/src/app/blocks/PageClient.tsx index 9cac4d86f..374213de5 100644 --- a/src/app/blocks/PageClient.tsx +++ b/src/app/blocks/PageClient.tsx @@ -32,6 +32,17 @@ const NonPaginatedBlockListGroupedByBurnBlock = dynamic( } ); +const BlocksPageBlockListGroupedByBtcBlock = dynamic( + () => + import('../_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock').then( + mod => mod.BlocksPageBlockListGroupedByBtcBlock + ), + { + loading: () => , + ssr: false, + } +); + const BlocksPage: NextPage = () => { const { activeNetworkKey } = useGlobalContext(); return ( diff --git a/src/common/queries/useBlockListInfinite.ts b/src/common/queries/useBlockListInfinite.ts index 4fa49877e..7244e2e3b 100644 --- a/src/common/queries/useBlockListInfinite.ts +++ b/src/common/queries/useBlockListInfinite.ts @@ -5,10 +5,13 @@ import { DEFAULT_LIST_LIMIT } from '../constants/constants'; import { getNextPageParam } from '../utils/utils'; import { TWO_MINUTES } from './query-stale-time'; + +export const BLOCK_LIST_QUERY_KEY = 'blockListInfinite' + export const useBlockListInfinite = () => { const api = useApi(); return useInfiniteQuery({ - queryKey: ['blockListInfinite'], + queryKey: [BLOCK_LIST_QUERY_KEY], queryFn: ({ pageParam }: { pageParam: number }) => api.blocksApi.getBlockList({ limit: DEFAULT_LIST_LIMIT, @@ -23,7 +26,7 @@ export const useBlockListInfinite = () => { export const useSuspenseBlockListInfinite = (limit = DEFAULT_LIST_LIMIT) => { const api = useApi(); return useSuspenseInfiniteQuery({ - queryKey: ['blockListInfinite', limit], + queryKey: [BLOCK_LIST_QUERY_KEY, limit], queryFn: ({ pageParam }: { pageParam: number }) => api.blocksApi.getBlockList({ limit, diff --git a/src/common/queries/useBlocksByBurnBlock.ts b/src/common/queries/useBlocksByBurnBlock.ts index 0bb6efd92..549173029 100644 --- a/src/common/queries/useBlocksByBurnBlock.ts +++ b/src/common/queries/useBlocksByBurnBlock.ts @@ -6,15 +6,15 @@ import { useSuspenseInfiniteQuery, } from '@tanstack/react-query'; -import { BurnBlock } from '@stacks/blockchain-api-client'; import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; import { useApi } from '../api/useApi'; -import { DEFAULT_BURN_BLOCKS_LIMIT } from '../constants/constants'; import { GenericResponseType } from '../hooks/useInfiniteQueryResult'; import { getNextPageParam } from '../utils/utils'; import { ONE_SECOND, TWO_MINUTES } from './query-stale-time'; +export const GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY = 'getBlocksByBurnBlock'; + export function useBlocksByBurnBlock( heightOrHash: string | number, limit: number, @@ -22,7 +22,7 @@ export function useBlocksByBurnBlock( ): UseInfiniteQueryResult>> { const api = useApi(); return useInfiniteQuery({ - queryKey: ['getBlocksByBurnBlock', heightOrHash], + queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY, heightOrHash], queryFn: ({ pageParam }: { pageParam: number }) => api.blocksApi.getBlocksByBurnBlock({ heightOrHash, @@ -43,7 +43,7 @@ export function useSuspenseBlocksByBurnBlock( ): UseSuspenseInfiniteQueryResult>> { const api = useApi(); return useSuspenseInfiniteQuery({ - queryKey: ['getBlocksByBurnBlock', heightOrHash], + queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY, heightOrHash], queryFn: ({ pageParam }: { pageParam: number }) => api.blocksApi.getBlocksByBurnBlock({ heightOrHash, diff --git a/src/common/queries/useBurnBlocks.ts b/src/common/queries/useBurnBlocks.ts index 5bc1af720..b56cd7fa3 100644 --- a/src/common/queries/useBurnBlocks.ts +++ b/src/common/queries/useBurnBlocks.ts @@ -5,7 +5,6 @@ import { useInfiniteQuery, useSuspenseInfiniteQuery, } from '@tanstack/react-query'; -import { address } from 'bitcoinjs-lib'; import { BurnBlock } from '@stacks/blockchain-api-client'; @@ -15,12 +14,14 @@ import { GenericResponseType } from '../hooks/useInfiniteQueryResult'; import { getNextPageParam } from '../utils/utils'; import { TWO_MINUTES } from './query-stale-time'; +export const BURN_BLOCKS_QUERY_KEY = 'burnBlocks'; + export function useBurnBlocks( options: any = {} ): UseInfiniteQueryResult>> { const api = useApi(); return useInfiniteQuery({ - queryKey: ['burnBlocks'], + queryKey: [BURN_BLOCKS_QUERY_KEY], queryFn: ({ pageParam }: { pageParam: number }) => api.burnBlocksApi.getBurnBlocks({ limit: DEFAULT_BURN_BLOCKS_LIMIT, @@ -39,7 +40,7 @@ export function useSuspenseBurnBlocks( ): UseSuspenseInfiniteQueryResult>> { const api = useApi(); return useSuspenseInfiniteQuery({ - queryKey: ['burnBlocks'], + queryKey: [BURN_BLOCKS_QUERY_KEY], queryFn: ({ pageParam }: { pageParam: number }) => api.burnBlocksApi.getBurnBlocks({ limit, From 2bbf0188d683d91b3cd98e334707ad69bb619ebe Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Wed, 27 Mar 2024 12:50:40 -0500 Subject: [PATCH 09/70] feat(grouped-by-btc-block-list-view-2): work in progress --- .../BlocksPageBlockListGroupedByBtcBlock.tsx | 31 +------ .../GroupedByBurnBlock/BlocksPageHeaders.tsx | 85 +++++++++++++++++++ .../HomePageBlockListGroupedByBtcBlock.tsx | 28 ------ 3 files changed, 87 insertions(+), 57 deletions(-) create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx index 46c37ab7e..0da639e85 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx @@ -12,6 +12,7 @@ import { BlockListProvider } from '../LayoutA/Provider'; import { UpdateBar } from '../LayoutA/UpdateBar'; import { useBlockListContext } from '../LayoutA/context'; import { BlocksGroup } from './BlocksGroup'; +import { BlocksPageHeaders } from './BlocksPageHeaders'; import { useBlockListGroupedByBtcBlockBlocksPage } from './useBlockListGroupedByBtcBlockBlocksPage'; function BlocksPageBlockListGroupedByBtcBlockBase() { @@ -26,34 +27,6 @@ function BlocksPageBlockListGroupedByBtcBlockBase() { fetchNextPage, } = useBlockListGroupedByBtcBlockBlocksPage(10); - // const blockList = [ - // { - // type: UIBlockType.StxBlock, - // height: 10001, - // hash: '0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - // }, - // { - // type: UIBlockType.StxBlock, - // height: 10002, - // hash: '0xrerqreqwjdhgjhdgj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - // }, - // { - // type: UIBlockType.StxBlock, - // height: 10003, - // hash: '0xbxvcbxvcbvxcbvxc0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - // }, - // { - // type: UIBlockType.StxBlock, - // height: 10004, - // hash: '0xjhjhfhgjhdjdhjhhj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - // }, - // ]; - - // const burnBlock = { - // height: 332141, - // hash: '0xhfgjdkhbafgkjhdafjkhdsafjkhflkjdsahfjkhdsafhdsafdsaf', - // timestamp: 0, - // }; const lastClickTimeRef = useRef(0); const toggleLiveUpdates = useCallback(() => { const now = Date.now(); @@ -118,7 +91,6 @@ export function BlocksPageBlockListGroupedByBtcBlock() { + diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx new file mode 100644 index 000000000..6a4e53590 --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { Card } from '../../../../common/components/Card'; +import { Stack } from '../../../../ui/Stack'; +import { Text } from '../../../../ui/Text'; +import { Flex } from '../../../../ui/Flex'; +import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocks'; + +function LastBlockCard() { +const lastBlock = useSuspenseBurnBlocks(1) + + return ( + + + LAST BLOCK + + + some number + + + Some more info + + + ); +} + +function AverageStacksBlockTimeCard() { + return ( + + Nothing yet + + ); +} + +function NextStackingCycleCard() { + return ( + + Nothing yet + + ); +} + +export function BlocksPageHeaderLayout({ + lastBlockCard, + averageStacksBlockTimeCard, + nextStackingCycleCard, +}: { + title: ReactNode; + lastBlockCard: ReactNode; + averageStacksBlockTimeCard: ReactNode; + nextStackingCycleCard: ReactNode; +}) { + return ( + *:not(:last-of-type)': { + borderBottom: ['1px solid #000', null, null, 'none'], // Apply bottom border on smaller screens + borderRight: [null, null, null, '1px solid #000'], // Apply right border on larger screens + }, + '& > *:last-of-type': { + borderBottom: 'none', // Ensure the last item has no bottom border + borderRight: 'none', // Ensure the last item has no right border + }, + }} + > + {lastBlockCard} + {averageStacksBlockTimeCard} + {nextStackingCycleCard} + + ); +} + +export function BlocksPageHeaders() { + return ( + } + averageStacksBlockTimeCard={} + nextStackingCycleCard={} + /> + ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx index cd9beb5d2..fec73f4c4 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx @@ -20,34 +20,6 @@ function HomePageBlockListGroupedByBtcBlockBase() { useBlockListContext(); const { blockList, updateBlockList, latestBlocksCount } = useBlockListGroupedByBtcBlockHomePage(); - // const blockList = [ - // { - // type: UIBlockType.StxBlock, - // height: 10001, - // hash: '0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - // }, - // { - // type: UIBlockType.StxBlock, - // height: 10002, - // hash: '0xrerqreqwjdhgjhdgj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - // }, - // { - // type: UIBlockType.StxBlock, - // height: 10003, - // hash: '0xbxvcbxvcbvxcbvxc0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - // }, - // { - // type: UIBlockType.StxBlock, - // height: 10004, - // hash: '0xjhjhfhgjhdjdhjhhj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - // }, - // ]; - - // const burnBlock = { - // height: 332141, - // hash: '0xhfgjdkhbafgkjhdafjkhdsafjkhflkjdsahfjkhdsafhdsafdsaf', - // timestamp: 0, - // }; const lastClickTimeRef = useRef(0); const toggleLiveUpdates = useCallback(() => { const now = Date.now(); From 17909206a5a2fd0fc1cf6f31468e914ec217829f Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Wed, 27 Mar 2024 13:09:49 -0500 Subject: [PATCH 10/70] feat(grouped-by-btc-block-list-view-2): finished headers ui --- .../GroupedByBurnBlock/BlocksPageHeaders.tsx | 90 ++++++++++++++----- 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx index 6a4e53590..68589a6af 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx @@ -1,24 +1,43 @@ 'use client'; +import { ReactNode } from 'react'; + import { Card } from '../../../../common/components/Card'; +import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocks'; +import { Flex } from '../../../../ui/Flex'; +import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; import { Text } from '../../../../ui/Text'; -import { Flex } from '../../../../ui/Flex'; -import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocks'; +import { BitcoinIcon } from '../../../../ui/icons/BitcoinIcon'; function LastBlockCard() { -const lastBlock = useSuspenseBurnBlocks(1) + const lastBlock = useSuspenseBurnBlocks(1); // TODO: might need to use web socket here return ( LAST BLOCK - - some number - - - Some more info + + + #124009 + + + + + #889300 + + + + + 435 transactions ); @@ -26,29 +45,56 @@ const lastBlock = useSuspenseBurnBlocks(1) function AverageStacksBlockTimeCard() { return ( - - Nothing yet - + + + AVERAGE STACKS BLOCK TIME + + + 48 sec. + + + In the last 24hs. + + ); } -function NextStackingCycleCard() { +function LastConfirmedBitcoinBlockCard() { return ( - - Nothing yet - + + + IN THE LAST CONFIRMED BITCOIN BLOCK + + + + + 214 + + + Stacks blocks + + + + + 4354 + + + Stacks transactions + + + + ); } export function BlocksPageHeaderLayout({ lastBlockCard, averageStacksBlockTimeCard, - nextStackingCycleCard, + lastConfirmedBitcoinBlockCard, }: { - title: ReactNode; lastBlockCard: ReactNode; averageStacksBlockTimeCard: ReactNode; - nextStackingCycleCard: ReactNode; + lastConfirmedBitcoinBlockCard: ReactNode; }) { return ( *:not(:last-of-type)': { - borderBottom: ['1px solid #000', null, null, 'none'], // Apply bottom border on smaller screens - borderRight: [null, null, null, '1px solid #000'], // Apply right border on larger screens + borderBottom: ['1px solid var(--stacks-colors-borderPrimary)', null, null, 'none'], // Apply bottom border on smaller screens + borderRight: [null, null, null, '1px solid var(--stacks-colors-borderPrimary)'], // Apply right border on larger screens }, '& > *:last-of-type': { borderBottom: 'none', // Ensure the last item has no bottom border @@ -69,7 +115,7 @@ export function BlocksPageHeaderLayout({ > {lastBlockCard} {averageStacksBlockTimeCard} - {nextStackingCycleCard} + {lastConfirmedBitcoinBlockCard} ); } @@ -79,7 +125,7 @@ export function BlocksPageHeaders() { } averageStacksBlockTimeCard={} - nextStackingCycleCard={} + lastConfirmedBitcoinBlockCard={} /> ); } From 52d3004e5e9dcd0e9dad46b565f5d2b13867bc6d Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Thu, 28 Mar 2024 12:32:49 -0500 Subject: [PATCH 11/70] feat(grouped-by-btc-block-list-view-2): converting grid into table --- .../BlocksPageBlockListGroupedByBtcBlock.tsx | 103 ++++++++++-------- .../GroupedByBurnBlock/BlocksPageHeaders.tsx | 10 +- .../{BlocksGroup.tsx => BurnBlockGroup.tsx} | 78 +++++++------ .../HomePageBlockListGroupedByBtcBlock.tsx | 13 ++- .../GroupedByBurnBlock/NonPaginated.tsx | 4 +- ...seBlockListGroupedByBtcBlockBlocksPage.tsx | 86 +++++++-------- .../useBlockListGroupedByBtcBlockHomePage.tsx | 4 +- .../BlockList/LayoutA/BlockCount.tsx | 2 +- .../LayoutA/BlockListWithControls.tsx | 4 +- .../BlockList/LayoutA/Paginated.tsx | 2 +- .../BlockList/LayoutA/Provider.tsx | 4 +- .../_components/BlockList/LayoutA/context.ts | 4 +- .../BlockList/LayoutA/useBlockList.ts | 2 +- src/common/queries/useBurnBlocks.ts | 7 +- 14 files changed, 178 insertions(+), 145 deletions(-) rename src/app/_components/BlockList/GroupedByBurnBlock/{BlocksGroup.tsx => BurnBlockGroup.tsx} (76%) diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx index 0da639e85..3d26ea073 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx @@ -1,23 +1,31 @@ 'use client'; import { ListFooter } from '@/common/components/ListFooter'; -import { useCallback, useRef } from 'react'; +import { Suspense, useCallback, useRef } from 'react'; import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; +import { Text } from '../../../../ui/Text'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; import { Controls } from '../Controls'; import { BlockListProvider } from '../LayoutA/Provider'; import { UpdateBar } from '../LayoutA/UpdateBar'; +import { FADE_DURATION } from '../LayoutA/consts'; +// TODO: move somewhere else import { useBlockListContext } from '../LayoutA/context'; -import { BlocksGroup } from './BlocksGroup'; import { BlocksPageHeaders } from './BlocksPageHeaders'; +import { BurnBlockGroup } from './BurnBlockGroup'; import { useBlockListGroupedByBtcBlockBlocksPage } from './useBlockListGroupedByBtcBlockBlocksPage'; function BlocksPageBlockListGroupedByBtcBlockBase() { - const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates, isUpdateListLoading } = - useBlockListContext(); + const { + groupedByBtc, + setGroupedByBtc, + liveUpdates, + setLiveUpdates, + isBlockListLoading: isUpdateListLoading, + } = useBlockListContext(); const { blockList, updateBlockList, @@ -40,53 +48,60 @@ function BlocksPageBlockListGroupedByBtcBlockBase() { return (
- - { - setGroupedByBtc(!groupedByBtc); - }, - isChecked: groupedByBtc, - isDisabled: true, - }} - liveUpdates={{ - onChange: toggleLiveUpdates, - isChecked: liveUpdates, - }} - // horizontal={horizontalControls} + { + setGroupedByBtc(!groupedByBtc); + }, + isChecked: groupedByBtc, + isDisabled: true, + }} + liveUpdates={{ + onChange: toggleLiveUpdates, + isChecked: liveUpdates, + }} + horizontal={true} + /> + {!liveUpdates && ( + - {!liveUpdates && ( - + {blockList.map(block => ( + + ))} + + + {(!liveUpdates || !enablePagination) && ( + )} - - {blockList.map(block => ( - - ))} - - - {(!liveUpdates || !enablePagination) && ( - - )} -
); } export function BlocksPageBlockListGroupedByBtcBlock() { + // TODO: fix the suspense fallback return ( - + loading...}> + + ); diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx index 68589a6af..d0a212d20 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx @@ -11,7 +11,7 @@ import { Text } from '../../../../ui/Text'; import { BitcoinIcon } from '../../../../ui/icons/BitcoinIcon'; function LastBlockCard() { - const lastBlock = useSuspenseBurnBlocks(1); // TODO: might need to use web socket here + // const lastBlock = useSuspenseBurnBlocks(1); return ( @@ -104,12 +104,12 @@ export function BlocksPageHeaderLayout({ gridTemplateColumns={['100%', '100%', '100%', 'repeat(3, 33.33%)']} sx={{ '& > *:not(:last-of-type)': { - borderBottom: ['1px solid var(--stacks-colors-borderPrimary)', null, null, 'none'], // Apply bottom border on smaller screens - borderRight: [null, null, null, '1px solid var(--stacks-colors-borderPrimary)'], // Apply right border on larger screens + borderBottom: ['1px solid var(--stacks-colors-borderPrimary)', null, null, 'none'], + borderRight: [null, null, null, '1px solid var(--stacks-colors-borderPrimary)'], }, '& > *:last-of-type': { - borderBottom: 'none', // Ensure the last item has no bottom border - borderRight: 'none', // Ensure the last item has no right border + borderBottom: 'none', + borderRight: 'none', }, }} > diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx similarity index 76% rename from src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx rename to src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx index 745737383..0291a6597 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksGroup.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx @@ -1,5 +1,4 @@ import { useColorModeValue } from '@chakra-ui/react'; -import { hash } from '@noble/hashes/_assert'; import { ReactNode, useEffect, useRef, useState } from 'react'; import { PiArrowElbowLeftDown } from 'react-icons/pi'; @@ -12,27 +11,44 @@ import { Flex } from '../../../../ui/Flex'; import { Grid } from '../../../../ui/Grid'; import { HStack } from '../../../../ui/HStack'; import { Icon } from '../../../../ui/Icon'; -import { Text } from '../../../../ui/Text'; +import { Text, TextProps } from '../../../../ui/Text'; import { BitcoinIcon, StxIcon } from '../../../../ui/icons'; import { Caption } from '../../../../ui/typography'; -import { ListHeader } from '../../ListHeader'; import { BlockCount } from '../LayoutA/BlockCount'; import { UISingleBlock } from '../types'; -import { BurnBlock } from '../LayoutA/BurnBlock'; -const GroupHeader = () => { - const borderColor = useColorModeValue('slate.300', 'slate.800'); +export function ListHeader({ children, ...textProps }: { children: ReactNode } & TextProps) { + const color = useColorModeValue('slate.700', 'slate.250'); + return ( + + {children} + + ); +} + +const GroupHeaderSkeleton = () => {}; +const GroupHeader = () => { return ( <> { bottom: 0, width: '2px', height: 'var(--stacks-sizes-14)', - backgroundColor: borderColor, + backgroundColor: 'borderPrimary', }, }} > @@ -59,23 +75,21 @@ const GroupHeader = () => { ); }; -const BlockItem = ({ block, icon }: { block: UISingleBlock; icon?: ReactNode }) => { - const textColor = useColorModeValue('slate.900', 'slate.50'); - const secondaryTextColor = useColorModeValue('slate.700', 'slate.600'); - const borderColor = useColorModeValue('slate.300', 'slate.800'); +const StxBlockRow = ({ block, icon }: { block: UISingleBlock; icon?: ReactNode }) => { return ( <> {icon} - - + + #{block.height} - - + + {block.hash} - + 100 @@ -109,7 +123,7 @@ const BlockItem = ({ block, icon }: { block: UISingleBlock; icon?: ReactNode }) ); }; - +// adds horizontal scrolling to its children if they overflow the container's width, and adds a class to the container when it has a horizontal scrollbar function ScrollableDiv({ children }: { children: ReactNode }) { const [hasHorizontalScroll, setHasHorizontalScroll] = useState(false); const divRef = useRef(null); @@ -149,19 +163,18 @@ export interface BlocksGroupProps { stxBlocksDisplayLimit?: number; } -export function BlocksGroup({ +export function BlocksGroupSkeleton() {} + +export function BurnBlockGroup({ burnBlock, stxBlocks, stxBlocksDisplayLimit = stxBlocks.length, }: BlocksGroupProps) { - const stxBlocksNotDisplayed = burnBlock.txsCount ? burnBlock.txsCount - (stxBlocksDisplayLimit || 0) : 0; - // console.log({ - // burnBlockHeight: burnBlock.height, - // burnBlock, - // stxBlocks, - // stxBlocksDisplayLimit, - // stxBlocksNotDisplayed - // }) + const stxBlocksNotDisplayed = burnBlock.txsCount + ? burnBlock.txsCount - (stxBlocksDisplayLimit || 0) + : 0; + console.log({ burnBlock, stxBlocks, stxBlocksDisplayLimit, stxBlocksNotDisplayed }); // TODO: remove + // TODO: why are we not using table here? return ( @@ -182,7 +195,7 @@ export function BlocksGroup({ {stxBlocks.slice(0, stxBlocksDisplayLimit).map((stxBlock, i) => ( <> - ) : ( - - {i < stxBlocks.length - 1 && } + {i < stxBlocks.length - 1 && }{' '} + {/* TODO: adds a border to the bottom. make this css */} ))} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx index fec73f4c4..ff5bee30f 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx @@ -10,14 +10,19 @@ import { Controls } from '../Controls'; import { BlockListProvider } from '../LayoutA/Provider'; import { UpdateBar } from '../LayoutA/UpdateBar'; import { useBlockListContext } from '../LayoutA/context'; -import { BlocksGroup } from './BlocksGroup'; +import { BurnBlockGroup } from './BurnBlockGroup'; import { useBlockListGroupedByBtcBlockHomePage } from './useBlockListGroupedByBtcBlockHomePage'; // const LIST_LENGTH = 17; function HomePageBlockListGroupedByBtcBlockBase() { - const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates, isUpdateListLoading } = - useBlockListContext(); + const { + groupedByBtc, + setGroupedByBtc, + liveUpdates, + setLiveUpdates, + isBlockListLoading: isUpdateListLoading, + } = useBlockListContext(); const { blockList, updateBlockList, latestBlocksCount } = useBlockListGroupedByBtcBlockHomePage(); const lastClickTimeRef = useRef(0); @@ -54,7 +59,7 @@ function HomePageBlockListGroupedByBtcBlockBase() { )} {blockList.map(block => ( - - (response); @@ -38,82 +37,77 @@ export function useBlockListGroupedByBtcBlockBlocksPage(blockListLimit: number) [latestBurnBlockStxBlocks] ); const burnBlockHashes = useMemo( - () => - new Set([ - ...burnBlocks.map(block => block.burn_block_hash), - ]), + () => new Set([...burnBlocks.map(block => block.burn_block_hash)]), [burnBlocks] ); const { - latestBlock: latestStxBlock, - latestBlocksCount: latestStxBlocksWaitingToBeLoaded, + latestBlock: latestStxBlockFromWebSocket, + latestBlocksCount: latestStxBlocksCountFromWebSocket, clearLatestBlocks: clearLatestStxBlocksFromWebSocket, } = useBlockListWebSocket(stxBlockHashes, burnBlockHashes); // TODO: fix this const updateBlockList = useCallback( async function () { - setIsBlockListUpdateLoading(true); + setBlockListLoading(true); await Promise.all([ - // invalidates queries so that they can be refetched + // Invalidates queries so they will be refetched queryClient.invalidateQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), queryClient.invalidateQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), ]); - clearLatestStxBlocksFromWebSocket(); // clears blocks from socket connection - setIsBlockListUpdateLoading(false); + clearLatestStxBlocksFromWebSocket(); // clears updates since we are get the latest data from refetching the queries + setBlockListLoading(false); }, - [clearLatestStxBlocksFromWebSocket, queryClient, setIsBlockListUpdateLoading] + [clearLatestStxBlocksFromWebSocket, queryClient, setBlockListLoading] ); - const prevIsLiveUpdateEnabledRef = useRef(isLiveUpdateEnabled); - const prevLatestBlocksCountRef = useRef(latestStxBlocksWaitingToBeLoaded); + const prevIsLiveUpdateEnabledRef = useRef(isLiveUpdatesEnabled); + const prevLatestStxBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); useEffect(() => { - const liveUpdatesJustToggled = prevIsLiveUpdateEnabledRef.current !== isLiveUpdateEnabled; + const liveUpdatesToggled = prevIsLiveUpdateEnabledRef.current !== isLiveUpdatesEnabled; - // If live updates are enabled and oe or more new blocks have been received, verified by checking the latest block count and the previous latest block count, then + // If live updates are enabled and one or more new blocks have been received, verified by checking the latest block count and the previous latest block count, then // add the latest block to the list of blocks - const receivedLatestBlockWhileLiveUpdates = - isLiveUpdateEnabled && - latestStxBlocksWaitingToBeLoaded > 0 && // data coming from the websocket - prevLatestBlocksCountRef.current !== latestStxBlocksWaitingToBeLoaded; - - // If live updates have just been toggled, then refetch/update the block list - if (liveUpdatesJustToggled) { - setIsBlockListUpdateLoading(true); - clearLatestStxBlocksFromWebSocket(); + const receivedLatestStxBlockFromLiveUpdates = + isLiveUpdatesEnabled && + latestStxBlocksCountFromWebSocket > 0 && // stx blocks data coming from the websocket + prevLatestStxBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; + + // If live updates have been toggled, then refetch/update the block list + if (liveUpdatesToggled) { + setBlockListLoading(true); // TODO: can I remove the setBlockListLoading(true) and setBlockListLoading(false) from here? since it's already in the updateBlockList function updateBlockList().then(() => { - setIsBlockListUpdateLoading(false); + setBlockListLoading(false); }); - } else if (receivedLatestBlockWhileLiveUpdates && latestStxBlock) { + } else if (receivedLatestStxBlockFromLiveUpdates && latestStxBlockFromWebSocket) { // If latest stx block belongs to the latest burn block, add it to the latest burn block list of stx blocks - if (latestStxBlock.burn_block_height === latestBurnBlock.burn_block_height) { - setIsBlockListUpdateLoading(true); + if (latestStxBlockFromWebSocket.burn_block_height === latestBurnBlock.burn_block_height) { + setBlockListLoading(true); setTimeout(() => { - latestBurnBlockStxBlocks.unshift(latestStxBlock); - latestBurnBlock.stacks_blocks.unshift(latestStxBlock.hash); - setIsBlockListUpdateLoading(false); + latestBurnBlockStxBlocks.unshift(latestStxBlockFromWebSocket); + latestBurnBlock.stacks_blocks.unshift(latestStxBlockFromWebSocket.hash); + setBlockListLoading(false); }, FADE_DURATION); } else { - // Otherwise, we have a new burn block, and in this situation, adding a new burn block is the equivalent of refetching/updating the block list - clearLatestStxBlocksFromWebSocket(); - void updateBlockList(); + // Otherwise, we have a new burn block, in which case, adding a new burn block is the equivalent of refetching/updating the block list + updateBlockList(); } } - prevIsLiveUpdateEnabledRef.current = isLiveUpdateEnabled; - prevLatestBlocksCountRef.current = latestStxBlocksWaitingToBeLoaded; + prevIsLiveUpdateEnabledRef.current = isLiveUpdatesEnabled; + prevLatestStxBlocksCountRef.current = latestStxBlocksCountFromWebSocket; }, [ - latestStxBlock, - latestStxBlocksWaitingToBeLoaded, - isLiveUpdateEnabled, + latestStxBlockFromWebSocket, + latestStxBlocksCountFromWebSocket, + isLiveUpdatesEnabled, latestBurnBlock, latestBurnBlockStxBlocks, latestBurnBlock.stacks_blocks, latestBurnBlock.burn_block_height, clearLatestStxBlocksFromWebSocket, updateBlockList, - setIsBlockListUpdateLoading, + setBlockListLoading, ]); const restOfBlockList: BlocksGroupProps[] = burnBlocks.slice(1).map(burnBlock => ({ @@ -141,7 +135,7 @@ export function useBlockListGroupedByBtcBlockBlocksPage(blockListLimit: number) type: UIBlockType.StxBlock, height: block.height, hash: block.hash, - timestamp: block.burn_block_time, + timestamp: block.burn_block_time, // block?.block_time TODO: this is the right timestamp to use, but it seems to be inaccurate })), stxBlocksDisplayLimit: blockListLimit, }, @@ -151,7 +145,7 @@ export function useBlockListGroupedByBtcBlockBlocksPage(blockListLimit: number) return { blockList, updateBlockList, - latestBlocksCount: latestStxBlocksWaitingToBeLoaded, + latestBlocksCount: latestStxBlocksCountFromWebSocket, isFetchingNextPage, fetchNextPage, hasNextPage, diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx index e2419d0c2..ec472aa87 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx @@ -6,13 +6,13 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { FADE_DURATION } from '../LayoutA/consts'; import { useBlockListContext } from '../LayoutA/context'; import { UIBlockType } from '../types'; -import { BlocksGroupProps } from './BlocksGroup'; +import { BlocksGroupProps } from './BurnBlockGroup'; import { useBlockListWebSocket } from './useBlockListWebSocket'; import { useInitialBlockListGroupedByBtcBlockHomePage } from './useInitialBlockListGroupedByBtcHomePage'; export function useBlockListGroupedByBtcBlockHomePage() { const queryClient = useQueryClient(); - const { setIsUpdateListLoading: setIsBlockListUpdateLoading, liveUpdates: isLiveUpdateEnabled } = + const { setBlockListLoading: setIsBlockListUpdateLoading, liveUpdates: isLiveUpdateEnabled } = useBlockListContext(); const { diff --git a/src/app/_components/BlockList/LayoutA/BlockCount.tsx b/src/app/_components/BlockList/LayoutA/BlockCount.tsx index 055fb19db..332cdbda0 100644 --- a/src/app/_components/BlockList/LayoutA/BlockCount.tsx +++ b/src/app/_components/BlockList/LayoutA/BlockCount.tsx @@ -16,7 +16,7 @@ export const BlockCount = memo(function ({ count }: { count: number }) { const iconColor = useColorModeValue('purple.600', 'purple.200'); const circleColor = useColorModeValue('white', 'black'); return ( - + ([]); const { diff --git a/src/app/_components/BlockList/LayoutA/Provider.tsx b/src/app/_components/BlockList/LayoutA/Provider.tsx index 58401740a..e4191580c 100644 --- a/src/app/_components/BlockList/LayoutA/Provider.tsx +++ b/src/app/_components/BlockList/LayoutA/Provider.tsx @@ -10,8 +10,8 @@ export function BlockListProvider({ children }: { children: ReactNode }) { return ( >; + isBlockListLoading: boolean; + setBlockListLoading: Dispatch>; groupedByBtc: boolean; setGroupedByBtc: Dispatch>; liveUpdates: boolean; diff --git a/src/app/_components/BlockList/LayoutA/useBlockList.ts b/src/app/_components/BlockList/LayoutA/useBlockList.ts index 739b74122..db1d3fc87 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList.ts @@ -55,7 +55,7 @@ const createUIBlockList = ( export function useBlockList(length: number) { const queryClient = useQueryClient(); - const { setIsUpdateListLoading, liveUpdates } = useBlockListContext(); + const { setBlockListLoading: setIsUpdateListLoading, liveUpdates } = useBlockListContext(); const { lastBurnBlock, diff --git a/src/common/queries/useBurnBlocks.ts b/src/common/queries/useBurnBlocks.ts index b56cd7fa3..de91fdb8f 100644 --- a/src/common/queries/useBurnBlocks.ts +++ b/src/common/queries/useBurnBlocks.ts @@ -36,11 +36,14 @@ export function useBurnBlocks( export function useSuspenseBurnBlocks( limit = DEFAULT_BURN_BLOCKS_LIMIT, - options: any = {} + options: any = {}, + queryKeyExtension?: string ): UseSuspenseInfiniteQueryResult>> { const api = useApi(); return useSuspenseInfiniteQuery({ - queryKey: [BURN_BLOCKS_QUERY_KEY], + queryKey: queryKeyExtension + ? [BURN_BLOCKS_QUERY_KEY, queryKeyExtension] + : [BURN_BLOCKS_QUERY_KEY], queryFn: ({ pageParam }: { pageParam: number }) => api.burnBlocksApi.getBurnBlocks({ limit, From 9497de3f0c4eb84c53d246dcf0d15b29fb01d857 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Mon, 1 Apr 2024 10:25:21 -0500 Subject: [PATCH 12/70] feat(grouped-by-btc-block-list-view-3): work in progress --- .../BlocksPageBlockListGroupedByBtcBlock.tsx | 11 ++- .../GroupedByBurnBlock/BlocksPageHeaders.tsx | 28 ++++++- .../GroupedByBurnBlock/BurnBlockGroup.tsx | 23 ++++++ .../HomePageBlockListGroupedByBtcBlock.tsx | 37 +++++---- .../BlockList/GroupedByBurnBlock/skeleton.tsx | 79 +++++++++++++++++++ src/app/blocks/PageClient.tsx | 3 +- src/common/components/Section.tsx | 5 +- 7 files changed, 161 insertions(+), 25 deletions(-) create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx index 3d26ea073..d2c08e24e 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx @@ -6,7 +6,6 @@ import { Suspense, useCallback, useRef } from 'react'; import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; -import { Text } from '../../../../ui/Text'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; import { Controls } from '../Controls'; import { BlockListProvider } from '../LayoutA/Provider'; @@ -14,8 +13,9 @@ import { UpdateBar } from '../LayoutA/UpdateBar'; import { FADE_DURATION } from '../LayoutA/consts'; // TODO: move somewhere else import { useBlockListContext } from '../LayoutA/context'; -import { BlocksPageHeaders } from './BlocksPageHeaders'; +import { BlockPageHeadersSkeleton, BlocksPageHeaders } from './BlocksPageHeaders'; import { BurnBlockGroup } from './BurnBlockGroup'; +import { BlocksPageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; import { useBlockListGroupedByBtcBlockBlocksPage } from './useBlockListGroupedByBtcBlockBlocksPage'; function BlocksPageBlockListGroupedByBtcBlockBase() { @@ -113,8 +113,11 @@ export function BlocksPageBlockListGroupedByBtcBlock() { tryAgainButton > - - loading...}> + }> + + + + }> diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx index d0a212d20..cc0efd6ac 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx @@ -3,9 +3,9 @@ import { ReactNode } from 'react'; import { Card } from '../../../../common/components/Card'; -import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocks'; import { Flex } from '../../../../ui/Flex'; import { Icon } from '../../../../ui/Icon'; +import { SkeletonText } from '../../../../ui/SkeletonText'; import { Stack } from '../../../../ui/Stack'; import { Text } from '../../../../ui/Text'; import { BitcoinIcon } from '../../../../ui/icons/BitcoinIcon'; @@ -87,6 +87,22 @@ function LastConfirmedBitcoinBlockCard() { ); } +export function BlockPageHeaderSkeleton() { + return ( + + + + + + + + + + + + ); +} + export function BlocksPageHeaderLayout({ lastBlockCard, averageStacksBlockTimeCard, @@ -129,3 +145,13 @@ export function BlocksPageHeaders() { /> ); } + +export function BlockPageHeadersSkeleton() { + return ( + } + averageStacksBlockTimeCard={} + lastConfirmedBitcoinBlockCard={} + /> + ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx index 0291a6597..28be7980c 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx @@ -173,6 +173,15 @@ export function BurnBlockGroup({ const stxBlocksNotDisplayed = burnBlock.txsCount ? burnBlock.txsCount - (stxBlocksDisplayLimit || 0) : 0; + const txSum = stxBlocks.reduce((txSum, stxBlock) => { + const txsCount = stxBlock?.txsCount ?? 0; + return txSum + txsCount; + }, 0); + // const totalTime = stxBlocks.reduce((totalTime, stxBlock) => { + // const blockTime = stxBlock.timestamp ?? 0; + // return totalTime + blockTime; + // }, 0); + // const averageBlockTime = stxBlocks.length ? Math.floor(totalTime / stxBlocks.length) : 0; console.log({ burnBlock, stxBlocks, stxBlocksDisplayLimit, stxBlocksNotDisplayed }); // TODO: remove // TODO: why are we not using table here? return ( @@ -244,6 +253,20 @@ export function BurnBlockGroup({ {stxBlocksNotDisplayed > 0 ? : null} + + + ∙} gap={1} pt={4}> + + {stxBlocks.length} blocks + + + {txSum} transactions + + + Average block time: 29 sec. + + + ); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx index ff5bee30f..96c9ba4de 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx @@ -1,10 +1,12 @@ 'use client'; +import { Stack } from '@/ui/Stack'; import { useCallback, useRef } from 'react'; import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; +import { Text } from '../../../../ui/Text'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; import { Controls } from '../Controls'; import { BlockListProvider } from '../LayoutA/Provider'; @@ -34,22 +36,25 @@ function HomePageBlockListGroupedByBtcBlockBase() { } }, [liveUpdates, setLiveUpdates]); return ( -
- - { - setGroupedByBtc(!groupedByBtc); - }, - isChecked: groupedByBtc, - isDisabled: true, - }} - liveUpdates={{ - onChange: toggleLiveUpdates, - isChecked: liveUpdates, - }} - // horizontal={horizontalControls} - /> +
+ + + Recent Blocks + { + setGroupedByBtc(!groupedByBtc); + }, + isChecked: groupedByBtc, + isDisabled: true, + }} + liveUpdates={{ + onChange: toggleLiveUpdates, + isChecked: liveUpdates, + }} + // horizontal={horizontalControls} + /> + {!liveUpdates && ( + + + {Array.from({ length: 4 }).map(() => ( + + + + ))} + {Array.from({ length: 10 }).map(() => + Array.from({ length: 4 }).map(() => ) + )} + + + + + + ); +} + +export function BurnBlockGroupWithoutTransactionsSkeleton() { + return ( + + + + {Array.from({ length: 4 }).map(() => ( + + + + ))} + + + + + + ); +} + +export function BurnBlockGroupListSkeleton() { + return ( + + + {Array.from({ length: 9 }).map(() => ( + + ))} + + ); +} + +export function BlocksPageBlockListGroupedByBtcBlockSkeleton() { + return ( +
}> + + +
+ ); +} diff --git a/src/app/blocks/PageClient.tsx b/src/app/blocks/PageClient.tsx index 374213de5..994b1afb5 100644 --- a/src/app/blocks/PageClient.tsx +++ b/src/app/blocks/PageClient.tsx @@ -6,6 +6,7 @@ import dynamic from 'next/dynamic'; import { SkeletonBlockList } from '../../common/components/loaders/skeleton-text'; import { useGlobalContext } from '../../common/context/useAppContext'; import { PageTitle } from '../_components/PageTitle'; +import { BlocksPageBlockListGroupedByBtcBlockSkeleton } from '../_components/BlockList/GroupedByBurnBlock/skeleton'; const BlocksList = dynamic(() => import('../_components/BlockList').then(mod => mod.BlocksList), { loading: () => , @@ -38,7 +39,7 @@ const BlocksPageBlockListGroupedByBtcBlock = dynamic( mod => mod.BlocksPageBlockListGroupedByBtcBlock ), { - loading: () => , + loading: () => , ssr: false, } ); diff --git a/src/common/components/Section.tsx b/src/common/components/Section.tsx index 427dc12ea..536f8e99d 100644 --- a/src/common/components/Section.tsx +++ b/src/common/components/Section.tsx @@ -21,8 +21,7 @@ export function Section({ px = 6, ...rest }: SectionProps) { - const titleColor = useColorModeValue('slate.900', 'white'); - const borderColor = useColorModeValue('slate.150', 'slate.900'); + const titleColor = useColorModeValue('slate.900', 'white'); // TODO: remove return ( {title || TopRight ? ( @@ -30,7 +29,7 @@ export function Section({ alignItems={['flex-start', 'center']} justifyContent="space-between" borderBottom="1px" - borderColor={borderColor} + borderColor='borderSecondary' borderTopRightRadius="xl" borderTopLeftRadius="xl" flexShrink={0} From 0592fc48e309f29c69b3225a6503ae79f023b7c7 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Mon, 1 Apr 2024 13:09:19 -0500 Subject: [PATCH 13/70] feat(grouped-by-btc-block-list-view-3): work in progress --- src/app/_components/BlockList/Controls.tsx | 9 +- .../BlocksPageBlockListGroupedByBtcBlock.tsx | 6 +- .../GroupedByBurnBlock/HomePageBlockList.tsx | 113 ++++++++++++++++++ .../HomePageBlockListGroupedByBtcBlock.tsx | 47 +++++--- .../BlockList/GroupedByBurnBlock/skeleton.tsx | 48 ++++++-- .../BlockList/LayoutA/UpdateBar.tsx | 32 ++--- src/ui/theme/componentTheme/Button.ts | 12 ++ src/ui/theme/theme.ts | 4 + 8 files changed, 225 insertions(+), 46 deletions(-) create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockList.tsx diff --git a/src/app/_components/BlockList/Controls.tsx b/src/app/_components/BlockList/Controls.tsx index ed8caa18c..34e87673b 100644 --- a/src/app/_components/BlockList/Controls.tsx +++ b/src/app/_components/BlockList/Controls.tsx @@ -1,18 +1,16 @@ -import React from 'react'; - import { Flex } from '../../../ui/Flex'; import { FormControl } from '../../../ui/FormControl'; import { FormLabel } from '../../../ui/FormLabel'; -import { Stack } from '../../../ui/Stack'; +import { Stack, StackProps } from '../../../ui/Stack'; import { Switch, SwitchProps } from '../../../ui/Switch'; -interface ControlsProps { +interface ControlsProps extends StackProps { groupByBtc: SwitchProps; liveUpdates: SwitchProps; horizontal?: boolean; } -export function Controls({ groupByBtc, liveUpdates, horizontal }: ControlsProps) { +export function Controls({ groupByBtc, liveUpdates, horizontal, ...rest }: ControlsProps) { return ( <> diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx index d2c08e24e..37af4c14a 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx @@ -24,7 +24,7 @@ function BlocksPageBlockListGroupedByBtcBlockBase() { setGroupedByBtc, liveUpdates, setLiveUpdates, - isBlockListLoading: isUpdateListLoading, + isBlockListLoading, } = useBlockListContext(); const { blockList, @@ -64,7 +64,7 @@ function BlocksPageBlockListGroupedByBtcBlockBase() { /> {!liveUpdates && ( @@ -75,7 +75,7 @@ function BlocksPageBlockListGroupedByBtcBlockBase() { pt={4} style={{ transition: `opacity ${FADE_DURATION / 1000}s`, - opacity: isUpdateListLoading ? 0 : 1, + opacity: isBlockListLoading ? 0 : 1, }} > {blockList.map(block => ( diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockList.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockList.tsx new file mode 100644 index 000000000..36507bcf0 --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockList.tsx @@ -0,0 +1,113 @@ +'use client'; + +import { Stack } from '@/ui/Stack'; +import { Suspense, useCallback, useRef } from 'react'; + +import { Section } from '../../../../common/components/Section'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { Text } from '../../../../ui/Text'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { Controls } from '../Controls'; +import { BlockListProvider } from '../LayoutA/Provider'; +import { UpdateBar } from '../LayoutA/UpdateBar'; +import { FADE_DURATION } from '../LayoutA/consts'; +import { useBlockListContext } from '../LayoutA/context'; +import { BurnBlockGroup } from './BurnBlockGroup'; +import { useBlockListGroupedByBtcBlockHomePage } from './useBlockListGroupedByBtcBlockHomePage'; +import { HomePageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; + +// const LIST_LENGTH = 17; + +function HomePageBlockListGroupedByBtcBlockBase() { + const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates, isBlockListLoading } = + useBlockListContext(); + const { blockList, updateBlockList, latestBlocksCount } = useBlockListGroupedByBtcBlockHomePage(); + + const lastClickTimeRef = useRef(0); + const toggleLiveUpdates = useCallback(() => { + const now = Date.now(); + if (now - lastClickTimeRef.current > 2000) { + lastClickTimeRef.current = now; + setLiveUpdates(!liveUpdates); + } + }, [liveUpdates, setLiveUpdates]); + return ( +
+ + + Recent Blocks + { + setGroupedByBtc(!groupedByBtc); + }, + isChecked: groupedByBtc, + // isDisabled: true, + }} + liveUpdates={{ + onChange: toggleLiveUpdates, + isChecked: liveUpdates, + }} + padding={0} + gap={3} + marginX={0} + border="none" + /> + + {!liveUpdates && ( + + )} + + {blockList.map(block => ( + + ))} + + +
+ ); +} + +export function HomePageBlockListGroupedByBtcBlock() { + return ( + + + }> + + + + + ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx index 96c9ba4de..36507bcf0 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx @@ -1,7 +1,7 @@ 'use client'; import { Stack } from '@/ui/Stack'; -import { useCallback, useRef } from 'react'; +import { Suspense, useCallback, useRef } from 'react'; import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; @@ -11,20 +11,17 @@ import { ExplorerErrorBoundary } from '../../ErrorBoundary'; import { Controls } from '../Controls'; import { BlockListProvider } from '../LayoutA/Provider'; import { UpdateBar } from '../LayoutA/UpdateBar'; +import { FADE_DURATION } from '../LayoutA/consts'; import { useBlockListContext } from '../LayoutA/context'; import { BurnBlockGroup } from './BurnBlockGroup'; import { useBlockListGroupedByBtcBlockHomePage } from './useBlockListGroupedByBtcBlockHomePage'; +import { HomePageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; // const LIST_LENGTH = 17; function HomePageBlockListGroupedByBtcBlockBase() { - const { - groupedByBtc, - setGroupedByBtc, - liveUpdates, - setLiveUpdates, - isBlockListLoading: isUpdateListLoading, - } = useBlockListContext(); + const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates, isBlockListLoading } = + useBlockListContext(); const { blockList, updateBlockList, latestBlocksCount } = useBlockListGroupedByBtcBlockHomePage(); const lastClickTimeRef = useRef(0); @@ -36,9 +33,14 @@ function HomePageBlockListGroupedByBtcBlockBase() { } }, [liveUpdates, setLiveUpdates]); return ( -
+
- + Recent Blocks {!liveUpdates && ( )} - + {blockList.map(block => ( - + }> + + ); diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx index e515ec986..d69a9e8fe 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx @@ -5,7 +5,7 @@ import { Grid } from '../../../../ui/Grid'; import { SkeletonText } from '../../../../ui/SkeletonText'; import { BlockPageHeadersSkeleton } from './BlocksPageHeaders'; -export function BurnBlockGroupWithTransactionsSkeleton() { +export function BurnBlockGroupWithTransactionsSkeleton({ numTxs }: { numTxs: number }) { return ( @@ -22,7 +22,7 @@ export function BurnBlockGroupWithTransactionsSkeleton() { ))} - {Array.from({ length: 10 }).map(() => + {Array.from({ length: numTxs }).map(() => Array.from({ length: 4 }).map(() => ) )} @@ -58,22 +58,54 @@ export function BurnBlockGroupWithoutTransactionsSkeleton() { ); } -export function BurnBlockGroupListSkeleton() { +export function BurnBlockGroupListSkeleton({ + numBurnBlockGroupsWithTxs, + numTransactionsinBurnBlockGroupWithTxs, + numBurnBlockGroupsWithoutTxs, +}: { + numBurnBlockGroupsWithTxs: number; + numTransactionsinBurnBlockGroupWithTxs: number; + numBurnBlockGroupsWithoutTxs: number; +}) { return ( - - {Array.from({ length: 9 }).map(() => ( - - ))} + {numBurnBlockGroupsWithTxs + ? Array.from({ length: numBurnBlockGroupsWithTxs }).map(() => ( + + )) + : null} + {numBurnBlockGroupsWithoutTxs + ? Array.from({ length: numBurnBlockGroupsWithoutTxs }).map(() => ( + + )) + : null} ); } +export function HomePageBlockListGroupedByBtcBlockSkeleton() { + return ( +
}> + +
+ ); +} + export function BlocksPageBlockListGroupedByBtcBlockSkeleton() { return (
}> - +
); } diff --git a/src/app/_components/BlockList/LayoutA/UpdateBar.tsx b/src/app/_components/BlockList/LayoutA/UpdateBar.tsx index a47b0ff82..88b2a5b07 100644 --- a/src/app/_components/BlockList/LayoutA/UpdateBar.tsx +++ b/src/app/_components/BlockList/LayoutA/UpdateBar.tsx @@ -1,23 +1,26 @@ import { useColorModeValue } from '@chakra-ui/react'; -import React, { useCallback, useMemo, useRef } from 'react'; +import { useCallback, useRef } from 'react'; import { TfiReload } from 'react-icons/tfi'; -import { Flex } from '../../../../ui/Flex'; +import { Button } from '../../../../ui/Button'; +import { Flex, FlexProps } from '../../../../ui/Flex'; import { Icon } from '../../../../ui/Icon'; import { Text } from '../../../../ui/Text'; import { FADE_DURATION } from './consts'; +interface UpdateBarProps extends FlexProps { + latestBlocksCount: number; + onClick: () => void; + isUpdateListLoading: boolean; +} + export function UpdateBar({ latestBlocksCount, onClick, isUpdateListLoading, -}: { - latestBlocksCount: number; - onClick: () => void; - isUpdateListLoading: boolean; -}) { + ...rest +}: UpdateBarProps) { const bgColor = useColorModeValue('purple.100', 'slate.900'); - const buttonColor = useColorModeValue('brand', 'purple.400'); const textColor = useColorModeValue('slate.800', 'slate.400'); const lastClickTimeRef = useRef(0); @@ -41,6 +44,7 @@ export function UpdateBar({ transition: `opacity ${FADE_DURATION / 1000}s`, opacity: isUpdateListLoading ? 0 : 1, }} + {...rest} > {' '} new Stacks blocks have come in - - + ); } diff --git a/src/ui/theme/componentTheme/Button.ts b/src/ui/theme/componentTheme/Button.ts index f4ffad3ce..3c0367205 100644 --- a/src/ui/theme/componentTheme/Button.ts +++ b/src/ui/theme/componentTheme/Button.ts @@ -25,5 +25,17 @@ export const buttonTheme = defineStyleConfig({ bgColor: mode(`slate.200`, `slate.900`)(props), }, })), + text: defineStyle(props => ({ + padding: 0, + border: 'none', + background: 'none', + fontWeight: 'medium', + fontSize: 'sm', + color: 'buttonText', + height: 'auto', + _hover: { + textDecoration: 'underline', + }, + })), }, }); diff --git a/src/ui/theme/theme.ts b/src/ui/theme/theme.ts index c6008808e..3f2179f6d 100644 --- a/src/ui/theme/theme.ts +++ b/src/ui/theme/theme.ts @@ -56,6 +56,10 @@ export const theme = extendTheme({ default: 'slate.700', _dark: 'slate.600', }, + buttonText: { + default: 'brand', + _dark: 'purple.400', + }, interactive: { default: 'purple.600', _dark: 'purple.400', From 1d88710cfb09f1cec22181cb4c831f4ac025940f Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Thu, 4 Apr 2024 20:19:12 -0500 Subject: [PATCH 14/70] feat(grouped-by-btc-block-list-view-3): skeletons.btcblock page --- .../BlocksPageBlockList.tsx | 65 ++++++ .../GroupedByBurnBlock/BurnBlockGroup.tsx | 202 +++++++++++------- .../BlockList/GroupedByBurnBlock/skeleton.tsx | 148 ++++++++----- ...seBlockListGroupedByBtcBlockBlocksPage.tsx | 1 + .../useBlockListGroupedByBtcBlockHomePage.tsx | 1 + .../useBlockListWebSocket.tsx | 4 +- .../BlockList/LayoutA/BlockCount.tsx | 4 +- .../BlockList/LayoutA/StxBlock.tsx | 12 +- .../LayoutA/useBlockListWebSocket.ts | 2 +- .../Sockets/use-stacks-api-socket-client.ts | 66 ++++++ .../BlockList/Sockets/useSubscribeBlocks2.ts | 37 ++++ .../BlockList/Ungrouped/BtcBlockListItem.tsx | 83 +++++++ .../BlockList/Ungrouped/StxBlockListItem.tsx | 111 ++++++++++ .../BlockList/Ungrouped/skeleton.tsx | 79 +++++++ src/app/_components/PageTitle.tsx | 7 - src/app/_components/PageWrapper.tsx | 2 +- src/app/block/[hash]/PageClient.tsx | 3 - .../btcblock/[hash]/BitcoinAnchorDetails.tsx | 133 ++++++++++++ src/app/btcblock/[hash]/NavBlock.tsx | 31 +++ src/app/btcblock/[hash]/PageClient.tsx | 82 +++++++ src/app/btcblock/[hash]/background.ts | 0 src/app/btcblock/[hash]/page.tsx | 13 ++ src/app/layout.tsx | 8 +- src/common/components/CopyButton.tsx | 8 +- src/common/components/KeyValueVertical.tsx | 56 ++--- src/common/context/GlobalContext.tsx | 22 +- src/common/queries/useBlocksByBurnBlock.ts | 6 +- src/common/queries/useBurnBlock.ts | 44 ++++ src/common/utils/utils.ts | 4 +- 29 files changed, 1039 insertions(+), 195 deletions(-) create mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockList.tsx create mode 100644 src/app/_components/BlockList/Sockets/use-stacks-api-socket-client.ts create mode 100644 src/app/_components/BlockList/Sockets/useSubscribeBlocks2.ts create mode 100644 src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx create mode 100644 src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx create mode 100644 src/app/_components/BlockList/Ungrouped/skeleton.tsx create mode 100644 src/app/btcblock/[hash]/BitcoinAnchorDetails.tsx create mode 100644 src/app/btcblock/[hash]/NavBlock.tsx create mode 100644 src/app/btcblock/[hash]/PageClient.tsx create mode 100644 src/app/btcblock/[hash]/background.ts create mode 100644 src/app/btcblock/[hash]/page.tsx create mode 100644 src/common/queries/useBurnBlock.ts diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockList.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockList.tsx new file mode 100644 index 000000000..5b3f694d8 --- /dev/null +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockList.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { Suspense, useCallback, useRef } from 'react'; + +import { Section } from '../../../../common/components/Section'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { Controls } from '../Controls'; +import { BlockListProvider } from '../LayoutA/Provider'; +import { useBlockListContext } from '../LayoutA/context'; +import { PaginatedBlockListLayoutA } from '../Ungrouped/Paginated2'; +import { BlocksPageBlockListGroupedByBtcBlock2 } from './BlocksPageBlockListGroupedByBtcBlock'; +import { BlocksPageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; + +function BlocksPageBlockListBase() { + const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates } = useBlockListContext(); + + const lastClickTimeRef = useRef(0); + const toggleLiveUpdates = useCallback(() => { + const now = Date.now(); + if (now - lastClickTimeRef.current > 2000) { + lastClickTimeRef.current = now; + setLiveUpdates(!liveUpdates); + } + }, [liveUpdates, setLiveUpdates]); + + return ( +
+ { + setGroupedByBtc(!groupedByBtc); + }, + isChecked: groupedByBtc, + }} + liveUpdates={{ + onChange: toggleLiveUpdates, + isChecked: liveUpdates, + }} + horizontal={true} + /> + {groupedByBtc ? : } +
+ ); +} + +export function BlocksPageBlockList() { + // TODO: fix the suspense fallback + return ( + + + }> + + + + + ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx index 28be7980c..e7fa1f744 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx @@ -3,7 +3,7 @@ import { ReactNode, useEffect, useRef, useState } from 'react'; import { PiArrowElbowLeftDown } from 'react-icons/pi'; import { Circle } from '../../../../common/components/Circle'; -import { BlockLink } from '../../../../common/components/ExplorerLinks'; +import { BlockLink, ExplorerLink } from '../../../../common/components/ExplorerLinks'; import { Timestamp } from '../../../../common/components/Timestamp'; import { truncateMiddle } from '../../../../common/utils/utils'; import { Box } from '../../../../ui/Box'; @@ -149,7 +149,7 @@ function ScrollableDiv({ children }: { children: ReactNode }) { ref={divRef} overflowX={'auto'} overflowY={'hidden'} - py={4} + // py={4} className={hasHorizontalScroll ? 'has-horizontal-scroll' : ''} > {children} @@ -160,10 +160,121 @@ function ScrollableDiv({ children }: { children: ReactNode }) { export interface BlocksGroupProps { burnBlock: UISingleBlock; stxBlocks: UISingleBlock[]; + /** + * TODO: change to + * burnBlock: BurnBlock; + * stxBlocks: Block[]; + */ stxBlocksDisplayLimit?: number; } -export function BlocksGroupSkeleton() {} +export function BurnBlockGroupGrid({ + burnBlock, + stxBlocks, + stxBlocksDisplayLimit, +}: BlocksGroupProps) { + const stxBlocksToDisplay = stxBlocksDisplayLimit + ? stxBlocks.slice(0, stxBlocksDisplayLimit) + : stxBlocks; + return ( + + + {stxBlocksToDisplay.map((stxBlock, i) => ( + <> + + + + ) : ( + + ) + } + /> + {i < stxBlocks.length - 1 && ( + + )}{' '} + {/* TODO: adds a border to the bottom. make this css */} + + ))} + + ); +} + +function BitcoinHeader({ burnBlock }: { burnBlock: UISingleBlock }) { + return ( + + + + + {burnBlock.height} + + ∙} gap={1}> + + {truncateMiddle(burnBlock.hash, 6)} + + + + + ); +} + +export function Footer({ stxBlocks, txSum }: { stxBlocks: UISingleBlock[]; txSum: number }) { + return ( + + ∙} gap={1} pt={4}> + + {stxBlocks.length} blocks + + + {txSum} transactions + + + Average block time: 29 sec. + + + + ); +} export function BurnBlockGroup({ burnBlock, @@ -186,87 +297,16 @@ export function BurnBlockGroup({ // TODO: why are we not using table here? return ( - - - - - {burnBlock.height} - - ∙} gap={1}> - - {truncateMiddle(burnBlock.hash, 6)} - - - - + - - - {stxBlocks.slice(0, stxBlocksDisplayLimit).map((stxBlock, i) => ( - <> - - - - ) : ( - - ) - } - /> - {i < stxBlocks.length - 1 && }{' '} - {/* TODO: adds a border to the bottom. make this css */} - - ))} - + {stxBlocksNotDisplayed > 0 ? : null} - - - ∙} gap={1} pt={4}> - - {stxBlocks.length} blocks - - - {txSum} transactions - - - Average block time: 29 sec. - - - +
); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx index d69a9e8fe..6a2188fc3 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx @@ -1,59 +1,99 @@ +import { SkeletonCircle, useColorModeValue } from '@chakra-ui/react'; + import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; import { Grid } from '../../../../ui/Grid'; import { SkeletonText } from '../../../../ui/SkeletonText'; -import { BlockPageHeadersSkeleton } from './BlocksPageHeaders'; -export function BurnBlockGroupWithTransactionsSkeleton({ numTxs }: { numTxs: number }) { +function BitcoinHeaderSkeleton() { return ( - - - - {Array.from({ length: 4 }).map(() => ( - - - - ))} - {Array.from({ length: numTxs }).map(() => - Array.from({ length: 4 }).map(() => ) - )} - - - - + + ); } -export function BurnBlockGroupWithoutTransactionsSkeleton() { +function FooterSkeleton() { + return ( + + + + ); +} + +function BlockCountSkeleton() { + // TODO: remove. use theme + const bgColor = useColorModeValue('purple.100', 'slate.900'); + + return ( + + + + + ); +} + +function GridHeaderRowSkeleton() { + return ( + <> + {Array.from({ length: 4 }).map((_, colIndex) => ( + + + + ))} + + ); +} + +function GridRowSkeleton({ numTxs }: { numTxs: number }) { + if (numTxs === 0) { + return null; + } + return ( + <> + {Array.from({ length: numTxs }).map((_, rowIndex) => + Array.from({ length: 4 }).map((_, colIndex) => ( + + )) + )} + + ); +} + +export function BurnBlockGroupSkeleton({ numTxs }: { numTxs: number }) { return ( - - - {Array.from({ length: 4 }).map(() => ( - - - - ))} - - - + + + + + + ); } @@ -68,17 +108,18 @@ export function BurnBlockGroupListSkeleton({ numBurnBlockGroupsWithoutTxs: number; }) { return ( - + {numBurnBlockGroupsWithTxs - ? Array.from({ length: numBurnBlockGroupsWithTxs }).map(() => ( - ( + )) : null} {numBurnBlockGroupsWithoutTxs - ? Array.from({ length: numBurnBlockGroupsWithoutTxs }).map(() => ( - + ? Array.from({ length: numBurnBlockGroupsWithoutTxs }).map((_, i) => ( + )) : null} @@ -99,13 +140,10 @@ export function HomePageBlockListGroupedByBtcBlockSkeleton() { export function BlocksPageBlockListGroupedByBtcBlockSkeleton() { return ( -
}> - - -
+ ); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx index 1126f05f8..0f4e9baec 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx @@ -10,6 +10,7 @@ import { useSuspenseBlocksByBurnBlock } from '../../../../common/queries/useBloc import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocks'; import { FADE_DURATION } from '../LayoutA/consts'; import { useBlockListContext } from '../LayoutA/context'; +import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; import { UIBlockType, UISingleBlock } from '../types'; import { BlocksGroupProps } from './BurnBlockGroup'; import { useBlockListWebSocket } from './useBlockListWebSocket'; diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx index ec472aa87..abba2ee6d 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx @@ -9,6 +9,7 @@ import { UIBlockType } from '../types'; import { BlocksGroupProps } from './BurnBlockGroup'; import { useBlockListWebSocket } from './useBlockListWebSocket'; import { useInitialBlockListGroupedByBtcBlockHomePage } from './useInitialBlockListGroupedByBtcHomePage'; +import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; export function useBlockListGroupedByBtcBlockHomePage() { const queryClient = useQueryClient(); diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx index 0528d3e09..2c2df8719 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx @@ -3,7 +3,7 @@ import { useCallback, useRef, useState } from 'react'; import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; import { UIBlockType, UISingleBlock } from '../types'; -import { useSubscribeBlocks } from '../useSubscribeBlocks'; +import { useSubscribeBlocks2 } from './useSubscribeBlocks2'; export function useBlockListWebSocket( initialBlockHashes: Set, @@ -57,7 +57,7 @@ export function useBlockListWebSocket( [initialBurnBlockHashes, initialBlockHashes] ); - useSubscribeBlocks(handleBlock); + useSubscribeBlocks2(handleBlock); const clearLatestBlocks = () => { setLatestBlocks([]); diff --git a/src/app/_components/BlockList/LayoutA/BlockCount.tsx b/src/app/_components/BlockList/LayoutA/BlockCount.tsx index 332cdbda0..9e0c03997 100644 --- a/src/app/_components/BlockList/LayoutA/BlockCount.tsx +++ b/src/app/_components/BlockList/LayoutA/BlockCount.tsx @@ -10,11 +10,11 @@ import { Icon } from '../../../../ui/Icon'; import { Text } from '../../../../ui/Text'; export const BlockCount = memo(function ({ count }: { count: number }) { + // TODO: remove. use theme const bgColor = useColorModeValue('purple.100', 'slate.900'); const bgColorHover = useColorModeValue('purple.200', 'slate.850'); const textColor = useColorModeValue('purple.600', 'purple.400'); const iconColor = useColorModeValue('purple.600', 'purple.200'); - const circleColor = useColorModeValue('white', 'black'); return ( @@ -36,7 +36,7 @@ export const BlockCount = memo(function ({ count }: { count: number }) { }} > +{count} {pluralize('block', count)} - + diff --git a/src/app/_components/BlockList/LayoutA/StxBlock.tsx b/src/app/_components/BlockList/LayoutA/StxBlock.tsx index 6242eb169..f5146526e 100644 --- a/src/app/_components/BlockList/LayoutA/StxBlock.tsx +++ b/src/app/_components/BlockList/LayoutA/StxBlock.tsx @@ -31,15 +31,13 @@ export const StxBlock = memo(function ({ // TODO: lots of new colors that aren't in the theme. We should either add them or make them conform to the theme const textColor = useColorModeValue('slate.900', 'slate.50'); const secondaryTextColor = useColorModeValue('slate.700', 'slate.600'); - const borderColor = useColorModeValue('slate.300', 'slate.800'); return ( , diff --git a/src/app/_components/BlockList/Sockets/use-stacks-api-socket-client.ts b/src/app/_components/BlockList/Sockets/use-stacks-api-socket-client.ts new file mode 100644 index 000000000..c47c1188e --- /dev/null +++ b/src/app/_components/BlockList/Sockets/use-stacks-api-socket-client.ts @@ -0,0 +1,66 @@ +import { useCallback, useRef, useState } from 'react'; + +import { StacksApiSocketClient } from '@stacks/blockchain-api-client'; + +import { NetworkModes } from '../../../../common/types/network'; + +export interface StacksApiSocketClientInfo { + client: StacksApiSocketClient | undefined; + connect: (handleOnConnect?: (client: StacksApiSocketClient) => void) => void; + disconnect: () => void; +} + +export function useStacksApiSocketClient(network: NetworkModes): StacksApiSocketClientInfo { + const [socketClient, setSocketClient] = useState(undefined); + const socketUrlTracker = useRef(null); + const isSocketClientConnecting = useRef(false); + + const connect = useCallback( + async (handleOnConnect?: (client: StacksApiSocketClient) => void) => { + if (!network) return; + if (socketClient?.socket.connected || isSocketClientConnecting.current) { + return; + } + try { + isSocketClientConnecting.current = true; + const socketUrl = `https://api.${network}.hiro.so/`; + socketUrlTracker.current = socketUrl; + console.log('Connecting to socket', socketUrl); + const client = await StacksApiSocketClient.connect({ url: socketUrl }); + client.socket.on('connect', () => { + console.log('Connected to socket'); + setSocketClient(client); + handleOnConnect?.(client); + isSocketClientConnecting.current = false; + }); + client.socket.on('disconnect', () => { + console.log('Disconnected from socket'); + setSocketClient(undefined); + isSocketClientConnecting.current = false; + }); + client.socket.on('connect_error', error => { + console.log('Socket connection error', error); + setSocketClient(undefined); + isSocketClientConnecting.current = false; + }); + } catch (error) { + setSocketClient(undefined); + isSocketClientConnecting.current = false; + } + }, + [network, socketClient] + ); + + const disconnect = useCallback(() => { + if (socketClient?.socket.connected) { + console.log('Disconnecting from socket'); + socketClient?.socket.close(); + } + }, [socketClient]); + + return { + client: socketClient, + connect, + disconnect, + }; +} diff --git a/src/app/_components/BlockList/Sockets/useSubscribeBlocks2.ts b/src/app/_components/BlockList/Sockets/useSubscribeBlocks2.ts new file mode 100644 index 000000000..baa41bb58 --- /dev/null +++ b/src/app/_components/BlockList/Sockets/useSubscribeBlocks2.ts @@ -0,0 +1,37 @@ +import { useEffect, useRef } from 'react'; + +import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; +import { Block } from '@stacks/stacks-blockchain-api-types'; + +import { useGlobalContext } from '../../../../common/context/useAppContext'; + +interface Subscription { + unsubscribe(): void; +} + +// TODO: with the new client code, we should be able to use the client directly +export function useSubscribeBlocks2(handleBlock: (block: NakamotoBlock) => any) { + const subscription = useRef(undefined); + const { stacksApiSocketClient } = useGlobalContext(); + + useEffect(() => { + const subscribe = async () => { + console.log('subscribing to blocks'); + subscription.current = stacksApiSocketClient?.subscribeBlocks((block: Block) => { + console.log('handling block', block); + handleBlock({ + ...block, + parent_index_block_hash: '', + tx_count: 0, + }); + }); + }; + if (stacksApiSocketClient?.socket.connected) { + subscribe(); + } + return () => { + subscription?.current?.unsubscribe(); + }; + }, [stacksApiSocketClient, handleBlock]); + return subscription; +} diff --git a/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx b/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx new file mode 100644 index 000000000..668bf1aec --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx @@ -0,0 +1,83 @@ +import { useColorModeValue } from '@chakra-ui/react'; +import { ReactNode } from 'react'; +import { BsArrowReturnLeft } from 'react-icons/bs'; + +import { Timestamp } from '../../../../common/components/Timestamp'; +import { useGlobalContext } from '../../../../common/context/useAppContext'; +import { truncateMiddle } from '../../../../common/utils/utils'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { HStack } from '../../../../ui/HStack'; +import { Icon } from '../../../../ui/Icon'; +import { Text } from '../../../../ui/Text'; +import { TextLink } from '../../../../ui/TextLink'; +import { BitcoinIcon } from '../../../../ui/icons'; + +interface BtcBlockListItemProps { + height: number | string; + hash: string; + timestamp?: number; +} +export function BtcBlockListItemLayout({ children }: { children: ReactNode }) { + const textColor = useColorModeValue('slate.700', 'slate.500'); // TODO: not in theme. remove + return ( + + {children} + + ); +} + +export function BtcBlockListItemContent({ timestamp, height, hash }: BtcBlockListItemProps) { + const { btcBlockBaseUrl } = useGlobalContext().activeNetwork; + const iconColor = useColorModeValue('slate.600', 'slate.800'); // TODO: not in theme. remove + return ( + <> + + + + + + #{height} + + + +  ∙ } fontSize={'xs'}> + {truncateMiddle(hash, 3)} + {timestamp && } + + + ); +} + +export function BtcBlockListItem({ timestamp, height, hash }: BtcBlockListItemProps) { + return ( + + + + ); +} diff --git a/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx b/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx new file mode 100644 index 000000000..707ca95ea --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx @@ -0,0 +1,111 @@ +import { ReactNode } from 'react'; + +import { Circle } from '../../../../common/components/Circle'; +import { BlockLink } from '../../../../common/components/ExplorerLinks'; +import { Timestamp } from '../../../../common/components/Timestamp'; +import { truncateMiddle } from '../../../../common/utils/utils'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { HStack } from '../../../../ui/HStack'; +import { Text } from '../../../../ui/Text'; + +interface StxBlockListItemLayoutProps { + children: ReactNode; + hasIcon: boolean; + hasBorder: boolean; +} + +export function StxBlockListItemLayout({ + children, + hasIcon, + hasBorder, +}: StxBlockListItemLayoutProps) { + return ( + + + {children} + + + ); +} + +function StxBlockListItemContent({ + height, + hash, + timestamp, + txsCount, + icon, +}: { + height: number | string; + hash: string; + timestamp: number; + txsCount?: number; + icon?: ReactNode; +}) { + return ( + <> + + {!!icon && ( + + {icon} + + )} + + + + #{height} + + + +  ∙ } fontSize={'12px'} color="textSubdued"> + {truncateMiddle(hash, 3)} + {txsCount !== undefined ? {txsCount} txn : null} + + + + ); +} + +export function StxBlockListItem({ children, hasIcon, hasBorder }: StxBlockListItemLayoutProps) { + return ( + + {children} + + ); +} diff --git a/src/app/_components/BlockList/Ungrouped/skeleton.tsx b/src/app/_components/BlockList/Ungrouped/skeleton.tsx new file mode 100644 index 000000000..0deec2148 --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/skeleton.tsx @@ -0,0 +1,79 @@ +import { Stack } from '@/ui/Stack'; + +import { Circle } from '../../../../common/components/Circle'; +import { Flex } from '../../../../ui/Flex'; +import { SkeletonText } from '../../../../ui/SkeletonText'; +import { BtcBlockListItemLayout } from './BtcBlockListItem'; +import { StxBlockListItem } from './StxBlockListItem'; + +function StxBlockListItemContentSkeleton({ hasIcon }: { hasIcon: boolean }) { + return ( + <> + + {hasIcon && } + + + + + ); +} +function StxBlockListItemSkeleton({ + hasIcon, + hasBorder, +}: { + hasIcon: boolean; + hasBorder: boolean; +}) { + return ( + + + + ); +} + +export function StxBlockListSkeleton({ numBlocks }: { numBlocks: number }) { + return ( + <> + {Array.from({ length: numBlocks }).map((_, i) => ( + + ))} + + ); +} + +function BtcBlockListItemContentSkeleton() { + return ( + <> + + + + ); +} + +function BtcBlockListItemSkeleton() { + return ( + + + + ); +} + +export function BlocksPageBlockListUngroupedSkeleton() { + return ( + + + + + + + + ); +} + +export function HomePageBlockListUngroupedSkeleton() { + return <>; +} diff --git a/src/app/_components/PageTitle.tsx b/src/app/_components/PageTitle.tsx index 951cc2dfb..38df323fd 100644 --- a/src/app/_components/PageTitle.tsx +++ b/src/app/_components/PageTitle.tsx @@ -1,15 +1,8 @@ import { ReactNode } from 'react'; -import * as React from 'react'; -import { TxTypeTag } from '../../common/components/TxTypeTag'; -import { TxStatusLabel } from '../../common/components/status'; -import { getTransactionStatus } from '../../common/utils/transactions'; -import { getTxTitle } from '../../common/utils/utils'; -import { Badge } from '../../ui/Badge'; import { Flex } from '../../ui/Flex'; import { HStack } from '../../ui/HStack'; import { Heading, HeadingProps } from '../../ui/Heading'; -import { TxAlerts } from '../txid/[txId]/TxAlerts'; export function PageTitle({ children, ...props }: { children: ReactNode } & HeadingProps) { return ( diff --git a/src/app/_components/PageWrapper.tsx b/src/app/_components/PageWrapper.tsx index f6e7420a0..a8c749208 100644 --- a/src/app/_components/PageWrapper.tsx +++ b/src/app/_components/PageWrapper.tsx @@ -1,7 +1,7 @@ 'use client'; import { useColorModeValue } from '@chakra-ui/react'; -import React, { ReactNode } from 'react'; +import { ReactNode } from 'react'; import { AddNetworkModal } from '../../common/components/modals/AddNetwork'; import { NakamotoModal } from '../../common/components/modals/Nakamoto'; diff --git a/src/app/block/[hash]/PageClient.tsx b/src/app/block/[hash]/PageClient.tsx index 6ded3bf2a..b452d0bf2 100644 --- a/src/app/block/[hash]/PageClient.tsx +++ b/src/app/block/[hash]/PageClient.tsx @@ -1,18 +1,15 @@ 'use client'; import dynamic from 'next/dynamic'; -import * as React from 'react'; import { BtcStxBlockLinks } from '../../../common/components/BtcStxBlockLinks'; import { KeyValueHorizontal } from '../../../common/components/KeyValueHorizontal'; import { Section } from '../../../common/components/Section'; import { Timestamp } from '../../../common/components/Timestamp'; -import { TwoColumnPage } from '../../../common/components/TwoColumnPage'; import { Value } from '../../../common/components/Value'; import '../../../common/components/loaders/skeleton-text'; import { useSuspenseBlockByHash } from '../../../common/queries/useBlockByHash'; import { SkeletonTxsList } from '../../../features/txs-list/SkeletonTxsList'; -import { Box, Flex, Grid } from '../../../ui/components'; import { PageTitle } from '../../_components/PageTitle'; import { TowColLayout } from '../../_components/TwoColLayout'; import { BlockBtcAnchorBlockCard } from './BlockBtcAnchorBlockCard'; diff --git a/src/app/btcblock/[hash]/BitcoinAnchorDetails.tsx b/src/app/btcblock/[hash]/BitcoinAnchorDetails.tsx new file mode 100644 index 000000000..a417a7522 --- /dev/null +++ b/src/app/btcblock/[hash]/BitcoinAnchorDetails.tsx @@ -0,0 +1,133 @@ +'use client'; + +import { useParamsBlockHash } from '@/app/block/[hash]/useParamsBlockHash'; +import { useSuspenseBurnBlock } from '@/common/queries/useBurnBlock'; +import { Flex } from '@/ui/Flex'; +import { Link } from '@/ui/Link'; +import { BitcoinIcon } from '@/ui/icons/BitcoinIcon'; +import styled from '@emotion/styled'; + +import { KeyValueVertical } from '../../../common/components/KeyValueVertical'; +import { Section } from '../../../common/components/Section'; +import { useGlobalContext } from '../../../common/context/useAppContext'; +import { toRelativeTime, truncateMiddle } from '../../../common/utils/utils'; +import { Text } from '../../../ui/Text'; +import { ExplorerErrorBoundary } from '../../_components/ErrorBoundary'; + +const StyledSection = styled(Section)` + .key-value-vertical:not(:last-child) { + border-bottom: 1px solid var(--stacks-colors-borderSecondary); + } +`; + +export function BitcoinAnchorDetailsBase() { + const { data: btcBlock } = useSuspenseBurnBlock(useParamsBlockHash(), { + refetchOnWindowFocus: true, + }); + + const { btcBlockBaseUrl, btcTxBaseUrl } = useGlobalContext().activeNetwork; + const btcBlockBlockTimeUTC = new Date(btcBlock.burn_block_time_iso).toUTCString(); + + if (!btcBlock) return null; + + return ( + + + + + + #{btcBlock.burn_block_height} + + + + } + copyValue={btcBlock.burn_block_height.toString()} + /> + + + {truncateMiddle(btcBlock.burn_block_hash, 8)} + + + } + copyValue={btcBlock.burn_block_hash} + /> + + + {truncateMiddle(btcBlock.burn_block_hash, 8)} + + + } + copyValue={btcBlock.burn_block_hash} + /> + + {toRelativeTime(btcBlock.burn_block_time * 1000)} + + + {btcBlockBlockTimeUTC} + + + } + copyValue={btcBlockBlockTimeUTC} + /> + + {btcBlock.stacks_blocks.length} + + } + /> + {/** TODO: rethink this. getting total stx txs would require querying for all stx blocks upfront, which means we wouldn't need a load more button */} + + 2421 + + } + /> + {/** TODO: the API doesn't currently support this */} + + 23 seconds + + } + /> + + ); +} + +export function BitcoinAnchorDetails() { + return ( + + + + ); +} diff --git a/src/app/btcblock/[hash]/NavBlock.tsx b/src/app/btcblock/[hash]/NavBlock.tsx new file mode 100644 index 000000000..5ae98e7ed --- /dev/null +++ b/src/app/btcblock/[hash]/NavBlock.tsx @@ -0,0 +1,31 @@ +import { useGlobalContext } from '@/common/context/useAppContext'; +import { buildUrl } from '@/common/utils/buildUrl'; + +import { ArrowLeftIcon } from '../../../common/components/icons/arrow-left'; +import { ArrowRightIcon } from '../../../common/components/icons/arrow-right'; +import { Flex } from '../../../ui/Flex'; +import { Icon } from '../../../ui/Icon'; +import { Link } from '../../../ui/Link'; + +export enum NavDirection { + Forward = 'forward', + Backward = 'backward', +} + +export function NavBlock({ href, direction }: { href: string; direction: NavDirection }) { + const network = useGlobalContext().activeNetwork; + + return ( + + + + + + ); +} diff --git a/src/app/btcblock/[hash]/PageClient.tsx b/src/app/btcblock/[hash]/PageClient.tsx new file mode 100644 index 000000000..069a0b344 --- /dev/null +++ b/src/app/btcblock/[hash]/PageClient.tsx @@ -0,0 +1,82 @@ +'use client'; + +import { BurnBlockGroupGrid } from '@/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup'; +import { UIBlockType, UISingleBlock } from '@/app/_components/BlockList/types'; +import { BlockBtcAnchorBlockCard } from '@/app/block/[hash]/BlockBtcAnchorBlockCard'; +import { NavBlock, NavDirection } from '@/app/btcblock/[hash]/NavBlock'; +import { ListFooter } from '@/common/components/ListFooter'; +import { useSuspenseInfiniteQueryResult } from '@/common/hooks/useInfiniteQueryResult'; +import { useSuspenseBlocksByBurnBlock } from '@/common/queries/useBlocksByBurnBlock'; +import { useSuspenseBurnBlock } from '@/common/queries/useBurnBlock'; + +import { NakamotoBlock } from '@stacks/blockchain-api-client'; + +import { Section } from '../../../common/components/Section'; +import '../../../common/components/loaders/skeleton-text'; +import { Box } from '../../../ui/Box'; +import { Flex } from '../../../ui/Flex'; +import { PageTitle } from '../../_components/PageTitle'; +import { TowColLayout } from '../../_components/TwoColLayout'; +import { BitcoinAnchorDetails } from './BitcoinAnchorDetails'; + +export default function BitcoinBlockPage({ params: { hash } }: any) { + const { data: btcBlock } = useSuspenseBurnBlock(hash); + const btcBlockHeight = btcBlock?.burn_block_height; + + const { data: prevBlock } = useSuspenseBurnBlock(btcBlockHeight - 1); + const { data: nextBlock } = useSuspenseBurnBlock(btcBlockHeight + 1); + + const stxBlocksResponse = useSuspenseBlocksByBurnBlock(btcBlock.burn_block_height, 15); + const { isFetchingNextPage, fetchNextPage, hasNextPage } = stxBlocksResponse; + const stxBlocks = useSuspenseInfiniteQueryResult(stxBlocksResponse); + + const burnBlockArg = { + type: UIBlockType.BurnBlock, + height: btcBlock.burn_block_height, + hash: btcBlock.burn_block_hash, + timestamp: btcBlock.burn_block_time, + txsCount: btcBlock.stacks_blocks.length, + } as UISingleBlock; + const stxBlocksArg = stxBlocks.map( + block => + ({ + type: UIBlockType.StxBlock, + height: block.height, + hash: block.hash, + timestamp: block?.block_time, // block?.block_time TODO: this is the right timestamp to use, but it seems to be inaccurate + txsCount: block.tx_count, + }) as UISingleBlock + ); + + return ( + <> + + + {`Block #${btcBlock?.burn_block_height.toLocaleString()}`} + + + +
+ + + + + + +
+ +
+ + ); +} diff --git a/src/app/btcblock/[hash]/background.ts b/src/app/btcblock/[hash]/background.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/app/btcblock/[hash]/page.tsx b/src/app/btcblock/[hash]/page.tsx new file mode 100644 index 000000000..6b778a5e2 --- /dev/null +++ b/src/app/btcblock/[hash]/page.tsx @@ -0,0 +1,13 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import * as React from 'react'; + +import { Box } from '../../..//ui/Box'; + +const Page = dynamic(() => import('./PageClient'), { + loading: () => Loading, + ssr: false, +}); + +export default Page; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 577b0da56..2268851a7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -30,10 +30,10 @@ export default async function RootLayout({ children }: { children: ReactNode }) {children} diff --git a/src/common/components/CopyButton.tsx b/src/common/components/CopyButton.tsx index 1aab3a94b..61b581f3f 100644 --- a/src/common/components/CopyButton.tsx +++ b/src/common/components/CopyButton.tsx @@ -1,6 +1,5 @@ import { useClipboard } from '@chakra-ui/react'; import { FC, memo } from 'react'; -import * as React from 'react'; import { AiOutlineCopy } from 'react-icons/ai'; import { IconButton, IconButtonProps } from '../../ui/IconButton'; @@ -12,7 +11,12 @@ export const CopyButton: FC = memo( return ( } + icon={ + + } onClick={onCopy} height={'auto'} _focus={{ background: 'none' }} diff --git a/src/common/components/KeyValueVertical.tsx b/src/common/components/KeyValueVertical.tsx index c2d383e39..607cbf338 100644 --- a/src/common/components/KeyValueVertical.tsx +++ b/src/common/components/KeyValueVertical.tsx @@ -1,16 +1,19 @@ -import { css } from '@emotion/react'; +import styled from '@emotion/styled'; import { FC, ReactNode } from 'react'; -import { Flex } from '../../ui/Flex'; +import { Box } from '../../ui/Box'; +import { Flex, FlexProps } from '../../ui/Flex'; +import { Text } from '../../ui/Text'; import { CopyButton } from './CopyButton'; -export interface KeyValueVerticalProps { +export interface KeyValueVerticalProps extends FlexProps { label: string; value: ReactNode; copyValue?: string; + className?: string; } -const rowStyle = css` +const StyledFlexContainer = styled(Flex)` .fancy-copy { opacity: 0; position: relative; @@ -27,28 +30,31 @@ const rowStyle = css` } `; -export const KeyValueVertical: FC = ({ label, value, copyValue }) => { +export const KeyValueVertical: FC = ({ + label, + value, + copyValue, + className, + ...rest +}) => { return ( - - - - {label} - - - {value} - + + + {label} + + + {value} + {copyValue && ( + + + + )} - {copyValue && ( - - )} - + ); }; diff --git a/src/common/context/GlobalContext.tsx b/src/common/context/GlobalContext.tsx index cdb33505a..27927762b 100644 --- a/src/common/context/GlobalContext.tsx +++ b/src/common/context/GlobalContext.tsx @@ -5,9 +5,10 @@ import { useSearchParams } from 'next/navigation'; import { FC, ReactNode, createContext, useCallback, useEffect, useMemo, useState } from 'react'; import { useCookies } from 'react-cookie'; -import { StacksApiWebSocketClient, connectWebSocketClient } from '@stacks/blockchain-api-client'; +import { StacksApiSocketClient, StacksApiWebSocketClient, connectWebSocketClient } from '@stacks/blockchain-api-client'; import { ChainID } from '@stacks/transactions'; +import { useStacksApiSocketClient } from '../../app/_components/BlockList/Sockets/use-stacks-api-socket-client'; import { buildCustomNetworkUrl, fetchCustomNetworkId } from '../components/modals/AddNetwork/utils'; import { DEFAULT_DEVNET_SERVER, IS_BROWSER } from '../constants/constants'; import { @@ -32,6 +33,7 @@ interface GlobalContextProps { removeCustomNetwork: (network: Network) => void; networks: Record; webSocketClient?: Promise; + // stacksApiSocketClient: StacksApiSocketClient | null; } export const GlobalContext = createContext({ @@ -54,6 +56,7 @@ export const GlobalContext = createContext({ removeCustomNetwork: () => true, networks: {}, webSocketClient: undefined, + // stacksApiSocketClient: null, }); export const AppContextProvider: FC<{ @@ -98,6 +101,22 @@ export const AppContextProvider: FC<{ const [_, setCookie] = useCookies(['customNetworks']); const [customNetworks, setCustomNetworks] = useState(customNetworksCookie); const activeNetworkKey = querySubnet || queryApiUrl || apiUrls[queryNetworkMode]; + + // const { + // client: stacksApiSocketClient, + // connect: connectStacksApiSocket, + // disconnect: disconnectStacksApiSocket, + // } = useStacksApiSocketClient(queryNetworkMode); + // Connect to the stacks api socket, disconnect when the component unmounts, and reconnect if the connection is lost + // useEffect(() => { + // if (!stacksApiSocketClient?.socket.connected) { + // connectStacksApiSocket(); + // } + // return () => { + // disconnectStacksApiSocket(); + // }; + // }, [stacksApiSocketClient, connectStacksApiSocket, disconnectStacksApiSocket]); + const isUrlPassedSubnet = !!querySubnet && !customNetworks[querySubnet]; const networks: Record = useMemo>( () => ({ @@ -247,6 +266,7 @@ export const AppContextProvider: FC<{ webSocketClient: activeNetworkKey ? connectWebSocketClient(activeNetworkKey.replace('https://', 'wss://')) : undefined, + // stacksApiSocketClient, }} > {children} diff --git a/src/common/queries/useBlocksByBurnBlock.ts b/src/common/queries/useBlocksByBurnBlock.ts index 549173029..da2353590 100644 --- a/src/common/queries/useBlocksByBurnBlock.ts +++ b/src/common/queries/useBlocksByBurnBlock.ts @@ -15,9 +15,11 @@ import { ONE_SECOND, TWO_MINUTES } from './query-stale-time'; export const GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY = 'getBlocksByBurnBlock'; +const MAX_STX_BLOCKS_PER_BURN_BLOCK_LIMIT = 30; + export function useBlocksByBurnBlock( heightOrHash: string | number, - limit: number, + limit: number = MAX_STX_BLOCKS_PER_BURN_BLOCK_LIMIT, options: any = {} ): UseInfiniteQueryResult>> { const api = useApi(); @@ -38,7 +40,7 @@ export function useBlocksByBurnBlock( export function useSuspenseBlocksByBurnBlock( heightOrHash: string | number, - limit: number, + limit: number = MAX_STX_BLOCKS_PER_BURN_BLOCK_LIMIT, options: any = {} ): UseSuspenseInfiniteQueryResult>> { const api = useApi(); diff --git a/src/common/queries/useBurnBlock.ts b/src/common/queries/useBurnBlock.ts new file mode 100644 index 000000000..b28aa2e6e --- /dev/null +++ b/src/common/queries/useBurnBlock.ts @@ -0,0 +1,44 @@ +import { + UseQueryResult, + UseSuspenseQueryResult, + useQuery, + useSuspenseQuery, +} from '@tanstack/react-query'; + +import { BurnBlock } from '@stacks/blockchain-api-client'; + +import { useApi } from '../api/useApi'; + +export const BURN_BLOCKS_QUERY_KEY = 'burnBlocks'; + +export function useBurnBlocks( + heightOrHash: number | string, + options: any = {} +): UseQueryResult { + const api = useApi(); + return useQuery({ + queryKey: ['burn-block', heightOrHash], + queryFn: () => + api.burnBlocksApi.getBurnBlock({ + heightOrHash, + }), + staleTime: Infinity, + ...options, + }); +} + +export function useSuspenseBurnBlock( + heightOrHash: number | string, + options: any = {} +): UseSuspenseQueryResult { + const api = useApi(); + return useSuspenseQuery({ + queryKey: ['burn-block', heightOrHash], + queryFn: () => + api.burnBlocksApi.getBurnBlock({ + heightOrHash, + }), + staleTime: Infinity, + ...options, + }); +} diff --git a/src/common/utils/utils.ts b/src/common/utils/utils.ts index 4a1f9d1a4..cd719fde0 100644 --- a/src/common/utils/utils.ts +++ b/src/common/utils/utils.ts @@ -268,8 +268,8 @@ export function getNextPageParam(lastPage?: GenericResponseType) { const { limit, offset, total } = lastPage; const sum = offset + limit; const delta = total - sum; - const isAtEnd = delta === 0 || Math.sign(delta) === -1; - if (Math.abs(delta) === sum || isAtEnd) return undefined; + const isAtEnd = delta <= 0; + if (isAtEnd) return undefined; return sum; } From a70d6c370d9859a614de81ef902723c47324ff48 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 5 Apr 2024 01:45:29 -0500 Subject: [PATCH 15/70] feat(grouped-by-btc-block-list-view-3): got all the data fetching done --- .../BlocksPageBlockList.tsx | 6 +- .../BlocksPageBlockListGroupedByBtcBlock.tsx | 58 ++---- .../GroupedByBurnBlock/BurnBlockGroup.tsx | 1 + .../HomePageBlockListGroupedByBtcBlock.tsx | 2 +- .../BlockList/GroupedByBurnBlock/skeleton.tsx | 3 +- ...seBlockListGroupedByBtcBlockBlocksPage.tsx | 23 +-- .../useBlockListGroupedByBtcBlockHomePage.tsx | 8 +- .../_components/BlockList/LayoutA/Blocks.tsx | 5 +- .../BlockList/LayoutA/Paginated.tsx | 12 +- .../BlockList/LayoutA/UpdateBar.tsx | 2 +- .../BlockList/LayoutA/useBlockList.ts | 12 +- .../LayoutA/useBlockListWebSocket.ts | 19 +- .../BlocksPageUngroupedBlockList.tsx | 167 ++++++++++++++++++ .../BlockList/Ungrouped/skeleton.tsx | 3 +- .../Ungrouped/useUngroupedBlockList.ts | 82 +++++++++ .../BlockList/{LayoutA => }/consts.ts | 0 16 files changed, 312 insertions(+), 91 deletions(-) create mode 100644 src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx create mode 100644 src/app/_components/BlockList/Ungrouped/useUngroupedBlockList.ts rename src/app/_components/BlockList/{LayoutA => }/consts.ts (100%) diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockList.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockList.tsx index 5b3f694d8..392ce2acf 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockList.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockList.tsx @@ -7,8 +7,8 @@ import { ExplorerErrorBoundary } from '../../ErrorBoundary'; import { Controls } from '../Controls'; import { BlockListProvider } from '../LayoutA/Provider'; import { useBlockListContext } from '../LayoutA/context'; -import { PaginatedBlockListLayoutA } from '../Ungrouped/Paginated2'; -import { BlocksPageBlockListGroupedByBtcBlock2 } from './BlocksPageBlockListGroupedByBtcBlock'; +import { BlocksPageUngroupedBlockList } from '../Ungrouped/BlocksPageUngroupedBlockList'; +import { BlocksPageBlockListGroupedByBtcBlock } from './BlocksPageBlockListGroupedByBtcBlock'; import { BlocksPageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; function BlocksPageBlockListBase() { @@ -38,7 +38,7 @@ function BlocksPageBlockListBase() { }} horizontal={true} /> - {groupedByBtc ? : } + {groupedByBtc ? : }
); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx index 37af4c14a..479452bf0 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx @@ -1,31 +1,21 @@ 'use client'; import { ListFooter } from '@/common/components/ListFooter'; -import { Suspense, useCallback, useRef } from 'react'; +import { Suspense } from 'react'; import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { Controls } from '../Controls'; -import { BlockListProvider } from '../LayoutA/Provider'; import { UpdateBar } from '../LayoutA/UpdateBar'; -import { FADE_DURATION } from '../LayoutA/consts'; -// TODO: move somewhere else import { useBlockListContext } from '../LayoutA/context'; -import { BlockPageHeadersSkeleton, BlocksPageHeaders } from './BlocksPageHeaders'; +import { FADE_DURATION } from '../consts'; import { BurnBlockGroup } from './BurnBlockGroup'; import { BlocksPageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; import { useBlockListGroupedByBtcBlockBlocksPage } from './useBlockListGroupedByBtcBlockBlocksPage'; function BlocksPageBlockListGroupedByBtcBlockBase() { - const { - groupedByBtc, - setGroupedByBtc, - liveUpdates, - setLiveUpdates, - isBlockListLoading, - } = useBlockListContext(); + const { liveUpdates, isBlockListLoading } = useBlockListContext(); const { blockList, updateBlockList, @@ -34,34 +24,12 @@ function BlocksPageBlockListGroupedByBtcBlockBase() { hasNextPage, fetchNextPage, } = useBlockListGroupedByBtcBlockBlocksPage(10); - - const lastClickTimeRef = useRef(0); - const toggleLiveUpdates = useCallback(() => { - const now = Date.now(); - if (now - lastClickTimeRef.current > 2000) { - lastClickTimeRef.current = now; - setLiveUpdates(!liveUpdates); - } - }, [liveUpdates, setLiveUpdates]); + console.log({ liveUpdates }); const enablePagination = true; return ( -
- { - setGroupedByBtc(!groupedByBtc); - }, - isChecked: groupedByBtc, - isDisabled: true, - }} - liveUpdates={{ - onChange: toggleLiveUpdates, - isChecked: liveUpdates, - }} - horizontal={true} - /> + <> {!liveUpdates && ( {blockList.map(block => ( )} -
+ ); } export function BlocksPageBlockListGroupedByBtcBlock() { - // TODO: fix the suspense fallback return ( - - }> - - - - }> - - - + }> + + ); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx index e7fa1f744..e350991d2 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx @@ -75,6 +75,7 @@ const GroupHeader = () => { ); }; +// TODO: ideally this would be a table const StxBlockRow = ({ block, icon }: { block: UISingleBlock; icon?: ReactNode }) => { return ( <> diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx index 36507bcf0..f73eebe7a 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx @@ -11,8 +11,8 @@ import { ExplorerErrorBoundary } from '../../ErrorBoundary'; import { Controls } from '../Controls'; import { BlockListProvider } from '../LayoutA/Provider'; import { UpdateBar } from '../LayoutA/UpdateBar'; -import { FADE_DURATION } from '../LayoutA/consts'; import { useBlockListContext } from '../LayoutA/context'; +import { FADE_DURATION } from '../consts'; import { BurnBlockGroup } from './BurnBlockGroup'; import { useBlockListGroupedByBtcBlockHomePage } from './useBlockListGroupedByBtcBlockHomePage'; import { HomePageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx index 6a2188fc3..96f37d739 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx @@ -5,6 +5,7 @@ import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; import { Grid } from '../../../../ui/Grid'; import { SkeletonText } from '../../../../ui/SkeletonText'; +import { Circle } from '../../../../common/components/Circle'; function BitcoinHeaderSkeleton() { return ( @@ -40,7 +41,7 @@ function BlockCountSkeleton() { mb={3} > - + ); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx index 0f4e9baec..559260c88 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx @@ -8,7 +8,6 @@ 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'; -import { FADE_DURATION } from '../LayoutA/consts'; import { useBlockListContext } from '../LayoutA/context'; import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; import { UIBlockType, UISingleBlock } from '../types'; @@ -43,8 +42,8 @@ export function useBlockListGroupedByBtcBlockBlocksPage(blockListLimit: number) ); const { - latestBlock: latestStxBlockFromWebSocket, - latestBlocksCount: latestStxBlocksCountFromWebSocket, + latestStxBlock: latestStxBlockFromWebSocket, + latestStxBlocksCount: latestStxBlocksCountFromWebSocket, clearLatestBlocks: clearLatestStxBlocksFromWebSocket, } = useBlockListWebSocket(stxBlockHashes, burnBlockHashes); // TODO: fix this @@ -85,14 +84,17 @@ export function useBlockListGroupedByBtcBlockBlocksPage(blockListLimit: number) // If latest stx block belongs to the latest burn block, add it to the latest burn block list of stx blocks if (latestStxBlockFromWebSocket.burn_block_height === latestBurnBlock.burn_block_height) { setBlockListLoading(true); - setTimeout(() => { - latestBurnBlockStxBlocks.unshift(latestStxBlockFromWebSocket); - latestBurnBlock.stacks_blocks.unshift(latestStxBlockFromWebSocket.hash); - setBlockListLoading(false); - }, FADE_DURATION); + latestBurnBlockStxBlocks.unshift(latestStxBlockFromWebSocket); + latestBurnBlock.stacks_blocks.unshift(latestStxBlockFromWebSocket.hash); + setBlockListLoading(false); + // setTimeout(() => { + // latestBurnBlockStxBlocks.unshift(latestStxBlockFromWebSocket); + // latestBurnBlock.stacks_blocks.unshift(latestStxBlockFromWebSocket.hash); + // setBlockListLoading(false); + // }, FADE_DURATION); } else { // Otherwise, we have a new burn block, in which case, adding a new burn block is the equivalent of refetching/updating the block list - updateBlockList(); + updateBlockList(); // TODO: I dont think we should query again since we have the data we need to make the update } } @@ -136,7 +138,8 @@ export function useBlockListGroupedByBtcBlockBlocksPage(blockListLimit: number) type: UIBlockType.StxBlock, height: block.height, hash: block.hash, - timestamp: block.burn_block_time, // block?.block_time TODO: this is the right timestamp to use, but it seems to be inaccurate + timestamp: block?.block_time, // TODO: this is the right timestamp to use, but it seems to be inaccurate + txsCount: block.tx_count, })), stxBlocksDisplayLimit: blockListLimit, }, diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx index abba2ee6d..086ed5372 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx @@ -3,13 +3,13 @@ import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocks'; import { useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { FADE_DURATION } from '../LayoutA/consts'; import { useBlockListContext } from '../LayoutA/context'; +import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; +import { FADE_DURATION } from '../consts'; import { UIBlockType } from '../types'; import { BlocksGroupProps } from './BurnBlockGroup'; import { useBlockListWebSocket } from './useBlockListWebSocket'; import { useInitialBlockListGroupedByBtcBlockHomePage } from './useInitialBlockListGroupedByBtcHomePage'; -import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; export function useBlockListGroupedByBtcBlockHomePage() { const queryClient = useQueryClient(); @@ -45,8 +45,8 @@ export function useBlockListGroupedByBtcBlockHomePage() { ); const { - latestBlock: latestStxBlock, - latestBlocksCount: latestStxBlocksWaitingToBeLoaded, + latestStxBlock: latestStxBlock, + latestStxBlocksCount: latestStxBlocksWaitingToBeLoaded, clearLatestBlocks: clearLatestStxBlocksFromWebSocket, } = useBlockListWebSocket(initialStxBlockHashes, initialBurnBlockHashes); // TODO: fix this diff --git a/src/app/_components/BlockList/LayoutA/Blocks.tsx b/src/app/_components/BlockList/LayoutA/Blocks.tsx index c505e34fa..c22966b49 100644 --- a/src/app/_components/BlockList/LayoutA/Blocks.tsx +++ b/src/app/_components/BlockList/LayoutA/Blocks.tsx @@ -1,11 +1,11 @@ import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; import { StxIcon } from '../../../../ui/icons'; +import { FADE_DURATION } from '../consts'; 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, @@ -31,6 +31,7 @@ export function Blocks({ const isFirstStxBlockInBurnBlock = i === 0 || (i > 0 && blockList[i - 1].type === UIBlockType.BurnBlock); // what is this check for? - (i > 0 && blockList[i - 1].type === UIBlockType.BurnBlock. It's to make sure to skip Burn Blocks that dont have any stxx txs. Stacks tx should be first return ( + // TODO: update to use new component ); - case UIBlockType.BurnBlock: + case UIBlockType.BurnBlock: // TODO: update to use new component return ( { setLatestBlocksToShow(prevLatestBlocksToShow => { diff --git a/src/app/_components/BlockList/LayoutA/UpdateBar.tsx b/src/app/_components/BlockList/LayoutA/UpdateBar.tsx index 88b2a5b07..29a5fbcec 100644 --- a/src/app/_components/BlockList/LayoutA/UpdateBar.tsx +++ b/src/app/_components/BlockList/LayoutA/UpdateBar.tsx @@ -6,7 +6,7 @@ import { Button } from '../../../../ui/Button'; import { Flex, FlexProps } from '../../../../ui/Flex'; import { Icon } from '../../../../ui/Icon'; import { Text } from '../../../../ui/Text'; -import { FADE_DURATION } from './consts'; +import { FADE_DURATION } from '../consts'; interface UpdateBarProps extends FlexProps { latestBlocksCount: number; diff --git a/src/app/_components/BlockList/LayoutA/useBlockList.ts b/src/app/_components/BlockList/LayoutA/useBlockList.ts index db1d3fc87..14e8ab09b 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList.ts @@ -4,8 +4,9 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { BurnBlock } from '@stacks/blockchain-api-client'; import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; +import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; +import { FADE_DURATION } from '../consts'; import { UIBlock, UIBlockType } from '../types'; -import { FADE_DURATION } from './consts'; import { useBlockListContext } from './context'; import { useBlockListWebSocket } from './useBlockListWebSocket'; import { useInitialBlockList } from './useInitialBlockList'; @@ -79,10 +80,11 @@ export function useBlockList(length: number) { [lastBurnBlock, secondToLastBurnBlock] ); - const { latestBlock, latestBlocksCount, clearLatestBlocks } = useBlockListWebSocket( - initialBlockHashes, - initialBurnBlockHashes - ); + const { + latestStxBlock: latestBlock, + latestStxBlocksCount: latestBlocksCount, + clearLatestBlocks, + } = useBlockListWebSocket(initialBlockHashes, initialBurnBlockHashes); const updateList = useCallback( async function () { diff --git a/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts index f985d7263..039f77bf3 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts @@ -6,24 +6,25 @@ import { UIBlockType, UISingleBlock } from '../types'; import { useSubscribeBlocks } from './useSubscribeBlocks'; export function useBlockListWebSocket( - initialBlockHashes: Set, + initialStxBlockHashes: Set, initialBurnBlockHashes: Set ) { const [latestBlocks, setLatestBlocks] = useState([]); - const [latestBlock, setLatestBlock] = useState(); - const latestBlockHashes = useRef(new Set()); + const [latestStxBlock, setLatestStxBlock] = useState(); + const latestStxBlockHashes = useRef(new Set()); const latestBurnBlockHashes = useRef(new Set()); const handleBlock = useCallback( (block: NakamotoBlock) => { function updateLatestBlocks() { + // TODO: remove function // If the block is already in the list, don't add it again - if (latestBlockHashes.current.has(block.hash) || initialBlockHashes.has(block.hash)) { + if (latestStxBlockHashes.current.has(block.hash) || initialStxBlockHashes.has(block.hash)) { return; } // Otherwise, add it to the list - setLatestBlock(block); - latestBlockHashes.current.add(block.hash); + setLatestStxBlock(block); + latestStxBlockHashes.current.add(block.hash); const isNewBurnBlock = !initialBurnBlockHashes.has(block.burn_block_hash) && @@ -54,7 +55,7 @@ export function useBlockListWebSocket( updateLatestBlocks(); }, - [initialBurnBlockHashes, initialBlockHashes] + [initialBurnBlockHashes, initialStxBlockHashes] ); useSubscribeBlocks(handleBlock); @@ -65,8 +66,8 @@ export function useBlockListWebSocket( return { latestUIBlocks: latestBlocks, - latestBlock, - latestBlocksCount: latestBlocks.filter(block => block.type === UIBlockType.StxBlock).length, + latestStxBlock: latestStxBlock, + latestStxBlocksCount: latestBlocks.filter(block => block.type === UIBlockType.StxBlock).length, clearLatestBlocks, }; } diff --git a/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx b/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx new file mode 100644 index 000000000..a32bac838 --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx @@ -0,0 +1,167 @@ +'use client'; + +import { ListFooter } from '@/common/components/ListFooter'; +import { BLOCK_LIST_QUERY_KEY } from '@/common/queries/useBlockListInfinite'; +import { Box } from '@/ui/Box'; +import { useQueryClient } from '@tanstack/react-query'; +import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { Section } from '../../../../common/components/Section'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { Blocks } from '../LayoutA/Blocks'; +import { UpdateBar } from '../LayoutA/UpdateBar'; +import { useBlockListContext } from '../LayoutA/context'; +import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; +import { FADE_DURATION } from '../consts'; +import { UISingleBlock } from '../types'; +import { BlocksPageBlockListUngroupedSkeleton } from './skeleton'; +import { useUngroupedBlockList } from './useUngroupedBlockList'; + +function runAfterFadeOut(callback: () => void) { + setTimeout(callback, FADE_DURATION); +} + +function BlocksPageUngroupedBlockListBase() { + const { + isBlockListLoading, + setBlockListLoading, + liveUpdates: isLiveUpdatesEnabled, + } = useBlockListContext(); + + // TODO: dont really need to have a separate hook for this. This is just doing all the organizing of the data behind the hook + const { initialBlockList, initialBurnBlocks, hasNextPage, isFetchingNextPage, fetchNextPage } = + useUngroupedBlockList(); + + const [latestBlocksToShow, setLatestBlocksToShow] = useState([]); + const blockList = useMemo( + () => [...latestBlocksToShow, ...initialBlockList], + [initialBlockList, latestBlocksToShow] + ); + + const stxBlockHashes = useMemo(() => { + return new Set(initialBlockList.map(block => block.hash)); + }, [initialBlockList]); + + const burnBlockHashes = useMemo(() => { + return new Set(Object.keys(initialBurnBlocks)); + }, [initialBurnBlocks]); + + const { + latestUIBlocks: latestUIBlockFromWebSocket, + latestStxBlocksCount: latestStxBlocksCountFromWebSocket, + clearLatestBlocks: clearLatestBlocksFromWebSocket, + } = useBlockListWebSocket(stxBlockHashes, burnBlockHashes); + + const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); + // This is used to trigger a fade out effect when the block list is updated. When the counter is updated, we finish loading and show the fade in effect + const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); + + useEffect(() => { + if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { + runAfterFadeOut(() => { + setBlockListLoading(false); + }); + } + }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); + + const showLatestBlocks2 = useCallback(() => { + setBlockListLoading(true); + runAfterFadeOut(() => { + setLatestBlocksToShow(prevBlockList => { + return [...latestUIBlockFromWebSocket, ...prevBlockList]; + }); + clearLatestBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }, [ + latestUIBlockFromWebSocket, + setLatestBlocksToShow, + setBlockListLoading, + clearLatestBlocksFromWebSocket, + ]); + + const queryClient = useQueryClient(); + const updateBlockListWithQuery = useCallback( + async function () { + setBlockListLoading(true); + runAfterFadeOut(async () => { + await Promise.all([ + // Invalidates queries so they will be refetched + queryClient.invalidateQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }), + ]).then(() => { + clearLatestBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }); + }, + [clearLatestBlocksFromWebSocket, queryClient, setBlockListLoading] + ); + + const prevLiveUpdatesRef = useRef(isLiveUpdatesEnabled); + const prevLatestBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); + + useEffect(() => { + const liveUpdatesToggled = prevLiveUpdatesRef.current !== isLiveUpdatesEnabled; + + const receivedLatestStxBlockFromLiveUpdates = + isLiveUpdatesEnabled && + latestStxBlocksCountFromWebSocket > 0 && + prevLatestBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; + + if (liveUpdatesToggled) { + updateBlockListWithQuery(); + } else if (receivedLatestStxBlockFromLiveUpdates) { + showLatestBlocks2(); + } + + prevLiveUpdatesRef.current = isLiveUpdatesEnabled; + prevLatestBlocksCountRef.current = latestStxBlocksCountFromWebSocket; + }, [ + isLiveUpdatesEnabled, + latestStxBlocksCountFromWebSocket, + showLatestBlocks2, + updateBlockListWithQuery, + ]); + + return ( + + {!isLiveUpdatesEnabled && ( + + )} + + + {!isLiveUpdatesEnabled && ( + + )} + + + ); +} + +export function BlocksPageUngroupedBlockList() { + return ( + + }> + + + + ); +} diff --git a/src/app/_components/BlockList/Ungrouped/skeleton.tsx b/src/app/_components/BlockList/Ungrouped/skeleton.tsx index 0deec2148..de2764d4a 100644 --- a/src/app/_components/BlockList/Ungrouped/skeleton.tsx +++ b/src/app/_components/BlockList/Ungrouped/skeleton.tsx @@ -67,9 +67,8 @@ export function BlocksPageBlockListUngroupedSkeleton() { - + - ); } diff --git a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockList.ts b/src/app/_components/BlockList/Ungrouped/useUngroupedBlockList.ts new file mode 100644 index 000000000..4929dd538 --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/useUngroupedBlockList.ts @@ -0,0 +1,82 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useMemo } 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'; + +export function useUngroupedBlockList() { + 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.StxBlock, + 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/LayoutA/consts.ts b/src/app/_components/BlockList/consts.ts similarity index 100% rename from src/app/_components/BlockList/LayoutA/consts.ts rename to src/app/_components/BlockList/consts.ts From 8f27d06a1ad02c22343e07927266842a31ea0bbb Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 5 Apr 2024 09:59:33 -0500 Subject: [PATCH 16/70] feat(grouped-by-btc-block-list-view-3): fixing up loading skeletons --- .../BlockList/Ungrouped/Blocks.tsx | 64 +++++++++++++++++++ .../BlocksPageUngroupedBlockList.tsx | 2 +- .../BlockList/Ungrouped/BtcBlockListItem.tsx | 18 ++---- .../BlockList/Ungrouped/StxBlockListItem.tsx | 28 +++++++- 4 files changed, 94 insertions(+), 18 deletions(-) create mode 100644 src/app/_components/BlockList/Ungrouped/Blocks.tsx diff --git a/src/app/_components/BlockList/Ungrouped/Blocks.tsx b/src/app/_components/BlockList/Ungrouped/Blocks.tsx new file mode 100644 index 000000000..3aef9f896 --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/Blocks.tsx @@ -0,0 +1,64 @@ +import { Icon } from '../../../../ui/Icon'; +import { Stack } from '../../../../ui/Stack'; +import { StxIcon } from '../../../../ui/icons'; +import { BlockCount } from '../LayoutA/BlockCount'; +import { FADE_DURATION } from '../consts'; +import { UIBlock, UIBlockType } from '../types'; +import { BtcBlockListItem } from './BtcBlockListItem'; +import { StxBlockListItem } from './StxBlockListItem'; + +export function Blocks({ + blockList, + isUpdateListLoading, +}: { + blockList: UIBlock[]; + isUpdateListLoading: boolean; +}) { + return ( + + {blockList.map((block, i) => { + switch (block.type) { + case UIBlockType.StxBlock: + const isFirstStxBlockInBurnBlock = + i === 0 || (i > 0 && blockList[i - 1].type === UIBlockType.BurnBlock); // what is this check for? - (i > 0 && blockList[i - 1].type === UIBlockType.BurnBlock. It's to make sure to skip Burn Blocks that dont have any stxx txs. Stacks tx should be first + return ( + // TODO: update to use new component + + ) : undefined + } + hasBorder={i < blockList.length && blockList[i + 1].type === UIBlockType.StxBlock} + /> + ); + case UIBlockType.BurnBlock: // TODO: update to use new component + return ( + + ); + case UIBlockType.Count: + return ; + } + })} + + ); +} diff --git a/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx b/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx index a32bac838..4cace6d8b 100644 --- a/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx +++ b/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx @@ -8,12 +8,12 @@ import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'rea import { Section } from '../../../../common/components/Section'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { Blocks } from '../LayoutA/Blocks'; import { UpdateBar } from '../LayoutA/UpdateBar'; import { useBlockListContext } from '../LayoutA/context'; import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; import { FADE_DURATION } from '../consts'; import { UISingleBlock } from '../types'; +import { Blocks } from './Blocks'; import { BlocksPageBlockListUngroupedSkeleton } from './skeleton'; import { useUngroupedBlockList } from './useUngroupedBlockList'; diff --git a/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx b/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx index 668bf1aec..79a9d284a 100644 --- a/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx +++ b/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx @@ -2,6 +2,7 @@ import { useColorModeValue } from '@chakra-ui/react'; import { ReactNode } from 'react'; import { BsArrowReturnLeft } from 'react-icons/bs'; +import { ExplorerLink } from '../../../../common/components/ExplorerLinks'; import { Timestamp } from '../../../../common/components/Timestamp'; import { useGlobalContext } from '../../../../common/context/useAppContext'; import { truncateMiddle } from '../../../../common/utils/utils'; @@ -9,8 +10,6 @@ import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; import { HStack } from '../../../../ui/HStack'; import { Icon } from '../../../../ui/Icon'; -import { Text } from '../../../../ui/Text'; -import { TextLink } from '../../../../ui/TextLink'; import { BitcoinIcon } from '../../../../ui/icons'; interface BtcBlockListItemProps { @@ -53,18 +52,9 @@ export function BtcBlockListItemContent({ timestamp, height, hash }: BtcBlockLis bottom={'1px'} /> - - - #{height} - - + + #{height} +  ∙ } fontSize={'xs'}> {truncateMiddle(hash, 3)} diff --git a/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx b/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx index 707ca95ea..ac27b558c 100644 --- a/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx +++ b/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx @@ -102,10 +102,32 @@ function StxBlockListItemContent({ ); } -export function StxBlockListItem({ children, hasIcon, hasBorder }: StxBlockListItemLayoutProps) { +interface StxBlockListItemProps { + height: number | string; + hash: string; + timestamp: number; + txsCount?: number; + icon?: ReactNode; + hasBorder: boolean; +} + +export function StxBlockListItem({ + height, + hash, + timestamp, + txsCount, + icon, + hasBorder, +}: StxBlockListItemProps) { return ( - - {children} + + ); } From 9ce356d8097b15a8421e286d7bca234a4858b9f5 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Mon, 8 Apr 2024 16:53:09 -0500 Subject: [PATCH 17/70] feat(grouped-by-btc-block-list-view-3): new burnblockgrp --- src/app/PageClient.tsx | 19 +- .../AnimatedBlockAndMicroblocksItem.tsx | 2 +- .../BlockList/BlockAndMicroblocksItem.tsx | 2 +- .../BlockList/{LayoutA => }/BlockCount.tsx | 10 +- .../context.ts => BlockListContext.ts} | 0 .../Provider.tsx => BlockListProvider.tsx} | 2 +- .../BlocksPageBlockList.tsx | 45 +++- src/app/_components/BlockList/Controls.tsx | 102 +++++---- .../BlocksPageBlockListGroupedByBtcBlock.tsx | 10 +- .../GroupedByBurnBlock/BlocksPageHeaders.tsx | 40 +--- .../GroupedByBurnBlock/BurnBlockGroup.tsx | 193 +++++++++++++----- .../HomePageBlockListGroupedByBtcBlock.tsx | 119 +++-------- .../GroupedByBurnBlock/NonPaginated.tsx | 73 ------- .../BlockList/GroupedByBurnBlock/skeleton.tsx | 67 +++++- ...seBlockListGroupedByBtcBlockBlocksPage.tsx | 6 +- .../useBlockListGroupedByBtcBlockHomePage.tsx | 4 +- ...seInitialBlockListGroupedByBtcHomePage.tsx | 2 +- .../HomePageBlockList.tsx | 58 ++++-- .../LayoutA/BlockListWithControls.tsx | 4 +- .../_components/BlockList/LayoutA/Blocks.tsx | 2 +- .../BlockList/LayoutA/NonPaginated.tsx | 2 +- .../BlockList/LayoutA/Paginated.tsx | 5 +- .../__tests__/BlockListWithControls.test.tsx | 2 +- .../LayoutA/use-stacks-api-socket-client.ts | 60 ------ .../BlockList/LayoutA/useBlockList copy.ts | 190 ----------------- .../BlockList/LayoutA/useBlockList.ts | 3 +- .../BlockList/LayoutA/useInitialBlockList.ts | 2 +- .../BlockList/SkeletonBlockList.tsx | 1 + .../BlockList/Ungrouped/Blocks.tsx | 4 +- .../BlocksPageUngroupedBlockList.tsx | 14 +- .../Ungrouped/HomePageUngroupedBlockList.tsx | 32 +++ .../Ungrouped/UngroupedBlocksList.tsx | 96 +++++++++ .../BlockList/Ungrouped/skeleton.tsx | 6 +- .../useUngroupedBlockListBlocksPage.ts | 118 +++++++++++ .../useUngroupedBlockListHomePage.tsx | 190 +++++++++++++++++ .../BlockList/{LayoutA => }/UpdateBar.tsx | 63 +++--- .../BlockList/UpdatedBlockList.tsx | 2 +- src/app/_components/BlockList/index.tsx | 2 +- src/app/_components/BlockList/types.ts | 26 +++ src/app/blocks/PageClient.tsx | 54 ++--- src/app/blocks/page.tsx | 7 +- src/app/blocks/skeleton.tsx | 22 +- .../loaders/skeleton-transaction.tsx | 2 +- src/common/queries/useBlockListInfinite.ts | 30 +++ ...BurnBlocks.ts => useBurnBlocksInfinite.ts} | 1 + 45 files changed, 978 insertions(+), 716 deletions(-) rename src/app/_components/BlockList/{LayoutA => }/BlockCount.tsx (82%) rename src/app/_components/BlockList/{LayoutA/context.ts => BlockListContext.ts} (100%) rename src/app/_components/BlockList/{LayoutA/Provider.tsx => BlockListProvider.tsx} (92%) rename src/app/_components/BlockList/{GroupedByBurnBlock => }/BlocksPageBlockList.tsx (53%) delete mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx rename src/app/_components/BlockList/{GroupedByBurnBlock => }/HomePageBlockList.tsx (64%) delete mode 100644 src/app/_components/BlockList/LayoutA/use-stacks-api-socket-client.ts delete mode 100644 src/app/_components/BlockList/LayoutA/useBlockList copy.ts create mode 100644 src/app/_components/BlockList/Ungrouped/HomePageUngroupedBlockList.tsx create mode 100644 src/app/_components/BlockList/Ungrouped/UngroupedBlocksList.tsx create mode 100644 src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts create mode 100644 src/app/_components/BlockList/Ungrouped/useUngroupedBlockListHomePage.tsx rename src/app/_components/BlockList/{LayoutA => }/UpdateBar.tsx (64%) rename src/common/queries/{useBurnBlocks.ts => useBurnBlocksInfinite.ts} (97%) diff --git a/src/app/PageClient.tsx b/src/app/PageClient.tsx index 1264f48de..f9efbbee3 100644 --- a/src/app/PageClient.tsx +++ b/src/app/PageClient.tsx @@ -11,33 +11,22 @@ import { UpdatedBlocksList } from './_components/BlockList/UpdatedBlockList'; import { PageTitle } from './_components/PageTitle'; import { Stats } from './_components/Stats/Stats'; -const NonPaginatedBlockListLayoutA = dynamic( - () => - import('./_components/BlockList/LayoutA/NonPaginated').then( - mod => mod.NonPaginatedBlockListLayoutA - ), +const BlocksListDynamic = dynamic( + () => import('./_components/BlockList').then(mod => mod.BlocksList), { loading: () => , ssr: false, } ); -const HomePageBlockListGroupedByBtcBlock = dynamic( - () => - import('./_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock').then( - mod => mod.HomePageBlockListGroupedByBtcBlock - ), +const HomePageBlockListDynamic = dynamic( + () => import('./_components/BlockList/HomePageBlockList').then(mod => mod.HomePageBlockList), { loading: () => , ssr: false, } ); -const BlocksList = dynamic(() => import('./_components/BlockList').then(mod => mod.BlocksList), { - loading: () => , - ssr: false, -}); - export default function Home() { const { activeNetwork, activeNetworkKey } = useGlobalContext(); return ( diff --git a/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx b/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx index 8121baa5d..71f0154b7 100644 --- a/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx +++ b/src/app/_components/BlockList/AnimatedBlockAndMicroblocksItem.tsx @@ -8,7 +8,7 @@ import { BlockAndMicroblocksItem } from './BlockAndMicroblocksItem'; import { EnhancedBlock } from './types'; export const animationDuration = 0.8; - +// TODO: delete file export const AnimatedBlockAndMicroblocksItem: FC<{ block: EnhancedBlock; onAnimationExit?: () => void; diff --git a/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx b/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx index ea7fdcfdb..a91224eb5 100644 --- a/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx +++ b/src/app/_components/BlockList/BlockAndMicroblocksItem.tsx @@ -10,7 +10,7 @@ import { Flex } from '../../../ui/Flex'; import { Text } from '../../../ui/Text'; import { BlockListItem } from './BlockListItem'; import { MicroblockListItem } from './MicroblockListItem'; - +// TODO: delete file export const BlockAndMicroblocksItem: React.FC<{ block: Block }> = ({ block }) => { return ( + import('./GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock').then( + mod => mod.BlocksPageBlockListGroupedByBtcBlock + ), + { + loading: () => , + ssr: false, + } +); + +const BlocksPageUngroupedBlockListDynamic = dynamic( + () => + import('./Ungrouped/BlocksPageUngroupedBlockList').then( + mod => mod.BlocksPageUngroupedBlockList + ), + { + loading: () => , + ssr: false, + } +); function BlocksPageBlockListBase() { const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates } = useBlockListContext(); @@ -38,7 +60,12 @@ function BlocksPageBlockListBase() { }} horizontal={true} /> - {groupedByBtc ? : } + {/* {groupedByBtc ? : } */} + {groupedByBtc ? ( + + ) : ( + + )}
); } diff --git a/src/app/_components/BlockList/Controls.tsx b/src/app/_components/BlockList/Controls.tsx index 34e87673b..a1b78faac 100644 --- a/src/app/_components/BlockList/Controls.tsx +++ b/src/app/_components/BlockList/Controls.tsx @@ -1,7 +1,8 @@ -import { Flex } from '../../../ui/Flex'; +import { Flex } from '@/ui/Flex'; + import { FormControl } from '../../../ui/FormControl'; import { FormLabel } from '../../../ui/FormLabel'; -import { Stack, StackProps } from '../../../ui/Stack'; +import { StackProps } from '../../../ui/Stack'; import { Switch, SwitchProps } from '../../../ui/Switch'; interface ControlsProps extends StackProps { @@ -10,50 +11,61 @@ interface ControlsProps extends StackProps { horizontal?: boolean; } +export function ControlsLayout({ + horizontal, + children, + ...rest +}: { + horizontal?: boolean; + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} + export function Controls({ groupByBtc, liveUpdates, horizontal, ...rest }: ControlsProps) { return ( - <> - - - - - - Group by Bitcoin block - - - - - - - Live updates - - - - + + + + + Group by Bitcoin block + + + + + + Live updates + + + ); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx index 479452bf0..b9246c6ea 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx @@ -7,8 +7,8 @@ import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { UpdateBar } from '../LayoutA/UpdateBar'; -import { useBlockListContext } from '../LayoutA/context'; +import { useBlockListContext } from '../BlockListContext'; +import { UpdateBar } from '../UpdateBar'; import { FADE_DURATION } from '../consts'; import { BurnBlockGroup } from './BurnBlockGroup'; import { BlocksPageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; @@ -31,11 +31,7 @@ function BlocksPageBlockListGroupedByBtcBlockBase() { return ( <> {!liveUpdates && ( - + )} - - - - - - - - - -
- ); -} - export function BlocksPageHeaderLayout({ lastBlockCard, averageStacksBlockTimeCard, @@ -138,20 +122,12 @@ export function BlocksPageHeaderLayout({ export function BlocksPageHeaders() { return ( - } - averageStacksBlockTimeCard={} - lastConfirmedBitcoinBlockCard={} - /> - ); -} - -export function BlockPageHeadersSkeleton() { - return ( - } - averageStacksBlockTimeCard={} - lastConfirmedBitcoinBlockCard={} - /> + }> + } + averageStacksBlockTimeCard={} + lastConfirmedBitcoinBlockCard={} + /> + ); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx index e350991d2..3e7c5eb4b 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup.tsx @@ -14,9 +14,11 @@ import { Icon } from '../../../../ui/Icon'; import { Text, TextProps } from '../../../../ui/Text'; import { BitcoinIcon, StxIcon } from '../../../../ui/icons'; import { Caption } from '../../../../ui/typography'; -import { BlockCount } from '../LayoutA/BlockCount'; +import { BlockCount } from '../BlockCount'; import { UISingleBlock } from '../types'; +const PADDING = 4; + export function ListHeader({ children, ...textProps }: { children: ReactNode } & TextProps) { const color = useColorModeValue('slate.700', 'slate.250'); return ( @@ -76,50 +78,111 @@ const GroupHeader = () => { }; // TODO: ideally this would be a table -const StxBlockRow = ({ block, icon }: { block: UISingleBlock; icon?: ReactNode }) => { +const StxBlockRow = ({ + block, + icon, + minimized = false, +}: { + block: UISingleBlock; + icon?: ReactNode; + minimized?: boolean; +}) => { return ( <> - - {icon} - - - #{block.height} + {minimized ? ( + <> + + {icon} + + + #{block.height} + + + + + ∙} gap={1} justifySelf="end"> + + + {truncateMiddle(block.hash)} + + + + {block.txsCount || 0} txn + + + + + + + ) : ( + <> + + {icon} + + + #{block.height} + + + + + + {block.hash} + + + + {block.txsCount} - - - - - {block.hash} - - - - 100 - - - - + + + + + )} ); }; @@ -167,26 +230,29 @@ export interface BlocksGroupProps { * stxBlocks: Block[]; */ stxBlocksDisplayLimit?: number; + minimized?: boolean; } export function BurnBlockGroupGrid({ burnBlock, stxBlocks, stxBlocksDisplayLimit, + minimized = false, }: BlocksGroupProps) { const stxBlocksToDisplay = stxBlocksDisplayLimit ? stxBlocks.slice(0, stxBlocksDisplayLimit) : stxBlocks; return ( - + {minimized ? null : } {stxBlocksToDisplay.map((stxBlock, i) => ( <> ) } + minimized={minimized} /> {i < stxBlocks.length - 1 && ( @@ -241,14 +308,30 @@ export function BurnBlockGroupGrid({ ); } -function BitcoinHeader({ burnBlock }: { burnBlock: UISingleBlock }) { +function BitcoinHeader({ + burnBlock, + minimized = false, +}: { + burnBlock: UISingleBlock; + minimized?: boolean; +}) { return ( - - - - - {burnBlock.height} - + + + + + + {burnBlock.height} + + ∙} gap={1}> {truncateMiddle(burnBlock.hash, 6)} @@ -262,7 +345,7 @@ function BitcoinHeader({ burnBlock }: { burnBlock: UISingleBlock }) { export function Footer({ stxBlocks, txSum }: { stxBlocks: UISingleBlock[]; txSum: number }) { return ( - ∙} gap={1} pt={4}> + ∙} gap={1} pt={4} whiteSpace="nowrap"> {stxBlocks.length} blocks @@ -281,6 +364,7 @@ export function BurnBlockGroup({ burnBlock, stxBlocks, stxBlocksDisplayLimit = stxBlocks.length, + minimized = false, }: BlocksGroupProps) { const stxBlocksNotDisplayed = burnBlock.txsCount ? burnBlock.txsCount - (stxBlocksDisplayLimit || 0) @@ -297,13 +381,14 @@ export function BurnBlockGroup({ console.log({ burnBlock, stxBlocks, stxBlocksDisplayLimit, stxBlocksNotDisplayed }); // TODO: remove // TODO: why are we not using table here? return ( - - + + {stxBlocksNotDisplayed > 0 ? : null} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx index f73eebe7a..bc0a42fd5 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx @@ -1,17 +1,11 @@ 'use client'; -import { Stack } from '@/ui/Stack'; -import { Suspense, useCallback, useRef } from 'react'; +import { Suspense } from 'react'; -import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; -import { Text } from '../../../../ui/Text'; -import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { Controls } from '../Controls'; -import { BlockListProvider } from '../LayoutA/Provider'; -import { UpdateBar } from '../LayoutA/UpdateBar'; -import { useBlockListContext } from '../LayoutA/context'; +import { useBlockListContext } from '../BlockListContext'; +import { UpdateBar } from '../UpdateBar'; import { FADE_DURATION } from '../consts'; import { BurnBlockGroup } from './BurnBlockGroup'; import { useBlockListGroupedByBtcBlockHomePage } from './useBlockListGroupedByBtcBlockHomePage'; @@ -20,94 +14,41 @@ import { HomePageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; // const LIST_LENGTH = 17; function HomePageBlockListGroupedByBtcBlockBase() { - const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates, isBlockListLoading } = - useBlockListContext(); + const { liveUpdates, isBlockListLoading } = useBlockListContext(); const { blockList, updateBlockList, latestBlocksCount } = useBlockListGroupedByBtcBlockHomePage(); - const lastClickTimeRef = useRef(0); - const toggleLiveUpdates = useCallback(() => { - const now = Date.now(); - if (now - lastClickTimeRef.current > 2000) { - lastClickTimeRef.current = now; - setLiveUpdates(!liveUpdates); - } - }, [liveUpdates, setLiveUpdates]); return ( -
- - - Recent Blocks - { - setGroupedByBtc(!groupedByBtc); - }, - isChecked: groupedByBtc, - // isDisabled: true, - }} - liveUpdates={{ - onChange: toggleLiveUpdates, - isChecked: liveUpdates, - }} - padding={0} - gap={3} - marginX={0} - border="none" + + {!liveUpdates && ( + + )} + + {blockList.map(block => ( + - - {!liveUpdates && ( - - )} - - {blockList.map(block => ( - - ))} - - -
+ ))} +
+ ); } export function HomePageBlockListGroupedByBtcBlock() { return ( - - - }> - - - - + }> + + ); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx deleted file mode 100644 index d7a1c54c3..000000000 --- a/src/app/_components/BlockList/GroupedByBurnBlock/NonPaginated.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client'; - -import { Section } from '../../../../common/components/Section'; -import { Box } from '../../../../ui/Box'; -import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { BlockListProvider } from '../LayoutA/Provider'; -import { UIBlockType } from '../types'; -import { BurnBlockGroup } from './BurnBlockGroup'; - -const LIST_LENGTH = 17; - -function NonPaginatedBlockListGroupedByBurnBlockBase() { - const blockList = [ - { - type: UIBlockType.StxBlock, - height: 10001, - hash: '0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - }, - { - type: UIBlockType.StxBlock, - height: 10002, - hash: '0xrerqreqwjdhgjhdgj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - }, - { - type: UIBlockType.StxBlock, - height: 10003, - hash: '0xbxvcbxvcbvxcbvxc0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - }, - { - type: UIBlockType.StxBlock, - height: 10004, - hash: '0xjhjhfhgjhdjdhjhhj0xfdsadfdasfdasfjhdgf0xfdsadfdasfdasfjhdgf', - }, - ]; - - const burnBlock = { - height: 332141, - hash: '0xhfgjdkhbafgkjhdafjkhdsafjkhflkjdsahfjkhdsafhdsafdsaf', - timestamp: 0, - }; - - return ( -
- - - -
- ); -} - -export function NonPaginatedBlockListGroupedByBurnBlock() { - return ( - - - - - - ); -} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx index 96f37d739..fddf0cee4 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx @@ -1,11 +1,16 @@ -import { SkeletonCircle, useColorModeValue } from '@chakra-ui/react'; +import { useColorModeValue } from '@chakra-ui/react'; +import { Circle } from '../../../../common/components/Circle'; import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; import { Grid } from '../../../../ui/Grid'; import { SkeletonText } from '../../../../ui/SkeletonText'; -import { Circle } from '../../../../common/components/Circle'; +import { Stack } from '../../../../ui/Stack'; +import { Text } from '../../../../ui/Text'; +import { ControlsLayout } from '../Controls'; +import { UpdateBarLayout } from '../UpdateBar'; +import { BlocksPageHeaderLayout } from './BlocksPageHeaders'; function BitcoinHeaderSkeleton() { return ( @@ -148,3 +153,61 @@ export function BlocksPageBlockListGroupedByBtcBlockSkeleton() { /> ); } + +export function BlockPageHeaderSkeleton() { + return ( + + + + + + + + + + + + ); +} + +export function BlockPageHeadersSkeleton() { + return ( + } + averageStacksBlockTimeCard={} + lastConfirmedBitcoinBlockCard={} + /> + ); +} + +function ControlsSkeleton({ horizontal }: { horizontal?: boolean }) { + return ( + + + + + + + ); +} + +function UpdateBarSkeleton() { + return ( + + + + + + + ); +} + +export function BlocksPageBlockListSkeleton() { + return ( +
+ + + +
+ ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx index 559260c88..5d02244f7 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx @@ -1,5 +1,5 @@ import { GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY } from '@/common/queries/useBlocksByBurnBlock'; -import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocks'; +import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocksInfinite'; import { useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useRef } from 'react'; @@ -7,8 +7,8 @@ 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'; -import { useBlockListContext } from '../LayoutA/context'; +import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocksInfinite'; +import { useBlockListContext } from '../BlockListContext'; import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; import { UIBlockType, UISingleBlock } from '../types'; import { BlocksGroupProps } from './BurnBlockGroup'; diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx index 086ed5372..17363a073 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx @@ -1,9 +1,9 @@ import { GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY } from '@/common/queries/useBlocksByBurnBlock'; -import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocks'; +import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocksInfinite'; import { useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { useBlockListContext } from '../LayoutA/context'; +import { useBlockListContext } from '../BlockListContext'; import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; import { FADE_DURATION } from '../consts'; import { UIBlockType } from '../types'; diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx index 06cdaeb3b..d60fd05ef 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx @@ -2,7 +2,7 @@ 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'; +import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocksInfinite'; const BURN_BLOCK_LENGTH = 3; const STX_BLOCK_LENGTH = 3; diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockList.tsx b/src/app/_components/BlockList/HomePageBlockList.tsx similarity index 64% rename from src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockList.tsx rename to src/app/_components/BlockList/HomePageBlockList.tsx index 36507bcf0..81c3811ef 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockList.tsx +++ b/src/app/_components/BlockList/HomePageBlockList.tsx @@ -1,21 +1,39 @@ 'use client'; import { Stack } from '@/ui/Stack'; +import dynamic from 'next/dynamic'; import { Suspense, useCallback, useRef } from 'react'; -import { Section } from '../../../../common/components/Section'; -import { Box } from '../../../../ui/Box'; -import { Flex } from '../../../../ui/Flex'; -import { Text } from '../../../../ui/Text'; -import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { Controls } from '../Controls'; -import { BlockListProvider } from '../LayoutA/Provider'; -import { UpdateBar } from '../LayoutA/UpdateBar'; -import { FADE_DURATION } from '../LayoutA/consts'; -import { useBlockListContext } from '../LayoutA/context'; -import { BurnBlockGroup } from './BurnBlockGroup'; -import { useBlockListGroupedByBtcBlockHomePage } from './useBlockListGroupedByBtcBlockHomePage'; -import { HomePageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; +import { Section } from '../../../common/components/Section'; +import { Box } from '../../../ui/Box'; +import { Text } from '../../../ui/Text'; +import { ExplorerErrorBoundary } from '../ErrorBoundary'; +import { useBlockListContext } from './BlockListContext'; +import { BlockListProvider } from './BlockListProvider'; +import { Controls } from './Controls'; +import { HomePageBlockListGroupedByBtcBlock } from './GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock'; +import { HomePageBlockListGroupedByBtcBlockSkeleton } from './GroupedByBurnBlock/skeleton'; +import { HomePageBlockListUngroupedSkeleton } from './Ungrouped/skeleton'; + +const HomePageBlockListGroupedByBtcBlockDynamic = dynamic( + () => + import('./GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock').then( + mod => mod.HomePageBlockListGroupedByBtcBlock + ), + { + loading: () => , + ssr: false, + } +); + +const HomePageUngroupedBlockListDynamic = dynamic( + () => + import('./Ungrouped/HomePageUngroupedBlockList').then(mod => mod.HomePageUngroupedBlockList), + { + loading: () => , + ssr: false, + } +); // const LIST_LENGTH = 17; @@ -32,6 +50,7 @@ function HomePageBlockListGroupedByBtcBlockBase() { setLiveUpdates(!liveUpdates); } }, [liveUpdates, setLiveUpdates]); + return (
@@ -60,13 +79,10 @@ function HomePageBlockListGroupedByBtcBlockBase() { border="none" /> - {!liveUpdates && ( - + {groupedByBtc ? ( + + ) : ( + )} }> - + diff --git a/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx b/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx index d85e81dd7..d2ba297e5 100644 --- a/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx +++ b/src/app/_components/BlockList/LayoutA/BlockListWithControls.tsx @@ -3,11 +3,11 @@ import { useCallback, useRef } from 'react'; import { ListFooter } from '../../../../common/components/ListFooter'; import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; +import { useBlockListContext } from '../BlockListContext'; import { Controls } from '../Controls'; +import { UpdateBar } from '../UpdateBar'; import { UIBlock } from '../types'; import { Blocks } from './Blocks'; -import { UpdateBar } from './UpdateBar'; -import { useBlockListContext } from './context'; export function BlockListWithControls({ blockList, diff --git a/src/app/_components/BlockList/LayoutA/Blocks.tsx b/src/app/_components/BlockList/LayoutA/Blocks.tsx index c22966b49..6141321e5 100644 --- a/src/app/_components/BlockList/LayoutA/Blocks.tsx +++ b/src/app/_components/BlockList/LayoutA/Blocks.tsx @@ -1,9 +1,9 @@ import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; import { StxIcon } from '../../../../ui/icons'; +import { BlockCount } from '../BlockCount'; import { FADE_DURATION } from '../consts'; import { UIBlock, UIBlockType } from '../types'; -import { BlockCount } from './BlockCount'; import { BurnBlock } from './BurnBlock'; import { StxBlock } from './StxBlock'; diff --git a/src/app/_components/BlockList/LayoutA/NonPaginated.tsx b/src/app/_components/BlockList/LayoutA/NonPaginated.tsx index ee05ee7ed..fb12be118 100644 --- a/src/app/_components/BlockList/LayoutA/NonPaginated.tsx +++ b/src/app/_components/BlockList/LayoutA/NonPaginated.tsx @@ -2,8 +2,8 @@ import { Section } from '../../../../common/components/Section'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { BlockListProvider } from '../BlockListProvider'; import { BlockListWithControls } from './BlockListWithControls'; -import { BlockListProvider } from './Provider'; import { useBlockList } from './useBlockList'; const LIST_LENGTH = 17; diff --git a/src/app/_components/BlockList/LayoutA/Paginated.tsx b/src/app/_components/BlockList/LayoutA/Paginated.tsx index 9cbc69e54..3c8ac00ee 100644 --- a/src/app/_components/BlockList/LayoutA/Paginated.tsx +++ b/src/app/_components/BlockList/LayoutA/Paginated.tsx @@ -4,13 +4,12 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { Section } from '../../../../common/components/Section'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { useBlockListContext } from '../BlockListContext'; +import { BlockListProvider } from '../BlockListProvider'; import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; import { FADE_DURATION } from '../consts'; import { UISingleBlock } from '../types'; import { BlockListWithControls } from './BlockListWithControls'; -import { BlockListProvider } from './Provider'; -import { useBlockListContext } from './context'; -import { useBlockListWebSocket } from './useBlockListWebSocket'; import { usePaginatedBlockList } from './usePaginatedBlockList'; function PaginatedBlockListLayoutABase() { diff --git a/src/app/_components/BlockList/LayoutA/__tests__/BlockListWithControls.test.tsx b/src/app/_components/BlockList/LayoutA/__tests__/BlockListWithControls.test.tsx index 686286b9a..dee590ba5 100644 --- a/src/app/_components/BlockList/LayoutA/__tests__/BlockListWithControls.test.tsx +++ b/src/app/_components/BlockList/LayoutA/__tests__/BlockListWithControls.test.tsx @@ -1,8 +1,8 @@ import { render } from '@testing-library/react'; +import { BlockListProvider } from '../../BlockListProvider'; import { UIBlock, UIBlockType } from '../../types'; import { BlockListWithControls } from '../BlockListWithControls'; -import { BlockListProvider } from '../Provider'; const date = new Date('2024-01-01T00:00:00Z').getTime(); diff --git a/src/app/_components/BlockList/LayoutA/use-stacks-api-socket-client.ts b/src/app/_components/BlockList/LayoutA/use-stacks-api-socket-client.ts deleted file mode 100644 index 81c67e348..000000000 --- a/src/app/_components/BlockList/LayoutA/use-stacks-api-socket-client.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { useCallback, useRef } from 'react'; - -import { StacksApiSocketClient } from '@stacks/blockchain-api-client'; - -import { useGlobalContext } from '../../../../common/context/useAppContext'; - -export function useStacksApiSocketClient(): { - connection: StacksApiSocketClient | null; - connect: (handleOnConnect?: (socketClient?: StacksApiSocketClient) => void) => void; - disconnect: () => void; -} { - const socketClient = useRef(null); - const socketUrlTracker = useRef(null); - const isSocketClientConnecting = useRef(false); - const activeNetwork = useGlobalContext().activeNetwork; - - const connect = useCallback( - async (handleOnConnect?: (sc?: StacksApiSocketClient) => void) => { - if (socketClient.current?.socket.connected || isSocketClientConnecting.current) { - return; - } - try { - isSocketClientConnecting.current = true; - const socketUrl = `https://api.${activeNetwork.mode}.hiro.so/`; - socketUrlTracker.current = socketUrl; - const connection = StacksApiSocketClient.connect({ url: socketUrl }); - socketClient.current = connection; - socketClient.current.socket.on('connect', () => { - console.log('Connected to socket. About to run handleOnConnect') - handleOnConnect?.(socketClient.current || undefined); - isSocketClientConnecting.current = false; - }); - socketClient.current.socket.on('disconnect', () => { - console.log('Disconnected from socket') - isSocketClientConnecting.current = false; - }); - socketClient.current.socket.on('connect_error', error => { - console.error('Socket connection error', error); - isSocketClientConnecting.current = false; - }); - } catch (error) { - isSocketClientConnecting.current = false; - } - }, - [activeNetwork.mode] - ); - - const disconnect = useCallback(() => { - if (socketClient.current?.socket.connected) { - console.log('Disconnecting from socket') - socketClient.current.socket.close(); - } - }, []); - - return { - connection: socketClient.current, - connect, - disconnect, - }; -} diff --git a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts b/src/app/_components/BlockList/LayoutA/useBlockList copy.ts deleted file mode 100644 index d9e364631..000000000 --- a/src/app/_components/BlockList/LayoutA/useBlockList copy.ts +++ /dev/null @@ -1,190 +0,0 @@ -import { useGlobalContext } from '@/common/context/useAppContext'; -import { useSuspenseInfiniteQueryResult } from '@/common/hooks/useInfiniteQueryResult'; -import { useSuspenseBlockListInfinite } from '@/common/queries/useBlockListInfinite'; -import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useEffect, useState } from 'react'; - -import { BurnBlock, connectWebSocketClient } from '@stacks/blockchain-api-client'; -import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; -import { Block } from '@stacks/stacks-blockchain-api-types'; - -import { EnhancedBlock, UIBlock, UIBlockType } from '../types'; - -// interface Subscription { -// unsubscribe(): Promise; -// } - -const createBurnBlockUIBlock = (burnBlock: BurnBlock): UIBlock => ({ - type: UIBlockType.BurnBlock, - height: burnBlock.burn_block_height, - hash: burnBlock.burn_block_hash, - timestamp: burnBlock.burn_block_time, -}); - -const createBlockUIBlock = (block: NakamotoBlock): UIBlock => ({ - type: UIBlockType.StxBlock, - height: block.height, - hash: block.hash, - timestamp: block.burn_block_time, - txsCount: block.tx_count, -}); - -const createCountUIBlock = (count: number): UIBlock => ({ - type: UIBlockType.Count, - count, -}); - -const createUIBlockList = ( - burnBlock: BurnBlock, - stxBlocks: NakamotoBlock[], - length: number -): UIBlock[] => { - const blockList: UIBlock[] = [createBurnBlockUIBlock(burnBlock)]; - if (length <= 1) { - return blockList; - } - const hasCount = burnBlock.stacks_blocks.length > length - 1; - const stxBlocksToShow = stxBlocks.slice(0, length - 1 - (hasCount ? 1 : 0)); - - if (hasCount) { - blockList.unshift(createCountUIBlock(burnBlock.stacks_blocks.length - stxBlocksToShow.length)); - } - - stxBlocksToShow.reverse().forEach(block => { - blockList.unshift(createBlockUIBlock(block)); - }); - - return blockList; -}; - -interface BlocksGroupedByParentHash { - [btcBlockHeight: string]: EnhancedBlock[]; -} - -function groupBlocksByBtcBlock(blocks: Block[]): Record { - const groupedBlocks: Record = {}; - - blocks.forEach(block => { - const btcBlockNum = block.burn_block_height; - if (!groupedBlocks[btcBlockNum]) { - groupedBlocks[btcBlockNum] = [block]; - } else { - groupedBlocks[btcBlockNum].push(block); - } - }); - - return groupedBlocks; -} - -export function useBlockList2(limit?: number): { - setIsLive: (value: React.SetStateAction) => void; - isLive: boolean; - setIsGroupedByBtcBlock: (value: React.SetStateAction) => void; - isGroupedByBtcBlock: boolean; - isFetchingNextPage: boolean; - fetchNextPage: () => void; - hasNextPage: boolean; - blocks: EnhancedBlock[] | BlocksGroupedByParentHash; - blocksGroupedByBtcBlock: BlocksGroupedByParentHash; - removeOldBlock: (block: EnhancedBlock) => void; -} { - const [isLive, setIsLive] = useState(false); - const [isGroupedByBtcBlock, setIsGroupedByBtcBlock] = useState(false); - - const [initialBlocks, setInitialBlocks] = useState([]); - const [latestBlocks, setLatestBlocks] = useState([]); - - const activeNetwork = useGlobalContext().activeNetwork; - - const response = useSuspenseBlockListInfinite(); // queryKey: ['blockListInfinite', limit] - console.log('useBlockList copy', { response }); - - const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; - const blocks = useSuspenseInfiniteQueryResult(response, limit); - console.log('useBlockList copy', { blocks }); - - // const { data: blocks, isFetchingNextPage, fetchNextPage, hasNextPage } = useSuspenseBlockListInfinite(); - - const queryClient = useQueryClient(); - - useEffect(() => { - setInitialBlocks(blocks); - }, [blocks]); - - // const { connect: stacksApiSocketConnect, disconnect: stacksApiSocketDisconnect } = - // useStacksApiSocketClient(); - - useEffect(() => { - // if (isLive) { - // void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); - // stacksApiSocketConnect((socketClient: StacksApiSocketClient | undefined) => { - // console.log('socketClient?.subscribeBlocks...'); - // socketClient?.subscribeBlocks((block: any) => { - // console.log('new block received', block); - // setLatestBlocks(prevLatestBlocks => [ - // // TODO: or I could just push this onto the blocks array - // { ...block, microblock_tx_count: {}, animate: true }, - // ...prevLatestBlocks, - // ]); - // }); - // }); - // } else { - // stacksApiSocketDisconnect(); - // } - if (!isLive) return; - void queryClient.invalidateQueries({ queryKey: ['blockListInfinite'] }); - let sub: { - unsubscribe?: () => Promise; - }; - const subscribe = async () => { - const client = await connectWebSocketClient(activeNetwork.url.replace('https://', 'wss://')); // TODO: Save this as ref so that when the live toggle is switched off, we can close the connection. Return subscribe and unsunscribe functions from the hook - sub = await client.subscribeBlocks((block: any) => { - setLatestBlocks(prevLatestBlocks => [ - { ...block, microblock_tx_count: {}, animate: true }, - ...prevLatestBlocks, - ]); - }); - }; - void subscribe(); - return () => { - if (sub?.unsubscribe) { - void sub.unsubscribe(); - } - }; - }, [isLive, activeNetwork.url, queryClient]); - - // const allBlocks = useMemo(() => { - // return [...latestBlocks, ...initialBlocks] - // .sort((a, b) => (b.height || 0) - (a.height || 0)) - // .reduce((acc: EnhancedBlock[], block, index) => { - // if (!acc.some(b => b.height === block.height)) { - // acc.push({ ...block, destroy: index >= (limit || DEFAULT_LIST_LIMIT) }); - // } - // return acc; - // }, []); - // }, [initialBlocks, latestBlocks, limit]); - - const removeOldBlock = useCallback((block: EnhancedBlock) => { - setInitialBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); - setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); - }, []); - - let formattedBlocks = [...latestBlocks, ...initialBlocks].sort( - (a, b) => (b.height || 0) - (a.height || 0) - ); // desc - - const blocksGroupedByParentHash = groupBlocksByBtcBlock(blocks); - - return { - setIsLive, - isLive, - setIsGroupedByBtcBlock, - isGroupedByBtcBlock, - isFetchingNextPage, - fetchNextPage, - hasNextPage, - blocks: formattedBlocks, - blocksGroupedByBtcBlock: blocksGroupedByParentHash, - removeOldBlock, - }; -} diff --git a/src/app/_components/BlockList/LayoutA/useBlockList.ts b/src/app/_components/BlockList/LayoutA/useBlockList.ts index 14e8ab09b..728bfa394 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockList.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockList.ts @@ -4,11 +4,10 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { BurnBlock } from '@stacks/blockchain-api-client'; import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; +import { useBlockListContext } from '../BlockListContext'; import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; import { FADE_DURATION } from '../consts'; import { UIBlock, UIBlockType } from '../types'; -import { useBlockListContext } from './context'; -import { useBlockListWebSocket } from './useBlockListWebSocket'; import { useInitialBlockList } from './useInitialBlockList'; const createBurnBlockUIBlock = (burnBlock: BurnBlock): UIBlock => ({ diff --git a/src/app/_components/BlockList/LayoutA/useInitialBlockList.ts b/src/app/_components/BlockList/LayoutA/useInitialBlockList.ts index 07740d697..86e0a8e34 100644 --- a/src/app/_components/BlockList/LayoutA/useInitialBlockList.ts +++ b/src/app/_components/BlockList/LayoutA/useInitialBlockList.ts @@ -2,7 +2,7 @@ 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'; +import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocksInfinite'; const BURN_BLOCK_LENGTH = 2; const STX_BLOCK_LENGTH = 20; diff --git a/src/app/_components/BlockList/SkeletonBlockList.tsx b/src/app/_components/BlockList/SkeletonBlockList.tsx index 2e6534f9a..14647ce11 100644 --- a/src/app/_components/BlockList/SkeletonBlockList.tsx +++ b/src/app/_components/BlockList/SkeletonBlockList.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { Section } from '../../../common/components/Section'; import { TwoColumnsListItemSkeleton } from '../../../common/components/TwoColumnsListItemSkeleton'; +// TODO: delete export const SkeletonBlockList = () => { return (
diff --git a/src/app/_components/BlockList/Ungrouped/Blocks.tsx b/src/app/_components/BlockList/Ungrouped/Blocks.tsx index 3aef9f896..1b806f7c0 100644 --- a/src/app/_components/BlockList/Ungrouped/Blocks.tsx +++ b/src/app/_components/BlockList/Ungrouped/Blocks.tsx @@ -1,7 +1,7 @@ import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; import { StxIcon } from '../../../../ui/icons'; -import { BlockCount } from '../LayoutA/BlockCount'; +import { BlockCount } from '../BlockCount'; import { FADE_DURATION } from '../consts'; import { UIBlock, UIBlockType } from '../types'; import { BtcBlockListItem } from './BtcBlockListItem'; @@ -10,9 +10,11 @@ import { StxBlockListItem } from './StxBlockListItem'; export function Blocks({ blockList, isUpdateListLoading, + stxBlocksCountLimit, }: { blockList: UIBlock[]; isUpdateListLoading: boolean; + stxBlocksCountLimit: number; }) { return ( { + const blockHashes = useMemo(() => { return new Set(initialBlockList.map(block => block.hash)); }, [initialBlockList]); @@ -50,7 +50,7 @@ function BlocksPageUngroupedBlockListBase() { latestUIBlocks: latestUIBlockFromWebSocket, latestStxBlocksCount: latestStxBlocksCountFromWebSocket, clearLatestBlocks: clearLatestBlocksFromWebSocket, - } = useBlockListWebSocket(stxBlockHashes, burnBlockHashes); + } = useBlockListWebSocket(blockHashes, burnBlockHashes); const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); // This is used to trigger a fade out effect when the block list is updated. When the counter is updated, we finish loading and show the fade in effect @@ -64,7 +64,7 @@ function BlocksPageUngroupedBlockListBase() { } }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); - const showLatestBlocks2 = useCallback(() => { + const showLatestBlocks = useCallback(() => { setBlockListLoading(true); runAfterFadeOut(() => { setLatestBlocksToShow(prevBlockList => { @@ -111,7 +111,7 @@ function BlocksPageUngroupedBlockListBase() { if (liveUpdatesToggled) { updateBlockListWithQuery(); } else if (receivedLatestStxBlockFromLiveUpdates) { - showLatestBlocks2(); + showLatestBlocks(); } prevLiveUpdatesRef.current = isLiveUpdatesEnabled; @@ -119,7 +119,7 @@ function BlocksPageUngroupedBlockListBase() { }, [ isLiveUpdatesEnabled, latestStxBlocksCountFromWebSocket, - showLatestBlocks2, + showLatestBlocks, updateBlockListWithQuery, ]); diff --git a/src/app/_components/BlockList/Ungrouped/HomePageUngroupedBlockList.tsx b/src/app/_components/BlockList/Ungrouped/HomePageUngroupedBlockList.tsx new file mode 100644 index 000000000..04a4185d9 --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/HomePageUngroupedBlockList.tsx @@ -0,0 +1,32 @@ +'use client'; + +import { ListFooter } from '../../../../common/components/ListFooter'; +import { Box } from '../../../../ui/Box'; +import { useBlockListContext } from '../BlockListContext'; +import { UpdateBar } from '../UpdateBar'; +import { UngroupedBlockList } from './UngroupedBlocksList'; +import { useUngroupedBlockListHomePage } from './useUngroupedBlockListHomePage'; + +// TODO: Create a layout component for this. It'll use the same one as +export function HomePageUngroupedBlockList() { + const { liveUpdates } = useBlockListContext(); + const { + latestBlocksCount: latestStxBlocksCountFromWebSocket, + updateBlockList, + blocksList, + } = useUngroupedBlockListHomePage(); + console.log({ blocksList }); + + return ( + + {!liveUpdates && ( + + )} + + {!liveUpdates && } + + ); +} diff --git a/src/app/_components/BlockList/Ungrouped/UngroupedBlocksList.tsx b/src/app/_components/BlockList/Ungrouped/UngroupedBlocksList.tsx new file mode 100644 index 000000000..003d188a3 --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/UngroupedBlocksList.tsx @@ -0,0 +1,96 @@ +import { ReactNode } from 'react'; + +import { Icon } from '../../../../ui/Icon'; +import { Stack } from '../../../../ui/Stack'; +import { StxIcon } from '../../../../ui/icons'; +import { BlockCount } from '../BlockCount'; +import { useBlockListContext } from '../BlockListContext'; +import { FADE_DURATION } from '../consts'; +import { BlockListBtcBlock, BlockListStxBlock } from '../types'; +import { BtcBlockListItem } from './BtcBlockListItem'; +import { StxBlockListItem } from './StxBlockListItem'; + +export interface BlocksByBtcBlock { + stxBlocks: BlockListStxBlock[]; + btcBlock: BlockListBtcBlock; +} + +export type UngroupedBlockList = BlocksByBtcBlock[]; // Ironic the ungrouped block list is grouped by btc block... + +export function UngroupedBlockListLayout({ children }: { children: ReactNode }) { + const { isBlockListLoading } = useBlockListContext(); + + return ( + + {children} + + ); +} + +function BlocksGroupedByBtcBlock({ + blocks, + stxBlocksLimit, +}: { + blocks: BlocksByBtcBlock; + stxBlocksLimit: number; +}) { + const btcBlock = blocks.btcBlock; + const stxBlocks = blocks.stxBlocks; + const stxBlocksShortList = stxBlocksLimit + ? blocks.stxBlocks.slice(0, stxBlocksLimit) + : blocks.stxBlocks; + + return ( + <> + {stxBlocksShortList.map((block, i) => ( + : undefined} + hasBorder={i < stxBlocks.length - 1} + /> + ))} + {stxBlocks.length > stxBlocksLimit && ( + + )} + + + ); +} + +export function UngroupedBlockList({ + ungroupedBlockList, + stxBlocksLimit, +}: { + ungroupedBlockList: UngroupedBlockList; + stxBlocksLimit: number; +}) { + return ( + + {ungroupedBlockList.map((blocksGroupedByBtcBlock, i) => ( + + ))} + + ); +} diff --git a/src/app/_components/BlockList/Ungrouped/skeleton.tsx b/src/app/_components/BlockList/Ungrouped/skeleton.tsx index de2764d4a..ce504e7fc 100644 --- a/src/app/_components/BlockList/Ungrouped/skeleton.tsx +++ b/src/app/_components/BlockList/Ungrouped/skeleton.tsx @@ -4,7 +4,7 @@ import { Circle } from '../../../../common/components/Circle'; import { Flex } from '../../../../ui/Flex'; import { SkeletonText } from '../../../../ui/SkeletonText'; import { BtcBlockListItemLayout } from './BtcBlockListItem'; -import { StxBlockListItem } from './StxBlockListItem'; +import { StxBlockListItemLayout } from './StxBlockListItem'; function StxBlockListItemContentSkeleton({ hasIcon }: { hasIcon: boolean }) { return ( @@ -25,9 +25,9 @@ function StxBlockListItemSkeleton({ hasBorder: boolean; }) { return ( - + - + ); } diff --git a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts b/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts new file mode 100644 index 000000000..23af311fc --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts @@ -0,0 +1,118 @@ +'use client'; + +import { BLOCK_LIST_QUERY_KEY } from '@/common/queries/useBlockListInfinite'; +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { useBlockListContext } from '../BlockListContext'; +import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; +import { FADE_DURATION } from '../consts'; +import { UISingleBlock } from '../types'; +import { useUngroupedBlockList } from './useUngroupedBlockList'; + +function runAfterFadeOut(callback: () => void) { + setTimeout(callback, FADE_DURATION); +} + +export function useUngroupedBlockListBlocksPage() { + const { + isBlockListLoading, + setBlockListLoading, + liveUpdates: isLiveUpdatesEnabled, + } = useBlockListContext(); + + // TODO: dont really need to have a separate hook for this. This is just doing all the organizing of the data behind the hook + const { initialBlockList, initialBurnBlocks, hasNextPage, isFetchingNextPage, fetchNextPage } = + useUngroupedBlockList(); + + const [latestBlocksToShow, setLatestBlocksToShow] = useState([]); + const blockList = useMemo( + () => [...latestBlocksToShow, ...initialBlockList], + [initialBlockList, latestBlocksToShow] + ); + + const blockHashes = useMemo(() => { + return new Set(initialBlockList.map(block => block.hash)); + }, [initialBlockList]); + + const burnBlockHashes = useMemo(() => { + return new Set(Object.keys(initialBurnBlocks)); + }, [initialBurnBlocks]); + + const { + latestUIBlocks: latestUIBlockFromWebSocket, + latestStxBlocksCount: latestStxBlocksCountFromWebSocket, + clearLatestBlocks: clearLatestBlocksFromWebSocket, + } = useBlockListWebSocket(blockHashes, burnBlockHashes); + + const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); + // This is used to trigger a fade out effect when the block list is updated. When the counter is updated, we finish loading and show the fade in effect + const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); + + useEffect(() => { + if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { + runAfterFadeOut(() => { + setBlockListLoading(false); + }); + } + }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); + + const showLatestBlocks = useCallback(() => { + setBlockListLoading(true); + runAfterFadeOut(() => { + setLatestBlocksToShow(prevBlockList => { + return [...latestUIBlockFromWebSocket, ...prevBlockList]; + }); + clearLatestBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }, [ + latestUIBlockFromWebSocket, + setLatestBlocksToShow, + setBlockListLoading, + clearLatestBlocksFromWebSocket, + ]); + + const queryClient = useQueryClient(); + const updateBlockListWithQuery = useCallback( + async function () { + setBlockListLoading(true); + runAfterFadeOut(async () => { + await Promise.all([ + // Invalidates queries so they will be refetched + queryClient.invalidateQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }), + ]).then(() => { + clearLatestBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }); + }, + [clearLatestBlocksFromWebSocket, queryClient, setBlockListLoading] + ); + + const prevLiveUpdatesRef = useRef(isLiveUpdatesEnabled); + const prevLatestBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); + + useEffect(() => { + const liveUpdatesToggled = prevLiveUpdatesRef.current !== isLiveUpdatesEnabled; + + const receivedLatestStxBlockFromLiveUpdates = + isLiveUpdatesEnabled && + latestStxBlocksCountFromWebSocket > 0 && + prevLatestBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; + + if (liveUpdatesToggled) { + updateBlockListWithQuery(); + } else if (receivedLatestStxBlockFromLiveUpdates) { + showLatestBlocks(); + } + + prevLiveUpdatesRef.current = isLiveUpdatesEnabled; + prevLatestBlocksCountRef.current = latestStxBlocksCountFromWebSocket; + }, [ + isLiveUpdatesEnabled, + latestStxBlocksCountFromWebSocket, + showLatestBlocks, + updateBlockListWithQuery, + ]); +} diff --git a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListHomePage.tsx b/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListHomePage.tsx new file mode 100644 index 000000000..b793f2139 --- /dev/null +++ b/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListHomePage.tsx @@ -0,0 +1,190 @@ +'use client'; + +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; + +import { NakamotoBlock } from '@stacks/blockchain-api-client'; + +import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; +import { + BLOCK_LIST_QUERY_KEY, + useSuspenseBlocksInfiniteNew, +} from '../../../../common/queries/useBlockListInfinite'; +import { useBlockListContext } from '../BlockListContext'; +import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; +import { FADE_DURATION } from '../consts'; +import { BlockListBtcBlock, BlockListStxBlock, UISingleBlock } from '../types'; + +const LIMIT = 3; + +// TODO: move into shared file +function runAfterFadeOut(callback: () => void) { + setTimeout(callback, FADE_DURATION); +} +/** + * Fetch the initial stx blocks and burn blocks + * Convert into rows and render + * Fetch the latest stx blocks and burn blocks from websocket + * To update, invalidate queries to requery + * If live, + * If just toggling live, requery to update + * If receving new blocks while live, reorganize state to accomodate new blocks + */ +export function useUngroupedBlockListHomePage() { + const { setBlockListLoading, liveUpdates } = useBlockListContext(); + + const [latestBlocks, setLatestBlocks] = useState([]); + + // TODO: + // what if I queried for recent btc blocks. Took the first three. then queried for stx blocks for those btc blocks + // This would give me the list of stx blocks for the first three btc blocks that I would need to show on the homepage + // When updating just requery + // when live updates are on, either insert them into the first btc block, or create a new btc block and pop the last one + // Do this only if we require a more balanced view + // This requires 4 queries + // For now I can just rely on the blocks endpoint and show what I can + const response = useSuspenseBlocksInfiniteNew(); + const initialStxBlocks = useSuspenseInfiniteQueryResult(response); + const initialBtcBlocks: Record = useMemo( + () => + initialStxBlocks.reduce( + (acc, block) => { + if (!acc[block.burn_block_hash]) { + acc[block.burn_block_hash] = { + type: 'btc_block', + height: block.burn_block_height, + hash: block.burn_block_hash, + timestamp: block.burn_block_time, + txsCount: undefined, // TODO: to get this I would have to make sure I have queried for all the stx blocks for this burn block and + }; + } + return acc; + }, + {} as Record + ), + [initialStxBlocks] + ); + const stxBlocksGroupedByBtcBlock: Record = useMemo( + // TODO: make a util function + () => + initialStxBlocks.reduce( + (acc, block) => { + if (!acc[block.burn_block_hash]) { + acc[block.burn_block_hash] = []; + } + acc[block.burn_block_hash].push({ + type: 'stx_block', + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + txsCount: block.tx_count, + }); + return acc; + }, + {} as Record + ), + [initialStxBlocks] + ); + + const initialBlockList = useMemo( + // TODO: make a util function + () => + Object.keys(stxBlocksGroupedByBtcBlock).reduce( + (acc, btcBlockHash) => { + const stxBlocks = stxBlocksGroupedByBtcBlock[btcBlockHash]; + const btcBlock = initialBtcBlocks[btcBlockHash]; + acc.push({ stxBlocks, btcBlock }); + return acc; + }, + [] as { stxBlocks: BlockListStxBlock[]; btcBlock: BlockListBtcBlock }[] + ), + [initialBtcBlocks, stxBlocksGroupedByBtcBlock] + ); + + // TODO: so far we have not limited the list to two btc blocks and how many stx bloxks are shown and put in a placeholder for the rest of the stx blocks + // for ensuring there are no duplicates + const stxBlockHashes = useMemo(() => { + return new Set(initialStxBlocks.map(block => block.hash)); + }, [initialStxBlocks]); + + const burnBlockHashes = useMemo(() => { + return new Set(Object.keys(initialBtcBlocks)); + }, [initialBtcBlocks]); + + const { + latestUIBlocks: latestUIBlockFromWebSocket, + latestStxBlocksCount: latestStxBlocksCountFromWebSocket, + clearLatestBlocks: clearLatestBlocksFromWebSocket, + } = useBlockListWebSocket(stxBlockHashes, burnBlockHashes); + + const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); + // This is used to trigger a fade out effect when the block list is updated. When the counter is updated, we finish loading and show the fade in effect + const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); + + useEffect(() => { + if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { + runAfterFadeOut(() => { + setBlockListLoading(false); + }); + } + }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); + + const queryClient = useQueryClient(); + const updateBlockListWithQuery = useCallback( + async function () { + setBlockListLoading(true); + runAfterFadeOut(async () => { + await Promise.all([ + // Invalidates queries so they will be refetched + queryClient.invalidateQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }), // TODO: might be better to manually run the query again so we can use the callback + ]).then(() => { + clearLatestBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }); + }, + [clearLatestBlocksFromWebSocket, queryClient, setBlockListLoading] + ); + + const showLatestBlocks = useCallback(() => { + setBlockListLoading(true); + runAfterFadeOut(() => { + setLatestBlocks(prevBlockList => { + return [...latestUIBlockFromWebSocket, ...prevBlockList]; + }); + clearLatestBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }, [ + latestUIBlockFromWebSocket, + setLatestBlocks, + setBlockListLoading, + clearLatestBlocksFromWebSocket, + ]); + + const prevLiveUpdatesRef = useRef(liveUpdates); + const prevLatestBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); + useEffect(() => { + const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; + + const receivedLatestStxBlockFromLiveUpdates = + liveUpdates && + latestStxBlocksCountFromWebSocket > 0 && + prevLatestBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; + + if (liveUpdatesToggled) { + updateBlockListWithQuery(); + } else if (receivedLatestStxBlockFromLiveUpdates) { + showLatestBlocks(); + } + + prevLiveUpdatesRef.current = liveUpdates; + prevLatestBlocksCountRef.current = latestStxBlocksCountFromWebSocket; + }, [liveUpdates, latestStxBlocksCountFromWebSocket, showLatestBlocks, updateBlockListWithQuery]); + + return { + latestBlocksCount: latestStxBlocksCountFromWebSocket, + blocksList: initialBlockList, + updateBlockList: updateBlockListWithQuery, + }; +} diff --git a/src/app/_components/BlockList/LayoutA/UpdateBar.tsx b/src/app/_components/BlockList/UpdateBar.tsx similarity index 64% rename from src/app/_components/BlockList/LayoutA/UpdateBar.tsx rename to src/app/_components/BlockList/UpdateBar.tsx index 29a5fbcec..3f7837ddf 100644 --- a/src/app/_components/BlockList/LayoutA/UpdateBar.tsx +++ b/src/app/_components/BlockList/UpdateBar.tsx @@ -1,36 +1,28 @@ import { useColorModeValue } from '@chakra-ui/react'; -import { useCallback, useRef } from 'react'; +import { ReactNode, useCallback, useRef } from 'react'; import { TfiReload } from 'react-icons/tfi'; -import { Button } from '../../../../ui/Button'; -import { Flex, FlexProps } from '../../../../ui/Flex'; -import { Icon } from '../../../../ui/Icon'; -import { Text } from '../../../../ui/Text'; -import { FADE_DURATION } from '../consts'; +import { Button } from '../../../ui/Button'; +import { Flex, FlexProps } from '../../../ui/Flex'; +import { Icon } from '../../../ui/Icon'; +import { Text } from '../../../ui/Text'; +import { useBlockListContext } from './BlockListContext'; +import { FADE_DURATION } from './consts'; interface UpdateBarProps extends FlexProps { latestBlocksCount: number; onClick: () => void; - isUpdateListLoading: boolean; } -export function UpdateBar({ - latestBlocksCount, - onClick, - isUpdateListLoading, +export function UpdateBarLayout({ + children, ...rest -}: UpdateBarProps) { - const bgColor = useColorModeValue('purple.100', 'slate.900'); - const textColor = useColorModeValue('slate.800', 'slate.400'); - const lastClickTimeRef = useRef(0); - - const update = useCallback(() => { - const now = Date.now(); - if (now - lastClickTimeRef.current > 2000) { - lastClickTimeRef.current = now; - onClick(); - } - }, [onClick]); +}: { + isUpdateListLoading: boolean; + children: ReactNode; +}) { + const { isBlockListLoading } = useBlockListContext(); + const bgColor = useColorModeValue('purple.100', 'slate.900'); // TODO: not in theme. remove return ( + {children} + + ); +} + +export function UpdateBar({ latestBlocksCount, onClick, ...rest }: UpdateBarProps) { + const textColor = useColorModeValue('slate.800', 'slate.400'); // TODO: not in theme. remove + const lastClickTimeRef = useRef(0); + + const update = useCallback(() => { + const now = Date.now(); + if (now - lastClickTimeRef.current > 2000) { + lastClickTimeRef.current = now; + onClick(); + } + }, [onClick]); + + return ( + - + ); } diff --git a/src/app/_components/BlockList/UpdatedBlockList.tsx b/src/app/_components/BlockList/UpdatedBlockList.tsx index b024d5aa3..b3f9ff9b2 100644 --- a/src/app/_components/BlockList/UpdatedBlockList.tsx +++ b/src/app/_components/BlockList/UpdatedBlockList.tsx @@ -178,7 +178,7 @@ function UpdatedBlocksListBase({ setLatestBlocks(prevBlocks => prevBlocks.filter(b => b.height !== block.height)); }, []); - if (!allBlocks?.length) return ; + // if (!allBlocks?.length) return ; // TODO: fix return (
prevBlocks.filter(b => b.height !== block.height)); }, []); - if (!allBlocks?.length) return ; + // if (!allBlocks?.length) return ; // TODO: fix return (
import('../_components/BlockList').then(mod => mod.BlocksList), { - loading: () => , - ssr: false, -}); - -const PaginatedBlockListLayoutA = dynamic( - () => - import('../_components/BlockList/LayoutA/Paginated').then(mod => mod.PaginatedBlockListLayoutA), - { - loading: () => , - ssr: false, - } -); - -const NonPaginatedBlockListGroupedByBurnBlock = dynamic( - () => - import('../_components/BlockList/GroupedByBurnBlock/NonPaginated').then( - mod => mod.NonPaginatedBlockListGroupedByBurnBlock - ), - { - loading: () => , - ssr: false, - } -); - -const BlocksPageBlockListGroupedByBtcBlock = dynamic( - () => - import('../_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock').then( - mod => mod.BlocksPageBlockListGroupedByBtcBlock - ), - { - loading: () => , - ssr: false, - } -); +export function BlocksPageLayout({ + blocksPageHeaders, + blocksList, +}: { + blocksPageHeaders: React.ReactNode; + blocksList: React.ReactNode; +}) { + return ( + <> + Recent blocks + {blocksPageHeaders} + {blocksList} + + ); +} const BlocksPage: NextPage = () => { const { activeNetworkKey } = useGlobalContext(); diff --git a/src/app/blocks/page.tsx b/src/app/blocks/page.tsx index 8b04d8ac6..14292b0ff 100644 --- a/src/app/blocks/page.tsx +++ b/src/app/blocks/page.tsx @@ -1,13 +1,12 @@ 'use client'; import dynamic from 'next/dynamic'; -import * as React from 'react'; -import Skeleton from './skeleton'; +import BlocksPageSkeleton from './skeleton'; const Page = dynamic(() => import('./PageClient'), { - loading: () => , - ssr: false, + loading: () => , + ssr: true, }); export default Page; diff --git a/src/app/blocks/skeleton.tsx b/src/app/blocks/skeleton.tsx index 5c165b7d7..35bed27a4 100644 --- a/src/app/blocks/skeleton.tsx +++ b/src/app/blocks/skeleton.tsx @@ -1,18 +1,16 @@ 'use client'; -import React from 'react'; +import { + BlockPageHeadersSkeleton, + BlocksPageBlockListSkeleton, +} from '../_components/BlockList/GroupedByBurnBlock/skeleton'; +import { BlocksPageLayout } from './PageClient'; -import { SkeletonBlockList } from '../../common/components/loaders/skeleton-text'; -import { SkeletonItem as SkeletonElement } from '../../ui/SkeletonItem'; -import { PageTitle } from '../_components/PageTitle'; - -export default function Skeleton() { +export default function BlocksPageSkeleton() { return ( - <> - - - - - + } + blocksList={} + /> ); } diff --git a/src/common/components/loaders/skeleton-transaction.tsx b/src/common/components/loaders/skeleton-transaction.tsx index 7818ce073..3c64446a5 100644 --- a/src/common/components/loaders/skeleton-transaction.tsx +++ b/src/common/components/loaders/skeleton-transaction.tsx @@ -18,7 +18,7 @@ import { Value } from '../Value'; export const SkeletonBlock = () => ( } + icon={} // Takes time to load in... leftContent={{ title: , subtitle: , diff --git a/src/common/queries/useBlockListInfinite.ts b/src/common/queries/useBlockListInfinite.ts index 7244e2e3b..263965def 100644 --- a/src/common/queries/useBlockListInfinite.ts +++ b/src/common/queries/useBlockListInfinite.ts @@ -37,3 +37,33 @@ export const useSuspenseBlockListInfinite = (limit = DEFAULT_LIST_LIMIT) => { initialPageParam: 0, }); }; + +export const useBlocksInfiniteNew = () => { + const api = useApi(); + return useInfiniteQuery({ + queryKey: [BLOCK_LIST_QUERY_KEY], + queryFn: ({ pageParam }: { pageParam: number }) => + api.blocksApi.getBlocks({ + limit: DEFAULT_LIST_LIMIT, + offset: pageParam || 0, + }), + staleTime: TWO_MINUTES, + getNextPageParam, + initialPageParam: 0, + }); +}; + +export const useSuspenseBlocksInfiniteNew = (limit = DEFAULT_LIST_LIMIT) => { + const api = useApi(); + return useSuspenseInfiniteQuery({ + queryKey: [BLOCK_LIST_QUERY_KEY, limit], + queryFn: ({ pageParam }: { pageParam: number }) => + api.blocksApi.getBlocks({ + limit, + offset: pageParam || 0, + }), + staleTime: TWO_MINUTES, + getNextPageParam, + initialPageParam: 0, + }); +}; diff --git a/src/common/queries/useBurnBlocks.ts b/src/common/queries/useBurnBlocksInfinite.ts similarity index 97% rename from src/common/queries/useBurnBlocks.ts rename to src/common/queries/useBurnBlocksInfinite.ts index de91fdb8f..ae439c1f4 100644 --- a/src/common/queries/useBurnBlocks.ts +++ b/src/common/queries/useBurnBlocksInfinite.ts @@ -16,6 +16,7 @@ import { TWO_MINUTES } from './query-stale-time'; export const BURN_BLOCKS_QUERY_KEY = 'burnBlocks'; +// TODO: move code into useBurnBlocks export function useBurnBlocks( options: any = {} ): UseInfiniteQueryResult>> { From 12d87d292213aff28b68897fa838d0b6a42adaa1 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Tue, 9 Apr 2024 11:13:11 -0500 Subject: [PATCH 18/70] feat(grouped-by-btc-block-list-view-3): add btc blocks at the end --- .../HomePageBlockListGroupedByBtcBlock.tsx | 2 + .../LayoutA/useBlockListWebSocket.ts | 2 +- .../Sockets/useBlockListWebSocket3.ts | 36 +++ .../BlocksPageUngroupedBlockList.tsx | 218 +++++++++--------- .../Ungrouped/UngroupedBlocksList.tsx | 6 +- .../useUngroupedBlockListBlocksPage.ts | 155 ++++++++----- src/app/_components/BlockList/UpdateBar.tsx | 8 +- .../_components/BlockList/useInitialBlocks.ts | 138 +++++++++++ 8 files changed, 383 insertions(+), 182 deletions(-) create mode 100644 src/app/_components/BlockList/Sockets/useBlockListWebSocket3.ts create mode 100644 src/app/_components/BlockList/useInitialBlocks.ts diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx index bc0a42fd5..6ed3ccbc6 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx @@ -2,6 +2,7 @@ import { Suspense } from 'react'; +import { ListFooter } from '../../../../common/components/ListFooter'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; import { useBlockListContext } from '../BlockListContext'; @@ -40,6 +41,7 @@ function HomePageBlockListGroupedByBtcBlockBase() { stxBlocksDisplayLimit={block.stxBlocksDisplayLimit} /> ))} + {!liveUpdates && } ); diff --git a/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts index 039f77bf3..6a3e10b14 100644 --- a/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts +++ b/src/app/_components/BlockList/LayoutA/useBlockListWebSocket.ts @@ -9,7 +9,7 @@ export function useBlockListWebSocket( initialStxBlockHashes: Set, initialBurnBlockHashes: Set ) { - const [latestBlocks, setLatestBlocks] = useState([]); + const [latestBlocks, setLatestBlocks] = useState([]); // TODO: convert to object structure so implementation isnt tied to one ui const [latestStxBlock, setLatestStxBlock] = useState(); const latestStxBlockHashes = useRef(new Set()); const latestBurnBlockHashes = useRef(new Set()); diff --git a/src/app/_components/BlockList/Sockets/useBlockListWebSocket3.ts b/src/app/_components/BlockList/Sockets/useBlockListWebSocket3.ts new file mode 100644 index 000000000..c7d422edd --- /dev/null +++ b/src/app/_components/BlockList/Sockets/useBlockListWebSocket3.ts @@ -0,0 +1,36 @@ +import { useCallback, useRef, useState } from 'react'; + +import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; + +import { useSubscribeBlocks } from './useSubscribeBlocks'; + +export function useBlockListWebSocket3(initialStxBlockHashes: Set) { + const [latestStxBlocks, setLatestStxBlocks] = useState([]); + const stxBlockHashes = useRef(new Set()); + + const handleBlock = useCallback( + (stxBlock: NakamotoBlock) => { + // If the block is already in the list, don't add it again + if (stxBlockHashes.current.has(stxBlock.hash) || initialStxBlockHashes.has(stxBlock.hash)) { + return; + } + + // Otherwise, add it to the list + setLatestStxBlocks(prevLatestStxBlocks => [stxBlock, ...prevLatestStxBlocks]); + stxBlockHashes.current.add(stxBlock.hash); + }, + [initialStxBlockHashes] + ); + + useSubscribeBlocks(handleBlock); + + const clearLatestBlocks = () => { + setLatestStxBlocks([]); + }; + + return { + latestStxBlocks, + latestStxBlocksCount: latestStxBlocks.length, + clearLatestBlocks, + }; +} diff --git a/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx b/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx index 2f31e680b..b70b69128 100644 --- a/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx +++ b/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx @@ -1,140 +1,140 @@ 'use client'; import { ListFooter } from '@/common/components/ListFooter'; -import { BLOCK_LIST_QUERY_KEY } from '@/common/queries/useBlockListInfinite'; import { Box } from '@/ui/Box'; -import { useQueryClient } from '@tanstack/react-query'; -import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { Suspense } from 'react'; import { Section } from '../../../../common/components/Section'; import { ExplorerErrorBoundary } from '../../ErrorBoundary'; import { useBlockListContext } from '../BlockListContext'; -import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; import { UpdateBar } from '../UpdateBar'; import { FADE_DURATION } from '../consts'; -import { UISingleBlock } from '../types'; -import { Blocks } from './Blocks'; +import { UngroupedBlockList } from './UngroupedBlocksList'; import { BlocksPageBlockListUngroupedSkeleton } from './skeleton'; -import { useUngroupedBlockList } from './useUngroupedBlockList'; +import { useUngroupedBlockListBlocksPage } from './useUngroupedBlockListBlocksPage'; function runAfterFadeOut(callback: () => void) { setTimeout(callback, FADE_DURATION); } function BlocksPageUngroupedBlockListBase() { - const { - isBlockListLoading, - setBlockListLoading, - liveUpdates: isLiveUpdatesEnabled, - } = useBlockListContext(); - - // TODO: dont really need to have a separate hook for this. This is just doing all the organizing of the data behind the hook - const { initialBlockList, initialBurnBlocks, hasNextPage, isFetchingNextPage, fetchNextPage } = - useUngroupedBlockList(); - - const [latestBlocksToShow, setLatestBlocksToShow] = useState([]); - const blockList = useMemo( - () => [...latestBlocksToShow, ...initialBlockList], - [initialBlockList, latestBlocksToShow] - ); - - const blockHashes = useMemo(() => { - return new Set(initialBlockList.map(block => block.hash)); - }, [initialBlockList]); - - const burnBlockHashes = useMemo(() => { - return new Set(Object.keys(initialBurnBlocks)); - }, [initialBurnBlocks]); + const { liveUpdates } = useBlockListContext(); + + // // TODO: dont really need to have a separate hook for this. This is just doing all the organizing of the data behind the hook + // const { initialBlockList, initialBurnBlocks, hasNextPage, isFetchingNextPage, fetchNextPage } = + // useUngroupedBlockList(); + + // const [latestBlocksToShow, setLatestBlocksToShow] = useState([]); + // const blockList = useMemo( + // () => [...latestBlocksToShow, ...initialBlockList], + // [initialBlockList, latestBlocksToShow] + // ); + + // const blockHashes = useMemo(() => { + // return new Set(initialBlockList.map(block => block.hash)); + // }, [initialBlockList]); + + // const burnBlockHashes = useMemo(() => { + // return new Set(Object.keys(initialBurnBlocks)); + // }, [initialBurnBlocks]); + + // const { + // latestUIBlocks: latestUIBlockFromWebSocket, + // latestStxBlocksCount: latestStxBlocksCountFromWebSocket, + // clearLatestBlocks: clearLatestBlocksFromWebSocket, + // } = useBlockListWebSocket(blockHashes, burnBlockHashes); + + // const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); + // // This is used to trigger a fade out effect when the block list is updated. When the counter is updated, we finish loading and show the fade in effect + // const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); + + // useEffect(() => { + // if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { + // runAfterFadeOut(() => { + // setBlockListLoading(false); + // }); + // } + // }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); + + // const showLatestBlocks = useCallback(() => { + // setBlockListLoading(true); + // runAfterFadeOut(() => { + // setLatestBlocksToShow(prevBlockList => { + // return [...latestUIBlockFromWebSocket, ...prevBlockList]; + // }); + // clearLatestBlocksFromWebSocket(); + // setBlockListUpdateCounter(prev => prev + 1); + // }); + // }, [ + // latestUIBlockFromWebSocket, + // setLatestBlocksToShow, + // setBlockListLoading, + // clearLatestBlocksFromWebSocket, + // ]); + + // const queryClient = useQueryClient(); + // const updateBlockListWithQuery = useCallback( + // async function () { + // setBlockListLoading(true); + // runAfterFadeOut(async () => { + // await Promise.all([ + // // Invalidates queries so they will be refetched + // queryClient.invalidateQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }), + // ]).then(() => { + // clearLatestBlocksFromWebSocket(); + // setBlockListUpdateCounter(prev => prev + 1); + // }); + // }); + // }, + // [clearLatestBlocksFromWebSocket, queryClient, setBlockListLoading] + // ); + + // const prevLiveUpdatesRef = useRef(isLiveUpdatesEnabled); + // const prevLatestBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); + + // useEffect(() => { + // const liveUpdatesToggled = prevLiveUpdatesRef.current !== isLiveUpdatesEnabled; + + // const receivedLatestStxBlockFromLiveUpdates = + // isLiveUpdatesEnabled && + // latestStxBlocksCountFromWebSocket > 0 && + // prevLatestBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; + + // if (liveUpdatesToggled) { + // updateBlockListWithQuery(); + // } else if (receivedLatestStxBlockFromLiveUpdates) { + // showLatestBlocks(); + // } + + // prevLiveUpdatesRef.current = isLiveUpdatesEnabled; + // prevLatestBlocksCountRef.current = latestStxBlocksCountFromWebSocket; + // }, [ + // isLiveUpdatesEnabled, + // latestStxBlocksCountFromWebSocket, + // showLatestBlocks, + // updateBlockListWithQuery, + // ]); const { - latestUIBlocks: latestUIBlockFromWebSocket, - latestStxBlocksCount: latestStxBlocksCountFromWebSocket, - clearLatestBlocks: clearLatestBlocksFromWebSocket, - } = useBlockListWebSocket(blockHashes, burnBlockHashes); - - const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); - // This is used to trigger a fade out effect when the block list is updated. When the counter is updated, we finish loading and show the fade in effect - const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); - - useEffect(() => { - if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { - runAfterFadeOut(() => { - setBlockListLoading(false); - }); - } - }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); - - const showLatestBlocks = useCallback(() => { - setBlockListLoading(true); - runAfterFadeOut(() => { - setLatestBlocksToShow(prevBlockList => { - return [...latestUIBlockFromWebSocket, ...prevBlockList]; - }); - clearLatestBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); - }); - }, [ - latestUIBlockFromWebSocket, - setLatestBlocksToShow, - setBlockListLoading, - clearLatestBlocksFromWebSocket, - ]); - - const queryClient = useQueryClient(); - const updateBlockListWithQuery = useCallback( - async function () { - setBlockListLoading(true); - runAfterFadeOut(async () => { - await Promise.all([ - // Invalidates queries so they will be refetched - queryClient.invalidateQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }), - ]).then(() => { - clearLatestBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); - }); - }); - }, - [clearLatestBlocksFromWebSocket, queryClient, setBlockListLoading] - ); - - const prevLiveUpdatesRef = useRef(isLiveUpdatesEnabled); - const prevLatestBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); - - useEffect(() => { - const liveUpdatesToggled = prevLiveUpdatesRef.current !== isLiveUpdatesEnabled; - - const receivedLatestStxBlockFromLiveUpdates = - isLiveUpdatesEnabled && - latestStxBlocksCountFromWebSocket > 0 && - prevLatestBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; - - if (liveUpdatesToggled) { - updateBlockListWithQuery(); - } else if (receivedLatestStxBlockFromLiveUpdates) { - showLatestBlocks(); - } - - prevLiveUpdatesRef.current = isLiveUpdatesEnabled; - prevLatestBlocksCountRef.current = latestStxBlocksCountFromWebSocket; - }, [ - isLiveUpdatesEnabled, + blockList, latestStxBlocksCountFromWebSocket, - showLatestBlocks, - updateBlockListWithQuery, - ]); + hasNextPage, + fetchNextPage, + isFetchingNextPage, + updateBlockList, + } = useUngroupedBlockListBlocksPage(); return ( - {!isLiveUpdatesEnabled && ( + {!liveUpdates && ( )} - + - {!isLiveUpdatesEnabled && ( + {!liveUpdates && ( ))} - {stxBlocks.length > stxBlocksLimit && ( + {stxBlocksLimit && stxBlocks.length > stxBlocksLimit && ( )} diff --git a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts b/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts index 23af311fc..48b7ee7ba 100644 --- a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts +++ b/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts @@ -1,118 +1,149 @@ 'use client'; -import { BLOCK_LIST_QUERY_KEY } from '@/common/queries/useBlockListInfinite'; -import { useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { NakamotoBlock } from '@stacks/blockchain-api-client'; + import { useBlockListContext } from '../BlockListContext'; -import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; +import { useBlockListWebSocket3 } from '../Sockets/useBlockListWebSocket3'; import { FADE_DURATION } from '../consts'; -import { UISingleBlock } from '../types'; -import { useUngroupedBlockList } from './useUngroupedBlockList'; +import { + BlockListData, + convertBlockToBlockListBtcBlock, + convertBlockToBlockListStxBlock, + generateBlockList, + useInitialBlockList, +} from '../useInitialBlocks'; function runAfterFadeOut(callback: () => void) { setTimeout(callback, FADE_DURATION); } +function mergeWebSocketUpdates( + latestStxBlocksFromWebSocket: NakamotoBlock[], + initialBlockListDataMap: Record +) { + const initialBlockListDataMapCopy = Object.assign({}, initialBlockListDataMap); + latestStxBlocksFromWebSocket.forEach(stxBlock => { + // add the stx block to the existing btc block + if (stxBlock.burn_block_hash in initialBlockListDataMap) { + initialBlockListDataMapCopy[stxBlock.burn_block_hash].stxBlocks.push( + convertBlockToBlockListStxBlock(stxBlock) + ); + } else { + // add a new btc block and stx block + initialBlockListDataMapCopy[stxBlock.burn_block_hash] = { + stxBlocks: [convertBlockToBlockListStxBlock(stxBlock)], + btcBlock: convertBlockToBlockListBtcBlock(stxBlock), + }; + } + }); + return generateBlockList(initialBlockListDataMapCopy); +} + export function useUngroupedBlockListBlocksPage() { - const { - isBlockListLoading, - setBlockListLoading, - liveUpdates: isLiveUpdatesEnabled, - } = useBlockListContext(); + const { setBlockListLoading, liveUpdates } = useBlockListContext(); // TODO: dont really need to have a separate hook for this. This is just doing all the organizing of the data behind the hook - const { initialBlockList, initialBurnBlocks, hasNextPage, isFetchingNextPage, fetchNextPage } = - useUngroupedBlockList(); + const { + initialStxBlocks, + initialStxBlocksHashes, + initialBtcBlocks, + initialBtcBlocksHashes, + initialBlockListDataMap, + initialBlockList, + isFetchingNextPage, + fetchNextPage, + refetchInitialBlockList, + hasNextPage, + } = useInitialBlockList(); + + const [latestBlocksToShow, setLatestBlocksToShow] = useState([]); + + const { + latestStxBlocks: latestStxBlocksFromWebSocket, + latestStxBlocksCount: latestStxBlocksCountFromWebSocket, + clearLatestBlocks: clearLatestBlocksFromWebSocket, + } = useBlockListWebSocket3(initialStxBlocksHashes); - const [latestBlocksToShow, setLatestBlocksToShow] = useState([]); const blockList = useMemo( () => [...latestBlocksToShow, ...initialBlockList], [initialBlockList, latestBlocksToShow] ); - const blockHashes = useMemo(() => { - return new Set(initialBlockList.map(block => block.hash)); - }, [initialBlockList]); - - const burnBlockHashes = useMemo(() => { - return new Set(Object.keys(initialBurnBlocks)); - }, [initialBurnBlocks]); - - const { - latestUIBlocks: latestUIBlockFromWebSocket, - latestStxBlocksCount: latestStxBlocksCountFromWebSocket, - clearLatestBlocks: clearLatestBlocksFromWebSocket, - } = useBlockListWebSocket(blockHashes, burnBlockHashes); - + // This is used to trigger a fade out effect when the block list is updated. + // When the counter is updated, we wait for the fade out effect to finish and then show the fade in effect const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); - // This is used to trigger a fade out effect when the block list is updated. When the counter is updated, we finish loading and show the fade in effect const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); - useEffect(() => { if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { - runAfterFadeOut(() => { - setBlockListLoading(false); - }); + setBlockListLoading(false); + // runAfterFadeOut(() => { + // setBlockListLoading(false); + // }); } }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); - const showLatestBlocks = useCallback(() => { + const showLatestStxBlocksFromWebSocket = useCallback(() => { setBlockListLoading(true); - runAfterFadeOut(() => { - setLatestBlocksToShow(prevBlockList => { - return [...latestUIBlockFromWebSocket, ...prevBlockList]; - }); - clearLatestBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); - }); + // const newBlockList = mergeWebSocketUpdates( + // latestStxBlocksFromWebSocket, + // initialBlockListDataMap + // ); + setLatestBlocksToShow(latestStxBlocksFromWebSocket); + clearLatestBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); }, [ - latestUIBlockFromWebSocket, + latestStxBlocksFromWebSocket, + initialBlockListDataMap, setLatestBlocksToShow, setBlockListLoading, clearLatestBlocksFromWebSocket, ]); - const queryClient = useQueryClient(); - const updateBlockListWithQuery = useCallback( + const updateBlockList = useCallback( async function () { setBlockListLoading(true); - runAfterFadeOut(async () => { - await Promise.all([ - // Invalidates queries so they will be refetched - queryClient.invalidateQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }), - ]).then(() => { - clearLatestBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); - }); + await refetchInitialBlockList(() => { + clearLatestBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); }); }, - [clearLatestBlocksFromWebSocket, queryClient, setBlockListLoading] + [clearLatestBlocksFromWebSocket, setBlockListLoading, refetchInitialBlockList] ); - const prevLiveUpdatesRef = useRef(isLiveUpdatesEnabled); + const prevLiveUpdatesRef = useRef(liveUpdates); const prevLatestBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); - + // Handles live updates useEffect(() => { - const liveUpdatesToggled = prevLiveUpdatesRef.current !== isLiveUpdatesEnabled; + const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; const receivedLatestStxBlockFromLiveUpdates = - isLiveUpdatesEnabled && + liveUpdates && latestStxBlocksCountFromWebSocket > 0 && prevLatestBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; if (liveUpdatesToggled) { - updateBlockListWithQuery(); + updateBlockList(); } else if (receivedLatestStxBlockFromLiveUpdates) { - showLatestBlocks(); + showLatestStxBlocksFromWebSocket(); } - prevLiveUpdatesRef.current = isLiveUpdatesEnabled; + prevLiveUpdatesRef.current = liveUpdates; prevLatestBlocksCountRef.current = latestStxBlocksCountFromWebSocket; }, [ - isLiveUpdatesEnabled, + liveUpdates, latestStxBlocksCountFromWebSocket, - showLatestBlocks, - updateBlockListWithQuery, + showLatestStxBlocksFromWebSocket, + updateBlockList, ]); + + return { + blockList, + updateBlockList, + latestStxBlocksCountFromWebSocket, + hasNextPage, + fetchNextPage, + isFetchingNextPage, + }; } diff --git a/src/app/_components/BlockList/UpdateBar.tsx b/src/app/_components/BlockList/UpdateBar.tsx index 3f7837ddf..ba4d69665 100644 --- a/src/app/_components/BlockList/UpdateBar.tsx +++ b/src/app/_components/BlockList/UpdateBar.tsx @@ -14,13 +14,7 @@ interface UpdateBarProps extends FlexProps { onClick: () => void; } -export function UpdateBarLayout({ - children, - ...rest -}: { - isUpdateListLoading: boolean; - children: ReactNode; -}) { +export function UpdateBarLayout({ children, ...rest }: { children: ReactNode }) { const { isBlockListLoading } = useBlockListContext(); const bgColor = useColorModeValue('purple.100', 'slate.900'); // TODO: not in theme. remove diff --git a/src/app/_components/BlockList/useInitialBlocks.ts b/src/app/_components/BlockList/useInitialBlocks.ts new file mode 100644 index 000000000..f98f3c8c5 --- /dev/null +++ b/src/app/_components/BlockList/useInitialBlocks.ts @@ -0,0 +1,138 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; + +import { Block, NakamotoBlock } from '@stacks/stacks-blockchain-api-types'; + +import { useSuspenseInfiniteQueryResult } from '../../../common/hooks/useInfiniteQueryResult'; +import { + BLOCK_LIST_QUERY_KEY, + useSuspenseBlockListInfinite, +} from '../../../common/queries/useBlockListInfinite'; +import { BlockListBtcBlock, BlockListStxBlock } from './types'; + +export function convertBlockToBlockListStxBlock(block: Block | NakamotoBlock): BlockListStxBlock { + return { + type: 'stx_block', + height: block.height, + hash: block.hash, + timestamp: block.burn_block_time, + txsCount: (block as Block)?.txs.length ?? (block as NakamotoBlock)?.tx_count, + }; +} +export function convertBlockToBlockListBtcBlock(block: Block | NakamotoBlock): BlockListBtcBlock { + return { + type: 'btc_block', + height: block.burn_block_height, + hash: block.burn_block_hash, + timestamp: block.burn_block_time, + }; +} + +export type BlockListData = { stxBlocks: BlockListStxBlock[]; btcBlock: BlockListBtcBlock }; + +// TODO: can and should probably simplify this code. Turn this into a function - make blocklist +export function generateBlockList( + blockListDataMap: Record +): BlockListData[] { + return Object.values(blockListDataMap).sort((a, b) => { + const bHeight = + typeof b.btcBlock.height === 'string' + ? Number.parseInt(b.btcBlock.height) + : b.btcBlock.height; + const aHeight = + typeof a.btcBlock.height === 'string' + ? Number.parseInt(a.btcBlock.height) + : a.btcBlock.height; + return bHeight - aHeight; + }); +} + +export function useInitialBlockList() { + const queryClient = useQueryClient(); + const response = useSuspenseBlockListInfinite(); + const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + const blocks = useSuspenseInfiniteQueryResult(response); + + const initialStxBlocks: Record = useMemo( + () => + blocks.reduce( + (acc, block) => { + if (!acc[block.burn_block_hash]) { + acc[block.burn_block_hash] = convertBlockToBlockListStxBlock(block); + } + return acc; + }, + {} as Record + ), + [blocks] + ); + + const initialStxBlocksHashes = useMemo( + () => new Set(Object.keys(initialStxBlocks)), + [initialStxBlocks] + ); + + const initialBtcBlocks: Record = useMemo( + () => + blocks.reduce( + (acc, block) => { + if (!acc[block.burn_block_hash]) { + acc[block.burn_block_hash] = convertBlockToBlockListBtcBlock(block); + } + return acc; + }, + {} as Record + ), + [blocks] + ); + + const initialBtcBlocksHashes = useMemo( + () => new Set(Object.keys(initialBtcBlocks)), + [initialBtcBlocks] + ); + + // btc block hash -> BlockListData + const initialBlockListDataMap: Record = useMemo( + () => + blocks.reduce( + (acc, block) => { + if (!acc[block.burn_block_hash]) { + acc[block.burn_block_hash] = { + stxBlocks: [], + btcBlock: convertBlockToBlockListBtcBlock(block), + }; + } + acc[block.burn_block_hash].stxBlocks.push(convertBlockToBlockListStxBlock(block)); + return acc; + }, + {} as Record + ), + [blocks] + ); + + const initialBlockList = useMemo( + () => generateBlockList(initialBlockListDataMap), + [initialBlockListDataMap] + ); + + const refetchInitialBlockList = useCallback( + function (callback: () => void) { + // return queryClient.resetQueries({ queryKey: ['blockListInfinite'] }); + queryClient.refetchQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }).then(() => callback()); + }, + [queryClient] + ); + + return { + initialStxBlocks, + initialStxBlocksHashes, + initialBtcBlocks, + initialBtcBlocksHashes, + initialBlockListDataMap, + initialBlockList, + refetchInitialBlockList, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + }; +} From 4dd6625a41300bcbe516252c23883d1eda4d3fb9 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Tue, 9 Apr 2024 23:40:34 -0500 Subject: [PATCH 19/70] feat(grouped-by-btc-block-list-view-3): added table layout for ungrouped --- src/app/PageClient.tsx | 8 +- .../{ => BlocksPage}/BlocksPageBlockList.tsx | 43 ++- .../BlocksPageHeaders.tsx | 2 +- .../Grouped/BlocksPageBlockListGrouped.tsx} | 30 +- .../useBlocksPageBlockListGrouped.tsx} | 17 +- .../BlocksPageBlockListUngrouped.tsx} | 27 +- .../useBlocksPageBlockListUngrouped.ts} | 94 +++--- src/app/_components/BlockList/Controls.tsx | 6 +- .../BlockListGrouped.tsx} | 42 ++- .../skeleton.tsx | 12 +- .../HomePageBlockListGroupedByBtcBlock.tsx | 56 ---- .../useBlockListWebSocket.tsx | 72 ---- .../Grouped/HomePageBlockListGrouped.tsx | 44 +++ .../Grouped/useHomePageBlockListGrouped.tsx} | 17 +- .../useHomePageInitialBlockListGrouped.tsx} | 8 +- .../BlockList/HomePage/HomePageBlockList.tsx | 99 ++++++ .../Ungrouped/HomePageBlockListUngrouped.tsx | 44 +++ .../useHomePageBlockListUngrouped.tsx} | 14 +- .../BlockList/Ungrouped/Blocks.tsx | 66 ---- ...BlocksList.tsx => BlocksListUngrouped.tsx} | 57 ++-- .../BlockList/Ungrouped/BtcBlockListItem.tsx | 13 +- .../Ungrouped/HomePageUngroupedBlockList.tsx | 32 -- .../BlockList/Ungrouped/StxBlockListItem.tsx | 313 +++++++++++++----- src/app/_components/BlockList/UpdateBar.tsx | 15 +- .../_components/BlockList/useInitialBlocks.ts | 62 ++-- src/app/blocks/PageClient.tsx | 27 ++ src/app/blocks/skeleton.tsx | 2 +- src/app/btcblock/[hash]/PageClient.tsx | 3 +- 28 files changed, 704 insertions(+), 521 deletions(-) rename src/app/_components/BlockList/{ => BlocksPage}/BlocksPageBlockList.tsx (52%) rename src/app/_components/BlockList/{GroupedByBurnBlock => BlocksPage}/BlocksPageHeaders.tsx (98%) rename src/app/_components/BlockList/{GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx => BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx} (64%) rename src/app/_components/BlockList/{GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx => BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx} (90%) rename src/app/_components/BlockList/{Ungrouped/BlocksPageUngroupedBlockList.tsx => BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx} (86%) rename src/app/_components/BlockList/{Ungrouped/useUngroupedBlockListBlocksPage.ts => BlocksPage/Ungrouped/useBlocksPageBlockListUngrouped.ts} (61%) rename src/app/_components/BlockList/{GroupedByBurnBlock/BurnBlockGroup.tsx => Grouped/BlockListGrouped.tsx} (92%) rename src/app/_components/BlockList/{GroupedByBurnBlock => Grouped}/skeleton.tsx (93%) delete mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx delete mode 100644 src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx create mode 100644 src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx rename src/app/_components/BlockList/{GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx => HomePage/Grouped/useHomePageBlockListGrouped.tsx} (91%) rename src/app/_components/BlockList/{GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx => HomePage/Grouped/useHomePageInitialBlockListGrouped.tsx} (76%) create mode 100644 src/app/_components/BlockList/HomePage/HomePageBlockList.tsx create mode 100644 src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx rename src/app/_components/BlockList/{Ungrouped/useUngroupedBlockListHomePage.tsx => HomePage/Ungrouped/useHomePageBlockListUngrouped.tsx} (94%) delete mode 100644 src/app/_components/BlockList/Ungrouped/Blocks.tsx rename src/app/_components/BlockList/Ungrouped/{UngroupedBlocksList.tsx => BlocksListUngrouped.tsx} (52%) delete mode 100644 src/app/_components/BlockList/Ungrouped/HomePageUngroupedBlockList.tsx diff --git a/src/app/PageClient.tsx b/src/app/PageClient.tsx index f9efbbee3..c3b951b71 100644 --- a/src/app/PageClient.tsx +++ b/src/app/PageClient.tsx @@ -10,6 +10,7 @@ import { SkeletonBlockList } from './_components/BlockList/SkeletonBlockList'; import { UpdatedBlocksList } from './_components/BlockList/UpdatedBlockList'; import { PageTitle } from './_components/PageTitle'; import { Stats } from './_components/Stats/Stats'; +import { NextPage } from 'next'; const BlocksListDynamic = dynamic( () => import('./_components/BlockList').then(mod => mod.BlocksList), @@ -20,14 +21,15 @@ const BlocksListDynamic = dynamic( ); const HomePageBlockListDynamic = dynamic( - () => import('./_components/BlockList/HomePageBlockList').then(mod => mod.HomePageBlockList), + () => + import('./_components/BlockList/HomePage/HomePageBlockList').then(mod => mod.HomePageBlockList), { loading: () => , ssr: false, } ); -export default function Home() { +const Home: NextPage = () => { const { activeNetwork, activeNetworkKey } = useGlobalContext(); return ( <> @@ -45,3 +47,5 @@ export default function Home() { ); } + +export default Home; diff --git a/src/app/_components/BlockList/BlocksPageBlockList.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx similarity index 52% rename from src/app/_components/BlockList/BlocksPageBlockList.tsx rename to src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx index 31dcf284e..5d4330504 100644 --- a/src/app/_components/BlockList/BlocksPageBlockList.tsx +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx @@ -3,29 +3,26 @@ import dynamic from 'next/dynamic'; import { Suspense, useCallback, useRef } from 'react'; -import { Section } from '../../../common/components/Section'; -import { ExplorerErrorBoundary } from '../ErrorBoundary'; -import { useBlockListContext } from './BlockListContext'; -import { BlockListProvider } from './BlockListProvider'; -import { Controls } from './Controls'; -import { BlocksPageBlockListGroupedByBtcBlockSkeleton } from './GroupedByBurnBlock/skeleton'; -import { BlocksPageBlockListUngroupedSkeleton } from './Ungrouped/skeleton'; +import { Section } from '../../../../common/components/Section'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { useBlockListContext } from '../BlockListContext'; +import { BlockListProvider } from '../BlockListProvider'; +import { Controls } from '../Controls'; +import { BlocksPageBlockListGroupedSkeleton } from '../Grouped/skeleton'; +import { BlocksPageBlockListUngroupedSkeleton } from '../Ungrouped/skeleton'; -const BlocksPageBlockListGroupedByBtcBlockDynamic = dynamic( - () => - import('./GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock').then( - mod => mod.BlocksPageBlockListGroupedByBtcBlock - ), +const BlocksPageBlockListGroupedDynamic = dynamic( + () => import('./Grouped/BlocksPageBlockListGrouped').then(mod => mod.BlocksPageBlockListGrouped), { - loading: () => , + loading: () => , ssr: false, } ); -const BlocksPageUngroupedBlockListDynamic = dynamic( +const BlocksPageBlockListUngroupedDynamic = dynamic( () => - import('./Ungrouped/BlocksPageUngroupedBlockList').then( - mod => mod.BlocksPageUngroupedBlockList + import('./Ungrouped/BlocksPageBlockListUngrouped').then( + mod => mod.BlocksPageBlockListUngrouped ), { loading: () => , @@ -60,18 +57,16 @@ function BlocksPageBlockListBase() { }} horizontal={true} /> - {/* {groupedByBtc ? : } */} {groupedByBtc ? ( - + ) : ( - + )}
); } export function BlocksPageBlockList() { - // TODO: fix the suspense fallback return ( - - }> + }> + - - + + ); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageHeaders.tsx similarity index 98% rename from src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx rename to src/app/_components/BlockList/BlocksPage/BlocksPageHeaders.tsx index bbf9f8ee1..63292ff67 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageHeaders.tsx +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageHeaders.tsx @@ -8,7 +8,7 @@ import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; import { Text } from '../../../../ui/Text'; import { BitcoinIcon } from '../../../../ui/icons/BitcoinIcon'; -import { BlockPageHeadersSkeleton } from './skeleton'; +import { BlockPageHeadersSkeleton } from '../Grouped/skeleton'; function LastBlockCard() { // const lastBlock = useSuspenseBurnBlocks(1); diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx similarity index 64% rename from src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx rename to src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx index b9246c6ea..bf044bfbd 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/BlocksPageBlockListGroupedByBtcBlock.tsx +++ b/src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx @@ -3,18 +3,18 @@ import { ListFooter } from '@/common/components/ListFooter'; import { Suspense } from 'react'; -import { Section } from '../../../../common/components/Section'; -import { Box } from '../../../../ui/Box'; -import { Flex } from '../../../../ui/Flex'; -import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { useBlockListContext } from '../BlockListContext'; -import { UpdateBar } from '../UpdateBar'; -import { FADE_DURATION } from '../consts'; -import { BurnBlockGroup } from './BurnBlockGroup'; -import { BlocksPageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; -import { useBlockListGroupedByBtcBlockBlocksPage } from './useBlockListGroupedByBtcBlockBlocksPage'; +import { Section } from '../../../../../common/components/Section'; +import { Box } from '../../../../../ui/Box'; +import { Flex } from '../../../../../ui/Flex'; +import { ExplorerErrorBoundary } from '../../../ErrorBoundary'; +import { useBlockListContext } from '../../BlockListContext'; +import { BurnBlockGroup } from '../../Grouped/BlockListGrouped'; +import { BlocksPageBlockListGroupedSkeleton } from '../../Grouped/skeleton'; +import { UpdateBar } from '../../UpdateBar'; +import { FADE_DURATION } from '../../consts'; +import { useBlocksPageBlockListGrouped } from './useBlocksPageBlockListGrouped'; -function BlocksPageBlockListGroupedByBtcBlockBase() { +function BlocksPageBlockListGroupedBase() { const { liveUpdates, isBlockListLoading } = useBlockListContext(); const { blockList, @@ -23,7 +23,7 @@ function BlocksPageBlockListGroupedByBtcBlockBase() { isFetchingNextPage, hasNextPage, fetchNextPage, - } = useBlockListGroupedByBtcBlockBlocksPage(10); + } = useBlocksPageBlockListGrouped(10); console.log({ liveUpdates }); const enablePagination = true; @@ -65,7 +65,7 @@ function BlocksPageBlockListGroupedByBtcBlockBase() { ); } -export function BlocksPageBlockListGroupedByBtcBlock() { +export function BlocksPageBlockListGrouped() { return ( - }> - + }> + ); diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx b/src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx similarity index 90% rename from src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx rename to src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx index 5d02244f7..e4696a9f4 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockBlocksPage.tsx +++ b/src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx @@ -5,19 +5,18 @@ import { useCallback, useEffect, useMemo, useRef } from 'react'; import { BurnBlock } from '@stacks/blockchain-api-client'; -import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; -import { useSuspenseBlocksByBurnBlock } from '../../../../common/queries/useBlocksByBurnBlock'; -import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocksInfinite'; -import { useBlockListContext } from '../BlockListContext'; -import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; -import { UIBlockType, UISingleBlock } from '../types'; -import { BlocksGroupProps } from './BurnBlockGroup'; -import { useBlockListWebSocket } from './useBlockListWebSocket'; +import { useSuspenseInfiniteQueryResult } from '../../../../../common/hooks/useInfiniteQueryResult'; +import { useSuspenseBlocksByBurnBlock } from '../../../../../common/queries/useBlocksByBurnBlock'; +import { useSuspenseBurnBlocks } from '../../../../../common/queries/useBurnBlocksInfinite'; +import { useBlockListContext } from '../../BlockListContext'; +import { BlocksGroupProps } from '../../Grouped/BlockListGrouped'; +import { useBlockListWebSocket } from '../../Sockets/useBlockListWebSocket'; +import { UIBlockType, UISingleBlock } from '../../types'; const STX_BLOCK_LENGTH = 10; const BURN_BLOCK_LENGTH = 10; -export function useBlockListGroupedByBtcBlockBlocksPage(blockListLimit: number) { +export function useBlocksPageBlockListGrouped(blockListLimit: number) { const queryClient = useQueryClient(); const { setBlockListLoading, liveUpdates: isLiveUpdatesEnabled } = useBlockListContext(); diff --git a/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx b/src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx similarity index 86% rename from src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx rename to src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx index b70b69128..4ed57d286 100644 --- a/src/app/_components/BlockList/Ungrouped/BlocksPageUngroupedBlockList.tsx +++ b/src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx @@ -4,20 +4,20 @@ import { ListFooter } from '@/common/components/ListFooter'; import { Box } from '@/ui/Box'; import { Suspense } from 'react'; -import { Section } from '../../../../common/components/Section'; -import { ExplorerErrorBoundary } from '../../ErrorBoundary'; -import { useBlockListContext } from '../BlockListContext'; -import { UpdateBar } from '../UpdateBar'; -import { FADE_DURATION } from '../consts'; -import { UngroupedBlockList } from './UngroupedBlocksList'; -import { BlocksPageBlockListUngroupedSkeleton } from './skeleton'; -import { useUngroupedBlockListBlocksPage } from './useUngroupedBlockListBlocksPage'; +import { Section } from '../../../../../common/components/Section'; +import { ExplorerErrorBoundary } from '../../../ErrorBoundary'; +import { useBlockListContext } from '../../BlockListContext'; +import { BlocksPageBlockListUngroupedSkeleton } from '../../Ungrouped/skeleton'; +import { UpdateBar } from '../../UpdateBar'; +import { FADE_DURATION } from '../../consts'; +import { useBlocksPageBlockListUngrouped } from './useBlocksPageBlockListUngrouped'; +import { BlockListUngrouped } from '../../Ungrouped/BlocksListUngrouped'; function runAfterFadeOut(callback: () => void) { setTimeout(callback, FADE_DURATION); } -function BlocksPageUngroupedBlockListBase() { +function BlocksPageBlockListUngroupedBase() { const { liveUpdates } = useBlockListContext(); // // TODO: dont really need to have a separate hook for this. This is just doing all the organizing of the data behind the hook @@ -122,7 +122,8 @@ function BlocksPageUngroupedBlockListBase() { fetchNextPage, isFetchingNextPage, updateBlockList, - } = useUngroupedBlockListBlocksPage(); + } = useBlocksPageBlockListUngrouped(); + console.log({ blockList }) return ( @@ -132,7 +133,7 @@ function BlocksPageUngroupedBlockListBase() { onClick={updateBlockList} /> )} - + {!liveUpdates && ( }> - + ); diff --git a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts b/src/app/_components/BlockList/BlocksPage/Ungrouped/useBlocksPageBlockListUngrouped.ts similarity index 61% rename from src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts rename to src/app/_components/BlockList/BlocksPage/Ungrouped/useBlocksPageBlockListUngrouped.ts index 48b7ee7ba..1d5369d39 100644 --- a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListBlocksPage.ts +++ b/src/app/_components/BlockList/BlocksPage/Ungrouped/useBlocksPageBlockListUngrouped.ts @@ -2,55 +2,69 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import { NakamotoBlock } from '@stacks/blockchain-api-client'; +import { Block, NakamotoBlock } from '@stacks/blockchain-api-client'; -import { useBlockListContext } from '../BlockListContext'; -import { useBlockListWebSocket3 } from '../Sockets/useBlockListWebSocket3'; -import { FADE_DURATION } from '../consts'; +import { useBlockListContext } from '../../BlockListContext'; +import { useBlockListWebSocket3 } from '../../Sockets/useBlockListWebSocket3'; +import { FADE_DURATION } from '../../consts'; import { BlockListData, convertBlockToBlockListBtcBlock, convertBlockToBlockListStxBlock, - generateBlockList, useInitialBlockList, -} from '../useInitialBlocks'; +} from '../../useInitialBlocks'; function runAfterFadeOut(callback: () => void) { setTimeout(callback, FADE_DURATION); } -function mergeWebSocketUpdates( - latestStxBlocksFromWebSocket: NakamotoBlock[], - initialBlockListDataMap: Record -) { - const initialBlockListDataMapCopy = Object.assign({}, initialBlockListDataMap); - latestStxBlocksFromWebSocket.forEach(stxBlock => { - // add the stx block to the existing btc block - if (stxBlock.burn_block_hash in initialBlockListDataMap) { - initialBlockListDataMapCopy[stxBlock.burn_block_hash].stxBlocks.push( - convertBlockToBlockListStxBlock(stxBlock) - ); +function generateBlockList(stxBlocks: (Block | NakamotoBlock)[]) { + if (stxBlocks.length === 0) return []; + const blockList = [ + { + stxBlocks: [convertBlockToBlockListStxBlock(stxBlocks[0])], + btcBlock: convertBlockToBlockListBtcBlock(stxBlocks[0]), + }, + ]; + if (stxBlocks.length === 1) return blockList; + for (let i = 1; i < stxBlocks.length; i++) { + const stxBlock = stxBlocks[i]; + const latestBtcBlock = blockList[blockList.length - 1]; + if (latestBtcBlock.btcBlock.hash === stxBlock.burn_block_hash) { + latestBtcBlock.stxBlocks.push(convertBlockToBlockListStxBlock(stxBlock)); } else { - // add a new btc block and stx block - initialBlockListDataMapCopy[stxBlock.burn_block_hash] = { + blockList.push({ stxBlocks: [convertBlockToBlockListStxBlock(stxBlock)], btcBlock: convertBlockToBlockListBtcBlock(stxBlock), - }; + }); } - }); - return generateBlockList(initialBlockListDataMapCopy); + } + return blockList; } -export function useUngroupedBlockListBlocksPage() { +function mergeBlockLists(newblockList: BlockListData[], initialBlockList: BlockListData[]) { + if (newblockList.length === 0) return initialBlockList; + const earliestBtcBlock = newblockList[newblockList.length - 1]; + const latestBtcBlock = initialBlockList[0]; + if (earliestBtcBlock.btcBlock.hash === latestBtcBlock.btcBlock.hash) { + const btcBlock = earliestBtcBlock.btcBlock || latestBtcBlock.btcBlock; + const stxBlocks = [...earliestBtcBlock.stxBlocks, ...latestBtcBlock.stxBlocks]; + return [ + ...newblockList.slice(0, newblockList.length - 1), + { btcBlock, stxBlocks }, + , + ...initialBlockList.slice(1), + ]; + } else { + return [...newblockList, ...initialBlockList]; + } +} + +export function useBlocksPageBlockListUngrouped() { const { setBlockListLoading, liveUpdates } = useBlockListContext(); - // TODO: dont really need to have a separate hook for this. This is just doing all the organizing of the data behind the hook const { - initialStxBlocks, initialStxBlocksHashes, - initialBtcBlocks, - initialBtcBlocksHashes, - initialBlockListDataMap, initialBlockList, isFetchingNextPage, fetchNextPage, @@ -58,7 +72,7 @@ export function useUngroupedBlockListBlocksPage() { hasNextPage, } = useInitialBlockList(); - const [latestBlocksToShow, setLatestBlocksToShow] = useState([]); + const [webSocketBlockList, setWebSocketBlockList] = useState([]); const { latestStxBlocks: latestStxBlocksFromWebSocket, @@ -66,12 +80,7 @@ export function useUngroupedBlockListBlocksPage() { clearLatestBlocks: clearLatestBlocksFromWebSocket, } = useBlockListWebSocket3(initialStxBlocksHashes); - const blockList = useMemo( - () => [...latestBlocksToShow, ...initialBlockList], - [initialBlockList, latestBlocksToShow] - ); - - // This is used to trigger a fade out effect when the block list is updated. + // This is used to trigger a fade out effect when the block list is updated. // When the counter is updated, we wait for the fade out effect to finish and then show the fade in effect const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); @@ -86,17 +95,13 @@ export function useUngroupedBlockListBlocksPage() { const showLatestStxBlocksFromWebSocket = useCallback(() => { setBlockListLoading(true); - // const newBlockList = mergeWebSocketUpdates( - // latestStxBlocksFromWebSocket, - // initialBlockListDataMap - // ); - setLatestBlocksToShow(latestStxBlocksFromWebSocket); + const websocketBlockList = generateBlockList(latestStxBlocksFromWebSocket); + setWebSocketBlockList(websocketBlockList); clearLatestBlocksFromWebSocket(); setBlockListUpdateCounter(prev => prev + 1); }, [ latestStxBlocksFromWebSocket, - initialBlockListDataMap, - setLatestBlocksToShow, + setWebSocketBlockList, setBlockListLoading, clearLatestBlocksFromWebSocket, ]); @@ -138,6 +143,11 @@ export function useUngroupedBlockListBlocksPage() { updateBlockList, ]); + const blockList = useMemo( + () => mergeBlockLists(webSocketBlockList, initialBlockList), + [webSocketBlockList, initialBlockList] + ); + return { blockList, updateBlockList, diff --git a/src/app/_components/BlockList/Controls.tsx b/src/app/_components/BlockList/Controls.tsx index a1b78faac..a9d051116 100644 --- a/src/app/_components/BlockList/Controls.tsx +++ b/src/app/_components/BlockList/Controls.tsx @@ -12,16 +12,18 @@ interface ControlsProps extends StackProps { } export function ControlsLayout({ + liveUpdates, horizontal, children, ...rest }: { + liveUpdates?: boolean; horizontal?: boolean; children: React.ReactNode; }) { return ( + + {block.hash} + {block.txsCount} + @@ -300,8 +306,8 @@ export function BurnBlockGroupGrid({ /> {i < stxBlocks.length - 1 && ( - )}{' '} - {/* TODO: adds a border to the bottom. make this css */} + )} + {/* TODO: adds a border to the bottom */} ))} @@ -396,3 +402,35 @@ export function BurnBlockGroup({ ); } + +export function BlockListGroupedLayout({ children }: { children: ReactNode }) { + const { isBlockListLoading } = useBlockListContext(); + + return ( + + {children} + + ); +} + +export function BlockListGrouped({ blockList }: { blockList: BlocksGroupProps[] }) { + return ( + + {blockList.map(block => ( + + ))} + + ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx b/src/app/_components/BlockList/Grouped/skeleton.tsx similarity index 93% rename from src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx rename to src/app/_components/BlockList/Grouped/skeleton.tsx index fddf0cee4..e063fb69b 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/skeleton.tsx +++ b/src/app/_components/BlockList/Grouped/skeleton.tsx @@ -8,9 +8,9 @@ import { Grid } from '../../../../ui/Grid'; import { SkeletonText } from '../../../../ui/SkeletonText'; import { Stack } from '../../../../ui/Stack'; import { Text } from '../../../../ui/Text'; +import { BlocksPageHeaderLayout } from '../BlocksPage/BlocksPageHeaders'; import { ControlsLayout } from '../Controls'; import { UpdateBarLayout } from '../UpdateBar'; -import { BlocksPageHeaderLayout } from './BlocksPageHeaders'; function BitcoinHeaderSkeleton() { return ( @@ -132,7 +132,7 @@ export function BurnBlockGroupListSkeleton({ ); } -export function HomePageBlockListGroupedByBtcBlockSkeleton() { +export function HomePageBlockListGroupedSkeleton() { return (
}> - + + @@ -207,7 +207,7 @@ export function BlocksPageBlockListSkeleton() {
- +
); } diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx deleted file mode 100644 index 6ed3ccbc6..000000000 --- a/src/app/_components/BlockList/GroupedByBurnBlock/HomePageBlockListGroupedByBtcBlock.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { Suspense } from 'react'; - -import { ListFooter } from '../../../../common/components/ListFooter'; -import { Box } from '../../../../ui/Box'; -import { Flex } from '../../../../ui/Flex'; -import { useBlockListContext } from '../BlockListContext'; -import { UpdateBar } from '../UpdateBar'; -import { FADE_DURATION } from '../consts'; -import { BurnBlockGroup } from './BurnBlockGroup'; -import { useBlockListGroupedByBtcBlockHomePage } from './useBlockListGroupedByBtcBlockHomePage'; -import { HomePageBlockListGroupedByBtcBlockSkeleton } from './skeleton'; - -// const LIST_LENGTH = 17; - -function HomePageBlockListGroupedByBtcBlockBase() { - const { liveUpdates, isBlockListLoading } = useBlockListContext(); - const { blockList, updateBlockList, latestBlocksCount } = useBlockListGroupedByBtcBlockHomePage(); - - return ( - - {!liveUpdates && ( - - )} - - {blockList.map(block => ( - - ))} - {!liveUpdates && } - - - ); -} - -export function HomePageBlockListGroupedByBtcBlock() { - return ( - }> - - - ); -} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx b/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx deleted file mode 100644 index 2c2df8719..000000000 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListWebSocket.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import { useCallback, useRef, useState } from 'react'; - -import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; - -import { UIBlockType, UISingleBlock } from '../types'; -import { useSubscribeBlocks2 } from './useSubscribeBlocks2'; - -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 the block is already in the list, don't add it again - if (latestBlockHashes.current.has(block.hash) || initialBlockHashes.has(block.hash)) { - return; - } - // Otherwise, add it to the list - 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.StxBlock, - height: block.height, - hash: block.hash, - timestamp: block.burn_block_time, - txsCount: block.tx_count, - }, - ...prevLatestBlocks, - ]); - } - - updateLatestBlocks(); - }, - [initialBurnBlockHashes, initialBlockHashes] - ); - - useSubscribeBlocks2(handleBlock); - - const clearLatestBlocks = () => { - setLatestBlocks([]); - }; - - return { - latestUIBlocks: latestBlocks, - latestBlock, - latestBlocksCount: latestBlocks.filter(block => block.type === UIBlockType.StxBlock).length, - clearLatestBlocks, - }; -} diff --git a/src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx b/src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx new file mode 100644 index 000000000..f8b2a814f --- /dev/null +++ b/src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { Suspense } from 'react'; + +import { ListFooter } from '../../../../../common/components/ListFooter'; +import { Flex } from '../../../../../ui/Flex'; +import { useBlockListContext } from '../../BlockListContext'; +import { BlockListGrouped } from '../../Grouped/BlockListGrouped'; +import { HomePageBlockListGroupedSkeleton } from '../../Grouped/skeleton'; +import { UpdateBar } from '../../UpdateBar'; +import { useHomePageBlockListGrouped } from './useHomePageBlockListGrouped'; + +function HomePageBlockListGroupedBase() { + const { liveUpdates, isBlockListLoading } = useBlockListContext(); + const { + blockList, + updateBlockList, + latestBlocksCount: latestStxBlocksCountFromWebSocket, + } = useHomePageBlockListGrouped(); + + return ( + <> + {!liveUpdates && ( + + )} + + + {!liveUpdates && } + + + ); +} + +export function HomePageBlockListGrouped() { + return ( + }> + + + ); +} diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx b/src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx similarity index 91% rename from src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx rename to src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx index 17363a073..5ef6e2adc 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useBlockListGroupedByBtcBlockHomePage.tsx +++ b/src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx @@ -3,15 +3,14 @@ import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocksInfinite'; import { useQueryClient } from '@tanstack/react-query'; import { useCallback, useEffect, useMemo, useRef } from 'react'; -import { useBlockListContext } from '../BlockListContext'; -import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; -import { FADE_DURATION } from '../consts'; -import { UIBlockType } from '../types'; -import { BlocksGroupProps } from './BurnBlockGroup'; -import { useBlockListWebSocket } from './useBlockListWebSocket'; -import { useInitialBlockListGroupedByBtcBlockHomePage } from './useInitialBlockListGroupedByBtcHomePage'; +import { useBlockListContext } from '../../BlockListContext'; +import { BlocksGroupProps } from '../../Grouped/BlockListGrouped'; +import { useBlockListWebSocket } from '../../Sockets/useBlockListWebSocket'; +import { FADE_DURATION } from '../../consts'; +import { UIBlockType } from '../../types'; +import { useHomePageInitialBlockListGrouped } from './useHomePageInitialBlockListGrouped'; -export function useBlockListGroupedByBtcBlockHomePage() { +export function useHomePageBlockListGrouped() { const queryClient = useQueryClient(); const { setBlockListLoading: setIsBlockListUpdateLoading, liveUpdates: isLiveUpdateEnabled } = useBlockListContext(); @@ -23,7 +22,7 @@ export function useBlockListGroupedByBtcBlockHomePage() { secondLatestBurnBlockStxBlocks, thirdLatestBurnBlock, thirdLatestBurnBlockStxBlocks, - } = useInitialBlockListGroupedByBtcBlockHomePage(); + } = useHomePageInitialBlockListGrouped(); const initialStxBlockHashes = useMemo( () => diff --git a/src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx b/src/app/_components/BlockList/HomePage/Grouped/useHomePageInitialBlockListGrouped.tsx similarity index 76% rename from src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx rename to src/app/_components/BlockList/HomePage/Grouped/useHomePageInitialBlockListGrouped.tsx index d60fd05ef..892e80251 100644 --- a/src/app/_components/BlockList/GroupedByBurnBlock/useInitialBlockListGroupedByBtcHomePage.tsx +++ b/src/app/_components/BlockList/HomePage/Grouped/useHomePageInitialBlockListGrouped.tsx @@ -1,13 +1,13 @@ import { BurnBlock } from '@stacks/blockchain-api-client'; -import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; -import { useSuspenseBlocksByBurnBlock } from '../../../../common/queries/useBlocksByBurnBlock'; -import { useSuspenseBurnBlocks } from '../../../../common/queries/useBurnBlocksInfinite'; +import { useSuspenseInfiniteQueryResult } from '../../../../../common/hooks/useInfiniteQueryResult'; +import { useSuspenseBlocksByBurnBlock } from '../../../../../common/queries/useBlocksByBurnBlock'; +import { useSuspenseBurnBlocks } from '../../../../../common/queries/useBurnBlocksInfinite'; const BURN_BLOCK_LENGTH = 3; const STX_BLOCK_LENGTH = 3; -export function useInitialBlockListGroupedByBtcBlockHomePage() { +export function useHomePageInitialBlockListGrouped() { const burnBlocks = useSuspenseInfiniteQueryResult( useSuspenseBurnBlocks(BURN_BLOCK_LENGTH), BURN_BLOCK_LENGTH diff --git a/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx b/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx new file mode 100644 index 000000000..e62c64c73 --- /dev/null +++ b/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx @@ -0,0 +1,99 @@ +'use client'; + +import { Stack } from '@/ui/Stack'; +import dynamic from 'next/dynamic'; +import { Suspense, useCallback, useRef } from 'react'; + +import { Section } from '../../../../common/components/Section'; +import { Text } from '../../../../ui/Text'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { useBlockListContext } from '../BlockListContext'; +import { BlockListProvider } from '../BlockListProvider'; +import { Controls } from '../Controls'; +import { HomePageBlockListGroupedSkeleton } from '../Grouped/skeleton'; +import { HomePageBlockListUngroupedSkeleton } from '../Ungrouped/skeleton'; + +const HomePageBlockListGroupedByBtcBlockDynamic = dynamic( + () => import('./Grouped/HomePageBlockListGrouped').then(mod => mod.HomePageBlockListGrouped), + { + loading: () => , + ssr: false, + } +); + +const HomePageUngroupedBlockListDynamic = dynamic( + () => + import('./Ungrouped/HomePageBlockListUngrouped').then(mod => mod.HomePageBlockListUngrouped), + { + loading: () => , + ssr: false, + } +); + +function HomePageBlockListBase() { + const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates } = useBlockListContext(); + + const lastClickTimeRef = useRef(0); + const toggleLiveUpdates = useCallback(() => { + const now = Date.now(); + if (now - lastClickTimeRef.current > 2000) { + lastClickTimeRef.current = now; + setLiveUpdates(!liveUpdates); + } + }, [liveUpdates, setLiveUpdates]); + + return ( +
+ + Recent Blocks + { + setGroupedByBtc(!groupedByBtc); + }, + isChecked: groupedByBtc, + }} + liveUpdates={{ + onChange: toggleLiveUpdates, + isChecked: liveUpdates, + }} + padding={0} + gap={3} + marginX={0} + border="none" + /> + + {groupedByBtc ? ( + + ) : ( + + )} +
+ ); +} + +export function HomePageBlockList() { + return ( + + + }> + + + + + ); +} diff --git a/src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx b/src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx new file mode 100644 index 000000000..039f0e7dc --- /dev/null +++ b/src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx @@ -0,0 +1,44 @@ +'use client'; + +import { Suspense } from 'react'; + +import { ListFooter } from '../../../../../common/components/ListFooter'; +import { Flex } from '../../../../../ui/Flex'; +import { useBlockListContext } from '../../BlockListContext'; +import { BlockListUngrouped } from '../../Ungrouped/BlocksListUngrouped'; +import { HomePageBlockListUngroupedSkeleton } from '../../Ungrouped/skeleton'; +import { UpdateBar } from '../../UpdateBar'; +import { useHomePageBlockListUngrouped } from './useHomePageBlockListUngrouped'; + +function HomePageBlockListUngroupedBase() { + const { liveUpdates } = useBlockListContext(); + const { + latestBlocksCount: latestStxBlocksCountFromWebSocket, + updateBlockList, + blocksList, + } = useHomePageBlockListUngrouped(); + + return ( + <> + {!liveUpdates && ( + + )} + + + {!liveUpdates && } + + + ); +} + +export function HomePageBlockListUngrouped() { + return ( + }> + + + ); +} diff --git a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListHomePage.tsx b/src/app/_components/BlockList/HomePage/Ungrouped/useHomePageBlockListUngrouped.tsx similarity index 94% rename from src/app/_components/BlockList/Ungrouped/useUngroupedBlockListHomePage.tsx rename to src/app/_components/BlockList/HomePage/Ungrouped/useHomePageBlockListUngrouped.tsx index b793f2139..4e139a799 100644 --- a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockListHomePage.tsx +++ b/src/app/_components/BlockList/HomePage/Ungrouped/useHomePageBlockListUngrouped.tsx @@ -5,15 +5,15 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { NakamotoBlock } from '@stacks/blockchain-api-client'; -import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; +import { useSuspenseInfiniteQueryResult } from '../../../../../common/hooks/useInfiniteQueryResult'; import { BLOCK_LIST_QUERY_KEY, useSuspenseBlocksInfiniteNew, -} from '../../../../common/queries/useBlockListInfinite'; -import { useBlockListContext } from '../BlockListContext'; -import { useBlockListWebSocket } from '../Sockets/useBlockListWebSocket'; -import { FADE_DURATION } from '../consts'; -import { BlockListBtcBlock, BlockListStxBlock, UISingleBlock } from '../types'; +} from '../../../../../common/queries/useBlockListInfinite'; +import { useBlockListContext } from '../../BlockListContext'; +import { useBlockListWebSocket } from '../../Sockets/useBlockListWebSocket'; +import { FADE_DURATION } from '../../consts'; +import { BlockListBtcBlock, BlockListStxBlock, UISingleBlock } from '../../types'; const LIMIT = 3; @@ -30,7 +30,7 @@ function runAfterFadeOut(callback: () => void) { * If just toggling live, requery to update * If receving new blocks while live, reorganize state to accomodate new blocks */ -export function useUngroupedBlockListHomePage() { +export function useHomePageBlockListUngrouped() { const { setBlockListLoading, liveUpdates } = useBlockListContext(); const [latestBlocks, setLatestBlocks] = useState([]); diff --git a/src/app/_components/BlockList/Ungrouped/Blocks.tsx b/src/app/_components/BlockList/Ungrouped/Blocks.tsx deleted file mode 100644 index 1b806f7c0..000000000 --- a/src/app/_components/BlockList/Ungrouped/Blocks.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { Icon } from '../../../../ui/Icon'; -import { Stack } from '../../../../ui/Stack'; -import { StxIcon } from '../../../../ui/icons'; -import { BlockCount } from '../BlockCount'; -import { FADE_DURATION } from '../consts'; -import { UIBlock, UIBlockType } from '../types'; -import { BtcBlockListItem } from './BtcBlockListItem'; -import { StxBlockListItem } from './StxBlockListItem'; - -export function Blocks({ - blockList, - isUpdateListLoading, - stxBlocksCountLimit, -}: { - blockList: UIBlock[]; - isUpdateListLoading: boolean; - stxBlocksCountLimit: number; -}) { - return ( - - {blockList.map((block, i) => { - switch (block.type) { - case UIBlockType.StxBlock: - const isFirstStxBlockInBurnBlock = - i === 0 || (i > 0 && blockList[i - 1].type === UIBlockType.BurnBlock); // what is this check for? - (i > 0 && blockList[i - 1].type === UIBlockType.BurnBlock. It's to make sure to skip Burn Blocks that dont have any stxx txs. Stacks tx should be first - return ( - // TODO: update to use new component - - ) : undefined - } - hasBorder={i < blockList.length && blockList[i + 1].type === UIBlockType.StxBlock} - /> - ); - case UIBlockType.BurnBlock: // TODO: update to use new component - return ( - - ); - case UIBlockType.Count: - return ; - } - })} - - ); -} diff --git a/src/app/_components/BlockList/Ungrouped/UngroupedBlocksList.tsx b/src/app/_components/BlockList/Ungrouped/BlocksListUngrouped.tsx similarity index 52% rename from src/app/_components/BlockList/Ungrouped/UngroupedBlocksList.tsx rename to src/app/_components/BlockList/Ungrouped/BlocksListUngrouped.tsx index 41d8ffe6f..af08770df 100644 --- a/src/app/_components/BlockList/Ungrouped/UngroupedBlocksList.tsx +++ b/src/app/_components/BlockList/Ungrouped/BlocksListUngrouped.tsx @@ -1,29 +1,25 @@ import { ReactNode } from 'react'; -import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; -import { StxIcon } from '../../../../ui/icons'; import { BlockCount } from '../BlockCount'; import { useBlockListContext } from '../BlockListContext'; import { FADE_DURATION } from '../consts'; import { BlockListBtcBlock, BlockListStxBlock } from '../types'; import { BtcBlockListItem } from './BtcBlockListItem'; -import { StxBlockListItem } from './StxBlockListItem'; +import { StxBlocksGrid } from './StxBlockListItem'; export interface BlocksByBtcBlock { stxBlocks: BlockListStxBlock[]; btcBlock: BlockListBtcBlock; } -export type UngroupedBlockList = BlocksByBtcBlock[]; // Ironic the ungrouped block list is grouped by btc block... +export type BlockListUngrouped = BlocksByBtcBlock[]; -export function UngroupedBlockListLayout({ children }: { children: ReactNode }) { +export function BlockListUngroupedLayout({ children }: { children: ReactNode }) { const { isBlockListLoading } = useBlockListContext(); return ( - {stxBlocksShortList.map((block, i) => ( - : undefined} - hasBorder={i < stxBlocks.length - 1} - /> - ))} + {stxBlocksLimit && stxBlocks.length > stxBlocksLimit && ( )} @@ -75,22 +63,25 @@ function BlocksGroupedByBtcBlock({ ); } -export function UngroupedBlockList({ - ungroupedBlockList, +export function BlockListUngrouped({ + blockList, stxBlocksLimit, + minimized = false, }: { - ungroupedBlockList: UngroupedBlockList; + blockList: BlockListUngrouped; stxBlocksLimit?: number; + minimized?: boolean; }) { return ( - - {ungroupedBlockList.map((blocksGroupedByBtcBlock, i) => ( - + {blockList.map(blocksGroupedByBtcBlock => ( + ))} - + ); } diff --git a/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx b/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx index 79a9d284a..121e9142e 100644 --- a/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx +++ b/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx @@ -4,7 +4,6 @@ import { BsArrowReturnLeft } from 'react-icons/bs'; import { ExplorerLink } from '../../../../common/components/ExplorerLinks'; import { Timestamp } from '../../../../common/components/Timestamp'; -import { useGlobalContext } from '../../../../common/context/useAppContext'; import { truncateMiddle } from '../../../../common/utils/utils'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; @@ -21,15 +20,12 @@ export function BtcBlockListItemLayout({ children }: { children: ReactNode }) { const textColor = useColorModeValue('slate.700', 'slate.500'); // TODO: not in theme. remove return ( {children} @@ -38,7 +34,6 @@ export function BtcBlockListItemLayout({ children }: { children: ReactNode }) { } export function BtcBlockListItemContent({ timestamp, height, hash }: BtcBlockListItemProps) { - const { btcBlockBaseUrl } = useGlobalContext().activeNetwork; const iconColor = useColorModeValue('slate.600', 'slate.800'); // TODO: not in theme. remove return ( <> diff --git a/src/app/_components/BlockList/Ungrouped/HomePageUngroupedBlockList.tsx b/src/app/_components/BlockList/Ungrouped/HomePageUngroupedBlockList.tsx deleted file mode 100644 index 04a4185d9..000000000 --- a/src/app/_components/BlockList/Ungrouped/HomePageUngroupedBlockList.tsx +++ /dev/null @@ -1,32 +0,0 @@ -'use client'; - -import { ListFooter } from '../../../../common/components/ListFooter'; -import { Box } from '../../../../ui/Box'; -import { useBlockListContext } from '../BlockListContext'; -import { UpdateBar } from '../UpdateBar'; -import { UngroupedBlockList } from './UngroupedBlocksList'; -import { useUngroupedBlockListHomePage } from './useUngroupedBlockListHomePage'; - -// TODO: Create a layout component for this. It'll use the same one as -export function HomePageUngroupedBlockList() { - const { liveUpdates } = useBlockListContext(); - const { - latestBlocksCount: latestStxBlocksCountFromWebSocket, - updateBlockList, - blocksList, - } = useUngroupedBlockListHomePage(); - console.log({ blocksList }); - - return ( - - {!liveUpdates && ( - - )} - - {!liveUpdates && } - - ); -} diff --git a/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx b/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx index ac27b558c..2bb1c68f0 100644 --- a/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx +++ b/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx @@ -1,3 +1,4 @@ +import { useColorModeValue } from '@chakra-ui/react'; import { ReactNode } from 'react'; import { Circle } from '../../../../common/components/Circle'; @@ -6,8 +7,12 @@ import { Timestamp } from '../../../../common/components/Timestamp'; import { truncateMiddle } from '../../../../common/utils/utils'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; +import { Grid } from '../../../../ui/Grid'; import { HStack } from '../../../../ui/HStack'; +import { Icon } from '../../../../ui/Icon'; import { Text } from '../../../../ui/Text'; +import { StxIcon } from '../../../../ui/icons'; +import { BlockListStxBlock } from '../types'; interface StxBlockListItemLayoutProps { children: ReactNode; @@ -15,6 +20,229 @@ interface StxBlockListItemLayoutProps { hasBorder: boolean; } +function LineAndNode({ + rowHeight = 14, + width = 6, + icon, +}: { + rowHeight: number; + width: number; + icon?: ReactNode; +}) { + return ( + + {icon ? ( + + + {icon} + + + + ) : ( + + + + + )} + + ); +} + +// TODO: copied from BlockListGrouped +export function ListHeader({ children, ...textProps }: { children: ReactNode } & TextProps) { + const color = useColorModeValue('slate.700', 'slate.250'); + return ( + + {children} + + ); +} + +// TODO: copied from BlockListGrouped +const GroupHeader = () => { + return ( + <> + + Block height + + + Block hash + + + Transactions + + + Timestamp + + + ); +}; + +export function StxBlocksGrid({ + stxBlocks, + minimized, +}: { + stxBlocks: BlockListStxBlock[]; + minimized: boolean; +}) { + return ( + + {minimized ? null : } + {stxBlocks.map((stxBlock, i) => ( + <> + : undefined} + hasBorder={i !== 0} + minimized={minimized} + /> + {i < stxBlocks.length - 1 && ( + + )} + + ))} + + ); +} + +function StxBlockRow({ + height, + hash, + timestamp, + txsCount, + icon, + minimized, +}: { + height: number | string; + hash: string; + timestamp: number; + txsCount?: number; + icon?: ReactNode; + hasBorder: boolean; + minimized?: boolean; +}) { + return minimized ? ( + <> + + + + + #{height} + + + + +  ∙ } fontSize={'12px'} color="textSubdued" gridColumn="3 / 4"> + {truncateMiddle(hash, 3)} + {txsCount !== undefined ? {txsCount} txn : null} + + + + ) : ( + <> + + + + + #{height} + + + + + + + + {hash} + + + + + + + {txsCount} + + + + + + + + ); +} + export function StxBlockListItemLayout({ children, hasIcon, @@ -22,24 +250,26 @@ export function StxBlockListItemLayout({ }: StxBlockListItemLayoutProps) { return ( ); } - -function StxBlockListItemContent({ - height, - hash, - timestamp, - txsCount, - icon, -}: { - height: number | string; - hash: string; - timestamp: number; - txsCount?: number; - icon?: ReactNode; -}) { - return ( - <> - - {!!icon && ( - - {icon} - - )} - - - - #{height} - - - -  ∙ } fontSize={'12px'} color="textSubdued"> - {truncateMiddle(hash, 3)} - {txsCount !== undefined ? {txsCount} txn : null} - - - - ); -} - -interface StxBlockListItemProps { - height: number | string; - hash: string; - timestamp: number; - txsCount?: number; - icon?: ReactNode; - hasBorder: boolean; -} - -export function StxBlockListItem({ - height, - hash, - timestamp, - txsCount, - icon, - hasBorder, -}: StxBlockListItemProps) { - return ( - - - - ); -} diff --git a/src/app/_components/BlockList/UpdateBar.tsx b/src/app/_components/BlockList/UpdateBar.tsx index ba4d69665..c557a4f4c 100644 --- a/src/app/_components/BlockList/UpdateBar.tsx +++ b/src/app/_components/BlockList/UpdateBar.tsx @@ -14,13 +14,19 @@ interface UpdateBarProps extends FlexProps { onClick: () => void; } -export function UpdateBarLayout({ children, ...rest }: { children: ReactNode }) { - const { isBlockListLoading } = useBlockListContext(); +export function UpdateBarLayout({ + isBlockListLoading, + children, + ...rest +}: { + isBlockListLoading: boolean; + children: ReactNode; +}) { const bgColor = useColorModeValue('purple.100', 'slate.900'); // TODO: not in theme. remove return ( { const now = Date.now(); @@ -50,7 +57,7 @@ export function UpdateBar({ latestBlocksCount, onClick, ...rest }: UpdateBarProp }, [onClick]); return ( - + (response); - const initialStxBlocks: Record = useMemo( - () => - blocks.reduce( - (acc, block) => { - if (!acc[block.burn_block_hash]) { - acc[block.burn_block_hash] = convertBlockToBlockListStxBlock(block); - } - return acc; - }, - {} as Record - ), + const initialStxBlocks: BlockListStxBlock[] = useMemo( + () => blocks.map(block => convertBlockToBlockListStxBlock(block)), [blocks] ); - const initialStxBlocksHashes = useMemo( - () => new Set(Object.keys(initialStxBlocks)), - [initialStxBlocks] - ); + const initialStxBlocksHashes = useMemo(() => { + const stxBlockHashSet = new Set(); + initialStxBlocks.forEach(block => stxBlockHashSet.add(block.hash)); + return stxBlockHashSet; + }, [initialStxBlocks]); - const initialBtcBlocks: Record = useMemo( - () => - blocks.reduce( - (acc, block) => { - if (!acc[block.burn_block_hash]) { - acc[block.burn_block_hash] = convertBlockToBlockListBtcBlock(block); - } - return acc; - }, - {} as Record - ), - [blocks] - ); +// const initialBtcBlocks: Record = useMemo( +// () => +// blocks.reduce( +// (acc, block) => { +// if (!acc[block.burn_block_hash]) { +// acc[block.burn_block_hash] = convertBlockToBlockListBtcBlock(block); +// } +// return acc; +// }, +// {} as Record +// ), +// [blocks] +// ); - const initialBtcBlocksHashes = useMemo( - () => new Set(Object.keys(initialBtcBlocks)), - [initialBtcBlocks] - ); +// const initialBtcBlocksHashes = useMemo( +// () => new Set(Object.keys(initialBtcBlocks)), +// [initialBtcBlocks] +// ); // btc block hash -> BlockListData const initialBlockListDataMap: Record = useMemo( @@ -126,9 +118,9 @@ export function useInitialBlockList() { return { initialStxBlocks, initialStxBlocksHashes, - initialBtcBlocks, - initialBtcBlocksHashes, - initialBlockListDataMap, + // initialBtcBlocks, + // initialBtcBlocksHashes, + // initialBlockListDataMap, initialBlockList, refetchInitialBlockList, isFetchingNextPage, diff --git a/src/app/blocks/PageClient.tsx b/src/app/blocks/PageClient.tsx index 7c25882a6..1ceb1e712 100644 --- a/src/app/blocks/PageClient.tsx +++ b/src/app/blocks/PageClient.tsx @@ -1,11 +1,38 @@ 'use client'; import type { NextPage } from 'next'; +import dynamic from 'next/dynamic'; import { useGlobalContext } from '../../common/context/useAppContext'; +<<<<<<< HEAD import { PaginatedBlockListLayoutA } from '../_components/BlockList/LayoutA/Paginated'; +======= +import { BlocksPageHeaders } from '../_components/BlockList/BlocksPage/BlocksPageHeaders'; +import { BlocksPageBlockListSkeleton } from '../_components/BlockList/Grouped/skeleton'; +import { SkeletonBlockList } from '../_components/BlockList/SkeletonBlockList'; +>>>>>>> c13e9712 (feat(grouped-by-btc-block-list-view-3): added table layout for ungrouped) import { PageTitle } from '../_components/PageTitle'; +const BlocksPageBlockListDynamic = dynamic( + () => + import('../_components/BlockList/BlocksPage/BlocksPageBlockList').then( + mod => mod.BlocksPageBlockList + ), + { + loading: () => , + ssr: false, + } +); + +const PaginatedBlockListLayoutADynamic = dynamic( + () => + import('../_components/BlockList/LayoutA/Paginated').then(mod => mod.PaginatedBlockListLayoutA), + { + loading: () => , // TODO: fix this + ssr: false, + } +); + export function BlocksPageLayout({ blocksPageHeaders, blocksList, diff --git a/src/app/blocks/skeleton.tsx b/src/app/blocks/skeleton.tsx index 35bed27a4..09351a6de 100644 --- a/src/app/blocks/skeleton.tsx +++ b/src/app/blocks/skeleton.tsx @@ -3,7 +3,7 @@ import { BlockPageHeadersSkeleton, BlocksPageBlockListSkeleton, -} from '../_components/BlockList/GroupedByBurnBlock/skeleton'; +} from '../_components/BlockList/Grouped/skeleton'; import { BlocksPageLayout } from './PageClient'; export default function BlocksPageSkeleton() { diff --git a/src/app/btcblock/[hash]/PageClient.tsx b/src/app/btcblock/[hash]/PageClient.tsx index 069a0b344..af5c3441d 100644 --- a/src/app/btcblock/[hash]/PageClient.tsx +++ b/src/app/btcblock/[hash]/PageClient.tsx @@ -1,8 +1,7 @@ 'use client'; -import { BurnBlockGroupGrid } from '@/app/_components/BlockList/GroupedByBurnBlock/BurnBlockGroup'; +import { BurnBlockGroupGrid } from '@/app/_components/BlockList/Grouped/BlockListGrouped'; import { UIBlockType, UISingleBlock } from '@/app/_components/BlockList/types'; -import { BlockBtcAnchorBlockCard } from '@/app/block/[hash]/BlockBtcAnchorBlockCard'; import { NavBlock, NavDirection } from '@/app/btcblock/[hash]/NavBlock'; import { ListFooter } from '@/common/components/ListFooter'; import { useSuspenseInfiniteQueryResult } from '@/common/hooks/useInfiniteQueryResult'; From 9d0ccdeb2a5b952940ac2e4b2174c2bf9ebe21d0 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Wed, 10 Apr 2024 00:40:20 -0500 Subject: [PATCH 20/70] feat(grouped-by-btc-block-list-view-3): updated grid layout --- .../Grouped/BlocksPageBlockListGrouped.tsx | 22 +- .../Grouped/useBlocksPageBlockListGrouped.tsx | 2 +- .../BlocksPageBlockListUngrouped.tsx | 4 +- .../BlockList/Grouped/BlockListGrouped.tsx | 330 +++++++----------- .../Grouped/HomePageBlockListGrouped.tsx | 2 +- .../Grouped/useHomePageBlockListGrouped.tsx | 6 +- .../Ungrouped/HomePageBlockListUngrouped.tsx | 2 +- ...ockListItem.tsx => BlockListUngrouped.tsx} | 186 +++++++--- .../Ungrouped/BlocksListUngrouped.tsx | 87 ----- .../BlockList/Ungrouped/skeleton.tsx | 2 +- 10 files changed, 281 insertions(+), 362 deletions(-) rename src/app/_components/BlockList/Ungrouped/{StxBlockListItem.tsx => BlockListUngrouped.tsx} (71%) delete mode 100644 src/app/_components/BlockList/Ungrouped/BlocksListUngrouped.tsx diff --git a/src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx b/src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx index bf044bfbd..7f4eedf7e 100644 --- a/src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx @@ -8,10 +8,9 @@ import { Box } from '../../../../../ui/Box'; import { Flex } from '../../../../../ui/Flex'; import { ExplorerErrorBoundary } from '../../../ErrorBoundary'; import { useBlockListContext } from '../../BlockListContext'; -import { BurnBlockGroup } from '../../Grouped/BlockListGrouped'; +import { BlockListGrouped } from '../../Grouped/BlockListGrouped'; import { BlocksPageBlockListGroupedSkeleton } from '../../Grouped/skeleton'; import { UpdateBar } from '../../UpdateBar'; -import { FADE_DURATION } from '../../consts'; import { useBlocksPageBlockListGrouped } from './useBlocksPageBlockListGrouped'; function BlocksPageBlockListGroupedBase() { @@ -33,23 +32,8 @@ function BlocksPageBlockListGroupedBase() { {!liveUpdates && ( )} - - {blockList.map(block => ( - - ))} + + {(!liveUpdates || !enablePagination) && ( diff --git a/src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx b/src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx index e4696a9f4..d7956dc99 100644 --- a/src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx @@ -140,7 +140,7 @@ export function useBlocksPageBlockListGrouped(blockListLimit: number) { timestamp: block?.block_time, // TODO: this is the right timestamp to use, but it seems to be inaccurate txsCount: block.tx_count, })), - stxBlocksDisplayLimit: blockListLimit, + stxBlocksLimit: blockListLimit, }, ...restOfBlockList, ]; diff --git a/src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx b/src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx index 4ed57d286..02b56657c 100644 --- a/src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx +++ b/src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx @@ -7,11 +7,11 @@ import { Suspense } from 'react'; import { Section } from '../../../../../common/components/Section'; import { ExplorerErrorBoundary } from '../../../ErrorBoundary'; import { useBlockListContext } from '../../BlockListContext'; +import { BlockListUngrouped } from '../../Ungrouped/BlockListUngrouped'; import { BlocksPageBlockListUngroupedSkeleton } from '../../Ungrouped/skeleton'; import { UpdateBar } from '../../UpdateBar'; import { FADE_DURATION } from '../../consts'; import { useBlocksPageBlockListUngrouped } from './useBlocksPageBlockListUngrouped'; -import { BlockListUngrouped } from '../../Ungrouped/BlocksListUngrouped'; function runAfterFadeOut(callback: () => void) { setTimeout(callback, FADE_DURATION); @@ -123,7 +123,7 @@ function BlocksPageBlockListUngroupedBase() { isFetchingNextPage, updateBlockList, } = useBlocksPageBlockListUngrouped(); - console.log({ blockList }) + console.log({ blockList }); return ( diff --git a/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx index 54e8494aa..afc782373 100644 --- a/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx +++ b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx @@ -2,7 +2,6 @@ import { useColorModeValue } from '@chakra-ui/react'; import { ReactNode, useEffect, useRef, useState } from 'react'; import { PiArrowElbowLeftDown } from 'react-icons/pi'; -import { Circle } from '../../../../common/components/Circle'; import { BlockLink, ExplorerLink } from '../../../../common/components/ExplorerLinks'; import { Timestamp } from '../../../../common/components/Timestamp'; import { truncateMiddle } from '../../../../common/utils/utils'; @@ -17,11 +16,13 @@ import { BitcoinIcon, StxIcon } from '../../../../ui/icons'; import { Caption } from '../../../../ui/typography'; import { BlockCount } from '../BlockCount'; import { useBlockListContext } from '../BlockListContext'; +import { LineAndNode } from '../Ungrouped/BlockListUngrouped'; import { FADE_DURATION } from '../consts'; import { UISingleBlock } from '../types'; const PADDING = 4; +// TODO: move to common components export function ListHeader({ children, ...textProps }: { children: ReactNode } & TextProps) { const color = useColorModeValue('slate.700', 'slate.250'); return ( @@ -29,10 +30,10 @@ export function ListHeader({ children, ...textProps }: { children: ReactNode } & py={2} px={2.5} color={color} - bg={'hoverBackground'} - fontSize={'xs'} - rounded={'md'} - whiteSpace={'nowrap'} + bg="hoverBackground" + fontSize="xs" + rounded="md" + whiteSpace="nowrap" {...textProps} > {children} @@ -40,31 +41,11 @@ export function ListHeader({ children, ...textProps }: { children: ReactNode } & ); } -const GroupHeaderSkeleton = () => {}; - +// TODO: move to common components const GroupHeader = () => { return ( <> - + Block height @@ -80,119 +61,7 @@ const GroupHeader = () => { ); }; -// TODO: ideally this would be a table -const StxBlockRow = ({ - block, - icon, - minimized = false, -}: { - block: UISingleBlock; - icon?: ReactNode; - minimized?: boolean; -}) => { - return ( - <> - {minimized ? ( - <> - - {icon} - - - #{block.height} - - - - - ∙} gap={1} justifySelf="end"> - - - {truncateMiddle(block.hash)} - - - - {block.txsCount || 0} txn - - - - - - - ) : ( - <> - - {icon} - - - #{block.height} - - - - - - - {block.hash} - - - - - {block.txsCount} - - - - - - - )} - - ); -}; - +// TODO: move to common components // adds horizontal scrolling to its children if they overflow the container's width, and adds a class to the container when it has a horizontal scrollbar function ScrollableDiv({ children }: { children: ReactNode }) { const [hasHorizontalScroll, setHasHorizontalScroll] = useState(false); @@ -219,7 +88,6 @@ function ScrollableDiv({ children }: { children: ReactNode }) { ref={divRef} overflowX={'auto'} overflowY={'hidden'} - // py={4} className={hasHorizontalScroll ? 'has-horizontal-scroll' : ''} > {children} @@ -228,86 +96,138 @@ function ScrollableDiv({ children }: { children: ReactNode }) { } export interface BlocksGroupProps { - burnBlock: UISingleBlock; + burnBlock: UISingleBlock; // TODO: don't use this. Have to change data fetching. Use new websocket hook stxBlocks: UISingleBlock[]; /** * TODO: change to * burnBlock: BurnBlock; * stxBlocks: Block[]; */ - stxBlocksDisplayLimit?: number; + stxBlocksLimit?: number; minimized?: boolean; } -export function BurnBlockGroupGrid({ - burnBlock, - stxBlocks, - stxBlocksDisplayLimit, +const mobileBorderCss = { + '.has-horizontal-scroll &:before': { + // Adds a border to the left of the first column + content: '""', + position: 'absolute', + right: 0, + top: 0, + bottom: 0, + width: '2px', + height: 'var(--stacks-sizes-14)', + backgroundColor: 'borderPrimary', + }, +}; + +const StxBlockRow = ({ + block, + icon, minimized = false, -}: BlocksGroupProps) { - const stxBlocksToDisplay = stxBlocksDisplayLimit - ? stxBlocks.slice(0, stxBlocksDisplayLimit) - : stxBlocks; +}: { + block: UISingleBlock; + icon?: ReactNode; + minimized?: boolean; +}) => { + return minimized ? ( + <> + + + + + #{block.height} + + + + + ∙} gap={1} whiteSpace="nowrap" gridColumn="3 / 4"> + + + {truncateMiddle(block.hash, 3)} + + + {block.txsCount !== undefined ? ( + + {block.txsCount || 0} txn + + ) : null} + + + + ) : ( + <> + + + + + #{block.height} + + + + + + + + {block.hash} + + + + + + + {block.txsCount} + + + + + + + + ); +}; + +export function BurnBlockGroupGrid({ burnBlock, stxBlocks, minimized }: BlocksGroupProps) { return ( {minimized ? null : } - {stxBlocksToDisplay.map((stxBlock, i) => ( + {stxBlocks.map((stxBlock, i) => ( <> - - - ) : ( - - ) - } + icon={i === 0 ? : undefined} minimized={minimized} /> {i < stxBlocks.length - 1 && ( )} - {/* TODO: adds a border to the bottom */} ))} @@ -369,16 +289,15 @@ export function Footer({ stxBlocks, txSum }: { stxBlocks: UISingleBlock[]; txSum export function BurnBlockGroup({ burnBlock, stxBlocks, - stxBlocksDisplayLimit = stxBlocks.length, + stxBlocksLimit, minimized = false, }: BlocksGroupProps) { - const stxBlocksNotDisplayed = burnBlock.txsCount - ? burnBlock.txsCount - (stxBlocksDisplayLimit || 0) - : 0; + const stxBlocksNotDisplayed = burnBlock.txsCount ? burnBlock.txsCount - (stxBlocksLimit || 0) : 0; const txSum = stxBlocks.reduce((txSum, stxBlock) => { const txsCount = stxBlock?.txsCount ?? 0; return txSum + txsCount; }, 0); + const stxBlocksShortList = stxBlocksLimit ? stxBlocks.slice(0, stxBlocksLimit) : stxBlocks; // const totalTime = stxBlocks.reduce((totalTime, stxBlock) => { // const blockTime = stxBlock.timestamp ?? 0; // return totalTime + blockTime; @@ -392,8 +311,7 @@ export function BurnBlockGroup({ @@ -420,15 +338,21 @@ export function BlockListGroupedLayout({ children }: { children: ReactNode }) { ); } -export function BlockListGrouped({ blockList }: { blockList: BlocksGroupProps[] }) { +export function BlockListGrouped({ + blockList, + minimized, +}: { + blockList: BlocksGroupProps[]; + minimized: boolean; +}) { return ( {blockList.map(block => ( ))} diff --git a/src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx b/src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx index f8b2a814f..569cc5e4a 100644 --- a/src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx @@ -28,7 +28,7 @@ function HomePageBlockListGroupedBase() { /> )} - + {!liveUpdates && } diff --git a/src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx b/src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx index 5ef6e2adc..81c820c12 100644 --- a/src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx @@ -132,7 +132,7 @@ export function useHomePageBlockListGrouped() { hash: block.hash, timestamp: block.burn_block_time, })), - stxBlocksDisplayLimit: 3, + stxBlocksLimit: 3, }, { burnBlock: { @@ -148,7 +148,7 @@ export function useHomePageBlockListGrouped() { hash: block.hash, timestamp: block.burn_block_time, })), - stxBlocksDisplayLimit: 3, + stxBlocksLimit: 3, }, { burnBlock: { @@ -164,7 +164,7 @@ export function useHomePageBlockListGrouped() { hash: block.hash, timestamp: block.burn_block_time, })), - stxBlocksDisplayLimit: 3, + stxBlocksLimit: 3, }, ]; diff --git a/src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx b/src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx index 039f0e7dc..e7d6b6586 100644 --- a/src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx +++ b/src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx @@ -5,7 +5,7 @@ import { Suspense } from 'react'; import { ListFooter } from '../../../../../common/components/ListFooter'; import { Flex } from '../../../../../ui/Flex'; import { useBlockListContext } from '../../BlockListContext'; -import { BlockListUngrouped } from '../../Ungrouped/BlocksListUngrouped'; +import { BlockListUngrouped } from '../../Ungrouped/BlockListUngrouped'; import { HomePageBlockListUngroupedSkeleton } from '../../Ungrouped/skeleton'; import { UpdateBar } from '../../UpdateBar'; import { useHomePageBlockListUngrouped } from './useHomePageBlockListUngrouped'; diff --git a/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx b/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx similarity index 71% rename from src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx rename to src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx index 2bb1c68f0..7f2dcec2e 100644 --- a/src/app/_components/BlockList/Ungrouped/StxBlockListItem.tsx +++ b/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx @@ -10,9 +10,38 @@ import { Flex } from '../../../../ui/Flex'; import { Grid } from '../../../../ui/Grid'; import { HStack } from '../../../../ui/HStack'; import { Icon } from '../../../../ui/Icon'; +import { Stack } from '../../../../ui/Stack'; import { Text } from '../../../../ui/Text'; import { StxIcon } from '../../../../ui/icons'; -import { BlockListStxBlock } from '../types'; +import { BlockCount } from '../BlockCount'; +import { useBlockListContext } from '../BlockListContext'; +import { FADE_DURATION } from '../consts'; +import { BlockListBtcBlock, BlockListStxBlock } from '../types'; +import { BtcBlockListItem } from './BtcBlockListItem'; + +export interface BlocksByBtcBlock { + stxBlocks: BlockListStxBlock[]; + btcBlock: BlockListBtcBlock; +} + +export type BlockListUngrouped = BlocksByBtcBlock[]; + +export function BlockListUngroupedLayout({ children }: { children: ReactNode }) { + const { isBlockListLoading } = useBlockListContext(); + + return ( + + {children} + + ); +} interface StxBlockListItemLayoutProps { children: ReactNode; @@ -20,7 +49,8 @@ interface StxBlockListItemLayoutProps { hasBorder: boolean; } -function LineAndNode({ +// TODO: move to common +export function LineAndNode({ rowHeight = 14, width = 6, icon, @@ -141,43 +171,6 @@ const GroupHeader = () => { ); }; -export function StxBlocksGrid({ - stxBlocks, - minimized, -}: { - stxBlocks: BlockListStxBlock[]; - minimized: boolean; -}) { - return ( - - {minimized ? null : } - {stxBlocks.map((stxBlock, i) => ( - <> - : undefined} - hasBorder={i !== 0} - minimized={minimized} - /> - {i < stxBlocks.length - 1 && ( - - )} - - ))} - - ); -} - function StxBlockRow({ height, hash, @@ -191,12 +184,11 @@ function StxBlockRow({ timestamp: number; txsCount?: number; icon?: ReactNode; - hasBorder: boolean; minimized?: boolean; }) { return minimized ? ( <> - + @@ -205,9 +197,23 @@ function StxBlockRow({ -  ∙ } fontSize={'12px'} color="textSubdued" gridColumn="3 / 4"> - {truncateMiddle(hash, 3)} - {txsCount !== undefined ? {txsCount} txn : null} +  ∙ } + gap={1} + whiteSpace="nowrap" + color="textSubdued" + gridColumn="3 / 4" + > + + + {truncateMiddle(hash, 3)} + + + {txsCount !== undefined ? ( + + {txsCount || 0} txn + + ) : null} @@ -243,6 +249,98 @@ function StxBlockRow({ ); } +function StxBlocksGrid({ + stxBlocks, + minimized, +}: { + stxBlocks: BlockListStxBlock[]; + minimized: boolean; +}) { + return ( + + {minimized ? null : } + {stxBlocks.map((stxBlock, i) => ( + <> + : undefined} + minimized={minimized} + /> + {i < stxBlocks.length - 1 && ( + + )} + + ))} + + ); +} + +// Ironic name for a component that is supposedly ungrouped... +function StxBlocksGroupedByBtcBlock({ + blockList, + stxBlocksLimit, + minimized = false, +}: { + blockList: BlocksByBtcBlock; + stxBlocksLimit?: number; + minimized?: boolean; +}) { + const btcBlock = blockList.btcBlock; + const stxBlocks = blockList.stxBlocks; + const stxBlocksShortList = stxBlocksLimit + ? blockList.stxBlocks.slice(0, stxBlocksLimit) + : blockList.stxBlocks; + + return ( + <> + + {stxBlocksLimit && stxBlocks.length > stxBlocksLimit && ( + + )} + + + ); +} + +export function BlockListUngrouped({ + blockList, + stxBlocksLimit, + minimized = false, +}: { + blockList: BlockListUngrouped; + stxBlocksLimit?: number; + minimized?: boolean; +}) { + return ( + + {blockList.map(blocksGroupedByBtcBlock => ( + + ))} + + ); +} + +// TODO: redo this component export function StxBlockListItemLayout({ children, hasIcon, diff --git a/src/app/_components/BlockList/Ungrouped/BlocksListUngrouped.tsx b/src/app/_components/BlockList/Ungrouped/BlocksListUngrouped.tsx deleted file mode 100644 index af08770df..000000000 --- a/src/app/_components/BlockList/Ungrouped/BlocksListUngrouped.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { ReactNode } from 'react'; - -import { Stack } from '../../../../ui/Stack'; -import { BlockCount } from '../BlockCount'; -import { useBlockListContext } from '../BlockListContext'; -import { FADE_DURATION } from '../consts'; -import { BlockListBtcBlock, BlockListStxBlock } from '../types'; -import { BtcBlockListItem } from './BtcBlockListItem'; -import { StxBlocksGrid } from './StxBlockListItem'; - -export interface BlocksByBtcBlock { - stxBlocks: BlockListStxBlock[]; - btcBlock: BlockListBtcBlock; -} - -export type BlockListUngrouped = BlocksByBtcBlock[]; - -export function BlockListUngroupedLayout({ children }: { children: ReactNode }) { - const { isBlockListLoading } = useBlockListContext(); - - return ( - - {children} - - ); -} - -function StxBlocksGroupedByBtcBlock({ - blockList, - stxBlocksLimit, - minimized = false, -}: { - blockList: BlocksByBtcBlock; - stxBlocksLimit?: number; - minimized?: boolean; -}) { - const btcBlock = blockList.btcBlock; - const stxBlocks = blockList.stxBlocks; - const stxBlocksShortList = stxBlocksLimit - ? blockList.stxBlocks.slice(0, stxBlocksLimit) - : blockList.stxBlocks; - - return ( - <> - - {stxBlocksLimit && stxBlocks.length > stxBlocksLimit && ( - - )} - - - ); -} - -export function BlockListUngrouped({ - blockList, - stxBlocksLimit, - minimized = false, -}: { - blockList: BlockListUngrouped; - stxBlocksLimit?: number; - minimized?: boolean; -}) { - return ( - - {blockList.map(blocksGroupedByBtcBlock => ( - - ))} - - ); -} diff --git a/src/app/_components/BlockList/Ungrouped/skeleton.tsx b/src/app/_components/BlockList/Ungrouped/skeleton.tsx index ce504e7fc..795b8e895 100644 --- a/src/app/_components/BlockList/Ungrouped/skeleton.tsx +++ b/src/app/_components/BlockList/Ungrouped/skeleton.tsx @@ -3,8 +3,8 @@ import { Stack } from '@/ui/Stack'; import { Circle } from '../../../../common/components/Circle'; import { Flex } from '../../../../ui/Flex'; import { SkeletonText } from '../../../../ui/SkeletonText'; +import { StxBlockListItemLayout } from './BlockListUngrouped'; import { BtcBlockListItemLayout } from './BtcBlockListItem'; -import { StxBlockListItemLayout } from './StxBlockListItem'; function StxBlockListItemContentSkeleton({ hasIcon }: { hasIcon: boolean }) { return ( From f3da4117afd39101c42402928dba75c98586bb32 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Thu, 11 Apr 2024 18:34:02 -0500 Subject: [PATCH 21/70] feat(grouped-by-btc-block-list-view-3): so so much --- .../BlocksPage/BlocksPageBlockList.tsx | 7 +- .../BlocksPageBlockListGrouped.tsx | 29 ++- .../BlocksPageBlockListUngrouped.tsx | 67 ++++++ .../Grouped/useBlocksPageBlockListGrouped.tsx | 156 ------------- .../BlocksPageBlockListUngrouped.tsx | 168 -------------- .../useBlocksPageBlockListUngrouped.ts | 159 ------------- .../BlockList/Grouped/BlockListGrouped.tsx | 175 +++++++------- .../BlockList/Grouped/skeleton.tsx | 76 ++++--- .../Grouped/useHomePageBlockListGrouped.tsx | 176 -------------- .../useHomePageInitialBlockListGrouped.tsx | 41 ---- .../BlockList/HomePage/HomePageBlockList.tsx | 5 +- .../HomePageBlockListGrouped.tsx | 21 +- .../HomePageBlockListUngrouped.tsx | 23 +- .../useHomePageBlockListUngrouped.tsx | 190 ---------------- src/app/_components/BlockList/LineAndNode.tsx | 72 ++++++ ...ebSocket3.ts => useBlockListWebSocket2.ts} | 7 +- .../BlockList/Sockets/useSubscribeBlocks2.ts | 63 +++-- .../Ungrouped/BlockListUngrouped.tsx | 215 ++++-------------- .../{BtcBlockListItem.tsx => BtcBlockRow.tsx} | 17 +- .../BlockList/Ungrouped/skeleton.tsx | 118 +++++++--- .../Ungrouped/useUngroupedBlockList.ts | 82 ------- .../BlockList/UpdatedBlockList.tsx | 1 - src/app/_components/BlockList/consts.ts | 2 + .../data/useBlocksPageBlockListGrouped.tsx | 118 ++++++++++ ...cksPageBlockListGroupedInitialBlockList.ts | 87 +++++++ .../data/useBlocksPageBlockListUngrouped.ts | 121 ++++++++++ .../BlockList/data/useHomePageBlockList.ts | 113 +++++++++ .../data/useHomePageInitialBlockList.ts | 97 ++++++++ .../data/useStxBlocksForBtcBlocks.ts | 25 ++ src/app/_components/BlockList/types.ts | 1 - .../_components/BlockList/useInitialBlocks.ts | 130 ----------- src/app/_components/BlockList/utils.ts | 94 ++++++++ src/app/layout.tsx | 1 - src/common/context/GlobalContext.tsx | 3 +- src/common/queries/useBlocksByBurnBlock.ts | 12 + 35 files changed, 1148 insertions(+), 1524 deletions(-) rename src/app/_components/BlockList/BlocksPage/{Grouped => }/BlocksPageBlockListGrouped.tsx (61%) create mode 100644 src/app/_components/BlockList/BlocksPage/BlocksPageBlockListUngrouped.tsx delete mode 100644 src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx delete mode 100644 src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx delete mode 100644 src/app/_components/BlockList/BlocksPage/Ungrouped/useBlocksPageBlockListUngrouped.ts delete mode 100644 src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx delete mode 100644 src/app/_components/BlockList/HomePage/Grouped/useHomePageInitialBlockListGrouped.tsx rename src/app/_components/BlockList/HomePage/{Grouped => }/HomePageBlockListGrouped.tsx (59%) rename src/app/_components/BlockList/HomePage/{Ungrouped => }/HomePageBlockListUngrouped.tsx (56%) delete mode 100644 src/app/_components/BlockList/HomePage/Ungrouped/useHomePageBlockListUngrouped.tsx create mode 100644 src/app/_components/BlockList/LineAndNode.tsx rename src/app/_components/BlockList/Sockets/{useBlockListWebSocket3.ts => useBlockListWebSocket2.ts} (85%) rename src/app/_components/BlockList/Ungrouped/{BtcBlockListItem.tsx => BtcBlockRow.tsx} (77%) delete mode 100644 src/app/_components/BlockList/Ungrouped/useUngroupedBlockList.ts create mode 100644 src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx create mode 100644 src/app/_components/BlockList/data/useBlocksPageBlockListGroupedInitialBlockList.ts create mode 100644 src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts create mode 100644 src/app/_components/BlockList/data/useHomePageBlockList.ts create mode 100644 src/app/_components/BlockList/data/useHomePageInitialBlockList.ts create mode 100644 src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts delete mode 100644 src/app/_components/BlockList/useInitialBlocks.ts create mode 100644 src/app/_components/BlockList/utils.ts diff --git a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx index 5d4330504..93743950a 100644 --- a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx @@ -12,7 +12,7 @@ import { BlocksPageBlockListGroupedSkeleton } from '../Grouped/skeleton'; import { BlocksPageBlockListUngroupedSkeleton } from '../Ungrouped/skeleton'; const BlocksPageBlockListGroupedDynamic = dynamic( - () => import('./Grouped/BlocksPageBlockListGrouped').then(mod => mod.BlocksPageBlockListGrouped), + () => import('./BlocksPageBlockListGrouped').then(mod => mod.BlocksPageBlockListGrouped), { loading: () => , ssr: false, @@ -20,10 +20,7 @@ const BlocksPageBlockListGroupedDynamic = dynamic( ); const BlocksPageBlockListUngroupedDynamic = dynamic( - () => - import('./Ungrouped/BlocksPageBlockListUngrouped').then( - mod => mod.BlocksPageBlockListUngrouped - ), + () => import('./BlocksPageBlockListUngrouped').then(mod => mod.BlocksPageBlockListUngrouped), { loading: () => , ssr: false, diff --git a/src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx similarity index 61% rename from src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx rename to src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx index 7f4eedf7e..edd19dd30 100644 --- a/src/app/_components/BlockList/BlocksPage/Grouped/BlocksPageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockListGrouped.tsx @@ -3,18 +3,18 @@ import { ListFooter } from '@/common/components/ListFooter'; import { Suspense } from 'react'; -import { Section } from '../../../../../common/components/Section'; -import { Box } from '../../../../../ui/Box'; -import { Flex } from '../../../../../ui/Flex'; -import { ExplorerErrorBoundary } from '../../../ErrorBoundary'; -import { useBlockListContext } from '../../BlockListContext'; -import { BlockListGrouped } from '../../Grouped/BlockListGrouped'; -import { BlocksPageBlockListGroupedSkeleton } from '../../Grouped/skeleton'; -import { UpdateBar } from '../../UpdateBar'; -import { useBlocksPageBlockListGrouped } from './useBlocksPageBlockListGrouped'; +import { Section } from '../../../../common/components/Section'; +import { Box } from '../../../../ui/Box'; +import { Flex } from '../../../../ui/Flex'; +import { ExplorerErrorBoundary } from '../../ErrorBoundary'; +import { useBlockListContext } from '../BlockListContext'; +import { BlockListGrouped } from '../Grouped/BlockListGrouped'; +import { BlocksPageBlockListGroupedSkeleton } from '../Grouped/skeleton'; +import { UpdateBar } from '../UpdateBar'; +import { useBlocksPageBlockListGrouped } from '../data/useBlocksPageBlockListGrouped'; function BlocksPageBlockListGroupedBase() { - const { liveUpdates, isBlockListLoading } = useBlockListContext(); + const { liveUpdates } = useBlockListContext(); const { blockList, updateBlockList, @@ -22,10 +22,7 @@ function BlocksPageBlockListGroupedBase() { isFetchingNextPage, hasNextPage, fetchNextPage, - } = useBlocksPageBlockListGrouped(10); - console.log({ liveUpdates }); - - const enablePagination = true; + } = useBlocksPageBlockListGrouped(); return ( <> @@ -33,10 +30,10 @@ function BlocksPageBlockListGroupedBase() { )} - + - {(!liveUpdates || !enablePagination) && ( + {!liveUpdates && ( + {!liveUpdates && ( + + )} + + + {!liveUpdates && ( + + )} + + + ); +} + +export function BlocksPageBlockListUngrouped() { + return ( + + }> + + + + ); +} diff --git a/src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx b/src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx deleted file mode 100644 index d7956dc99..000000000 --- a/src/app/_components/BlockList/BlocksPage/Grouped/useBlocksPageBlockListGrouped.tsx +++ /dev/null @@ -1,156 +0,0 @@ -import { GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY } from '@/common/queries/useBlocksByBurnBlock'; -import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocksInfinite'; -import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; - -import { BurnBlock } from '@stacks/blockchain-api-client'; - -import { useSuspenseInfiniteQueryResult } from '../../../../../common/hooks/useInfiniteQueryResult'; -import { useSuspenseBlocksByBurnBlock } from '../../../../../common/queries/useBlocksByBurnBlock'; -import { useSuspenseBurnBlocks } from '../../../../../common/queries/useBurnBlocksInfinite'; -import { useBlockListContext } from '../../BlockListContext'; -import { BlocksGroupProps } from '../../Grouped/BlockListGrouped'; -import { useBlockListWebSocket } from '../../Sockets/useBlockListWebSocket'; -import { UIBlockType, UISingleBlock } from '../../types'; - -const STX_BLOCK_LENGTH = 10; -const BURN_BLOCK_LENGTH = 10; - -export function useBlocksPageBlockListGrouped(blockListLimit: number) { - const queryClient = useQueryClient(); - const { setBlockListLoading, liveUpdates: isLiveUpdatesEnabled } = useBlockListContext(); - - const response = useSuspenseBurnBlocks(BURN_BLOCK_LENGTH, {}, 'blockList'); - const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; - const burnBlocks = useSuspenseInfiniteQueryResult(response); - - const latestBurnBlock = useMemo(() => burnBlocks[0], [burnBlocks]); - - const latestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( - useSuspenseBlocksByBurnBlock(latestBurnBlock.burn_block_height, STX_BLOCK_LENGTH), - STX_BLOCK_LENGTH - ); - - const stxBlockHashes = useMemo( - () => new Set([...latestBurnBlockStxBlocks.map(block => block.hash)]), - [latestBurnBlockStxBlocks] - ); - const burnBlockHashes = useMemo( - () => new Set([...burnBlocks.map(block => block.burn_block_hash)]), - [burnBlocks] - ); - - const { - latestStxBlock: latestStxBlockFromWebSocket, - latestStxBlocksCount: latestStxBlocksCountFromWebSocket, - clearLatestBlocks: clearLatestStxBlocksFromWebSocket, - } = useBlockListWebSocket(stxBlockHashes, burnBlockHashes); // TODO: fix this - - const updateBlockList = useCallback( - async function () { - setBlockListLoading(true); - await Promise.all([ - // Invalidates queries so they will be refetched - queryClient.invalidateQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), - queryClient.invalidateQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), - ]); - clearLatestStxBlocksFromWebSocket(); // clears updates since we are get the latest data from refetching the queries - setBlockListLoading(false); - }, - [clearLatestStxBlocksFromWebSocket, queryClient, setBlockListLoading] - ); - - const prevIsLiveUpdateEnabledRef = useRef(isLiveUpdatesEnabled); - const prevLatestStxBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); - - useEffect(() => { - const liveUpdatesToggled = prevIsLiveUpdateEnabledRef.current !== isLiveUpdatesEnabled; - - // If live updates are enabled and one or more new blocks have been received, verified by checking the latest block count and the previous latest block count, then - // add the latest block to the list of blocks - const receivedLatestStxBlockFromLiveUpdates = - isLiveUpdatesEnabled && - latestStxBlocksCountFromWebSocket > 0 && // stx blocks data coming from the websocket - prevLatestStxBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; - - // If live updates have been toggled, then refetch/update the block list - if (liveUpdatesToggled) { - setBlockListLoading(true); // TODO: can I remove the setBlockListLoading(true) and setBlockListLoading(false) from here? since it's already in the updateBlockList function - updateBlockList().then(() => { - setBlockListLoading(false); - }); - } else if (receivedLatestStxBlockFromLiveUpdates && latestStxBlockFromWebSocket) { - // If latest stx block belongs to the latest burn block, add it to the latest burn block list of stx blocks - if (latestStxBlockFromWebSocket.burn_block_height === latestBurnBlock.burn_block_height) { - setBlockListLoading(true); - latestBurnBlockStxBlocks.unshift(latestStxBlockFromWebSocket); - latestBurnBlock.stacks_blocks.unshift(latestStxBlockFromWebSocket.hash); - setBlockListLoading(false); - // setTimeout(() => { - // latestBurnBlockStxBlocks.unshift(latestStxBlockFromWebSocket); - // latestBurnBlock.stacks_blocks.unshift(latestStxBlockFromWebSocket.hash); - // setBlockListLoading(false); - // }, FADE_DURATION); - } else { - // Otherwise, we have a new burn block, in which case, adding a new burn block is the equivalent of refetching/updating the block list - updateBlockList(); // TODO: I dont think we should query again since we have the data we need to make the update - } - } - - prevIsLiveUpdateEnabledRef.current = isLiveUpdatesEnabled; - prevLatestStxBlocksCountRef.current = latestStxBlocksCountFromWebSocket; - }, [ - latestStxBlockFromWebSocket, - latestStxBlocksCountFromWebSocket, - isLiveUpdatesEnabled, - latestBurnBlock, - latestBurnBlockStxBlocks, - latestBurnBlock.stacks_blocks, - latestBurnBlock.burn_block_height, - clearLatestStxBlocksFromWebSocket, - updateBlockList, - setBlockListLoading, - ]); - - const restOfBlockList: BlocksGroupProps[] = burnBlocks.slice(1).map(burnBlock => ({ - burnBlock: { - type: UIBlockType.BurnBlock, - height: burnBlock.burn_block_height, - hash: burnBlock.burn_block_hash, - timestamp: burnBlock.burn_block_time, - txsCount: burnBlock.stacks_blocks.length, - }, - stxBlocks: [] as UISingleBlock[], - stxBlocksDisplayLimit: 0, - })); - - const blockList: BlocksGroupProps[] = [ - { - burnBlock: { - type: UIBlockType.BurnBlock, - height: latestBurnBlock.burn_block_height, - hash: latestBurnBlock.burn_block_hash, - timestamp: latestBurnBlock.burn_block_time, - txsCount: latestBurnBlock.stacks_blocks.length, - }, - stxBlocks: latestBurnBlockStxBlocks.map(block => ({ - type: UIBlockType.StxBlock, - height: block.height, - hash: block.hash, - timestamp: block?.block_time, // TODO: this is the right timestamp to use, but it seems to be inaccurate - txsCount: block.tx_count, - })), - stxBlocksLimit: blockListLimit, - }, - ...restOfBlockList, - ]; - - return { - blockList, - updateBlockList, - latestBlocksCount: latestStxBlocksCountFromWebSocket, - isFetchingNextPage, - fetchNextPage, - hasNextPage, - }; -} diff --git a/src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx b/src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx deleted file mode 100644 index 02b56657c..000000000 --- a/src/app/_components/BlockList/BlocksPage/Ungrouped/BlocksPageBlockListUngrouped.tsx +++ /dev/null @@ -1,168 +0,0 @@ -'use client'; - -import { ListFooter } from '@/common/components/ListFooter'; -import { Box } from '@/ui/Box'; -import { Suspense } from 'react'; - -import { Section } from '../../../../../common/components/Section'; -import { ExplorerErrorBoundary } from '../../../ErrorBoundary'; -import { useBlockListContext } from '../../BlockListContext'; -import { BlockListUngrouped } from '../../Ungrouped/BlockListUngrouped'; -import { BlocksPageBlockListUngroupedSkeleton } from '../../Ungrouped/skeleton'; -import { UpdateBar } from '../../UpdateBar'; -import { FADE_DURATION } from '../../consts'; -import { useBlocksPageBlockListUngrouped } from './useBlocksPageBlockListUngrouped'; - -function runAfterFadeOut(callback: () => void) { - setTimeout(callback, FADE_DURATION); -} - -function BlocksPageBlockListUngroupedBase() { - const { liveUpdates } = useBlockListContext(); - - // // TODO: dont really need to have a separate hook for this. This is just doing all the organizing of the data behind the hook - // const { initialBlockList, initialBurnBlocks, hasNextPage, isFetchingNextPage, fetchNextPage } = - // useUngroupedBlockList(); - - // const [latestBlocksToShow, setLatestBlocksToShow] = useState([]); - // const blockList = useMemo( - // () => [...latestBlocksToShow, ...initialBlockList], - // [initialBlockList, latestBlocksToShow] - // ); - - // const blockHashes = useMemo(() => { - // return new Set(initialBlockList.map(block => block.hash)); - // }, [initialBlockList]); - - // const burnBlockHashes = useMemo(() => { - // return new Set(Object.keys(initialBurnBlocks)); - // }, [initialBurnBlocks]); - - // const { - // latestUIBlocks: latestUIBlockFromWebSocket, - // latestStxBlocksCount: latestStxBlocksCountFromWebSocket, - // clearLatestBlocks: clearLatestBlocksFromWebSocket, - // } = useBlockListWebSocket(blockHashes, burnBlockHashes); - - // const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); - // // This is used to trigger a fade out effect when the block list is updated. When the counter is updated, we finish loading and show the fade in effect - // const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); - - // useEffect(() => { - // if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { - // runAfterFadeOut(() => { - // setBlockListLoading(false); - // }); - // } - // }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); - - // const showLatestBlocks = useCallback(() => { - // setBlockListLoading(true); - // runAfterFadeOut(() => { - // setLatestBlocksToShow(prevBlockList => { - // return [...latestUIBlockFromWebSocket, ...prevBlockList]; - // }); - // clearLatestBlocksFromWebSocket(); - // setBlockListUpdateCounter(prev => prev + 1); - // }); - // }, [ - // latestUIBlockFromWebSocket, - // setLatestBlocksToShow, - // setBlockListLoading, - // clearLatestBlocksFromWebSocket, - // ]); - - // const queryClient = useQueryClient(); - // const updateBlockListWithQuery = useCallback( - // async function () { - // setBlockListLoading(true); - // runAfterFadeOut(async () => { - // await Promise.all([ - // // Invalidates queries so they will be refetched - // queryClient.invalidateQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }), - // ]).then(() => { - // clearLatestBlocksFromWebSocket(); - // setBlockListUpdateCounter(prev => prev + 1); - // }); - // }); - // }, - // [clearLatestBlocksFromWebSocket, queryClient, setBlockListLoading] - // ); - - // const prevLiveUpdatesRef = useRef(isLiveUpdatesEnabled); - // const prevLatestBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); - - // useEffect(() => { - // const liveUpdatesToggled = prevLiveUpdatesRef.current !== isLiveUpdatesEnabled; - - // const receivedLatestStxBlockFromLiveUpdates = - // isLiveUpdatesEnabled && - // latestStxBlocksCountFromWebSocket > 0 && - // prevLatestBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; - - // if (liveUpdatesToggled) { - // updateBlockListWithQuery(); - // } else if (receivedLatestStxBlockFromLiveUpdates) { - // showLatestBlocks(); - // } - - // prevLiveUpdatesRef.current = isLiveUpdatesEnabled; - // prevLatestBlocksCountRef.current = latestStxBlocksCountFromWebSocket; - // }, [ - // isLiveUpdatesEnabled, - // latestStxBlocksCountFromWebSocket, - // showLatestBlocks, - // updateBlockListWithQuery, - // ]); - - const { - blockList, - latestStxBlocksCountFromWebSocket, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - updateBlockList, - } = useBlocksPageBlockListUngrouped(); - console.log({ blockList }); - - return ( - - {!liveUpdates && ( - - )} - - - {!liveUpdates && ( - - )} - - - ); -} - -export function BlocksPageBlockListUngrouped() { - return ( - - }> - - - - ); -} diff --git a/src/app/_components/BlockList/BlocksPage/Ungrouped/useBlocksPageBlockListUngrouped.ts b/src/app/_components/BlockList/BlocksPage/Ungrouped/useBlocksPageBlockListUngrouped.ts deleted file mode 100644 index 1d5369d39..000000000 --- a/src/app/_components/BlockList/BlocksPage/Ungrouped/useBlocksPageBlockListUngrouped.ts +++ /dev/null @@ -1,159 +0,0 @@ -'use client'; - -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -import { Block, NakamotoBlock } from '@stacks/blockchain-api-client'; - -import { useBlockListContext } from '../../BlockListContext'; -import { useBlockListWebSocket3 } from '../../Sockets/useBlockListWebSocket3'; -import { FADE_DURATION } from '../../consts'; -import { - BlockListData, - convertBlockToBlockListBtcBlock, - convertBlockToBlockListStxBlock, - useInitialBlockList, -} from '../../useInitialBlocks'; - -function runAfterFadeOut(callback: () => void) { - setTimeout(callback, FADE_DURATION); -} - -function generateBlockList(stxBlocks: (Block | NakamotoBlock)[]) { - if (stxBlocks.length === 0) return []; - const blockList = [ - { - stxBlocks: [convertBlockToBlockListStxBlock(stxBlocks[0])], - btcBlock: convertBlockToBlockListBtcBlock(stxBlocks[0]), - }, - ]; - if (stxBlocks.length === 1) return blockList; - for (let i = 1; i < stxBlocks.length; i++) { - const stxBlock = stxBlocks[i]; - const latestBtcBlock = blockList[blockList.length - 1]; - if (latestBtcBlock.btcBlock.hash === stxBlock.burn_block_hash) { - latestBtcBlock.stxBlocks.push(convertBlockToBlockListStxBlock(stxBlock)); - } else { - blockList.push({ - stxBlocks: [convertBlockToBlockListStxBlock(stxBlock)], - btcBlock: convertBlockToBlockListBtcBlock(stxBlock), - }); - } - } - return blockList; -} - -function mergeBlockLists(newblockList: BlockListData[], initialBlockList: BlockListData[]) { - if (newblockList.length === 0) return initialBlockList; - const earliestBtcBlock = newblockList[newblockList.length - 1]; - const latestBtcBlock = initialBlockList[0]; - if (earliestBtcBlock.btcBlock.hash === latestBtcBlock.btcBlock.hash) { - const btcBlock = earliestBtcBlock.btcBlock || latestBtcBlock.btcBlock; - const stxBlocks = [...earliestBtcBlock.stxBlocks, ...latestBtcBlock.stxBlocks]; - return [ - ...newblockList.slice(0, newblockList.length - 1), - { btcBlock, stxBlocks }, - , - ...initialBlockList.slice(1), - ]; - } else { - return [...newblockList, ...initialBlockList]; - } -} - -export function useBlocksPageBlockListUngrouped() { - const { setBlockListLoading, liveUpdates } = useBlockListContext(); - - const { - initialStxBlocksHashes, - initialBlockList, - isFetchingNextPage, - fetchNextPage, - refetchInitialBlockList, - hasNextPage, - } = useInitialBlockList(); - - const [webSocketBlockList, setWebSocketBlockList] = useState([]); - - const { - latestStxBlocks: latestStxBlocksFromWebSocket, - latestStxBlocksCount: latestStxBlocksCountFromWebSocket, - clearLatestBlocks: clearLatestBlocksFromWebSocket, - } = useBlockListWebSocket3(initialStxBlocksHashes); - - // This is used to trigger a fade out effect when the block list is updated. - // When the counter is updated, we wait for the fade out effect to finish and then show the fade in effect - const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); - const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); - useEffect(() => { - if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { - setBlockListLoading(false); - // runAfterFadeOut(() => { - // setBlockListLoading(false); - // }); - } - }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); - - const showLatestStxBlocksFromWebSocket = useCallback(() => { - setBlockListLoading(true); - const websocketBlockList = generateBlockList(latestStxBlocksFromWebSocket); - setWebSocketBlockList(websocketBlockList); - clearLatestBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); - }, [ - latestStxBlocksFromWebSocket, - setWebSocketBlockList, - setBlockListLoading, - clearLatestBlocksFromWebSocket, - ]); - - const updateBlockList = useCallback( - async function () { - setBlockListLoading(true); - await refetchInitialBlockList(() => { - clearLatestBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); - }); - }, - [clearLatestBlocksFromWebSocket, setBlockListLoading, refetchInitialBlockList] - ); - - const prevLiveUpdatesRef = useRef(liveUpdates); - const prevLatestBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); - // Handles live updates - useEffect(() => { - const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; - - const receivedLatestStxBlockFromLiveUpdates = - liveUpdates && - latestStxBlocksCountFromWebSocket > 0 && - prevLatestBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; - - if (liveUpdatesToggled) { - updateBlockList(); - } else if (receivedLatestStxBlockFromLiveUpdates) { - showLatestStxBlocksFromWebSocket(); - } - - prevLiveUpdatesRef.current = liveUpdates; - prevLatestBlocksCountRef.current = latestStxBlocksCountFromWebSocket; - }, [ - liveUpdates, - latestStxBlocksCountFromWebSocket, - showLatestStxBlocksFromWebSocket, - updateBlockList, - ]); - - const blockList = useMemo( - () => mergeBlockLists(webSocketBlockList, initialBlockList), - [webSocketBlockList, initialBlockList] - ); - - return { - blockList, - updateBlockList, - latestStxBlocksCountFromWebSocket, - hasNextPage, - fetchNextPage, - isFetchingNextPage, - }; -} diff --git a/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx index afc782373..4a87f9fe9 100644 --- a/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx +++ b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx @@ -1,4 +1,3 @@ -import { useColorModeValue } from '@chakra-ui/react'; import { ReactNode, useEffect, useRef, useState } from 'react'; import { PiArrowElbowLeftDown } from 'react-icons/pi'; @@ -11,51 +10,41 @@ import { Grid } from '../../../../ui/Grid'; import { HStack } from '../../../../ui/HStack'; import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; -import { Text, TextProps } from '../../../../ui/Text'; +import { Text } from '../../../../ui/Text'; import { BitcoinIcon, StxIcon } from '../../../../ui/icons'; import { Caption } from '../../../../ui/typography'; +import { ListHeader } from '../../ListHeader'; import { BlockCount } from '../BlockCount'; import { useBlockListContext } from '../BlockListContext'; -import { LineAndNode } from '../Ungrouped/BlockListUngrouped'; +import { LineAndNode } from '../LineAndNode'; import { FADE_DURATION } from '../consts'; -import { UISingleBlock } from '../types'; +import { BlockListBtcBlock, BlockListStxBlock } from '../types'; +import { BlockListData } from '../utils'; const PADDING = 4; -// TODO: move to common components -export function ListHeader({ children, ...textProps }: { children: ReactNode } & TextProps) { - const color = useColorModeValue('slate.700', 'slate.250'); - return ( - - {children} - - ); -} - -// TODO: move to common components const GroupHeader = () => { return ( <> - Block height + + Block height + - Block hash + + Block hash + - Transactions + + Transactions + - Timestamp + + Timestamp + ); @@ -95,18 +84,6 @@ function ScrollableDiv({ children }: { children: ReactNode }) { ); } -export interface BlocksGroupProps { - burnBlock: UISingleBlock; // TODO: don't use this. Have to change data fetching. Use new websocket hook - stxBlocks: UISingleBlock[]; - /** - * TODO: change to - * burnBlock: BurnBlock; - * stxBlocks: Block[]; - */ - stxBlocksLimit?: number; - minimized?: boolean; -} - const mobileBorderCss = { '.has-horizontal-scroll &:before': { // Adds a border to the left of the first column @@ -122,11 +99,11 @@ const mobileBorderCss = { }; const StxBlockRow = ({ - block, + stxBlock, icon, minimized = false, }: { - block: UISingleBlock; + stxBlock: BlockListStxBlock; icon?: ReactNode; minimized?: boolean; }) => { @@ -138,30 +115,30 @@ const StxBlockRow = ({ gap={2} fontSize="xs" sx={mobileBorderCss} - key={block.hash} + key={stxBlock.hash} gridColumn="1 / 2" alignItems="center" > - + - #{block.height} + #{stxBlock.height} ∙} gap={1} whiteSpace="nowrap" gridColumn="3 / 4"> - + - {truncateMiddle(block.hash, 3)} + {truncateMiddle(stxBlock.hash, 3)} - {block.txsCount !== undefined ? ( + {stxBlock.txsCount !== undefined ? ( - {block.txsCount || 0} txn + {stxBlock.txsCount || 0} txn ) : null} - + ) : ( @@ -174,54 +151,73 @@ const StxBlockRow = ({ gap={2} fontSize="xs" sx={mobileBorderCss} - key={block.hash} + key={stxBlock.hash} alignItems="center" > - + - #{block.height} + #{stxBlock.height} - + - {block.hash} + {stxBlock.hash} - {block.txsCount} + {stxBlock.txsCount} - + ); }; -export function BurnBlockGroupGrid({ burnBlock, stxBlocks, minimized }: BlocksGroupProps) { +export function BurnBlockGroupGridLayout({ + minimized, + children, +}: { + minimized?: boolean; + children: ReactNode; +}) { return ( + {children} + + ); +} + +export function BurnBlockGroupGrid({ + stxBlocks, + minimized, +}: { + stxBlocks: BlockListStxBlock[]; // TODO: remove + minimized: boolean; +}) { + return ( + {minimized ? null : } {stxBlocks.map((stxBlock, i) => ( <> : undefined} minimized={minimized} /> @@ -230,15 +226,15 @@ export function BurnBlockGroupGrid({ burnBlock, stxBlocks, minimized }: BlocksGr )} ))} - + ); } function BitcoinHeader({ - burnBlock, + btcBlock, minimized = false, }: { - burnBlock: UISingleBlock; + btcBlock: BlockListBtcBlock; minimized?: boolean; }) { return ( @@ -255,20 +251,20 @@ function BitcoinHeader({ - {burnBlock.height} + {btcBlock.height} ∙} gap={1}> - - {truncateMiddle(burnBlock.hash, 6)} + + {truncateMiddle(btcBlock.hash, 6)} - + ); } -export function Footer({ stxBlocks, txSum }: { stxBlocks: UISingleBlock[]; txSum: number }) { +export function Footer({ stxBlocks, txSum }: { stxBlocks: BlockListStxBlock[]; txSum: number }) { return ( ∙} gap={1} pt={4} whiteSpace="nowrap"> @@ -286,13 +282,21 @@ export function Footer({ stxBlocks, txSum }: { stxBlocks: UISingleBlock[]; txSum ); } +export interface BlocksGroupProps { + btcBlock: BlockListBtcBlock; + stxBlocks: BlockListStxBlock[]; + stxBlocksLimit?: number; + minimized?: boolean; +} + export function BurnBlockGroup({ - burnBlock, + btcBlock, stxBlocks, stxBlocksLimit, minimized = false, }: BlocksGroupProps) { - const stxBlocksNotDisplayed = burnBlock.txsCount ? burnBlock.txsCount - (stxBlocksLimit || 0) : 0; + const numStxBlocks = btcBlock.txsCount ?? stxBlocks.length; + const numStxBlocksNotDisplayed = numStxBlocks - (stxBlocksLimit || 0); const txSum = stxBlocks.reduce((txSum, stxBlock) => { const txsCount = stxBlock?.txsCount ?? 0; return txSum + txsCount; @@ -303,19 +307,13 @@ export function BurnBlockGroup({ // return totalTime + blockTime; // }, 0); // const averageBlockTime = stxBlocks.length ? Math.floor(totalTime / stxBlocks.length) : 0; - console.log({ burnBlock, stxBlocks, stxBlocksDisplayLimit, stxBlocksNotDisplayed }); // TODO: remove - // TODO: why are we not using table here? return ( - - + + - + - {stxBlocksNotDisplayed > 0 ? : null} + {numStxBlocksNotDisplayed > 0 ? : null}
); @@ -341,18 +339,21 @@ export function BlockListGroupedLayout({ children }: { children: ReactNode }) { export function BlockListGrouped({ blockList, minimized, + stxBlocksLimit, }: { - blockList: BlocksGroupProps[]; + blockList: BlockListData[]; minimized: boolean; + stxBlocksLimit?: number; }) { return ( {blockList.map(block => ( ))} diff --git a/src/app/_components/BlockList/Grouped/skeleton.tsx b/src/app/_components/BlockList/Grouped/skeleton.tsx index e063fb69b..5b04b998c 100644 --- a/src/app/_components/BlockList/Grouped/skeleton.tsx +++ b/src/app/_components/BlockList/Grouped/skeleton.tsx @@ -1,16 +1,19 @@ +import { Icon } from '@/ui/Icon'; +import { StxIcon } from '@/ui/icons'; import { useColorModeValue } from '@chakra-ui/react'; import { Circle } from '../../../../common/components/Circle'; import { Section } from '../../../../common/components/Section'; import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; -import { Grid } from '../../../../ui/Grid'; import { SkeletonText } from '../../../../ui/SkeletonText'; import { Stack } from '../../../../ui/Stack'; import { Text } from '../../../../ui/Text'; import { BlocksPageHeaderLayout } from '../BlocksPage/BlocksPageHeaders'; import { ControlsLayout } from '../Controls'; +import { BlockListRowSkeleton } from '../Ungrouped/skeleton'; import { UpdateBarLayout } from '../UpdateBar'; +import { BurnBlockGroupGridLayout } from './BlockListGrouped'; function BitcoinHeaderSkeleton() { return ( @@ -51,11 +54,11 @@ function BlockCountSkeleton() { ); } -function GridHeaderRowSkeleton() { +export function BlockListGridHeaderRowSkeleton() { return ( <> {Array.from({ length: 4 }).map((_, colIndex) => ( - - {Array.from({ length: numTxs }).map((_, rowIndex) => - Array.from({ length: 4 }).map((_, colIndex) => ( - - )) - )} - - ); -} - -export function BurnBlockGroupSkeleton({ numTxs }: { numTxs: number }) { +export function BurnBlockGroupSkeleton({ + numTxs, + minimized, +}: { + numTxs: number; + minimized?: boolean; +}) { return ( - - - - + + {minimized ? null : } + {Array.from({ length: numTxs }).map((_, rowIndex) => ( + : undefined} + minimized={minimized} + /> + ))} + @@ -108,24 +104,31 @@ export function BurnBlockGroupListSkeleton({ numBurnBlockGroupsWithTxs, numTransactionsinBurnBlockGroupWithTxs, numBurnBlockGroupsWithoutTxs, + minimized, }: { numBurnBlockGroupsWithTxs: number; numTransactionsinBurnBlockGroupWithTxs: number; numBurnBlockGroupsWithoutTxs: number; + minimized?: boolean; }) { return ( - + {numBurnBlockGroupsWithTxs ? Array.from({ length: numBurnBlockGroupsWithTxs }).map((_, i) => ( )) : null} {numBurnBlockGroupsWithoutTxs ? Array.from({ length: numBurnBlockGroupsWithoutTxs }).map((_, i) => ( - + )) : null} @@ -134,13 +137,12 @@ export function BurnBlockGroupListSkeleton({ export function HomePageBlockListGroupedSkeleton() { return ( -
}> - -
+ ); } @@ -194,7 +196,7 @@ function ControlsSkeleton({ horizontal }: { horizontal?: boolean }) { function UpdateBarSkeleton() { return ( - + diff --git a/src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx b/src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx deleted file mode 100644 index 81c820c12..000000000 --- a/src/app/_components/BlockList/HomePage/Grouped/useHomePageBlockListGrouped.tsx +++ /dev/null @@ -1,176 +0,0 @@ -import { GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY } from '@/common/queries/useBlocksByBurnBlock'; -import { BURN_BLOCKS_QUERY_KEY } from '@/common/queries/useBurnBlocksInfinite'; -import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo, useRef } from 'react'; - -import { useBlockListContext } from '../../BlockListContext'; -import { BlocksGroupProps } from '../../Grouped/BlockListGrouped'; -import { useBlockListWebSocket } from '../../Sockets/useBlockListWebSocket'; -import { FADE_DURATION } from '../../consts'; -import { UIBlockType } from '../../types'; -import { useHomePageInitialBlockListGrouped } from './useHomePageInitialBlockListGrouped'; - -export function useHomePageBlockListGrouped() { - const queryClient = useQueryClient(); - const { setBlockListLoading: setIsBlockListUpdateLoading, liveUpdates: isLiveUpdateEnabled } = - useBlockListContext(); - - const { - latestBurnBlock, - latestBurnBlockStxBlocks, - secondLatestBurnBlock, - secondLatestBurnBlockStxBlocks, - thirdLatestBurnBlock, - thirdLatestBurnBlockStxBlocks, - } = useHomePageInitialBlockListGrouped(); - - const initialStxBlockHashes = useMemo( - () => - new Set([ - ...latestBurnBlockStxBlocks.map(block => block.hash), - ...secondLatestBurnBlockStxBlocks.map(block => block.hash), - ...thirdLatestBurnBlockStxBlocks.map(block => block.hash), - ]), - [latestBurnBlockStxBlocks, secondLatestBurnBlockStxBlocks, thirdLatestBurnBlockStxBlocks] - ); - const initialBurnBlockHashes = useMemo( - () => - new Set([ - latestBurnBlock.burn_block_hash, - secondLatestBurnBlock.burn_block_hash, - thirdLatestBurnBlock.burn_block_hash, - ]), - [latestBurnBlock, secondLatestBurnBlock, thirdLatestBurnBlock] - ); - - const { - latestStxBlock: latestStxBlock, - latestStxBlocksCount: latestStxBlocksWaitingToBeLoaded, - clearLatestBlocks: clearLatestStxBlocksFromWebSocket, - } = useBlockListWebSocket(initialStxBlockHashes, initialBurnBlockHashes); // TODO: fix this - - const updateBlockList = useCallback( - async function () { - setIsBlockListUpdateLoading(true); - await Promise.all([ - // invalidates queries so that they can be refetched - queryClient.invalidateQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), - queryClient.invalidateQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), - ]); - clearLatestStxBlocksFromWebSocket(); // clears blocks from socket connection - setIsBlockListUpdateLoading(false); - }, - [clearLatestStxBlocksFromWebSocket, queryClient, setIsBlockListUpdateLoading] - ); - - const prevIsLiveUpdateEnabledRef = useRef(isLiveUpdateEnabled); - const prevLatestBlocksCountRef = useRef(latestStxBlocksWaitingToBeLoaded); - - useEffect(() => { - const liveUpdatesJustToggled = prevIsLiveUpdateEnabledRef.current !== isLiveUpdateEnabled; - - // If live updates are enabled and oe or more new blocks have been received, verified by checking the latest block count and the previous latest block count, then - // add the latest block to the list of blocks - const receivedLatestBlockWhileLiveUpdates = - isLiveUpdateEnabled && - latestStxBlocksWaitingToBeLoaded > 0 && // data coming from the websocket - prevLatestBlocksCountRef.current !== latestStxBlocksWaitingToBeLoaded; - - // If live updates have just been toggled, then refetch/update the block list - if (liveUpdatesJustToggled) { - setIsBlockListUpdateLoading(true); - clearLatestStxBlocksFromWebSocket(); - updateBlockList().then(() => { - setIsBlockListUpdateLoading(false); - }); - } else if (receivedLatestBlockWhileLiveUpdates && latestStxBlock) { - // If latest stx block belongs to the latest burn block, add it to the latest burn block list of stx blocks - if (latestStxBlock.burn_block_height === latestBurnBlock.burn_block_height) { - setIsBlockListUpdateLoading(true); - setTimeout(() => { - // latestBurnBlockStxBlocks.pop(); - // latestBurnBlock.stacks_blocks.pop(); - latestBurnBlockStxBlocks.unshift(latestStxBlock); - latestBurnBlock.stacks_blocks.unshift(latestStxBlock.hash); - setIsBlockListUpdateLoading(false); - }, FADE_DURATION); - } else { - // Otherwise, we have a new burn block, and in this situation, adding a new burn block is the equivalent of refetching/updating the block list - clearLatestStxBlocksFromWebSocket(); - void updateBlockList(); - } - } - - prevIsLiveUpdateEnabledRef.current = isLiveUpdateEnabled; - prevLatestBlocksCountRef.current = latestStxBlocksWaitingToBeLoaded; - }, [ - latestStxBlock, - latestStxBlocksWaitingToBeLoaded, - isLiveUpdateEnabled, - latestBurnBlock, - latestBurnBlockStxBlocks, - latestBurnBlock.stacks_blocks, - latestBurnBlock.burn_block_height, - clearLatestStxBlocksFromWebSocket, - updateBlockList, - setIsBlockListUpdateLoading, - ]); - - // all btc block groups are rendered the same - const blockList: BlocksGroupProps[] = [ - { - burnBlock: { - type: UIBlockType.BurnBlock, - height: latestBurnBlock.burn_block_height, - hash: latestBurnBlock.burn_block_hash, - timestamp: latestBurnBlock.burn_block_time, - txsCount: latestBurnBlock.stacks_blocks.length, - }, - stxBlocks: latestBurnBlockStxBlocks.map(block => ({ - type: UIBlockType.StxBlock, - height: block.height, - hash: block.hash, - timestamp: block.burn_block_time, - })), - stxBlocksLimit: 3, - }, - { - burnBlock: { - type: UIBlockType.BurnBlock, - height: secondLatestBurnBlock.burn_block_height, - hash: secondLatestBurnBlock.burn_block_hash, - timestamp: secondLatestBurnBlock.burn_block_time, - txsCount: secondLatestBurnBlock.stacks_blocks.length, - }, - stxBlocks: secondLatestBurnBlockStxBlocks.map(block => ({ - type: UIBlockType.StxBlock, - height: block.height, - hash: block.hash, - timestamp: block.burn_block_time, - })), - stxBlocksLimit: 3, - }, - { - burnBlock: { - type: UIBlockType.BurnBlock, - height: thirdLatestBurnBlock.burn_block_height, - hash: thirdLatestBurnBlock.burn_block_hash, - timestamp: thirdLatestBurnBlock.burn_block_time, - txsCount: thirdLatestBurnBlock.stacks_blocks.length, - }, - stxBlocks: thirdLatestBurnBlockStxBlocks.map(block => ({ - type: UIBlockType.StxBlock, - height: block.height, - hash: block.hash, - timestamp: block.burn_block_time, - })), - stxBlocksLimit: 3, - }, - ]; - - return { - blockList, - updateBlockList, - latestBlocksCount: latestStxBlocksWaitingToBeLoaded, - }; -} diff --git a/src/app/_components/BlockList/HomePage/Grouped/useHomePageInitialBlockListGrouped.tsx b/src/app/_components/BlockList/HomePage/Grouped/useHomePageInitialBlockListGrouped.tsx deleted file mode 100644 index 892e80251..000000000 --- a/src/app/_components/BlockList/HomePage/Grouped/useHomePageInitialBlockListGrouped.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { BurnBlock } from '@stacks/blockchain-api-client'; - -import { useSuspenseInfiniteQueryResult } from '../../../../../common/hooks/useInfiniteQueryResult'; -import { useSuspenseBlocksByBurnBlock } from '../../../../../common/queries/useBlocksByBurnBlock'; -import { useSuspenseBurnBlocks } from '../../../../../common/queries/useBurnBlocksInfinite'; - -const BURN_BLOCK_LENGTH = 3; -const STX_BLOCK_LENGTH = 3; - -export function useHomePageInitialBlockListGrouped() { - const burnBlocks = useSuspenseInfiniteQueryResult( - useSuspenseBurnBlocks(BURN_BLOCK_LENGTH), - BURN_BLOCK_LENGTH - ); - - const latestBurnBlock = burnBlocks[0]; - const secondLatestBurnBlock = burnBlocks[1]; - const thirdLatestBurnBlock = burnBlocks[2]; - - const latestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( - useSuspenseBlocksByBurnBlock(latestBurnBlock.burn_block_height, STX_BLOCK_LENGTH), - STX_BLOCK_LENGTH - ); - const secondLatestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( - useSuspenseBlocksByBurnBlock(secondLatestBurnBlock.burn_block_height, STX_BLOCK_LENGTH), - STX_BLOCK_LENGTH - ); - const thirdLatestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( - useSuspenseBlocksByBurnBlock(thirdLatestBurnBlock.burn_block_height, STX_BLOCK_LENGTH), - STX_BLOCK_LENGTH - ); - - return { - latestBurnBlock, - latestBurnBlockStxBlocks, - secondLatestBurnBlock, - secondLatestBurnBlockStxBlocks, - thirdLatestBurnBlock, - thirdLatestBurnBlockStxBlocks, - }; -} diff --git a/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx b/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx index e62c64c73..a5f73450d 100644 --- a/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx +++ b/src/app/_components/BlockList/HomePage/HomePageBlockList.tsx @@ -14,7 +14,7 @@ import { HomePageBlockListGroupedSkeleton } from '../Grouped/skeleton'; import { HomePageBlockListUngroupedSkeleton } from '../Ungrouped/skeleton'; const HomePageBlockListGroupedByBtcBlockDynamic = dynamic( - () => import('./Grouped/HomePageBlockListGrouped').then(mod => mod.HomePageBlockListGrouped), + () => import('./HomePageBlockListGrouped').then(mod => mod.HomePageBlockListGrouped), { loading: () => , ssr: false, @@ -22,8 +22,7 @@ const HomePageBlockListGroupedByBtcBlockDynamic = dynamic( ); const HomePageUngroupedBlockListDynamic = dynamic( - () => - import('./Ungrouped/HomePageBlockListUngrouped').then(mod => mod.HomePageBlockListUngrouped), + () => import('./HomePageBlockListUngrouped').then(mod => mod.HomePageBlockListUngrouped), { loading: () => , ssr: false, diff --git a/src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx b/src/app/_components/BlockList/HomePage/HomePageBlockListGrouped.tsx similarity index 59% rename from src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx rename to src/app/_components/BlockList/HomePage/HomePageBlockListGrouped.tsx index 569cc5e4a..5c41b7bcf 100644 --- a/src/app/_components/BlockList/HomePage/Grouped/HomePageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/HomePage/HomePageBlockListGrouped.tsx @@ -2,22 +2,21 @@ import { Suspense } from 'react'; -import { ListFooter } from '../../../../../common/components/ListFooter'; -import { Flex } from '../../../../../ui/Flex'; -import { useBlockListContext } from '../../BlockListContext'; -import { BlockListGrouped } from '../../Grouped/BlockListGrouped'; -import { HomePageBlockListGroupedSkeleton } from '../../Grouped/skeleton'; -import { UpdateBar } from '../../UpdateBar'; -import { useHomePageBlockListGrouped } from './useHomePageBlockListGrouped'; +import { ListFooter } from '../../../../common/components/ListFooter'; +import { Flex } from '../../../../ui/Flex'; +import { useBlockListContext } from '../BlockListContext'; +import { BlockListGrouped } from '../Grouped/BlockListGrouped'; +import { HomePageBlockListGroupedSkeleton } from '../Grouped/skeleton'; +import { UpdateBar } from '../UpdateBar'; +import { useHomePageBlockList } from '../data/useHomePageBlockList'; function HomePageBlockListGroupedBase() { - const { liveUpdates, isBlockListLoading } = useBlockListContext(); + const { liveUpdates } = useBlockListContext(); const { blockList, updateBlockList, latestBlocksCount: latestStxBlocksCountFromWebSocket, - } = useHomePageBlockListGrouped(); - + } = useHomePageBlockList(); return ( <> {!liveUpdates && ( @@ -28,7 +27,7 @@ function HomePageBlockListGroupedBase() { /> )} - + {!liveUpdates && } diff --git a/src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx b/src/app/_components/BlockList/HomePage/HomePageBlockListUngrouped.tsx similarity index 56% rename from src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx rename to src/app/_components/BlockList/HomePage/HomePageBlockListUngrouped.tsx index e7d6b6586..2bb1f0129 100644 --- a/src/app/_components/BlockList/HomePage/Ungrouped/HomePageBlockListUngrouped.tsx +++ b/src/app/_components/BlockList/HomePage/HomePageBlockListUngrouped.tsx @@ -2,22 +2,21 @@ import { Suspense } from 'react'; -import { ListFooter } from '../../../../../common/components/ListFooter'; -import { Flex } from '../../../../../ui/Flex'; -import { useBlockListContext } from '../../BlockListContext'; -import { BlockListUngrouped } from '../../Ungrouped/BlockListUngrouped'; -import { HomePageBlockListUngroupedSkeleton } from '../../Ungrouped/skeleton'; -import { UpdateBar } from '../../UpdateBar'; -import { useHomePageBlockListUngrouped } from './useHomePageBlockListUngrouped'; +import { ListFooter } from '../../../../common/components/ListFooter'; +import { Flex } from '../../../../ui/Flex'; +import { useBlockListContext } from '../BlockListContext'; +import { BlockListUngrouped } from '../Ungrouped/BlockListUngrouped'; +import { HomePageBlockListUngroupedSkeleton } from '../Ungrouped/skeleton'; +import { UpdateBar } from '../UpdateBar'; +import { useHomePageBlockList } from '../data/useHomePageBlockList'; function HomePageBlockListUngroupedBase() { const { liveUpdates } = useBlockListContext(); const { - latestBlocksCount: latestStxBlocksCountFromWebSocket, + blockList, updateBlockList, - blocksList, - } = useHomePageBlockListUngrouped(); - + latestBlocksCount: latestStxBlocksCountFromWebSocket, + } = useHomePageBlockList(); return ( <> {!liveUpdates && ( @@ -28,7 +27,7 @@ function HomePageBlockListUngroupedBase() { /> )} - + {!liveUpdates && } diff --git a/src/app/_components/BlockList/HomePage/Ungrouped/useHomePageBlockListUngrouped.tsx b/src/app/_components/BlockList/HomePage/Ungrouped/useHomePageBlockListUngrouped.tsx deleted file mode 100644 index 4e139a799..000000000 --- a/src/app/_components/BlockList/HomePage/Ungrouped/useHomePageBlockListUngrouped.tsx +++ /dev/null @@ -1,190 +0,0 @@ -'use client'; - -import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; - -import { NakamotoBlock } from '@stacks/blockchain-api-client'; - -import { useSuspenseInfiniteQueryResult } from '../../../../../common/hooks/useInfiniteQueryResult'; -import { - BLOCK_LIST_QUERY_KEY, - useSuspenseBlocksInfiniteNew, -} from '../../../../../common/queries/useBlockListInfinite'; -import { useBlockListContext } from '../../BlockListContext'; -import { useBlockListWebSocket } from '../../Sockets/useBlockListWebSocket'; -import { FADE_DURATION } from '../../consts'; -import { BlockListBtcBlock, BlockListStxBlock, UISingleBlock } from '../../types'; - -const LIMIT = 3; - -// TODO: move into shared file -function runAfterFadeOut(callback: () => void) { - setTimeout(callback, FADE_DURATION); -} -/** - * Fetch the initial stx blocks and burn blocks - * Convert into rows and render - * Fetch the latest stx blocks and burn blocks from websocket - * To update, invalidate queries to requery - * If live, - * If just toggling live, requery to update - * If receving new blocks while live, reorganize state to accomodate new blocks - */ -export function useHomePageBlockListUngrouped() { - const { setBlockListLoading, liveUpdates } = useBlockListContext(); - - const [latestBlocks, setLatestBlocks] = useState([]); - - // TODO: - // what if I queried for recent btc blocks. Took the first three. then queried for stx blocks for those btc blocks - // This would give me the list of stx blocks for the first three btc blocks that I would need to show on the homepage - // When updating just requery - // when live updates are on, either insert them into the first btc block, or create a new btc block and pop the last one - // Do this only if we require a more balanced view - // This requires 4 queries - // For now I can just rely on the blocks endpoint and show what I can - const response = useSuspenseBlocksInfiniteNew(); - const initialStxBlocks = useSuspenseInfiniteQueryResult(response); - const initialBtcBlocks: Record = useMemo( - () => - initialStxBlocks.reduce( - (acc, block) => { - if (!acc[block.burn_block_hash]) { - acc[block.burn_block_hash] = { - type: 'btc_block', - height: block.burn_block_height, - hash: block.burn_block_hash, - timestamp: block.burn_block_time, - txsCount: undefined, // TODO: to get this I would have to make sure I have queried for all the stx blocks for this burn block and - }; - } - return acc; - }, - {} as Record - ), - [initialStxBlocks] - ); - const stxBlocksGroupedByBtcBlock: Record = useMemo( - // TODO: make a util function - () => - initialStxBlocks.reduce( - (acc, block) => { - if (!acc[block.burn_block_hash]) { - acc[block.burn_block_hash] = []; - } - acc[block.burn_block_hash].push({ - type: 'stx_block', - height: block.height, - hash: block.hash, - timestamp: block.burn_block_time, - txsCount: block.tx_count, - }); - return acc; - }, - {} as Record - ), - [initialStxBlocks] - ); - - const initialBlockList = useMemo( - // TODO: make a util function - () => - Object.keys(stxBlocksGroupedByBtcBlock).reduce( - (acc, btcBlockHash) => { - const stxBlocks = stxBlocksGroupedByBtcBlock[btcBlockHash]; - const btcBlock = initialBtcBlocks[btcBlockHash]; - acc.push({ stxBlocks, btcBlock }); - return acc; - }, - [] as { stxBlocks: BlockListStxBlock[]; btcBlock: BlockListBtcBlock }[] - ), - [initialBtcBlocks, stxBlocksGroupedByBtcBlock] - ); - - // TODO: so far we have not limited the list to two btc blocks and how many stx bloxks are shown and put in a placeholder for the rest of the stx blocks - // for ensuring there are no duplicates - const stxBlockHashes = useMemo(() => { - return new Set(initialStxBlocks.map(block => block.hash)); - }, [initialStxBlocks]); - - const burnBlockHashes = useMemo(() => { - return new Set(Object.keys(initialBtcBlocks)); - }, [initialBtcBlocks]); - - const { - latestUIBlocks: latestUIBlockFromWebSocket, - latestStxBlocksCount: latestStxBlocksCountFromWebSocket, - clearLatestBlocks: clearLatestBlocksFromWebSocket, - } = useBlockListWebSocket(stxBlockHashes, burnBlockHashes); - - const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); - // This is used to trigger a fade out effect when the block list is updated. When the counter is updated, we finish loading and show the fade in effect - const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); - - useEffect(() => { - if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { - runAfterFadeOut(() => { - setBlockListLoading(false); - }); - } - }, [blockListUpdateCounter, clearLatestBlocksFromWebSocket, setBlockListLoading]); - - const queryClient = useQueryClient(); - const updateBlockListWithQuery = useCallback( - async function () { - setBlockListLoading(true); - runAfterFadeOut(async () => { - await Promise.all([ - // Invalidates queries so they will be refetched - queryClient.invalidateQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }), // TODO: might be better to manually run the query again so we can use the callback - ]).then(() => { - clearLatestBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); - }); - }); - }, - [clearLatestBlocksFromWebSocket, queryClient, setBlockListLoading] - ); - - const showLatestBlocks = useCallback(() => { - setBlockListLoading(true); - runAfterFadeOut(() => { - setLatestBlocks(prevBlockList => { - return [...latestUIBlockFromWebSocket, ...prevBlockList]; - }); - clearLatestBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); - }); - }, [ - latestUIBlockFromWebSocket, - setLatestBlocks, - setBlockListLoading, - clearLatestBlocksFromWebSocket, - ]); - - const prevLiveUpdatesRef = useRef(liveUpdates); - const prevLatestBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); - useEffect(() => { - const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; - - const receivedLatestStxBlockFromLiveUpdates = - liveUpdates && - latestStxBlocksCountFromWebSocket > 0 && - prevLatestBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; - - if (liveUpdatesToggled) { - updateBlockListWithQuery(); - } else if (receivedLatestStxBlockFromLiveUpdates) { - showLatestBlocks(); - } - - prevLiveUpdatesRef.current = liveUpdates; - prevLatestBlocksCountRef.current = latestStxBlocksCountFromWebSocket; - }, [liveUpdates, latestStxBlocksCountFromWebSocket, showLatestBlocks, updateBlockListWithQuery]); - - return { - latestBlocksCount: latestStxBlocksCountFromWebSocket, - blocksList: initialBlockList, - updateBlockList: updateBlockListWithQuery, - }; -} diff --git a/src/app/_components/BlockList/LineAndNode.tsx b/src/app/_components/BlockList/LineAndNode.tsx new file mode 100644 index 000000000..c70baedaf --- /dev/null +++ b/src/app/_components/BlockList/LineAndNode.tsx @@ -0,0 +1,72 @@ +import { ReactNode } from 'react'; + +import { Circle } from '../../../common/components/Circle'; +import { Box } from '../../../ui/Box'; +import { Flex } from '../../../ui/Flex'; + +// TODO: move to common +export function LineAndNode({ + rowHeight = 14, + width = 6, + icon, +}: { + rowHeight: number; + width: number; + icon?: ReactNode; +}) { + return ( + + {icon ? ( + + + {icon} + + + + ) : ( + + + + + )} + + ); +} diff --git a/src/app/_components/BlockList/Sockets/useBlockListWebSocket3.ts b/src/app/_components/BlockList/Sockets/useBlockListWebSocket2.ts similarity index 85% rename from src/app/_components/BlockList/Sockets/useBlockListWebSocket3.ts rename to src/app/_components/BlockList/Sockets/useBlockListWebSocket2.ts index c7d422edd..4b1d633a9 100644 --- a/src/app/_components/BlockList/Sockets/useBlockListWebSocket3.ts +++ b/src/app/_components/BlockList/Sockets/useBlockListWebSocket2.ts @@ -4,7 +4,7 @@ import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/model import { useSubscribeBlocks } from './useSubscribeBlocks'; -export function useBlockListWebSocket3(initialStxBlockHashes: Set) { +export function useBlockListWebSocket2(initialStxBlockHashes: Set) { const [latestStxBlocks, setLatestStxBlocks] = useState([]); const stxBlockHashes = useRef(new Set()); @@ -23,14 +23,15 @@ export function useBlockListWebSocket3(initialStxBlockHashes: Set) { ); useSubscribeBlocks(handleBlock); + // useSubscribeBlocks2(handleBlock); - const clearLatestBlocks = () => { + const clearLatestStxBlocks = () => { setLatestStxBlocks([]); }; return { latestStxBlocks, latestStxBlocksCount: latestStxBlocks.length, - clearLatestBlocks, + clearLatestStxBlocks, }; } diff --git a/src/app/_components/BlockList/Sockets/useSubscribeBlocks2.ts b/src/app/_components/BlockList/Sockets/useSubscribeBlocks2.ts index baa41bb58..fb7cca59a 100644 --- a/src/app/_components/BlockList/Sockets/useSubscribeBlocks2.ts +++ b/src/app/_components/BlockList/Sockets/useSubscribeBlocks2.ts @@ -1,37 +1,36 @@ -import { useEffect, useRef } from 'react'; +// import { useEffect, useRef } from 'react'; -import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; -import { Block } from '@stacks/stacks-blockchain-api-types'; +// import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; +// import { Block } from '@stacks/stacks-blockchain-api-types'; -import { useGlobalContext } from '../../../../common/context/useAppContext'; +// import { useGlobalContext } from '../../../../common/context/useAppContext'; -interface Subscription { - unsubscribe(): void; -} +// interface Subscription { +// unsubscribe(): void; +// } -// TODO: with the new client code, we should be able to use the client directly -export function useSubscribeBlocks2(handleBlock: (block: NakamotoBlock) => any) { - const subscription = useRef(undefined); - const { stacksApiSocketClient } = useGlobalContext(); +// export function useSubscribeBlocks2(handleBlock: (block: NakamotoBlock) => any) { +// const subscription = useRef(undefined); +// const { stacksApiSocketClient } = useGlobalContext(); - useEffect(() => { - const subscribe = async () => { - console.log('subscribing to blocks'); - subscription.current = stacksApiSocketClient?.subscribeBlocks((block: Block) => { - console.log('handling block', block); - handleBlock({ - ...block, - parent_index_block_hash: '', - tx_count: 0, - }); - }); - }; - if (stacksApiSocketClient?.socket.connected) { - subscribe(); - } - return () => { - subscription?.current?.unsubscribe(); - }; - }, [stacksApiSocketClient, handleBlock]); - return subscription; -} +// useEffect(() => { +// const subscribe = async () => { +// console.log('subscribing to blocks'); +// subscription.current = stacksApiSocketClient?.subscribeBlocks((block: Block) => { +// console.log('handling block', block); +// handleBlock({ +// ...block, +// parent_index_block_hash: '', +// tx_count: 0, +// }); +// }); +// }; +// if (stacksApiSocketClient?.socket.connected) { +// subscribe(); +// } +// return () => { +// subscription?.current?.unsubscribe(); +// }; +// }, [stacksApiSocketClient, handleBlock]); +// return subscription; +// } diff --git a/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx b/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx index 7f2dcec2e..5a48ea24e 100644 --- a/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx +++ b/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx @@ -1,7 +1,5 @@ -import { useColorModeValue } from '@chakra-ui/react'; import { ReactNode } from 'react'; -import { Circle } from '../../../../common/components/Circle'; import { BlockLink } from '../../../../common/components/ExplorerLinks'; import { Timestamp } from '../../../../common/components/Timestamp'; import { truncateMiddle } from '../../../../common/utils/utils'; @@ -13,18 +11,14 @@ import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; import { Text } from '../../../../ui/Text'; import { StxIcon } from '../../../../ui/icons'; +import { ListHeader } from '../../ListHeader'; import { BlockCount } from '../BlockCount'; import { useBlockListContext } from '../BlockListContext'; +import { LineAndNode } from '../LineAndNode'; import { FADE_DURATION } from '../consts'; -import { BlockListBtcBlock, BlockListStxBlock } from '../types'; -import { BtcBlockListItem } from './BtcBlockListItem'; - -export interface BlocksByBtcBlock { - stxBlocks: BlockListStxBlock[]; - btcBlock: BlockListBtcBlock; -} - -export type BlockListUngrouped = BlocksByBtcBlock[]; +import { BlockListStxBlock } from '../types'; +import { BlockListData } from '../utils'; +import { BtcBlockRow } from './BtcBlockRow'; export function BlockListUngroupedLayout({ children }: { children: ReactNode }) { const { isBlockListLoading } = useBlockListContext(); @@ -43,96 +37,6 @@ export function BlockListUngroupedLayout({ children }: { children: ReactNode }) ); } -interface StxBlockListItemLayoutProps { - children: ReactNode; - hasIcon: boolean; - hasBorder: boolean; -} - -// TODO: move to common -export function LineAndNode({ - rowHeight = 14, - width = 6, - icon, -}: { - rowHeight: number; - width: number; - icon?: ReactNode; -}) { - return ( - - {icon ? ( - - - {icon} - - - - ) : ( - - - - - )} - - ); -} - -// TODO: copied from BlockListGrouped -export function ListHeader({ children, ...textProps }: { children: ReactNode } & TextProps) { - const color = useColorModeValue('slate.700', 'slate.250'); - return ( - - {children} - - ); -} - -// TODO: copied from BlockListGrouped const GroupHeader = () => { return ( <> @@ -156,16 +60,24 @@ const GroupHeader = () => { }, }} > - Block height + + Block height + {' '} - Block hash + + Block hash + - Transactions + + Transactions + - Timestamp + + Timestamp + ); @@ -249,12 +161,12 @@ function StxBlockRow({ ); } -function StxBlocksGrid({ - stxBlocks, +export function StxBlocksGridLayout({ + children, minimized, }: { - stxBlocks: BlockListStxBlock[]; - minimized: boolean; + children: ReactNode; + minimized?: boolean; }) { return ( + {children} + + ); +} + +function StxBlocksGrid({ + stxBlocks, + minimized, +}: { + stxBlocks: BlockListStxBlock[]; + minimized: boolean; +}) { + return ( + {minimized ? null : } {stxBlocks.map((stxBlock, i) => ( <> @@ -281,7 +207,7 @@ function StxBlocksGrid({ )} ))} - + ); } @@ -291,7 +217,7 @@ function StxBlocksGroupedByBtcBlock({ stxBlocksLimit, minimized = false, }: { - blockList: BlocksByBtcBlock; + blockList: BlockListData; stxBlocksLimit?: number; minimized?: boolean; }) { @@ -300,6 +226,9 @@ function StxBlocksGroupedByBtcBlock({ const stxBlocksShortList = stxBlocksLimit ? blockList.stxBlocks.slice(0, stxBlocksLimit) : blockList.stxBlocks; + const numStxBlocks = btcBlock.txsCount ?? stxBlocks.length; + const numStxBlocksNotDisplayed = numStxBlocks - (stxBlocksLimit || stxBlocks.length); + console.log({ numStxBlocks, numStxBlocksNotDisplayed, stxBlocksLimit, btcBlock, stxBlocks }); return ( <> @@ -307,7 +236,8 @@ function StxBlocksGroupedByBtcBlock({ {stxBlocksLimit && stxBlocks.length > stxBlocksLimit && ( )} - 0 ? : null} + - {blockList.map(blocksGroupedByBtcBlock => ( + {blockList.map(bl => ( @@ -339,56 +269,3 @@ export function BlockListUngrouped({ ); } - -// TODO: redo this component -export function StxBlockListItemLayout({ - children, - hasIcon, - hasBorder, -}: StxBlockListItemLayoutProps) { - return ( - - - {children} - - - ); -} diff --git a/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx b/src/app/_components/BlockList/Ungrouped/BtcBlockRow.tsx similarity index 77% rename from src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx rename to src/app/_components/BlockList/Ungrouped/BtcBlockRow.tsx index 121e9142e..5ec40affd 100644 --- a/src/app/_components/BlockList/Ungrouped/BtcBlockListItem.tsx +++ b/src/app/_components/BlockList/Ungrouped/BtcBlockRow.tsx @@ -6,17 +6,17 @@ import { ExplorerLink } from '../../../../common/components/ExplorerLinks'; import { Timestamp } from '../../../../common/components/Timestamp'; import { truncateMiddle } from '../../../../common/utils/utils'; import { Box } from '../../../../ui/Box'; -import { Flex } from '../../../../ui/Flex'; +import { Flex, FlexProps } from '../../../../ui/Flex'; import { HStack } from '../../../../ui/HStack'; import { Icon } from '../../../../ui/Icon'; import { BitcoinIcon } from '../../../../ui/icons'; -interface BtcBlockListItemProps { +interface BtcBlockRowProps { height: number | string; hash: string; timestamp?: number; } -export function BtcBlockListItemLayout({ children }: { children: ReactNode }) { +export function BtcBlockRowLayout({ children, ...rest }: FlexProps & { children: ReactNode }) { const textColor = useColorModeValue('slate.700', 'slate.500'); // TODO: not in theme. remove return ( {children} ); } -export function BtcBlockListItemContent({ timestamp, height, hash }: BtcBlockListItemProps) { +export function BtcBlockRowContent({ timestamp, height, hash }: BtcBlockRowProps) { const iconColor = useColorModeValue('slate.600', 'slate.800'); // TODO: not in theme. remove return ( <> @@ -59,10 +60,10 @@ export function BtcBlockListItemContent({ timestamp, height, hash }: BtcBlockLis ); } -export function BtcBlockListItem({ timestamp, height, hash }: BtcBlockListItemProps) { +export function BtcBlockRow({ timestamp, height, hash }: BtcBlockRowProps) { return ( - - - + + + ); } diff --git a/src/app/_components/BlockList/Ungrouped/skeleton.tsx b/src/app/_components/BlockList/Ungrouped/skeleton.tsx index 795b8e895..761440cbe 100644 --- a/src/app/_components/BlockList/Ungrouped/skeleton.tsx +++ b/src/app/_components/BlockList/Ungrouped/skeleton.tsx @@ -1,47 +1,88 @@ import { Stack } from '@/ui/Stack'; +import { ReactNode } from 'react'; -import { Circle } from '../../../../common/components/Circle'; +import { Box } from '../../../../ui/Box'; import { Flex } from '../../../../ui/Flex'; +import { HStack } from '../../../../ui/HStack'; +import { Icon } from '../../../../ui/Icon'; import { SkeletonText } from '../../../../ui/SkeletonText'; -import { StxBlockListItemLayout } from './BlockListUngrouped'; -import { BtcBlockListItemLayout } from './BtcBlockListItem'; +import { StxIcon } from '../../../../ui/icons'; +import { BlockListGridHeaderRowSkeleton } from '../Grouped/skeleton'; +import { LineAndNode } from '../LineAndNode'; +import { StxBlocksGridLayout } from './BlockListUngrouped'; +import { BtcBlockRowLayout } from './BtcBlockRow'; -function StxBlockListItemContentSkeleton({ hasIcon }: { hasIcon: boolean }) { - return ( +// layout was copied +export function BlockListRowSkeleton({ + icon, + minimized, +}: { + icon?: ReactNode; + minimized?: boolean; +}) { + return minimized ? ( + <> + + + + + +  ∙ } + gap={1} + whiteSpace="nowrap" + color="textSubdued" + gridColumn="3 / 4" + > + + + + + + ) : ( <> - {hasIcon && } + + + + + + + + + + + + + - ); } -function StxBlockListItemSkeleton({ - hasIcon, - hasBorder, + +export function StxBlocksGridSkeleton({ + numBlocks, + minimized, }: { - hasIcon: boolean; - hasBorder: boolean; + numBlocks: number; + minimized?: boolean; }) { return ( - - - - ); -} - -export function StxBlockListSkeleton({ numBlocks }: { numBlocks: number }) { - return ( - <> + + {minimized ? null : } {Array.from({ length: numBlocks }).map((_, i) => ( - + <> + : undefined} + minimized={minimized} + /> + {i < numBlocks - 1 && ( + + )} + ))} - + ); } @@ -54,25 +95,34 @@ function BtcBlockListItemContentSkeleton() { ); } -function BtcBlockListItemSkeleton() { +function BtcBlockListItemSkeleton({ minimized }: { minimized?: boolean }) { return ( - + - + ); } export function BlocksPageBlockListUngroupedSkeleton() { return ( - - + + - + ); } export function HomePageBlockListUngroupedSkeleton() { - return <>; + return ( + + + + + + + + + ); } diff --git a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockList.ts b/src/app/_components/BlockList/Ungrouped/useUngroupedBlockList.ts deleted file mode 100644 index 4929dd538..000000000 --- a/src/app/_components/BlockList/Ungrouped/useUngroupedBlockList.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useMemo } 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'; - -export function useUngroupedBlockList() { - 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.StxBlock, - 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/UpdatedBlockList.tsx b/src/app/_components/BlockList/UpdatedBlockList.tsx index b3f9ff9b2..0669575a2 100644 --- a/src/app/_components/BlockList/UpdatedBlockList.tsx +++ b/src/app/_components/BlockList/UpdatedBlockList.tsx @@ -8,7 +8,6 @@ import { Block } from '@stacks/stacks-blockchain-api-types'; import { ListFooter } from '../../../common/components/ListFooter'; import { Section } from '../../../common/components/Section'; -import { SkeletonBlockList } from '../../../common/components/loaders/skeleton-text'; import { DEFAULT_LIST_LIMIT } from '../../../common/constants/constants'; import { useGlobalContext } from '../../../common/context/useAppContext'; import { useSuspenseInfiniteQueryResult } from '../../../common/hooks/useInfiniteQueryResult'; diff --git a/src/app/_components/BlockList/consts.ts b/src/app/_components/BlockList/consts.ts index 1168e566f..e5f750f5a 100644 --- a/src/app/_components/BlockList/consts.ts +++ b/src/app/_components/BlockList/consts.ts @@ -1 +1,3 @@ export const FADE_DURATION = 700; +export const BURN_BLOCKS_QUERY_KEY_EXTENSION = 'blockList'; + diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx b/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx new file mode 100644 index 000000000..2dac60526 --- /dev/null +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx @@ -0,0 +1,118 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useBlockListContext } from '../BlockListContext'; +import { useBlockListWebSocket2 } from '../Sockets/useBlockListWebSocket2'; +import { BlockListData, generateBlockList, mergeBlockLists, waitForFadeAnimation } from '../utils'; +import { useBlocksPageBlockListGroupedInitialBlockList } from './useBlocksPageBlockListGroupedInitialBlockList'; + +export function useBlocksPageBlockListGrouped(btcBlockLimit: number = 10) { + const { setBlockListLoading, liveUpdates } = useBlockListContext(); + + const { + initialStxBlockHashes, + initialBlockList, + refetchInitialBlockList, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + } = useBlocksPageBlockListGroupedInitialBlockList(btcBlockLimit); + + // initially the block list is the initial blocklist + const [blockList, setBlockList] = useState(initialBlockList); + // when the initial block list changes, reset the block list to the initial blocklist + useEffect(() => { + setBlockList(initialBlockList); + }, [initialBlockList]); + + const { + latestStxBlocks: latestStxBlocksFromWebSocket, + latestStxBlocksCount: latestStxBlocksCountFromWebSocket, + clearLatestStxBlocks: clearLatestStxBlocksFromWebSocket, + } = useBlockListWebSocket2(initialStxBlockHashes); + + // This is used to trigger a fade out effect when the block list is updated. + // When the counter is updated, we wait for the fade out effect to finish and then show the fade in effect + const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); + const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); + useEffect(() => { + if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { + waitForFadeAnimation(() => { + setBlockListLoading(false); + }); + } + }, [blockListUpdateCounter, clearLatestStxBlocksFromWebSocket, setBlockListLoading]); + + // manually update the block list with block list updates from the websocket + const updateBlockListManually = useCallback( + (blockListUpdates: BlockListData[]) => { + setBlockList(prevBlockList => { + const newBlockList = mergeBlockLists(blockListUpdates, prevBlockList); + return newBlockList; + }); + }, + [setBlockList] + ); + + const showLatestStxBlocksFromWebSocket = useCallback(() => { + setBlockListLoading(true); + waitForFadeAnimation(() => { + const websocketBlockList = generateBlockList(latestStxBlocksFromWebSocket); + updateBlockListManually(websocketBlockList); + clearLatestStxBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }, [ + latestStxBlocksFromWebSocket, + updateBlockListManually, + setBlockListLoading, + clearLatestStxBlocksFromWebSocket, + ]); + + const updateBlockListWithQuery = useCallback( + async function () { + setBlockListLoading(true); + waitForFadeAnimation(async () => { + clearLatestStxBlocksFromWebSocket(); + await refetchInitialBlockList(() => { + setBlockListUpdateCounter(prev => prev + 1); + }); + }); + }, + [clearLatestStxBlocksFromWebSocket, setBlockListLoading, refetchInitialBlockList] + ); + + const prevLiveUpdatesRef = useRef(liveUpdates); + const prevLatestStxBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); + // Handles live updates + useEffect(() => { + const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; + + const receivedLatestStxBlockFromLiveUpdates = + liveUpdates && + latestStxBlocksCountFromWebSocket > 0 && + prevLatestStxBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; + + if (liveUpdatesToggled) { + updateBlockListWithQuery(); + } else if (receivedLatestStxBlockFromLiveUpdates) { + showLatestStxBlocksFromWebSocket(); + } + + prevLiveUpdatesRef.current = liveUpdates; + prevLatestStxBlocksCountRef.current = latestStxBlocksCountFromWebSocket; + }, [ + liveUpdates, + latestStxBlocksCountFromWebSocket, + showLatestStxBlocksFromWebSocket, + updateBlockListWithQuery, + ]); + + return { + blockList, + updateBlockList: updateBlockListWithQuery, + latestBlocksCount: latestStxBlocksCountFromWebSocket, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + }; +} diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListGroupedInitialBlockList.ts b/src/app/_components/BlockList/data/useBlocksPageBlockListGroupedInitialBlockList.ts new file mode 100644 index 000000000..6dc6852f5 --- /dev/null +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListGroupedInitialBlockList.ts @@ -0,0 +1,87 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; + +import { BurnBlock } from '@stacks/blockchain-api-client'; + +import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; +import { + GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY, + useSuspenseBlocksByBurnBlock, +} from '../../../../common/queries/useBlocksByBurnBlock'; +import { + BURN_BLOCKS_QUERY_KEY, + useSuspenseBurnBlocks, +} from '../../../../common/queries/useBurnBlocksInfinite'; +import { BlockListBtcBlock } from '../types'; +import { generateBlockList } from '../utils'; + +const BURN_BLOCKS_QUERY_KEY_EXTENSION = 'blockList'; + +export function useBlocksPageBlockListGroupedInitialBlockList(blockListLimit: number) { + const response = useSuspenseBurnBlocks(blockListLimit, {}, BURN_BLOCKS_QUERY_KEY_EXTENSION); + const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + const burnBlocks = useSuspenseInfiniteQueryResult(response); + + const latestBurnBlock = useMemo(() => burnBlocks[0], [burnBlocks]); + + const btcBlocksMap = useMemo(() => { + const map = {} as Record; + burnBlocks.forEach(block => { + map[block.burn_block_hash] = block; + }); + return map; + }, [burnBlocks]); + + const latestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(latestBurnBlock.burn_block_height) + ); + + const initialStxBlockHashes = useMemo( + () => new Set([...latestBurnBlockStxBlocks.map(block => block.hash)]), + [latestBurnBlockStxBlocks] + ); + + const queryClient = useQueryClient(); + const refetchInitialBlockList = useCallback( + async function (callback: () => void) { + // Invalidate queries first + await Promise.all([ + queryClient.invalidateQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), + queryClient.invalidateQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), + ]); + + // After invalidation, refetch the required queries + await Promise.all([ + queryClient.refetchQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), + queryClient.refetchQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), + ]); + + // Run your callback after refetching + callback(); + }, + [queryClient] + ); + + const initialBlockList = useMemo(() => { + const startOfBlockList = generateBlockList(latestBurnBlockStxBlocks, btcBlocksMap); + const restOfBlockList = burnBlocks.map(block => ({ + btcBlock: { + type: 'btc_block', + height: block.burn_block_height, + hash: block.burn_block_hash, + timestamp: block.burn_block_time, + } as BlockListBtcBlock, + stxBlocks: [], + })); + return [...startOfBlockList, ...restOfBlockList]; + }, [latestBurnBlockStxBlocks, burnBlocks, btcBlocksMap]); + + return { + initialStxBlockHashes, + initialBlockList, + refetchInitialBlockList, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + }; +} diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts b/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts new file mode 100644 index 000000000..1ff80c698 --- /dev/null +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts @@ -0,0 +1,121 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useBlockListContext } from '../BlockListContext'; +import { useBlockListWebSocket2 } from '../Sockets/useBlockListWebSocket2'; +import { BlockListData, generateBlockList, mergeBlockLists, waitForFadeAnimation } from '../utils'; +import { useHomePageInitialBlockList } from './useHomePageInitialBlockList'; + +export function useBlocksPageBlockListUngrouped() { + const { setBlockListLoading, liveUpdates } = useBlockListContext(); + + const { + initialStxBlocksHashes, + initialBlockList, + isFetchingNextPage, + fetchNextPage, + refetchInitialBlockList, + hasNextPage, + } = useHomePageInitialBlockList(); + + // initially the block list is the initial blocklist + const [blockList, setBlockList] = useState(initialBlockList); + // when the initial block list changes, reset the block list to the initial blocklist + useEffect(() => { + setBlockList(initialBlockList); + }, [initialBlockList]); + + const { + latestStxBlocks: latestStxBlocksFromWebSocket, + latestStxBlocksCount: latestStxBlocksCountFromWebSocket, + clearLatestStxBlocks: clearLatestStxBlocksFromWebSocket, + } = useBlockListWebSocket2(initialStxBlocksHashes); + + // This is used to trigger a fade out effect when the block list is updated. + // When the counter is updated, we wait for the fade out effect to finish and then show the fade in effect + const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); + const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); + useEffect(() => { + if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { + waitForFadeAnimation(() => { + setBlockListLoading(false); + }); + } + }, [blockListUpdateCounter, clearLatestStxBlocksFromWebSocket, setBlockListLoading]); + + // manually update the block list with block list updates from the websocket + const updateBlockListManually = useCallback( + (blockListUpdates: BlockListData[]) => { + setBlockList(prevBlockList => { + const newBlockList = mergeBlockLists(blockListUpdates, prevBlockList); + return newBlockList; + }); + }, + [setBlockList] + ); + + const showLatestStxBlocksFromWebSocket = useCallback(() => { + setBlockListLoading(true); + waitForFadeAnimation(() => { + const websocketBlockList = generateBlockList(latestStxBlocksFromWebSocket); + updateBlockListManually(websocketBlockList); + clearLatestStxBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }, [ + latestStxBlocksFromWebSocket, + updateBlockListManually, + setBlockListLoading, + clearLatestStxBlocksFromWebSocket, + ]); + + const updateBlockListWithQuery = useCallback( + async function () { + setBlockListLoading(true); + waitForFadeAnimation(async () => { + clearLatestStxBlocksFromWebSocket(); + await refetchInitialBlockList(() => { + setBlockListUpdateCounter(prev => prev + 1); + }); + }); + }, + [clearLatestStxBlocksFromWebSocket, setBlockListLoading, refetchInitialBlockList] + ); + + const prevLiveUpdatesRef = useRef(liveUpdates); + const prevLatestStxBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); + // Handles live updates + useEffect(() => { + const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; + + const receivedLatestStxBlockFromLiveUpdates = + liveUpdates && + latestStxBlocksCountFromWebSocket > 0 && + prevLatestStxBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; + + if (liveUpdatesToggled) { + updateBlockListWithQuery(); + } else if (receivedLatestStxBlockFromLiveUpdates) { + showLatestStxBlocksFromWebSocket(); + } + + prevLiveUpdatesRef.current = liveUpdates; + prevLatestStxBlocksCountRef.current = latestStxBlocksCountFromWebSocket; + }, [ + liveUpdates, + latestStxBlocksCountFromWebSocket, + showLatestStxBlocksFromWebSocket, + updateBlockListWithQuery, + ]); + + return { + blockList, + latestStxBlocksCountFromWebSocket, + updateBlockList: updateBlockListWithQuery, + latestBlocksCount: latestStxBlocksCountFromWebSocket, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + }; +} diff --git a/src/app/_components/BlockList/data/useHomePageBlockList.ts b/src/app/_components/BlockList/data/useHomePageBlockList.ts new file mode 100644 index 000000000..0cd889d0c --- /dev/null +++ b/src/app/_components/BlockList/data/useHomePageBlockList.ts @@ -0,0 +1,113 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +import { useBlockListContext } from '../BlockListContext'; +import { useBlockListWebSocket2 } from '../Sockets/useBlockListWebSocket2'; +import { BlockListData, generateBlockList, mergeBlockLists, waitForFadeAnimation } from '../utils'; +import { useHomePageInitialBlockList } from './useHomePageInitialBlockList'; + +export function useHomePageBlockList(btcBlockLimit: number = 3) { + const { setBlockListLoading, liveUpdates } = useBlockListContext(); + + const { initialBlockList, initialStxBlocksHashes, refetchInitialBlockList } = + useHomePageInitialBlockList(); + + // initially the block list is the initial blocklist + const [blockList, setBlockList] = useState(initialBlockList); + + // when the initial block list changes, reset the block list to the initial blocklist + useEffect(() => { + setBlockList(initialBlockList); + }, [initialBlockList]); + + // manually update the block list with block list updates from the websocket + const updateBlockListManually = useCallback( + (blockListUpdates: BlockListData[]) => { + setBlockList(prevBlockList => { + const newBlockList = mergeBlockLists(blockListUpdates, prevBlockList).slice( + 0, + btcBlockLimit + ); + return newBlockList; + }); + }, + [setBlockList, btcBlockLimit] + ); + + const { + latestStxBlocks: latestStxBlocksFromWebSocket, + latestStxBlocksCount: latestStxBlocksCountFromWebSocket, + clearLatestStxBlocks: clearLatestStxBlocksFromWebSocket, + } = useBlockListWebSocket2(initialStxBlocksHashes); + + // This is used to trigger a fade out effect when the block list is updated. + // When the counter is updated, we wait for the fade out effect to finish and then show the fade in effect + const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); + const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); + useEffect(() => { + if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { + waitForFadeAnimation(() => { + setBlockListLoading(false); + }); + } + }, [blockListUpdateCounter, clearLatestStxBlocksFromWebSocket, setBlockListLoading]); + + const showLatestStxBlocksFromWebSocket = useCallback(() => { + setBlockListLoading(true); + waitForFadeAnimation(() => { + const websocketBlockList = generateBlockList(latestStxBlocksFromWebSocket); + updateBlockListManually(websocketBlockList); + clearLatestStxBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }, [ + latestStxBlocksFromWebSocket, + updateBlockListManually, + setBlockListLoading, + clearLatestStxBlocksFromWebSocket, + ]); + + const updateBlockListWithQuery = useCallback( + async function () { + setBlockListLoading(true); + waitForFadeAnimation(async () => { + await refetchInitialBlockList(() => { + clearLatestStxBlocksFromWebSocket(); + setBlockListUpdateCounter(prev => prev + 1); + }); + }); + }, + [clearLatestStxBlocksFromWebSocket, setBlockListLoading, refetchInitialBlockList] + ); + + const prevLiveUpdatesRef = useRef(liveUpdates); + const prevLatestStxBlocksCountRef = useRef(latestStxBlocksCountFromWebSocket); + // Handles live updates + useEffect(() => { + const liveUpdatesToggled = prevLiveUpdatesRef.current !== liveUpdates; + + const receivedLatestStxBlockFromLiveUpdates = + liveUpdates && + latestStxBlocksCountFromWebSocket > 0 && + prevLatestStxBlocksCountRef.current !== latestStxBlocksCountFromWebSocket; + + if (liveUpdatesToggled) { + updateBlockListWithQuery(); + } else if (receivedLatestStxBlockFromLiveUpdates) { + showLatestStxBlocksFromWebSocket(); + } + + prevLiveUpdatesRef.current = liveUpdates; + prevLatestStxBlocksCountRef.current = latestStxBlocksCountFromWebSocket; + }, [ + liveUpdates, + latestStxBlocksCountFromWebSocket, + showLatestStxBlocksFromWebSocket, + updateBlockListWithQuery, + ]); + + return { + blockList, + updateBlockList: updateBlockListWithQuery, + latestBlocksCount: latestStxBlocksCountFromWebSocket, + }; +} diff --git a/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts b/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts new file mode 100644 index 000000000..ed1a75cd0 --- /dev/null +++ b/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts @@ -0,0 +1,97 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; + +import { BurnBlock } from '@stacks/blockchain-api-client'; + +import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; +import { + GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY, + useSuspenseBlocksByBurnBlock, +} from '../../../../common/queries/useBlocksByBurnBlock'; +import { + BURN_BLOCKS_QUERY_KEY, + useSuspenseBurnBlocks, +} from '../../../../common/queries/useBurnBlocksInfinite'; +import { BURN_BLOCKS_QUERY_KEY_EXTENSION } from '../consts'; +import { generateBlockList } from '../utils'; + +export function useHomePageInitialBlockList(blockListLimit: number = 3) { + const response = useSuspenseBurnBlocks(blockListLimit, {}, BURN_BLOCKS_QUERY_KEY_EXTENSION); + const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + const btcBlocks = useSuspenseInfiniteQueryResult(response); + + // const stxBlocks = useStxBlocksForBtcBlocks(btcBlocks, numStxBlocksperBtcBlock, btcBlocksRequiringStxBlocks); + + const latestBurnBlock = btcBlocks[0]; + const secondLatestBurnBlock = btcBlocks[1]; + const thirdLatestBurnBlock = btcBlocks[2]; + + const btcBlocksMap = useMemo(() => { + const map = {} as Record; + btcBlocks.forEach(block => { + map[block.burn_block_hash] = block; + }); + return map; + }, [btcBlocks]); + + // TODO: + const latestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(latestBurnBlock.burn_block_height) + ); + const secondLatestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(secondLatestBurnBlock.burn_block_height) + ); + const thirdLatestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( + useSuspenseBlocksByBurnBlock(thirdLatestBurnBlock.burn_block_height) + ); + + const queryClient = useQueryClient(); + const refetchInitialBlockList = useCallback( + async function (callback: () => void) { + // Invalidate queries first + await Promise.all([ + queryClient.invalidateQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), + queryClient.invalidateQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), + ]); + + // After invalidation, refetch the required queries + await Promise.all([ + queryClient.refetchQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), + queryClient.refetchQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), + ]); + + // Run your callback after refetching + callback(); + }, + [queryClient] + ); + + const initialStxBlocks = useMemo( + () => [ + ...latestBurnBlockStxBlocks, + ...secondLatestBurnBlockStxBlocks, + ...thirdLatestBurnBlockStxBlocks, + ], + [latestBurnBlockStxBlocks, secondLatestBurnBlockStxBlocks, thirdLatestBurnBlockStxBlocks] + ); + + const initialStxBlocksHashes = useMemo( + () => new Set(initialStxBlocks.map(block => block.hash)), + [initialStxBlocks] + ); + + const initialBlockList = useMemo( + () => generateBlockList(initialStxBlocks, btcBlocksMap), + [initialStxBlocks, btcBlocksMap] + ); + + return { + initialBlockList, + initialStxBlocksHashes, + initialStxBlocks, + refetchInitialBlockList, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + }; +} diff --git a/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts b/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts new file mode 100644 index 000000000..a46cf9b58 --- /dev/null +++ b/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts @@ -0,0 +1,25 @@ +import { useGetBlocksByBurnBlockQuery } from '@/common/queries/useBlocksByBurnBlock'; +import { useQueries } from '@tanstack/react-query'; + +export function useStxBlocksForBtcBlocks( + btcBlocks: BurnBlock[], + numStxBlocksperBtcBlock: number, + btcBlocksRequiringStxBlocks?: boolean[] +) { + const getQuery = useGetBlocksByBurnBlockQuery(); + + const stxBlockQueries = btcBlocks.map((btcBlock, i) => { + const isEnabled = !!btcBlocksRequiringStxBlocks && btcBlocksRequiringStxBlocks[i]; + + return isEnabled + ? getQuery(btcBlock.burn_block_height, numStxBlocksperBtcBlock) + : { + queryKey: ['stxBlocks', btcBlock.burn_block_height, 'disabled'], + queryFn: () => Promise.resolve(null), + enabled: false, + }; + }); + + const stxBlocksResults = useQueries({ queries: stxBlockQueries }); + return stxBlocksResults.map(result => result.data ?? null); +} diff --git a/src/app/_components/BlockList/types.ts b/src/app/_components/BlockList/types.ts index a3aa41484..580319114 100644 --- a/src/app/_components/BlockList/types.ts +++ b/src/app/_components/BlockList/types.ts @@ -47,4 +47,3 @@ export interface BlockListBlock { timestamp: number; txsCount?: number; } - diff --git a/src/app/_components/BlockList/useInitialBlocks.ts b/src/app/_components/BlockList/useInitialBlocks.ts deleted file mode 100644 index 920234010..000000000 --- a/src/app/_components/BlockList/useInitialBlocks.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { useQueryClient } from '@tanstack/react-query'; -import { useCallback, useMemo } from 'react'; - -import { Block, NakamotoBlock } from '@stacks/stacks-blockchain-api-types'; - -import { useSuspenseInfiniteQueryResult } from '../../../common/hooks/useInfiniteQueryResult'; -import { - BLOCK_LIST_QUERY_KEY, - useSuspenseBlockListInfinite, -} from '../../../common/queries/useBlockListInfinite'; -import { BlockListBtcBlock, BlockListStxBlock } from './types'; - -export function convertBlockToBlockListStxBlock(block: Block | NakamotoBlock): BlockListStxBlock { - return { - type: 'stx_block', - height: block.height, - hash: block.hash, - timestamp: block.burn_block_time, - txsCount: (block as Block)?.txs.length ?? (block as NakamotoBlock)?.tx_count, - }; -} -export function convertBlockToBlockListBtcBlock(block: Block | NakamotoBlock): BlockListBtcBlock { - return { - type: 'btc_block', - height: block.burn_block_height, - hash: block.burn_block_hash, - timestamp: block.burn_block_time, - }; -} - -export type BlockListData = { stxBlocks: BlockListStxBlock[]; btcBlock: BlockListBtcBlock }; - -// TODO: can and should probably simplify this code. Turn this into a function - make blocklist -export function generateBlockList( - blockListDataMap: Record -): BlockListData[] { - return Object.values(blockListDataMap).sort((a, b) => { - const bHeight = - typeof b.btcBlock.height === 'string' - ? Number.parseInt(b.btcBlock.height) - : b.btcBlock.height; - const aHeight = - typeof a.btcBlock.height === 'string' - ? Number.parseInt(a.btcBlock.height) - : a.btcBlock.height; - return bHeight - aHeight; - }); -} - -export function useInitialBlockList() { - const queryClient = useQueryClient(); - const response = useSuspenseBlockListInfinite(); - const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; - const blocks = useSuspenseInfiniteQueryResult(response); - - const initialStxBlocks: BlockListStxBlock[] = useMemo( - () => blocks.map(block => convertBlockToBlockListStxBlock(block)), - [blocks] - ); - - const initialStxBlocksHashes = useMemo(() => { - const stxBlockHashSet = new Set(); - initialStxBlocks.forEach(block => stxBlockHashSet.add(block.hash)); - return stxBlockHashSet; - }, [initialStxBlocks]); - -// const initialBtcBlocks: Record = useMemo( -// () => -// blocks.reduce( -// (acc, block) => { -// if (!acc[block.burn_block_hash]) { -// acc[block.burn_block_hash] = convertBlockToBlockListBtcBlock(block); -// } -// return acc; -// }, -// {} as Record -// ), -// [blocks] -// ); - -// const initialBtcBlocksHashes = useMemo( -// () => new Set(Object.keys(initialBtcBlocks)), -// [initialBtcBlocks] -// ); - - // btc block hash -> BlockListData - const initialBlockListDataMap: Record = useMemo( - () => - blocks.reduce( - (acc, block) => { - if (!acc[block.burn_block_hash]) { - acc[block.burn_block_hash] = { - stxBlocks: [], - btcBlock: convertBlockToBlockListBtcBlock(block), - }; - } - acc[block.burn_block_hash].stxBlocks.push(convertBlockToBlockListStxBlock(block)); - return acc; - }, - {} as Record - ), - [blocks] - ); - - const initialBlockList = useMemo( - () => generateBlockList(initialBlockListDataMap), - [initialBlockListDataMap] - ); - - const refetchInitialBlockList = useCallback( - function (callback: () => void) { - // return queryClient.resetQueries({ queryKey: ['blockListInfinite'] }); - queryClient.refetchQueries({ queryKey: [BLOCK_LIST_QUERY_KEY] }).then(() => callback()); - }, - [queryClient] - ); - - return { - initialStxBlocks, - initialStxBlocksHashes, - // initialBtcBlocks, - // initialBtcBlocksHashes, - // initialBlockListDataMap, - initialBlockList, - refetchInitialBlockList, - isFetchingNextPage, - fetchNextPage, - hasNextPage, - }; -} diff --git a/src/app/_components/BlockList/utils.ts b/src/app/_components/BlockList/utils.ts new file mode 100644 index 000000000..8d04555ce --- /dev/null +++ b/src/app/_components/BlockList/utils.ts @@ -0,0 +1,94 @@ +import { Block, BurnBlock, NakamotoBlock } from '@stacks/blockchain-api-client'; + +import { FADE_DURATION } from './consts'; +import { BlockListBtcBlock, BlockListStxBlock } from './types'; + +export function createBlockListStxBlock(stxBlock: Block | NakamotoBlock): BlockListStxBlock { + return { + type: 'stx_block', + height: stxBlock.height, + hash: stxBlock.hash, + timestamp: stxBlock.burn_block_time, + txsCount: + 'txs' in stxBlock + ? (stxBlock as Block).txs.length + : 'tx_count' in stxBlock + ? (stxBlock as NakamotoBlock).tx_count + : 0, + }; +} +export function createBlockListBtcBlock( + stxBlock: Block | NakamotoBlock, + txsCount?: number +): BlockListBtcBlock { + return { + type: 'btc_block', + height: stxBlock.burn_block_height, + hash: stxBlock.burn_block_hash, + timestamp: stxBlock.burn_block_time, + txsCount, + }; +} + +export type BtcBlockMap = Record; + +export function getBtcTxsCount(btcBlockMap: BtcBlockMap, stxBlock: Block | NakamotoBlock) { + const btcBlockHash = stxBlock.burn_block_hash; + return btcBlockMap[btcBlockHash].stacks_blocks.length; +} + +export type BlockListData = { stxBlocks: BlockListStxBlock[]; btcBlock: BlockListBtcBlock }; + +export function waitForFadeAnimation(callback: () => void) { + setTimeout(callback, FADE_DURATION); +} + +export function generateBlockList( + stxBlocks: (Block | NakamotoBlock)[], + btcBlocksMap?: BtcBlockMap +) { + if (stxBlocks.length === 0) return []; + const blockList = [ + { + stxBlocks: [createBlockListStxBlock(stxBlocks[0])], + btcBlock: btcBlocksMap + ? createBlockListBtcBlock(stxBlocks[0], getBtcTxsCount(btcBlocksMap, stxBlocks[0])) + : createBlockListBtcBlock(stxBlocks[0]), + }, + ]; + if (stxBlocks.length === 1) return blockList; + for (let i = 1; i < stxBlocks.length; i++) { + const stxBlock = stxBlocks[i]; + const latestBtcBlock = blockList[blockList.length - 1].btcBlock; + const latesStxBlocks = blockList[blockList.length - 1].stxBlocks; + if (latestBtcBlock.hash === stxBlock.burn_block_hash) { + latesStxBlocks.push(createBlockListStxBlock(stxBlock)); + } else { + blockList.push({ + stxBlocks: [createBlockListStxBlock(stxBlock)], + btcBlock: btcBlocksMap + ? createBlockListBtcBlock(stxBlock, getBtcTxsCount(btcBlocksMap, stxBlock)) + : createBlockListBtcBlock(stxBlock), + }); + } + } + return blockList; +} + +export function mergeBlockLists(newblockList: BlockListData[], initialBlockList: BlockListData[]) { + console.log('merging block lists', { newblockList, initialBlockList }); + if (newblockList.length === 0) return initialBlockList; + const earliestBtcBlock = newblockList[newblockList.length - 1]; + const latestBtcBlock = initialBlockList[0]; + if (earliestBtcBlock.btcBlock.hash === latestBtcBlock.btcBlock.hash) { + const btcBlock = earliestBtcBlock.btcBlock || latestBtcBlock.btcBlock; + const stxBlocks = [...earliestBtcBlock.stxBlocks, ...latestBtcBlock.stxBlocks]; + return [ + ...newblockList.slice(0, newblockList.length - 1), + { btcBlock, stxBlocks }, + ...initialBlockList.slice(1), + ]; + } else { + return [...newblockList, ...initialBlockList]; + } +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 2268851a7..49ce5da3d 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,5 @@ import { Metadata } from 'next'; import { headers } from 'next/headers'; -import * as React from 'react'; import { ReactNode } from 'react'; import { meta } from '../common/constants/meta'; diff --git a/src/common/context/GlobalContext.tsx b/src/common/context/GlobalContext.tsx index 27927762b..f6b583828 100644 --- a/src/common/context/GlobalContext.tsx +++ b/src/common/context/GlobalContext.tsx @@ -5,10 +5,9 @@ import { useSearchParams } from 'next/navigation'; import { FC, ReactNode, createContext, useCallback, useEffect, useMemo, useState } from 'react'; import { useCookies } from 'react-cookie'; -import { StacksApiSocketClient, StacksApiWebSocketClient, connectWebSocketClient } from '@stacks/blockchain-api-client'; +import { StacksApiWebSocketClient, connectWebSocketClient } from '@stacks/blockchain-api-client'; import { ChainID } from '@stacks/transactions'; -import { useStacksApiSocketClient } from '../../app/_components/BlockList/Sockets/use-stacks-api-socket-client'; import { buildCustomNetworkUrl, fetchCustomNetworkId } from '../components/modals/AddNetwork/utils'; import { DEFAULT_DEVNET_SERVER, IS_BROWSER } from '../constants/constants'; import { diff --git a/src/common/queries/useBlocksByBurnBlock.ts b/src/common/queries/useBlocksByBurnBlock.ts index da2353590..218beb2b9 100644 --- a/src/common/queries/useBlocksByBurnBlock.ts +++ b/src/common/queries/useBlocksByBurnBlock.ts @@ -17,6 +17,18 @@ export const GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY = 'getBlocksByBurnBlock'; const MAX_STX_BLOCKS_PER_BURN_BLOCK_LIMIT = 30; +export function useGetBlocksByBurnBlockQuery() { + const api = useApi(); + + // Return a function that constructs the query structure + return (heightOrHash: string | number, numStxBlocksperBtcBlock: number) => ({ + queryKey: ['stxBlocks', heightOrHash], + queryFn: () => api.blocksApi.getBlocksByBurnBlock({ + heightOrHash, + limit: numStxBlocksperBtcBlock, + }), + }); +} export function useBlocksByBurnBlock( heightOrHash: string | number, limit: number = MAX_STX_BLOCKS_PER_BURN_BLOCK_LIMIT, From a21d8c3f83fd027577665cea4dcd6693df0971ea Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 12 Apr 2024 13:38:40 -0500 Subject: [PATCH 22/70] feat(grouped-by-btc-block-list-view-3): attempting to fix fading --- .../BlocksPage/BlocksPageBlockList.tsx | 38 ++++---- .../Ungrouped/BlockListUngrouped.tsx | 96 +++++++++++++++---- .../BlockList/Ungrouped/BtcBlockRow.tsx | 69 ------------- .../BlockList/Ungrouped/skeleton.tsx | 19 ++-- src/app/_components/BlockList/consts.ts | 2 + .../data/useBlocksPageBlockListUngrouped.ts | 13 +-- ...sPageBlockListUngroupedInitialBlockList.ts | 77 +++++++++++++++ .../data/useHomePageInitialBlockList.ts | 2 - .../data/useStxBlocksForBtcBlocks.ts | 26 ++--- src/app/blocks/PageClient.tsx | 7 +- src/common/components/ListFooter.tsx | 7 +- src/common/queries/useBlocksByBurnBlock.ts | 15 +-- src/common/queries/useBurnBlocksInfinite.ts | 7 +- 13 files changed, 222 insertions(+), 156 deletions(-) delete mode 100644 src/app/_components/BlockList/Ungrouped/BtcBlockRow.tsx create mode 100644 src/app/_components/BlockList/data/useBlocksPageBlockListUngroupedInitialBlockList.ts diff --git a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx index 93743950a..812180f98 100644 --- a/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx +++ b/src/app/_components/BlockList/BlocksPage/BlocksPageBlockList.tsx @@ -1,6 +1,5 @@ 'use client'; -import dynamic from 'next/dynamic'; import { Suspense, useCallback, useRef } from 'react'; import { Section } from '../../../../common/components/Section'; @@ -9,23 +8,24 @@ import { useBlockListContext } from '../BlockListContext'; import { BlockListProvider } from '../BlockListProvider'; import { Controls } from '../Controls'; import { BlocksPageBlockListGroupedSkeleton } from '../Grouped/skeleton'; -import { BlocksPageBlockListUngroupedSkeleton } from '../Ungrouped/skeleton'; +import { BlocksPageBlockListGrouped } from './BlocksPageBlockListGrouped'; +import { BlocksPageBlockListUngrouped } from './BlocksPageBlockListUngrouped'; -const BlocksPageBlockListGroupedDynamic = dynamic( - () => import('./BlocksPageBlockListGrouped').then(mod => mod.BlocksPageBlockListGrouped), - { - loading: () => , - ssr: false, - } -); +// const BlocksPageBlockListGroupedDynamic = dynamic( +// () => import('./BlocksPageBlockListGrouped').then(mod => mod.BlocksPageBlockListGrouped), +// { +// loading: () => , +// ssr: false, +// } +// ); -const BlocksPageBlockListUngroupedDynamic = dynamic( - () => import('./BlocksPageBlockListUngrouped').then(mod => mod.BlocksPageBlockListUngrouped), - { - loading: () => , - ssr: false, - } -); +// const BlocksPageBlockListUngroupedDynamic = dynamic( +// () => import('./BlocksPageBlockListUngrouped').then(mod => mod.BlocksPageBlockListUngrouped), +// { +// loading: () => , +// ssr: false, +// } +// ); function BlocksPageBlockListBase() { const { groupedByBtc, setGroupedByBtc, liveUpdates, setLiveUpdates } = useBlockListContext(); @@ -55,9 +55,11 @@ function BlocksPageBlockListBase() { horizontal={true} /> {groupedByBtc ? ( - + // + ) : ( - + // + )}
); diff --git a/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx b/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx index 5a48ea24e..3a48bb1bd 100644 --- a/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx +++ b/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx @@ -1,16 +1,18 @@ +import { useColorModeValue } from '@chakra-ui/react'; import { ReactNode } from 'react'; +import { BsArrowReturnLeft } from 'react-icons/bs'; -import { BlockLink } from '../../../../common/components/ExplorerLinks'; +import { BlockLink, ExplorerLink } from '../../../../common/components/ExplorerLinks'; import { Timestamp } from '../../../../common/components/Timestamp'; import { truncateMiddle } from '../../../../common/utils/utils'; import { Box } from '../../../../ui/Box'; -import { Flex } from '../../../../ui/Flex'; +import { Flex, FlexProps } from '../../../../ui/Flex'; import { Grid } from '../../../../ui/Grid'; import { HStack } from '../../../../ui/HStack'; import { Icon } from '../../../../ui/Icon'; import { Stack } from '../../../../ui/Stack'; import { Text } from '../../../../ui/Text'; -import { StxIcon } from '../../../../ui/icons'; +import { BitcoinIcon, StxIcon } from '../../../../ui/icons'; import { ListHeader } from '../../ListHeader'; import { BlockCount } from '../BlockCount'; import { useBlockListContext } from '../BlockListContext'; @@ -18,22 +20,61 @@ import { LineAndNode } from '../LineAndNode'; import { FADE_DURATION } from '../consts'; import { BlockListStxBlock } from '../types'; import { BlockListData } from '../utils'; -import { BtcBlockRow } from './BtcBlockRow'; - -export function BlockListUngroupedLayout({ children }: { children: ReactNode }) { - const { isBlockListLoading } = useBlockListContext(); +interface BtcBlockRowProps { + height: number | string; + hash: string; + timestamp?: number; +} +export function BtcBlockRowLayout({ children, ...rest }: FlexProps & { children: ReactNode }) { + const textColor = useColorModeValue('slate.700', 'slate.500'); // TODO: not in theme. remove return ( - {children} - + + ); +} + +export function BtcBlockRowContent({ timestamp, height, hash }: BtcBlockRowProps) { + const iconColor = useColorModeValue('slate.600', 'slate.800'); // TODO: not in theme. remove + return ( + <> + + + + + #{height} + + +  ∙ } fontSize={'xs'}> + {truncateMiddle(hash, 3)} + {timestamp && } + + + ); +} + +export function BtcBlockRow({ timestamp, height, hash }: BtcBlockRowProps) { + return ( + + + ); } @@ -131,7 +172,7 @@ function StxBlockRow({ ) : ( <> - + @@ -228,14 +269,10 @@ function StxBlocksGroupedByBtcBlock({ : blockList.stxBlocks; const numStxBlocks = btcBlock.txsCount ?? stxBlocks.length; const numStxBlocksNotDisplayed = numStxBlocks - (stxBlocksLimit || stxBlocks.length); - console.log({ numStxBlocks, numStxBlocksNotDisplayed, stxBlocksLimit, btcBlock, stxBlocks }); return ( <> - {stxBlocksLimit && stxBlocks.length > stxBlocksLimit && ( - - )} {numStxBlocksNotDisplayed > 0 ? : null} + {children} + + ); +} + export function BlockListUngrouped({ blockList, stxBlocksLimit, @@ -260,7 +314,7 @@ export function BlockListUngrouped({ {blockList.map(bl => ( - {children} - - ); -} - -export function BtcBlockRowContent({ timestamp, height, hash }: BtcBlockRowProps) { - const iconColor = useColorModeValue('slate.600', 'slate.800'); // TODO: not in theme. remove - return ( - <> - - - - - #{height} - - -  ∙ } fontSize={'xs'}> - {truncateMiddle(hash, 3)} - {timestamp && } - - - ); -} - -export function BtcBlockRow({ timestamp, height, hash }: BtcBlockRowProps) { - return ( - - - - ); -} diff --git a/src/app/_components/BlockList/Ungrouped/skeleton.tsx b/src/app/_components/BlockList/Ungrouped/skeleton.tsx index 761440cbe..a6a48da77 100644 --- a/src/app/_components/BlockList/Ungrouped/skeleton.tsx +++ b/src/app/_components/BlockList/Ungrouped/skeleton.tsx @@ -9,8 +9,7 @@ import { SkeletonText } from '../../../../ui/SkeletonText'; import { StxIcon } from '../../../../ui/icons'; import { BlockListGridHeaderRowSkeleton } from '../Grouped/skeleton'; import { LineAndNode } from '../LineAndNode'; -import { StxBlocksGridLayout } from './BlockListUngrouped'; -import { BtcBlockRowLayout } from './BtcBlockRow'; +import { BtcBlockRowLayout, StxBlocksGridLayout } from './BlockListUngrouped'; // layout was copied export function BlockListRowSkeleton({ @@ -86,7 +85,7 @@ export function StxBlocksGridSkeleton({ ); } -function BtcBlockListItemContentSkeleton() { +function BtcBlockRowContentSkeleton() { return ( <> @@ -95,10 +94,10 @@ function BtcBlockListItemContentSkeleton() { ); } -function BtcBlockListItemSkeleton({ minimized }: { minimized?: boolean }) { +function BtcBlockRowSkeleton({ minimized }: { minimized?: boolean }) { return ( - + ); } @@ -107,9 +106,9 @@ export function BlocksPageBlockListUngroupedSkeleton() { return ( - + - + ); } @@ -118,11 +117,11 @@ export function HomePageBlockListUngroupedSkeleton() { return ( - + - + - + ); } diff --git a/src/app/_components/BlockList/consts.ts b/src/app/_components/BlockList/consts.ts index e5f750f5a..22ca62336 100644 --- a/src/app/_components/BlockList/consts.ts +++ b/src/app/_components/BlockList/consts.ts @@ -1,3 +1,5 @@ export const FADE_DURATION = 700; export const BURN_BLOCKS_QUERY_KEY_EXTENSION = 'blockList'; +export const BURN_BLOCKS_BLOCK_LIST_UNGROUPED_QUERY_KEY_EXTENSION = 'blockList-ungrouped'; + diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts b/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts index 1ff80c698..a4c32f147 100644 --- a/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts @@ -5,7 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useBlockListContext } from '../BlockListContext'; import { useBlockListWebSocket2 } from '../Sockets/useBlockListWebSocket2'; import { BlockListData, generateBlockList, mergeBlockLists, waitForFadeAnimation } from '../utils'; -import { useHomePageInitialBlockList } from './useHomePageInitialBlockList'; +import { useBlocksPageBlockListUngroupedInitialBlockList } from './useBlocksPageBlockListUngroupedInitialBlockList'; export function useBlocksPageBlockListUngrouped() { const { setBlockListLoading, liveUpdates } = useBlockListContext(); @@ -17,7 +17,7 @@ export function useBlocksPageBlockListUngrouped() { fetchNextPage, refetchInitialBlockList, hasNextPage, - } = useHomePageInitialBlockList(); + } = useBlocksPageBlockListUngroupedInitialBlockList(); // initially the block list is the initial blocklist const [blockList, setBlockList] = useState(initialBlockList); @@ -38,9 +38,9 @@ export function useBlocksPageBlockListUngrouped() { const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); useEffect(() => { if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { - waitForFadeAnimation(() => { - setBlockListLoading(false); - }); + // waitForFadeAnimation(() => { + setBlockListLoading(false); + // }); } }, [blockListUpdateCounter, clearLatestStxBlocksFromWebSocket, setBlockListLoading]); @@ -61,7 +61,8 @@ export function useBlocksPageBlockListUngrouped() { const websocketBlockList = generateBlockList(latestStxBlocksFromWebSocket); updateBlockListManually(websocketBlockList); clearLatestStxBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); + setBlockListLoading(false); + // setBlockListUpdateCounter(prev => prev + 1); }); }, [ latestStxBlocksFromWebSocket, diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListUngroupedInitialBlockList.ts b/src/app/_components/BlockList/data/useBlocksPageBlockListUngroupedInitialBlockList.ts new file mode 100644 index 000000000..02d41d2ca --- /dev/null +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListUngroupedInitialBlockList.ts @@ -0,0 +1,77 @@ +import { useQueryClient } from '@tanstack/react-query'; +import { useCallback, useMemo } from 'react'; + +import { BurnBlock } from '@stacks/blockchain-api-client'; + +import { useSuspenseInfiniteQueryResult } from '../../../../common/hooks/useInfiniteQueryResult'; +import { GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY } from '../../../../common/queries/useBlocksByBurnBlock'; +import { + BURN_BLOCKS_QUERY_KEY, + useSuspenseBurnBlocks, +} from '../../../../common/queries/useBurnBlocksInfinite'; +import { BURN_BLOCKS_BLOCK_LIST_UNGROUPED_QUERY_KEY_EXTENSION } from '../consts'; +import { generateBlockList } from '../utils'; +import { useStxBlocksForBtcBlocks } from './useStxBlocksForBtcBlocks'; + +export function useBlocksPageBlockListUngroupedInitialBlockList(blockListLimit: number = 3) { + const response = useSuspenseBurnBlocks( + blockListLimit, + {}, + BURN_BLOCKS_BLOCK_LIST_UNGROUPED_QUERY_KEY_EXTENSION + ); + const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; + const btcBlocks = useSuspenseInfiniteQueryResult(response); + console.log({ btcBlocks }); + + const initialStxBlocks = useStxBlocksForBtcBlocks(btcBlocks); + console.log({ initialStxBlocks }); + + const btcBlocksMap = useMemo(() => { + const map = {} as Record; + btcBlocks.forEach(block => { + map[block.burn_block_hash] = block; + }); + return map; + }, [btcBlocks]); + + const queryClient = useQueryClient(); + const refetchInitialBlockList = useCallback( + async function (callback: () => void) { + // Invalidate queries first + await Promise.all([ + queryClient.invalidateQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), + queryClient.invalidateQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), + ]); + + // After invalidation, refetch the required queries + await Promise.all([ + queryClient.refetchQueries({ queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY] }), + queryClient.refetchQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), + ]); + + // Run your callback after refetching + callback(); + }, + [queryClient] + ); + + const initialStxBlocksHashes = useMemo( + () => new Set(initialStxBlocks.map(block => block.hash)), + [initialStxBlocks] + ); + + const initialBlockList = useMemo( + () => generateBlockList(initialStxBlocks, btcBlocksMap), + [initialStxBlocks, btcBlocksMap] + ); + + return { + initialBlockList, + initialStxBlocksHashes, + initialStxBlocks, + refetchInitialBlockList, + isFetchingNextPage, + fetchNextPage, + hasNextPage, + }; +} diff --git a/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts b/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts index ed1a75cd0..51d757289 100644 --- a/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts +++ b/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts @@ -20,8 +20,6 @@ export function useHomePageInitialBlockList(blockListLimit: number = 3) { const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; const btcBlocks = useSuspenseInfiniteQueryResult(response); - // const stxBlocks = useStxBlocksForBtcBlocks(btcBlocks, numStxBlocksperBtcBlock, btcBlocksRequiringStxBlocks); - const latestBurnBlock = btcBlocks[0]; const secondLatestBurnBlock = btcBlocks[1]; const thirdLatestBurnBlock = btcBlocks[2]; diff --git a/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts b/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts index a46cf9b58..4d455b2ba 100644 --- a/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts +++ b/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts @@ -1,25 +1,19 @@ import { useGetBlocksByBurnBlockQuery } from '@/common/queries/useBlocksByBurnBlock'; import { useQueries } from '@tanstack/react-query'; -export function useStxBlocksForBtcBlocks( - btcBlocks: BurnBlock[], - numStxBlocksperBtcBlock: number, - btcBlocksRequiringStxBlocks?: boolean[] -) { - const getQuery = useGetBlocksByBurnBlockQuery(); +import { BurnBlock } from '@stacks/blockchain-api-client'; - const stxBlockQueries = btcBlocks.map((btcBlock, i) => { - const isEnabled = !!btcBlocksRequiringStxBlocks && btcBlocksRequiringStxBlocks[i]; +export function useStxBlocksForBtcBlocks(btcBlocks: BurnBlock[]) { + const getQuery = useGetBlocksByBurnBlockQuery(); - return isEnabled - ? getQuery(btcBlock.burn_block_height, numStxBlocksperBtcBlock) - : { - queryKey: ['stxBlocks', btcBlock.burn_block_height, 'disabled'], - queryFn: () => Promise.resolve(null), - enabled: false, - }; + const stxBlockQueries = btcBlocks.map(btcBlock => { + return getQuery(btcBlock.burn_block_height); }); const stxBlocksResults = useQueries({ queries: stxBlockQueries }); - return stxBlocksResults.map(result => result.data ?? null); + console.log({ stxBlocksResults }); + const stxBlocks = stxBlocksResults.flatMap( + result => result.data?.results || (result.data as any)?.pages[0].results || [] + ); + return stxBlocks; } diff --git a/src/app/blocks/PageClient.tsx b/src/app/blocks/PageClient.tsx index 1ceb1e712..b9a21b024 100644 --- a/src/app/blocks/PageClient.tsx +++ b/src/app/blocks/PageClient.tsx @@ -28,11 +28,16 @@ const PaginatedBlockListLayoutADynamic = dynamic( () => import('../_components/BlockList/LayoutA/Paginated').then(mod => mod.PaginatedBlockListLayoutA), { - loading: () => , // TODO: fix this + loading: () => , ssr: false, } ); +const BlocksListDynamic = dynamic(() => import('../_components/BlockList').then(mod => mod.BlocksList), { + loading: () => , + ssr: false, +}); + export function BlocksPageLayout({ blocksPageHeaders, blocksList, diff --git a/src/common/components/ListFooter.tsx b/src/common/components/ListFooter.tsx index 6bd533f01..168b5da6b 100644 --- a/src/common/components/ListFooter.tsx +++ b/src/common/components/ListFooter.tsx @@ -7,7 +7,7 @@ import { Button } from '../../ui/Button'; import { Icon } from '../../ui/Icon'; import { ExplorerLink } from './ExplorerLinks'; -interface SectionFooterButtonPropsBase { +interface SectionFooterButtonPropsBase extends ButtonProps { isLoading?: boolean; hasNextPage?: boolean; fetchNextPage?: () => void; @@ -21,11 +21,12 @@ export const ListFooter: React.FC = ({ href, label, hasNextPage, + ...rest }) => { if (href) { return ( - @@ -33,7 +34,7 @@ export const ListFooter: React.FC = ({ } if (fetchNextPage && hasNextPage) { return ( - ); diff --git a/src/common/queries/useBlocksByBurnBlock.ts b/src/common/queries/useBlocksByBurnBlock.ts index 218beb2b9..0018d38a1 100644 --- a/src/common/queries/useBlocksByBurnBlock.ts +++ b/src/common/queries/useBlocksByBurnBlock.ts @@ -20,15 +20,16 @@ const MAX_STX_BLOCKS_PER_BURN_BLOCK_LIMIT = 30; export function useGetBlocksByBurnBlockQuery() { const api = useApi(); - // Return a function that constructs the query structure - return (heightOrHash: string | number, numStxBlocksperBtcBlock: number) => ({ - queryKey: ['stxBlocks', heightOrHash], - queryFn: () => api.blocksApi.getBlocksByBurnBlock({ - heightOrHash, - limit: numStxBlocksperBtcBlock, - }), + return (heightOrHash: string | number, numStxBlocksperBtcBlock?: number) => ({ + queryKey: [GET_BLOCKS_BY_BURN_BLOCK_QUERY_KEY, heightOrHash], + queryFn: () => + api.blocksApi.getBlocksByBurnBlock({ + heightOrHash, + limit: numStxBlocksperBtcBlock || MAX_STX_BLOCKS_PER_BURN_BLOCK_LIMIT, + }), }); } + export function useBlocksByBurnBlock( heightOrHash: string | number, limit: number = MAX_STX_BLOCKS_PER_BURN_BLOCK_LIMIT, diff --git a/src/common/queries/useBurnBlocksInfinite.ts b/src/common/queries/useBurnBlocksInfinite.ts index ae439c1f4..7214be44b 100644 --- a/src/common/queries/useBurnBlocksInfinite.ts +++ b/src/common/queries/useBurnBlocksInfinite.ts @@ -45,11 +45,12 @@ export function useSuspenseBurnBlocks( queryKey: queryKeyExtension ? [BURN_BLOCKS_QUERY_KEY, queryKeyExtension] : [BURN_BLOCKS_QUERY_KEY], - queryFn: ({ pageParam }: { pageParam: number }) => - api.burnBlocksApi.getBurnBlocks({ + queryFn: ({ pageParam }: { pageParam: number }) => { + return api.burnBlocksApi.getBurnBlocks({ limit, offset: pageParam, - }), + }); + }, getNextPageParam, initialPageParam: 0, staleTime: TWO_MINUTES, From cc894263cfdc94a5bd3ad5420d603dc852783671 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 12 Apr 2024 15:41:16 -0500 Subject: [PATCH 23/70] feat(grouped-by-btc-block-list-view-3): fixed state loop --- .../BlockList/Grouped/BlockListGrouped.tsx | 2 +- .../BlockList/Grouped/skeleton.tsx | 1 + .../Sockets/useBlockListWebSocket2.ts | 34 ++++++++++--------- .../Ungrouped/BlockListUngrouped.tsx | 4 +-- .../BlockList/Ungrouped/skeleton.tsx | 8 ++++- .../data/useBlocksPageBlockListGrouped.tsx | 19 ++++------- ...cksPageBlockListGroupedInitialBlockList.ts | 2 +- .../data/useBlocksPageBlockListUngrouped.ts | 14 ++++---- ...sPageBlockListUngroupedInitialBlockList.ts | 4 +-- .../BlockList/data/useHomePageBlockList.ts | 21 +++++------- .../data/useHomePageInitialBlockList.ts | 7 ++-- .../data/useStxBlocksForBtcBlocks.ts | 3 +- 12 files changed, 58 insertions(+), 61 deletions(-) diff --git a/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx index 4a87f9fe9..01a8c6d69 100644 --- a/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx +++ b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx @@ -222,7 +222,7 @@ export function BurnBlockGroupGrid({ minimized={minimized} /> {i < stxBlocks.length - 1 && ( - + )} ))} diff --git a/src/app/_components/BlockList/Grouped/skeleton.tsx b/src/app/_components/BlockList/Grouped/skeleton.tsx index 5b04b998c..ae008e21a 100644 --- a/src/app/_components/BlockList/Grouped/skeleton.tsx +++ b/src/app/_components/BlockList/Grouped/skeleton.tsx @@ -91,6 +91,7 @@ export function BurnBlockGroupSkeleton({ : undefined} minimized={minimized} + key={`block-list-row-skeleton-${rowIndex}`} /> ))} diff --git a/src/app/_components/BlockList/Sockets/useBlockListWebSocket2.ts b/src/app/_components/BlockList/Sockets/useBlockListWebSocket2.ts index 4b1d633a9..7c1df41ef 100644 --- a/src/app/_components/BlockList/Sockets/useBlockListWebSocket2.ts +++ b/src/app/_components/BlockList/Sockets/useBlockListWebSocket2.ts @@ -1,4 +1,4 @@ -import { useCallback, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef, useState } from 'react'; import { NakamotoBlock } from '@stacks/blockchain-api-client/src/generated/models'; @@ -6,21 +6,23 @@ import { useSubscribeBlocks } from './useSubscribeBlocks'; export function useBlockListWebSocket2(initialStxBlockHashes: Set) { const [latestStxBlocks, setLatestStxBlocks] = useState([]); - const stxBlockHashes = useRef(new Set()); - - const handleBlock = useCallback( - (stxBlock: NakamotoBlock) => { - // If the block is already in the list, don't add it again - if (stxBlockHashes.current.has(stxBlock.hash) || initialStxBlockHashes.has(stxBlock.hash)) { - return; - } - - // Otherwise, add it to the list - setLatestStxBlocks(prevLatestStxBlocks => [stxBlock, ...prevLatestStxBlocks]); - stxBlockHashes.current.add(stxBlock.hash); - }, - [initialStxBlockHashes] - ); + + const stxBlockHashes = useRef(new Set(initialStxBlockHashes)); + // update ref when initialStxBlockHashes changes + useEffect(() => { + stxBlockHashes.current = new Set(initialStxBlockHashes); + }, [initialStxBlockHashes]); + + const handleBlock = useCallback((stxBlock: NakamotoBlock) => { + // If the block is already in the list, don't add it again + if (stxBlockHashes.current.has(stxBlock.hash)) { + return; + } + + // Otherwise, add it to the list + setLatestStxBlocks(prevLatestStxBlocks => [stxBlock, ...prevLatestStxBlocks]); + stxBlockHashes.current.add(stxBlock.hash); + }, []); useSubscribeBlocks(handleBlock); // useSubscribeBlocks2(handleBlock); diff --git a/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx b/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx index 3a48bb1bd..fdf4eb88e 100644 --- a/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx +++ b/src/app/_components/BlockList/Ungrouped/BlockListUngrouped.tsx @@ -235,7 +235,7 @@ function StxBlocksGrid({ {stxBlocks.map((stxBlock, i) => ( <> - + {numStxBlocksNotDisplayed > 0 ? : null} : undefined} minimized={minimized} + key={`block-list-row-skeleton-${i}`} /> {i < numBlocks - 1 && ( - + )} ))} diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx b/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx index 2dac60526..b8d4102d0 100644 --- a/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx @@ -18,10 +18,10 @@ export function useBlocksPageBlockListGrouped(btcBlockLimit: number = 10) { } = useBlocksPageBlockListGroupedInitialBlockList(btcBlockLimit); // initially the block list is the initial blocklist - const [blockList, setBlockList] = useState(initialBlockList); + const blockList = useRef(initialBlockList); // when the initial block list changes, reset the block list to the initial blocklist useEffect(() => { - setBlockList(initialBlockList); + blockList.current = initialBlockList; }, [initialBlockList]); const { @@ -43,15 +43,10 @@ export function useBlocksPageBlockListGrouped(btcBlockLimit: number = 10) { }, [blockListUpdateCounter, clearLatestStxBlocksFromWebSocket, setBlockListLoading]); // manually update the block list with block list updates from the websocket - const updateBlockListManually = useCallback( - (blockListUpdates: BlockListData[]) => { - setBlockList(prevBlockList => { - const newBlockList = mergeBlockLists(blockListUpdates, prevBlockList); - return newBlockList; - }); - }, - [setBlockList] - ); + const updateBlockListManually = useCallback((blockListUpdates: BlockListData[]) => { + const newBlockList = mergeBlockLists(blockListUpdates, blockList.current); + blockList.current = newBlockList; + }, []); const showLatestStxBlocksFromWebSocket = useCallback(() => { setBlockListLoading(true); @@ -108,7 +103,7 @@ export function useBlocksPageBlockListGrouped(btcBlockLimit: number = 10) { ]); return { - blockList, + blockList: blockList.current, updateBlockList: updateBlockListWithQuery, latestBlocksCount: latestStxBlocksCountFromWebSocket, isFetchingNextPage, diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListGroupedInitialBlockList.ts b/src/app/_components/BlockList/data/useBlocksPageBlockListGroupedInitialBlockList.ts index 6dc6852f5..59b7b4b67 100644 --- a/src/app/_components/BlockList/data/useBlocksPageBlockListGroupedInitialBlockList.ts +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListGroupedInitialBlockList.ts @@ -64,7 +64,7 @@ export function useBlocksPageBlockListGroupedInitialBlockList(blockListLimit: nu const initialBlockList = useMemo(() => { const startOfBlockList = generateBlockList(latestBurnBlockStxBlocks, btcBlocksMap); - const restOfBlockList = burnBlocks.map(block => ({ + const restOfBlockList = burnBlocks.slice(1).map(block => ({ btcBlock: { type: 'btc_block', height: block.burn_block_height, diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts b/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts index a4c32f147..a48fde87a 100644 --- a/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts @@ -20,10 +20,10 @@ export function useBlocksPageBlockListUngrouped() { } = useBlocksPageBlockListUngroupedInitialBlockList(); // initially the block list is the initial blocklist - const [blockList, setBlockList] = useState(initialBlockList); + const blockList = useRef(initialBlockList); // when the initial block list changes, reset the block list to the initial blocklist useEffect(() => { - setBlockList(initialBlockList); + blockList.current = initialBlockList; }, [initialBlockList]); const { @@ -47,12 +47,10 @@ export function useBlocksPageBlockListUngrouped() { // manually update the block list with block list updates from the websocket const updateBlockListManually = useCallback( (blockListUpdates: BlockListData[]) => { - setBlockList(prevBlockList => { - const newBlockList = mergeBlockLists(blockListUpdates, prevBlockList); - return newBlockList; - }); + const newBlockList = mergeBlockLists(blockListUpdates, blockList.current); + blockList.current = newBlockList; }, - [setBlockList] + [] ); const showLatestStxBlocksFromWebSocket = useCallback(() => { @@ -111,7 +109,7 @@ export function useBlocksPageBlockListUngrouped() { ]); return { - blockList, + blockList: blockList.current, latestStxBlocksCountFromWebSocket, updateBlockList: updateBlockListWithQuery, latestBlocksCount: latestStxBlocksCountFromWebSocket, diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListUngroupedInitialBlockList.ts b/src/app/_components/BlockList/data/useBlocksPageBlockListUngroupedInitialBlockList.ts index 02d41d2ca..3bcc22d9f 100644 --- a/src/app/_components/BlockList/data/useBlocksPageBlockListUngroupedInitialBlockList.ts +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListUngroupedInitialBlockList.ts @@ -21,10 +21,8 @@ export function useBlocksPageBlockListUngroupedInitialBlockList(blockListLimit: ); const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; const btcBlocks = useSuspenseInfiniteQueryResult(response); - console.log({ btcBlocks }); const initialStxBlocks = useStxBlocksForBtcBlocks(btcBlocks); - console.log({ initialStxBlocks }); const btcBlocksMap = useMemo(() => { const map = {} as Record; @@ -49,7 +47,7 @@ export function useBlocksPageBlockListUngroupedInitialBlockList(blockListLimit: queryClient.refetchQueries({ queryKey: [BURN_BLOCKS_QUERY_KEY] }), ]); - // Run your callback after refetching + // Run callback after refetching callback(); }, [queryClient] diff --git a/src/app/_components/BlockList/data/useHomePageBlockList.ts b/src/app/_components/BlockList/data/useHomePageBlockList.ts index 0cd889d0c..1f2c3c5f4 100644 --- a/src/app/_components/BlockList/data/useHomePageBlockList.ts +++ b/src/app/_components/BlockList/data/useHomePageBlockList.ts @@ -12,25 +12,22 @@ export function useHomePageBlockList(btcBlockLimit: number = 3) { useHomePageInitialBlockList(); // initially the block list is the initial blocklist - const [blockList, setBlockList] = useState(initialBlockList); - + const blockList = useRef(initialBlockList); // when the initial block list changes, reset the block list to the initial blocklist useEffect(() => { - setBlockList(initialBlockList); + blockList.current = initialBlockList; }, [initialBlockList]); // manually update the block list with block list updates from the websocket const updateBlockListManually = useCallback( (blockListUpdates: BlockListData[]) => { - setBlockList(prevBlockList => { - const newBlockList = mergeBlockLists(blockListUpdates, prevBlockList).slice( - 0, - btcBlockLimit - ); - return newBlockList; - }); + const newBlockList = mergeBlockLists(blockListUpdates, blockList.current).slice( + 0, + btcBlockLimit + ); + blockList.current = newBlockList; }, - [setBlockList, btcBlockLimit] + [btcBlockLimit] ); const { @@ -106,7 +103,7 @@ export function useHomePageBlockList(btcBlockLimit: number = 3) { ]); return { - blockList, + blockList: blockList.current, updateBlockList: updateBlockListWithQuery, latestBlocksCount: latestStxBlocksCountFromWebSocket, }; diff --git a/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts b/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts index 51d757289..6d18d08df 100644 --- a/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts +++ b/src/app/_components/BlockList/data/useHomePageInitialBlockList.ts @@ -20,9 +20,9 @@ export function useHomePageInitialBlockList(blockListLimit: number = 3) { const { isFetchingNextPage, fetchNextPage, hasNextPage } = response; const btcBlocks = useSuspenseInfiniteQueryResult(response); - const latestBurnBlock = btcBlocks[0]; - const secondLatestBurnBlock = btcBlocks[1]; - const thirdLatestBurnBlock = btcBlocks[2]; + const latestBurnBlock = useMemo(() => btcBlocks[0], [btcBlocks]); + const secondLatestBurnBlock = useMemo(() => btcBlocks[1], [btcBlocks]); + const thirdLatestBurnBlock = useMemo(() => btcBlocks[2], [btcBlocks]); const btcBlocksMap = useMemo(() => { const map = {} as Record; @@ -32,7 +32,6 @@ export function useHomePageInitialBlockList(blockListLimit: number = 3) { return map; }, [btcBlocks]); - // TODO: const latestBurnBlockStxBlocks = useSuspenseInfiniteQueryResult( useSuspenseBlocksByBurnBlock(latestBurnBlock.burn_block_height) ); diff --git a/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts b/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts index 4d455b2ba..e1f63b4b5 100644 --- a/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts +++ b/src/app/_components/BlockList/data/useStxBlocksForBtcBlocks.ts @@ -11,9 +11,10 @@ export function useStxBlocksForBtcBlocks(btcBlocks: BurnBlock[]) { }); const stxBlocksResults = useQueries({ queries: stxBlockQueries }); - console.log({ stxBlocksResults }); + const stxBlocks = stxBlocksResults.flatMap( result => result.data?.results || (result.data as any)?.pages[0].results || [] ); + return stxBlocks; } From c1e9d4364d518d77acb92cc9ec8bbb3444e03954 Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 12 Apr 2024 15:50:20 -0500 Subject: [PATCH 24/70] feat(grouped-by-btc-block-list-view-3): fixed loading animations --- .../data/useBlocksPageBlockListGrouped.tsx | 18 +++--------------- .../data/useBlocksPageBlockListUngrouped.ts | 15 +-------------- .../BlockList/data/useHomePageBlockList.ts | 18 +++--------------- 3 files changed, 7 insertions(+), 44 deletions(-) diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx b/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx index b8d4102d0..74162ad95 100644 --- a/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListGrouped.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { useBlockListContext } from '../BlockListContext'; import { useBlockListWebSocket2 } from '../Sockets/useBlockListWebSocket2'; @@ -30,18 +30,6 @@ export function useBlocksPageBlockListGrouped(btcBlockLimit: number = 10) { clearLatestStxBlocks: clearLatestStxBlocksFromWebSocket, } = useBlockListWebSocket2(initialStxBlockHashes); - // This is used to trigger a fade out effect when the block list is updated. - // When the counter is updated, we wait for the fade out effect to finish and then show the fade in effect - const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); - const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); - useEffect(() => { - if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { - waitForFadeAnimation(() => { - setBlockListLoading(false); - }); - } - }, [blockListUpdateCounter, clearLatestStxBlocksFromWebSocket, setBlockListLoading]); - // manually update the block list with block list updates from the websocket const updateBlockListManually = useCallback((blockListUpdates: BlockListData[]) => { const newBlockList = mergeBlockLists(blockListUpdates, blockList.current); @@ -54,7 +42,7 @@ export function useBlocksPageBlockListGrouped(btcBlockLimit: number = 10) { const websocketBlockList = generateBlockList(latestStxBlocksFromWebSocket); updateBlockListManually(websocketBlockList); clearLatestStxBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); + setBlockListLoading(false); }); }, [ latestStxBlocksFromWebSocket, @@ -69,7 +57,7 @@ export function useBlocksPageBlockListGrouped(btcBlockLimit: number = 10) { waitForFadeAnimation(async () => { clearLatestStxBlocksFromWebSocket(); await refetchInitialBlockList(() => { - setBlockListUpdateCounter(prev => prev + 1); + setBlockListLoading(false); }); }); }, diff --git a/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts b/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts index a48fde87a..6bb0b5bef 100644 --- a/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts +++ b/src/app/_components/BlockList/data/useBlocksPageBlockListUngrouped.ts @@ -32,18 +32,6 @@ export function useBlocksPageBlockListUngrouped() { clearLatestStxBlocks: clearLatestStxBlocksFromWebSocket, } = useBlockListWebSocket2(initialStxBlocksHashes); - // This is used to trigger a fade out effect when the block list is updated. - // When the counter is updated, we wait for the fade out effect to finish and then show the fade in effect - const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); - const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); - useEffect(() => { - if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { - // waitForFadeAnimation(() => { - setBlockListLoading(false); - // }); - } - }, [blockListUpdateCounter, clearLatestStxBlocksFromWebSocket, setBlockListLoading]); - // manually update the block list with block list updates from the websocket const updateBlockListManually = useCallback( (blockListUpdates: BlockListData[]) => { @@ -60,7 +48,6 @@ export function useBlocksPageBlockListUngrouped() { updateBlockListManually(websocketBlockList); clearLatestStxBlocksFromWebSocket(); setBlockListLoading(false); - // setBlockListUpdateCounter(prev => prev + 1); }); }, [ latestStxBlocksFromWebSocket, @@ -75,7 +62,7 @@ export function useBlocksPageBlockListUngrouped() { waitForFadeAnimation(async () => { clearLatestStxBlocksFromWebSocket(); await refetchInitialBlockList(() => { - setBlockListUpdateCounter(prev => prev + 1); + setBlockListLoading(false); }); }); }, diff --git a/src/app/_components/BlockList/data/useHomePageBlockList.ts b/src/app/_components/BlockList/data/useHomePageBlockList.ts index 1f2c3c5f4..ed8033802 100644 --- a/src/app/_components/BlockList/data/useHomePageBlockList.ts +++ b/src/app/_components/BlockList/data/useHomePageBlockList.ts @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { useBlockListContext } from '../BlockListContext'; import { useBlockListWebSocket2 } from '../Sockets/useBlockListWebSocket2'; @@ -36,25 +36,13 @@ export function useHomePageBlockList(btcBlockLimit: number = 3) { clearLatestStxBlocks: clearLatestStxBlocksFromWebSocket, } = useBlockListWebSocket2(initialStxBlocksHashes); - // This is used to trigger a fade out effect when the block list is updated. - // When the counter is updated, we wait for the fade out effect to finish and then show the fade in effect - const [blockListUpdateCounter, setBlockListUpdateCounter] = useState(0); - const prevBlockListUpdateCounterRef = useRef(blockListUpdateCounter); - useEffect(() => { - if (prevBlockListUpdateCounterRef.current !== blockListUpdateCounter) { - waitForFadeAnimation(() => { - setBlockListLoading(false); - }); - } - }, [blockListUpdateCounter, clearLatestStxBlocksFromWebSocket, setBlockListLoading]); - const showLatestStxBlocksFromWebSocket = useCallback(() => { setBlockListLoading(true); waitForFadeAnimation(() => { const websocketBlockList = generateBlockList(latestStxBlocksFromWebSocket); updateBlockListManually(websocketBlockList); clearLatestStxBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); + setBlockListLoading(false); }); }, [ latestStxBlocksFromWebSocket, @@ -69,7 +57,7 @@ export function useHomePageBlockList(btcBlockLimit: number = 3) { waitForFadeAnimation(async () => { await refetchInitialBlockList(() => { clearLatestStxBlocksFromWebSocket(); - setBlockListUpdateCounter(prev => prev + 1); + setBlockListLoading(false); }); }); }, From 3a8828587a1badbfa2ad5daf1fcd6e099a3fd3aa Mon Sep 17 00:00:00 2001 From: Nicholas Barnett Date: Fri, 12 Apr 2024 16:40:09 -0500 Subject: [PATCH 25/70] feat(grouped-by-btc-block-list-view-3): added skeleton for blocks page --- .../BlockList/Grouped/BlockListGrouped.tsx | 48 +++--------- .../BlockList/Grouped/skeleton.tsx | 6 +- .../_components/BlockList/ScrollableDiv.tsx | 36 +++++++++ .../data/useBlocksPageBlockListUngrouped.ts | 2 +- src/app/btcblock/[hash]/Header.tsx | 20 +++++ src/app/btcblock/[hash]/PageClient.tsx | 28 ++----- src/app/btcblock/[hash]/page.tsx | 5 +- src/app/btcblock/[hash]/skeleton.tsx | 74 +++++++++++++++++++ src/common/components/KeyValueVertical.tsx | 6 +- 9 files changed, 155 insertions(+), 70 deletions(-) create mode 100644 src/app/_components/BlockList/ScrollableDiv.tsx create mode 100644 src/app/btcblock/[hash]/Header.tsx create mode 100644 src/app/btcblock/[hash]/skeleton.tsx diff --git a/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx index 01a8c6d69..c5309eca8 100644 --- a/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx +++ b/src/app/_components/BlockList/Grouped/BlockListGrouped.tsx @@ -1,4 +1,4 @@ -import { ReactNode, useEffect, useRef, useState } from 'react'; +import { ReactNode } from 'react'; import { PiArrowElbowLeftDown } from 'react-icons/pi'; import { BlockLink, ExplorerLink } from '../../../../common/components/ExplorerLinks'; @@ -17,6 +17,7 @@ import { ListHeader } from '../../ListHeader'; import { BlockCount } from '../BlockCount'; import { useBlockListContext } from '../BlockListContext'; import { LineAndNode } from '../LineAndNode'; +import { ScrollableBox } from '../ScrollableDiv'; import { FADE_DURATION } from '../consts'; import { BlockListBtcBlock, BlockListStxBlock } from '../types'; import { BlockListData } from '../utils'; @@ -50,40 +51,6 @@ const GroupHeader = () => { ); }; -// TODO: move to common components -// adds horizontal scrolling to its children if they overflow the container's width, and adds a class to the container when it has a horizontal scrollbar -function ScrollableDiv({ children }: { children: ReactNode }) { - const [hasHorizontalScroll, setHasHorizontalScroll] = useState(false); - const divRef = useRef(null); - - useEffect(() => { - const checkForScroll = () => { - if (divRef.current) { - const { scrollWidth, clientWidth } = divRef.current; - if (scrollWidth > clientWidth) { - setHasHorizontalScroll(true); - } else { - setHasHorizontalScroll(false); - } - } - }; - checkForScroll(); - window.addEventListener('resize', checkForScroll); - return () => window.removeEventListener('resize', checkForScroll); - }, []); - - return ( - - {children} - - ); -} - const mobileBorderCss = { '.has-horizontal-scroll &:before': { // Adds a border to the left of the first column @@ -222,7 +189,12 @@ export function BurnBlockGroupGrid({ minimized={minimized} /> {i < stxBlocks.length - 1 && ( - + )} ))} @@ -310,9 +282,9 @@ export function BurnBlockGroup({ return ( - + - + {numStxBlocksNotDisplayed > 0 ? : null}