import { Typography } from '@mui/material' import { useVirtualizer } from '@tanstack/react-virtual' import type { FC } from 'react' import { useCallback, useEffect, useMemo, useRef } from 'react' import { useTranslation } from 'react-i18next' import { useAvailableChains } from '../../hooks/useAvailableChains.js' import type { TokenAmount } from '../../types/token.js' import { TokenDetailsSheet } from './TokenDetailsSheet.js' import { List } from './TokenList.style.js' import { TokenListItem, TokenListItemSkeleton } from './TokenListItem.js' import type { TokenDetailsSheetBase, VirtualizedTokenListProps, } from './types.js' const tokenItemHeight = 64 // 60 + 4px margin-bottom export const VirtualizedTokenList: FC = ({ tokens, scrollElementRef, chainId, selectedTokenAddress, isLoading, isBalanceLoading, showCategories, showPinnedTokens, onClick, isAllNetworks, }) => { const { t } = useTranslation() const { chains } = useAvailableChains() // Create Set for O(1) chain lookup instead of O(n) find const chainsSet = useMemo(() => { if (!chains) { return undefined } return new Map(chains.map((chain) => [chain.id, chain])) }, [chains]) const tokenDetailsSheetRef = useRef(null) const onShowTokenDetails = useCallback( (tokenAddress: string, noContractAddress: boolean, chainId: number) => { tokenDetailsSheetRef.current?.open( tokenAddress, noContractAddress, chainId ) }, [] ) const getItemKey = useCallback( (index: number) => { const token = tokens[index] return `${token.chainId}-${token.address}-${index}` }, [tokens] ) const estimateSize = useCallback( (index: number) => { const currentToken = tokens[index] const previousToken = tokens[index - 1] let size = tokenItemHeight // Pinned tokens (always shown, even in all networks mode) if (currentToken.pinned && index === 0) { size += 24 } if (previousToken?.pinned && !currentToken.pinned) { size += 32 } if (!showCategories) { return size } if (currentToken.featured && !currentToken.pinned && index === 0) { size += 24 } // Category transition (excluding pinned tokens) const isNotPinned = !currentToken.pinned && !previousToken?.pinned if ( isNotPinned && ((previousToken?.amount && !currentToken.amount) || (previousToken?.featured && !currentToken.featured) || (previousToken?.popular && !currentToken.popular)) ) { size += 32 } return size }, [tokens, showCategories] ) const virtualizerConfig = useMemo( () => ({ count: tokens.length, overscan: 5, getScrollElement: () => scrollElementRef.current, estimateSize, getItemKey, paddingEnd: 8, }), [tokens.length, estimateSize, getItemKey, scrollElementRef] ) const { getVirtualItems, getTotalSize, scrollToIndex, measure } = useVirtualizer(virtualizerConfig) // Address the issue of disappearing tokens on rerender useEffect(() => { if (scrollElementRef.current) { measure() } }, [measure, scrollElementRef.current]) // biome-ignore lint/correctness/useExhaustiveDependencies: run only when chainId changes useEffect(() => { // Scroll to the top of the list when switching the chains if (getVirtualItems().length) { scrollToIndex(0, { align: 'start' }) } // Close the token details sheet when switching the chains tokenDetailsSheetRef.current?.close() }, [scrollToIndex, isAllNetworks, chainId, getVirtualItems]) return ( <> {getVirtualItems().map((item) => { const currentToken = tokens[item.index] const previousToken: TokenAmount | undefined = tokens[item.index - 1] const chain = chainsSet?.get(currentToken.chainId) const isNotPinned = !currentToken.pinned const isFirstPinnedToken = currentToken.pinned && item.index === 0 const isTransitionFromPinned = previousToken?.pinned && isNotPinned // Category transitions (excluding pinned) const isTransitionFromFeatured = previousToken?.featured && !currentToken.featured && isNotPinned const isTransitionFromMyTokens = previousToken?.amount && !currentToken.amount && isNotPinned const isTransitionFromPopular = previousToken?.popular && !currentToken.popular && isNotPinned // Determine which category label to show const startAdornmentLabel = (() => { if (showPinnedTokens && isFirstPinnedToken) { return t('main.pinnedTokens') } if (showPinnedTokens && !showCategories && isTransitionFromPinned) { return t('main.allTokens') } if (!showCategories) { return null } if ( (isTransitionFromPinned && currentToken.featured) || (currentToken.featured && isNotPinned && item.index === 0) ) { return t('main.featuredTokens') } if ( (isTransitionFromFeatured || isTransitionFromPinned) && currentToken.amount && isNotPinned ) { return t('main.myTokens') } if ( (isTransitionFromFeatured || isTransitionFromMyTokens || isTransitionFromPinned) && currentToken.popular && isNotPinned ) { return t('main.popularTokens') } if ( isTransitionFromMyTokens || isTransitionFromFeatured || isTransitionFromPinned || isTransitionFromPopular ) { return t('main.allTokens') } return null })() const isSelected = selectedTokenAddress === currentToken.address && chainId === currentToken.chainId return ( {startAdornmentLabel} ) : null } /> ) })} {isLoading && ( {Array.from({ length: 3 }).map((_, index) => ( ))} )} ) }