Commit 28538214 authored by lynn's avatar lynn Committed by GitHub

feat: new lazy load that scrolls whole window and not inside fixed size container (#4684)

* init

* messy but working omfg

* dont set initial to 500 set to just 1 for testing purposes

* it looks pretty now and works well

* sorting filtering and suspense loading are working

* fix comments

* handle token rows lacking addresS

* start working with new data schema

* new gql schema

* initial commit

* improved performance, added filtering

* lint

* removed comments and accidental settings.json changes

* refactor: switch explore over to new queries (#4657)

* initial commit
* improved performance, added filtering
* addressed pr comments
* fixed typescript issue

* merges

* fix

* fix oopsies

* fix accidental changes

* its working

* drop leftover comment

* clean up loaded row props

* respond to comments

* respond to jordan comments

* init

* remove unnecessary pkgs

* undo yarn lock changes

* loading rows fix

* change loading rows to 3 as per fred instruction

* remove anys
Co-authored-by: default avatarJordan Frankfurt <jordanwfrankfurt@gmail.com>
Co-authored-by: default avatarcartcrom <cartergcromer@gmail.com>
Co-authored-by: default avatarcartcrom <39385577+cartcrom@users.noreply.github.com>
parent 4f48f337
...@@ -10,7 +10,8 @@ import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens' ...@@ -10,7 +10,8 @@ import { TokenSortMethod, TopToken } from 'graphql/data/TopTokens'
import { TimePeriod } from 'graphql/data/util' import { TimePeriod } from 'graphql/data/util'
import { useCurrency } from 'hooks/Tokens' import { useCurrency } from 'hooks/Tokens'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react' import { ForwardedRef, forwardRef } from 'react'
import { CSSProperties, HTMLProps, ReactHTMLElement, ReactNode } from 'react'
import { ArrowDown, ArrowUp, Heart } from 'react-feather' import { ArrowDown, ArrowUp, Heart } from 'react-feather'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import styled, { css, useTheme } from 'styled-components/macro' import styled, { css, useTheme } from 'styled-components/macro'
...@@ -37,6 +38,8 @@ import { ...@@ -37,6 +38,8 @@ import {
import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart' import { formatDelta, getDeltaArrow } from '../TokenDetails/PriceChart'
import { DISPLAYS } from './TimeSelector' import { DISPLAYS } from './TimeSelector'
export const MAX_TOKENS_TO_LOAD = 100
const Cell = styled.div` const Cell = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
...@@ -401,6 +404,7 @@ export function TokenRow({ ...@@ -401,6 +404,7 @@ export function TokenRow({
tokenInfo: ReactNode tokenInfo: ReactNode
volume: ReactNode volume: ReactNode
last?: boolean last?: boolean
style?: CSSProperties
}) { }) {
const favoriteTokensEnabled = useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled const favoriteTokensEnabled = useFavoriteTokensFlag() === FavoriteTokensVariant.Enabled
const rowCells = ( const rowCells = (
...@@ -463,14 +467,15 @@ export function LoadingRow() { ...@@ -463,14 +467,15 @@ export function LoadingRow() {
) )
} }
interface LoadedRowProps { interface LoadedRowProps extends HTMLProps<ReactHTMLElement<HTMLElement>> {
tokenListIndex: number tokenListIndex: number
tokenListLength: number tokenListLength: number
token: TopToken token: TopToken
} }
/* Loaded State: row component with token information */ /* Loaded State: row component with token information */
export default function LoadedRow({ tokenListIndex, tokenListLength, token }: LoadedRowProps) { export const LoadedRow = forwardRef((props: LoadedRowProps, ref: ForwardedRef<HTMLDivElement>) => {
const { tokenListIndex, tokenListLength, token } = props
const tokenAddress = token?.address const tokenAddress = token?.address
const currency = useCurrency(tokenAddress) const currency = useCurrency(tokenAddress)
const tokenName = token?.name const tokenName = token?.name
...@@ -498,80 +503,84 @@ export default function LoadedRow({ tokenListIndex, tokenListLength, token }: Lo ...@@ -498,80 +503,84 @@ export default function LoadedRow({ tokenListIndex, tokenListLength, token }: Lo
// TODO: currency logo sizing mobile (32px) vs. desktop (24px) // TODO: currency logo sizing mobile (32px) vs. desktop (24px)
return ( return (
<StyledLink <div ref={ref}>
to={`/tokens/${tokenAddress}`} <StyledLink
onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)} to={`/tokens/${tokenAddress}`}
> onClick={() => sendAnalyticsEvent(EventName.EXPLORE_TOKEN_ROW_CLICKED, exploreTokenSelectedEventProperties)}
<TokenRow >
header={false} <TokenRow
favorited={ header={false}
<ClickFavorited favorited={
onClick={(e) => { <ClickFavorited
e.preventDefault() onClick={(e) => {
toggleFavorite() e.preventDefault()
}} toggleFavorite()
> }}
<FavoriteIcon isFavorited={isFavorited} /> >
</ClickFavorited> <FavoriteIcon isFavorited={isFavorited} />
} </ClickFavorited>
listNumber={sortAscending ? 100 - tokenListIndex : tokenListIndex + 1} }
tokenInfo={ listNumber={sortAscending ? MAX_TOKENS_TO_LOAD - tokenListIndex : tokenListIndex + 1}
<ClickableName> tokenInfo={
<LogoContainer> <ClickableName>
<CurrencyLogo currency={currency} symbol={tokenSymbol} /> <LogoContainer>
<L2NetworkLogo networkUrl={L2Icon} /> <CurrencyLogo currency={currency} symbol={tokenSymbol} />
</LogoContainer> <L2NetworkLogo networkUrl={L2Icon} />
<TokenInfoCell> </LogoContainer>
<TokenName>{tokenName}</TokenName> <TokenInfoCell>
<TokenSymbol>{tokenSymbol}</TokenSymbol> <TokenName>{tokenName}</TokenName>
</TokenInfoCell> <TokenSymbol>{tokenSymbol}</TokenSymbol>
</ClickableName> </TokenInfoCell>
} </ClickableName>
price={ }
<ClickableContent> price={
<PriceInfoCell> <ClickableContent>
{token?.market?.price?.value ? formatDollarAmount(token.market.price.value) : '-'} <PriceInfoCell>
<PercentChangeInfoCell> {token?.market?.price?.value ? formatDollarAmount(token.market.price.value) : '-'}
{formattedDelta} <PercentChangeInfoCell>
{arrow} {formattedDelta}
</PercentChangeInfoCell> {arrow}
</PriceInfoCell> </PercentChangeInfoCell>
</ClickableContent> </PriceInfoCell>
} </ClickableContent>
percentChange={ }
<ClickableContent> percentChange={
{formattedDelta ?? '-'} <ClickableContent>
{arrow} {formattedDelta ?? '-'}
</ClickableContent> {arrow}
} </ClickableContent>
marketCap={ }
<ClickableContent> marketCap={
{token?.market?.totalValueLocked?.value ? formatDollarAmount(token.market.totalValueLocked.value) : '-'} <ClickableContent>
</ClickableContent> {token?.market?.totalValueLocked?.value ? formatDollarAmount(token.market.totalValueLocked.value) : '-'}
} </ClickableContent>
volume={ }
<ClickableContent> volume={
{token?.market?.volume?.value ? formatDollarAmount(token.market.volume.value) : '-'} <ClickableContent>
</ClickableContent> {token?.market?.volume?.value ? formatDollarAmount(token.market.volume.value) : '-'}
} </ClickableContent>
sparkLine={ }
<SparkLine> sparkLine={
<ParentSize> <SparkLine>
{({ width, height }) => ( <ParentSize>
<SparklineChart {({ width, height }) => (
width={width} <SparklineChart
height={height} width={width}
tokenData={token} height={height}
pricePercentChange={token?.market?.pricePercentChange?.value} tokenData={token}
timePeriod={timePeriod} pricePercentChange={token?.market?.pricePercentChange?.value}
/> timePeriod={timePeriod}
)} />
</ParentSize> )}
</SparkLine> </ParentSize>
} </SparkLine>
first={tokenListIndex === 0} }
last={tokenListIndex === tokenListLength - 1} first={tokenListIndex === 0}
/> last={tokenListIndex === tokenListLength - 1}
</StyledLink> />
</StyledLink>
</div>
) )
} })
LoadedRow.displayName = 'LoadedRow'
...@@ -2,12 +2,15 @@ import { Trans } from '@lingui/macro' ...@@ -2,12 +2,15 @@ import { Trans } from '@lingui/macro'
import { showFavoritesAtom } from 'components/Tokens/state' import { showFavoritesAtom } from 'components/Tokens/state'
import { usePrefetchTopTokens, useTopTokens } from 'graphql/data/TopTokens' import { usePrefetchTopTokens, useTopTokens } from 'graphql/data/TopTokens'
import { useAtomValue } from 'jotai/utils' import { useAtomValue } from 'jotai/utils'
import { ReactNode } from 'react' import { ReactNode, useCallback, useRef } from 'react'
import { AlertTriangle } from 'react-feather' import { AlertTriangle } from 'react-feather'
import styled from 'styled-components/macro' import styled from 'styled-components/macro'
import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants' import { MAX_WIDTH_MEDIA_BREAKPOINT } from '../constants'
import LoadedRow, { HeaderRow, LoadingRow } from './TokenRow' import { HeaderRow, LoadedRow, LoadingRow, MAX_TOKENS_TO_LOAD } from './TokenRow'
const LOADING_ROWS_COUNT = 3
const ROWS_PER_PAGE_FETCH = 20
const GridContainer = styled.div` const GridContainer = styled.div`
display: flex; display: flex;
...@@ -23,6 +26,12 @@ const GridContainer = styled.div` ...@@ -23,6 +26,12 @@ const GridContainer = styled.div`
align-items: center; align-items: center;
border: 1px solid ${({ theme }) => theme.backgroundOutline}; border: 1px solid ${({ theme }) => theme.backgroundOutline};
` `
const TokenDataContainer = styled.div`
height: 100%;
width: 100%;
`
const NoTokenDisplay = styled.div` const NoTokenDisplay = styled.div`
display: flex; display: flex;
justify-content: center; justify-content: center;
...@@ -35,9 +44,6 @@ const NoTokenDisplay = styled.div` ...@@ -35,9 +44,6 @@ const NoTokenDisplay = styled.div`
padding: 0px 28px; padding: 0px 28px;
gap: 8px; gap: 8px;
` `
const TokenRowsContainer = styled.div`
width: 100%;
`
function NoTokensState({ message }: { message: ReactNode }) { function NoTokensState({ message }: { message: ReactNode }) {
return ( return (
...@@ -48,15 +54,14 @@ function NoTokensState({ message }: { message: ReactNode }) { ...@@ -48,15 +54,14 @@ function NoTokensState({ message }: { message: ReactNode }) {
) )
} }
const LOADING_ROWS = Array.from({ length: 100 }) const LoadingMoreRows = Array(LOADING_ROWS_COUNT).fill(<LoadingRow />)
.fill(0) const InitialLoadingRows = Array(ROWS_PER_PAGE_FETCH).fill(<LoadingRow />)
.map((_item, index) => <LoadingRow key={index} />)
export function LoadingTokenTable() { export function LoadingTokenTable() {
return ( return (
<GridContainer> <GridContainer>
<HeaderRow /> <HeaderRow />
<TokenRowsContainer>{LOADING_ROWS}</TokenRowsContainer> <TokenDataContainer>{InitialLoadingRows}</TokenDataContainer>
</GridContainer> </GridContainer>
) )
} }
...@@ -67,9 +72,25 @@ export default function TokenTable() { ...@@ -67,9 +72,25 @@ export default function TokenTable() {
// TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s // TODO: consider moving prefetched call into app.tsx and passing it here, use a preloaded call & updated on interval every 60s
const prefetchedTokens = usePrefetchTopTokens() const prefetchedTokens = usePrefetchTopTokens()
const { loading, tokens, loadMoreTokens } = useTopTokens(prefetchedTokens) const { loading, tokens, loadMoreTokens } = useTopTokens(prefetchedTokens)
const hasMore = !tokens || tokens.length < MAX_TOKENS_TO_LOAD
const observer = useRef<IntersectionObserver>()
const lastTokenRef = useCallback(
(node: HTMLDivElement) => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
loadMoreTokens()
}
})
if (node) observer.current.observe(node)
},
[loading, hasMore, loadMoreTokens]
)
/* loading and error state */ /* loading and error state */
if (loading) { if (loading && (!tokens || tokens?.length === 0)) {
return <LoadingTokenTable /> return <LoadingTokenTable />
} else { } else {
if (!tokens) { if (!tokens) {
...@@ -94,13 +115,32 @@ export default function TokenTable() { ...@@ -94,13 +115,32 @@ export default function TokenTable() {
<> <>
<GridContainer> <GridContainer>
<HeaderRow /> <HeaderRow />
<TokenRowsContainer> <TokenDataContainer>
{tokens?.map((token, index) => ( {tokens.map((token, index) => {
<LoadedRow key={token?.name} tokenListIndex={index} tokenListLength={tokens.length} token={token} /> if (tokens.length === index + 1) {
))} return (
</TokenRowsContainer> <LoadedRow
key={token?.name}
tokenListIndex={index}
tokenListLength={tokens?.length ?? 0}
token={token}
ref={lastTokenRef}
/>
)
} else {
return (
<LoadedRow
key={token?.name}
tokenListIndex={index}
tokenListLength={tokens?.length ?? 0}
token={token}
/>
)
}
})}
{loading && LoadingMoreRows}
</TokenDataContainer>
</GridContainer> </GridContainer>
<button onClick={loadMoreTokens}>load more</button>
</> </>
) )
} }
......
...@@ -157,10 +157,12 @@ export function useTopTokens(prefetchedData: TopTokens100Query['response']) { ...@@ -157,10 +157,12 @@ export function useTopTokens(prefetchedData: TopTokens100Query['response']) {
) )
const loadMoreTokens = useCallback(() => { const loadMoreTokens = useCallback(() => {
setLoading(true)
const contracts = prefetchedSelectedTokens.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE).map(toContractInput) const contracts = prefetchedSelectedTokens.slice(page * PAGE_SIZE, (page + 1) * PAGE_SIZE).map(toContractInput)
loadTokens(contracts, (data) => { loadTokens(contracts, (data) => {
if (data?.tokens) { if (data?.tokens) {
setTokens([...(tokens ?? []), ...data.tokens]) setTokens([...(tokens ?? []), ...data.tokens])
setLoading(false)
setPage(page + 1) setPage(page + 1)
} }
}) })
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment