Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: portfolio performance improvements #1746

Merged
merged 18 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@talismn/token-rates": "workspace:*",
"@talismn/util": "workspace:*",
"@tanstack/react-query": "5.59.16",
"@tanstack/react-virtual": "^3.11.1",
"@types/blueimp-md5": "2.18.2",
"@types/chrome": "0.0.279",
"@types/color": "3.0.6",
Expand Down
18 changes: 17 additions & 1 deletion apps/extension/src/@talisman/components/ScrollContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { classNames } from "@talismn/util"
import { forwardRef, RefObject, useEffect, useMemo, useRef, useState } from "react"

import { provideContext } from "@talisman/util/provideContext"

type ScrollContainerProps = {
className?: string
children?: React.ReactNode
Expand Down Expand Up @@ -67,7 +69,7 @@ export const ScrollContainer = forwardRef<HTMLDivElement, ScrollContainerProps>(
innerClassName,
)}
>
{children}
<ScrollContainerProvider refContainer={refDiv}>{children}</ScrollContainerProvider>
</div>
<div
className={classNames(
Expand All @@ -86,3 +88,17 @@ export const ScrollContainer = forwardRef<HTMLDivElement, ScrollContainerProps>(
},
)
ScrollContainer.displayName = "ScrollContainer"

const useScrollContainerProvider = ({
refContainer,
}: {
refContainer: RefObject<HTMLDivElement>
}) => {
return refContainer
}

const [ScrollContainerProvider, useScrollContainer] = provideContext(useScrollContainerProvider)

// this hook will provite a way for its children to access the ref of the scrollable element
// mainly useful when using a virtualizer or other scroll related libraries
export { useScrollContainer }
11 changes: 6 additions & 5 deletions apps/extension/src/ui/apps/dashboard/routes/Portfolio/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,10 @@ const BuyTokensOpener = () => {
}

export const PortfolioRoutes = () => (
<DashboardLayout sidebar="accounts">
<BuyTokensOpener />
<PortfolioContainer>
<PortfolioContainer>
<DashboardLayout sidebar="accounts">
<BuyTokensOpener />

{/* share layout to prevent tabs flickering */}
<PortfolioLayout toolbar={<PortfolioToolbar />}>
<Routes>
Expand All @@ -44,8 +45,8 @@ export const PortfolioRoutes = () => (
<Route path="*" element={<NavigateWithQuery url="tokens" />} />
</Routes>
</PortfolioLayout>
</PortfolioContainer>
</DashboardLayout>
</DashboardLayout>
</PortfolioContainer>
)

const PortfolioToolbar = () => (
Expand Down
23 changes: 13 additions & 10 deletions apps/extension/src/ui/apps/dashboard/routes/TxHistory.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next"
import { useOpenClose } from "talisman-ui"

import { ChainLogo } from "@ui/domains/Asset/ChainLogo"
import { PortfolioContainer } from "@ui/domains/Portfolio/PortfolioContainer"
import { usePortfolioNavigation } from "@ui/domains/Portfolio/usePortfolioNavigation"
import { TxHistoryList, TxHistoryProvider } from "@ui/domains/Transactions/TxHistory"
import { useTxHistory } from "@ui/domains/Transactions/TxHistory/TxHistoryContext"
Expand Down Expand Up @@ -86,15 +87,17 @@ const TxHistoryAccountFilter = () => {

export const TxHistory = () => {
return (
<DashboardLayout sidebar="accounts">
<TxHistoryProvider>
<TxHistoryAccountFilter />
<div className="min-w-[60rem]">
<Header />
<div className="h-8"></div>
<TxHistoryList />
</div>
</TxHistoryProvider>
</DashboardLayout>
<PortfolioContainer>
<DashboardLayout sidebar="accounts">
<TxHistoryProvider>
<TxHistoryAccountFilter />
<div className="min-w-[60rem]">
<Header />
<div className="h-8"></div>
<TxHistoryList />
</div>
</TxHistoryProvider>
</DashboardLayout>
</PortfolioContainer>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export const PortfolioAssetsHeader: FC<{ backBtnTo?: string }> = ({ backBtnTo })
balancesByAddress.get(balance.address)?.push(balance)
})
return balancesByAddress
}, [allBalances.each])
}, [allBalances])

const balances = useMemo(
() =>
Expand Down
196 changes: 123 additions & 73 deletions apps/extension/src/ui/domains/Asset/TokenPicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@ import { Balances } from "@talismn/balances"
import { Token, TokenId } from "@talismn/chaindata-provider"
import { CheckCircleIcon } from "@talismn/icons"
import { classNames, planckToTokens } from "@talismn/util"
import { useVirtualizer } from "@tanstack/react-virtual"
import sortBy from "lodash/sortBy"
import { FC, useCallback, useDeferredValue, useMemo, useRef, useState } from "react"
import { useTranslation } from "react-i18next"
import { useIntersection } from "react-use"

import { Address } from "@extension/core"
import { ScrollContainer } from "@talisman/components/ScrollContainer"
import { ScrollContainer, useScrollContainer } from "@talisman/components/ScrollContainer"
import { SearchInput } from "@talisman/components/SearchInput"
import {
useAccountByAddress,
Expand Down Expand Up @@ -67,6 +67,74 @@ const TokenRowSkeleton = () => (
</div>
)

type TokenData = {
id: string
token: Token
balances: Balances
chainNameSearch: string | null | undefined
chainName: string
chainLogo: string | null | undefined
hasFiatRate: boolean
}

const TokenRows: FC<{
tokens: TokenData[]
selectedTokenId?: TokenId
allowUntransferable?: boolean
onTokenClick: (tokenId: TokenId) => void
}> = ({ tokens, selectedTokenId, allowUntransferable, onTokenClick }) => {
const refContainer = useScrollContainer()
const ref = useRef<HTMLDivElement>(null)

const virtualizer = useVirtualizer({
count: tokens.length,
estimateSize: () => 58,
overscan: 5,
getScrollElement: () => refContainer.current,
})

if (!tokens.length) return null

return (
<div ref={ref}>
<div
className="relative w-full"
style={{
height: `${virtualizer.getTotalSize()}px`,
}}
>
{virtualizer.getVirtualItems().map((item) => {
const tokenData = tokens[item.index]
if (!tokenData) return null

return (
<div
key={item.key}
className="absolute left-0 top-0 w-full"
style={{
height: `${item.size}px`,
transform: `translateY(${item.start}px)`,
}}
>
<TokenRow
key={item.key}
selected={tokenData.token.id === selectedTokenId}
token={tokenData.token}
balances={tokenData.balances}
chainName={tokenData.chainName}
chainLogo={tokenData.chainLogo}
hasFiatRate={tokenData.hasFiatRate}
allowUntransferable={allowUntransferable}
onClick={() => onTokenClick(tokenData.token.id)}
/>
</div>
)
})}
</div>
</div>
)
}

const TokenRow: FC<TokenRowProps> = ({
token,
selected,
Expand All @@ -84,23 +152,15 @@ const TokenRow: FC<TokenRowProps> = ({
tokensTotal: planckToTokens(planck.toString(), token.decimals),
isLoading: balances.each.find((b) => b.status === "cache"),
}
}, [balances.each, token.decimals])
}, [balances, token.decimals])

const isTransferable = useMemo(() => isTransferableToken(token), [token])

// there are more than 250 tokens so we should render only visible tokens to prevent performance issues
const refButton = useRef<HTMLButtonElement>(null)
const intersection = useIntersection(refButton, {
root: null,
rootMargin: "1000px",
})

const currency = useSelectedCurrency()
const isUniswapV2LpToken = token?.type === "evm-uniswapv2"

return (
<button
ref={refButton}
disabled={!allowUntransferable && !isTransferable}
title={
allowUntransferable || isTransferable
Expand All @@ -117,53 +177,49 @@ const TokenRow: FC<TokenRowProps> = ({
selected && "bg-grey-800 text-body-secondary",
)}
>
{intersection?.isIntersecting && (
<>
<div className="w-16 shrink-0">
<TokenLogo tokenId={token.id} className="!text-xl" />
<div className="w-16 shrink-0">
<TokenLogo tokenId={token.id} className="!text-xl" />
</div>
<div className="grow space-y-[5px]">
<div
className={classNames(
"flex w-full justify-between text-sm font-bold",
selected ? "text-body-secondary" : "text-body",
)}
>
<div className="flex items-center">
<span>{token.symbol}</span>
<TokenTypePill type={token.type} className="rounded-xs ml-3 px-1 py-0.5" />
{selected && <CheckCircleIcon className="ml-3 inline align-text-top" />}
</div>
<div className="grow space-y-[5px]">
<div
className={classNames(
"flex w-full justify-between text-sm font-bold",
selected ? "text-body-secondary" : "text-body",
)}
>
<div className="flex items-center">
<span>{token.symbol}</span>
<TokenTypePill type={token.type} className="rounded-xs ml-3 px-1 py-0.5" />
{selected && <CheckCircleIcon className="ml-3 inline align-text-top" />}
</div>
<div className={classNames(isLoading && "animate-pulse")}>
<Tokens
amount={tokensTotal}
decimals={token.decimals}
symbol={isUniswapV2LpToken ? "" : token.symbol}
isBalance
noCountUp
/>
</div>
</div>
<div className="text-body-secondary flex w-full items-center justify-between gap-2 text-right text-xs font-light">
<div className="flex flex-col justify-center">
<ChainLogoBase
logo={chainLogo}
name={chainName ?? ""}
className="inline-block text-sm"
/>
</div>
<div>{chainName}</div>
<div className={classNames("grow", isLoading && "animate-pulse")}>
{hasFiatRate ? (
<Fiat amount={balances.sum.fiat(currency).transferable} isBalance noCountUp />
) : (
"-"
)}
</div>
</div>
<div className={classNames(isLoading && "animate-pulse")}>
<Tokens
amount={tokensTotal}
decimals={token.decimals}
symbol={isUniswapV2LpToken ? "" : token.symbol}
isBalance
noCountUp
/>
</div>
</>
)}
</div>
<div className="text-body-secondary flex w-full items-center justify-between gap-2 text-right text-xs font-light">
<div className="flex flex-col justify-center">
<ChainLogoBase
logo={chainLogo}
name={chainName ?? ""}
className="inline-block text-sm"
/>
</div>
<div>{chainName}</div>
<div className={classNames("grow", isLoading && "animate-pulse")}>
{hasFiatRate ? (
<Fiat amount={balances.sum.fiat(currency).transferable} isBalance noCountUp />
) : (
"-"
)}
</div>
</div>
</div>
</button>
)
}
Expand Down Expand Up @@ -258,7 +314,7 @@ const TokensList: FC<TokensListProps> = ({
tokenRatesMap,
])

const tokensWithBalances = useMemo(() => {
const tokensWithBalances = useMemo<TokenData[]>(() => {
// wait until balances are loaded
if (!accountBalances.count) return []

Expand Down Expand Up @@ -336,9 +392,9 @@ const TokensList: FC<TokensListProps> = ({
})
}, [search, tokensWithBalances])

const handleAccountClick = useCallback(
(address: string) => () => {
onSelect?.(address)
const handleTokenClick = useCallback(
(tokenId: string) => {
onSelect?.(tokenId)
},
[onSelect],
)
Expand All @@ -347,19 +403,13 @@ const TokensList: FC<TokensListProps> = ({
<div className="min-h-full">
{accountBalances.count ? (
<>
{tokens?.map(({ token, balances, chainName, chainLogo, hasFiatRate }) => (
<TokenRow
key={token.id}
selected={token.id === selected}
token={token}
balances={balances}
chainName={chainName}
chainLogo={chainLogo}
hasFiatRate={hasFiatRate}
allowUntransferable={allowUntransferable}
onClick={handleAccountClick(token.id)}
/>
))}
<TokenRows
tokens={tokens}
selectedTokenId={selected}
onTokenClick={handleTokenClick}
allowUntransferable={allowUntransferable}
/>

{!tokens?.length && (
<div className="text-body-secondary flex h-[5.8rem] w-full items-center px-12 text-left">
{t("No token matches your search")}
Expand Down
Loading
Loading