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

Improve NFT collection tab performance #1296

Merged
merged 1 commit into from
Jun 29, 2023
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
73 changes: 72 additions & 1 deletion packages/stateful/components/NftCard.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,18 @@
import { ComponentProps } from 'react'
import { useTranslation } from 'react-i18next'

import { NftCardProps, NftCard as StatelessNftCard } from '@dao-dao/stateless'
import {
NftCardProps,
NftCard as StatelessNftCard,
useCachedLoadingWithError,
} from '@dao-dao/stateless'
import { WithChainId } from '@dao-dao/types'
import { CHAIN_ID } from '@dao-dao/utils'

import {
nftCardInfoSelector,
nftStakerOrOwnerSelector,
} from '../recoil/selectors/nft'
import { EntityDisplay } from './EntityDisplay'

export const NftCard = (props: Omit<NftCardProps, 'EntityDisplay'>) => (
Expand All @@ -17,3 +27,64 @@ export const StakedNftCard = (props: ComponentProps<typeof NftCard>) => {
const { t } = useTranslation()
return <NftCard hideCollection ownerLabel={t('title.staker')} {...props} />
}

export type LazyNftCardProps = WithChainId<{
collectionAddress: string
tokenId: string
// If passed and the NFT is staked, get staker info from this contract.
stakingContractAddress?: string
}>

export const LazyNftCard = ({
collectionAddress,
tokenId,
stakingContractAddress,
chainId,
}: LazyNftCardProps) => {
const info = useCachedLoadingWithError(
nftCardInfoSelector({
collection: collectionAddress,
tokenId,
chainId,
})
)

const stakerOrOwner = useCachedLoadingWithError(
nftStakerOrOwnerSelector({
collectionAddress,
tokenId,
stakingContractAddress,
chainId,
})
)

const staked =
!stakerOrOwner.loading &&
!stakerOrOwner.errored &&
stakerOrOwner.data.staked

const NftCardToUse = staked ? StakedNftCard : NftCardNoCollection

return info.loading || info.errored ? (
<NftCardToUse
chainId={chainId || CHAIN_ID}
className="animate-pulse"
collection={{
address: collectionAddress,
name: 'Loading...',
}}
description={undefined}
name="Loading..."
tokenId={tokenId}
/>
) : (
<NftCardToUse
owner={
stakerOrOwner.loading || stakerOrOwner.errored
? undefined
: stakerOrOwner.data.address
}
{...info.data}
/>
)
}
43 changes: 43 additions & 0 deletions packages/stateful/recoil/selectors/nft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
refreshWalletBalancesIdAtom,
refreshWalletStargazeNftsAtom,
} from '@dao-dao/state'
import { stakerForNftSelector } from '@dao-dao/state/recoil/selectors/contracts/DaoVotingCw721Staked'
import { NftCardInfo, WithChainId } from '@dao-dao/types'
import { StargazeNft } from '@dao-dao/types/nft'
import {
Expand Down Expand Up @@ -309,3 +310,45 @@ export const walletStakedNftCardInfosSelector = selectorFamily<
}))
},
})

// Get owner of NFT, or staker if NFT is staked with the given staking contract.
export const nftStakerOrOwnerSelector = selectorFamily<
{
staked: boolean
address: string
},
WithChainId<{
collectionAddress: string
tokenId: string
stakingContractAddress?: string
}>
>({
key: 'nftStakerOrOwner',
get:
({ collectionAddress, tokenId, stakingContractAddress, chainId }) =>
async ({ get }) => {
const { owner } = get(
Cw721BaseSelectors.ownerOfSelector({
contractAddress: collectionAddress,
params: [{ tokenId }],
chainId,
})
)

const staker =
stakingContractAddress && owner === stakingContractAddress
? get(
stakerForNftSelector({
contractAddress: stakingContractAddress,
tokenId,
chainId,
})
)
: undefined

return {
staked: staker !== undefined,
address: staker || owner,
}
},
})
Original file line number Diff line number Diff line change
@@ -1,145 +1,38 @@
import { useState } from 'react'
import { useTranslation } from 'react-i18next'
import { constSelector, waitForAll } from 'recoil'

import { Cw721BaseSelectors } from '@dao-dao/state/recoil'
import { stakerForNftSelector } from '@dao-dao/state/recoil/selectors/contracts/DaoVotingCw721Staked'
import {
NftsTab,
useCachedLoadable,
useCachedLoading,
} from '@dao-dao/stateless'
import { NftsTab, useCachedLoading } from '@dao-dao/stateless'

import { NftCardNoCollection, StakedNftCard } from '../../../../components'
import { nftCardInfoSelector } from '../../../../recoil/selectors/nft'
import { LazyNftCard } from '../../../../components'
import { useGovernanceCollectionInfo } from '../hooks'

enum Filter {
All = 'all',
Staked = 'staked',
Unstaked = 'unstaked',
}

export const NftCollectionTab = () => {
const { t } = useTranslation()
const { collectionAddress, stakingContractAddress } =
useGovernanceCollectionInfo()

const allTokens = useCachedLoadable(
const allTokens = useCachedLoading(
Cw721BaseSelectors.allTokensSelector({
contractAddress: collectionAddress,
})
)

const nftCardInfosLoading = useCachedLoading(
allTokens.state === 'hasValue'
? waitForAll(
allTokens.contents.map((tokenId) =>
nftCardInfoSelector({
collection: collectionAddress,
tokenId,
})
)
)
: undefined,
}),
[]
)

const tokenOwners = useCachedLoading(
allTokens.state === 'hasValue'
? waitForAll(
allTokens.contents.map((tokenId) =>
Cw721BaseSelectors.ownerOfSelector({
contractAddress: collectionAddress,
params: [
{
tokenId,
},
],
})
)
)
: undefined,
[]
)

// Show the owner by checking if owner is staking contract and using the
// staker instead if so. If not staked with staking contract, use owner.
const stakerOrOwnerForTokens = useCachedLoading(
allTokens.state === 'hasValue' && !tokenOwners.loading
? waitForAll(
allTokens.contents.map((tokenId, index) =>
tokenOwners.data[index].owner === stakingContractAddress
? stakerForNftSelector({
contractAddress: stakingContractAddress,
tokenId,
})
: constSelector(tokenOwners.data[index].owner)
)
)
: undefined,
[]
)

const [filter, setFilter] = useState(Filter.All)

return (
<NftsTab
NftCard={NftCardNoCollection}
description={t('info.nftCollectionExplanation', { context: filter })}
filterDropdownProps={{
onSelect: (value) => setFilter(value),
options: [
{
label: t('title.allNfts'),
value: Filter.All,
},
{
label: t('title.stakedNfts'),
value: Filter.Staked,
},
{
label: t('title.unstakedNfts'),
value: Filter.Unstaked,
},
],
selected: filter,
}}
NftCard={LazyNftCard}
description={t('info.nftCollectionExplanation', { context: 'all' })}
nfts={
nftCardInfosLoading.loading ||
tokenOwners.loading ||
stakerOrOwnerForTokens.loading
allTokens.loading
? { loading: true }
: {
loading: false,
data: nftCardInfosLoading.data
.map((nft, index) => ({
...nft,
owner: stakerOrOwnerForTokens.data[index],
// If staked, show staked card instead of default.
OverrideNftCard:
tokenOwners.data[index].owner === stakingContractAddress
? StakedNftCard
: undefined,
}))
// Filter by selected filter.
.filter((nft) =>
filter === Filter.All
? true
: filter === Filter.Staked
? nft.OverrideNftCard
: !nft.OverrideNftCard
)
// Sort staked NFTs first.
.sort((a, b) =>
a.OverrideNftCard && b.OverrideNftCard
? a.name.localeCompare(b.name)
: a.OverrideNftCard
? -1
: b.OverrideNftCard
? 1
: 0
),
data: allTokens.data.map((tokenId) => ({
collectionAddress,
tokenId,
stakingContractAddress,
key: collectionAddress + tokenId,
})),
}
}
/>
Expand Down
2 changes: 1 addition & 1 deletion packages/stateless/components/dao/tabs/NftsTab.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Default.args = {
makeNftCardProps(),
makeNftCardProps(),
makeNftCardProps(),
],
].map((props) => ({ ...props, key: props.tokenId })),
},
NftCard,
description: 'This is the NFTs tab.',
Expand Down
47 changes: 27 additions & 20 deletions packages/stateless/components/dao/tabs/NftsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,35 @@
import { Image } from '@mui/icons-material'
import { ComponentType } from 'react'
import { ComponentType, useState } from 'react'
import { useTranslation } from 'react-i18next'

import { LoadingData, NftCardInfo } from '@dao-dao/types'
import { LoadingData } from '@dao-dao/types'

import { GridCardContainer } from '../../GridCardContainer'
import { Dropdown, DropdownProps } from '../../inputs/Dropdown'
import { Loader } from '../../logo/Loader'
import { NoContent } from '../../NoContent'
import { PAGINATION_MIN_PAGE, Pagination } from '../../Pagination'

export interface NftsTabProps<N extends NftCardInfo> {
nfts: LoadingData<(N & { OverrideNftCard?: ComponentType<N> })[]>
export interface NftsTabProps<N = any> {
nfts: LoadingData<(N & { key: string })[]>
NftCard: ComponentType<N>
description?: string
// If present, will show a dropdown to filter the NFTs.
filterDropdownProps?: DropdownProps<any>
}

export const NftsTab = <N extends NftCardInfo>({
const NFTS_PER_PAGE = 30

export const NftsTab = <N extends object>({
nfts,
NftCard,
description,
filterDropdownProps,
}: NftsTabProps<N>) => {
const { t } = useTranslation()

const [page, setPage] = useState(PAGINATION_MIN_PAGE)

return nfts.loading || nfts.data.length > 0 ? (
<>
<div className="flex min-h-[3.5rem] flex-col gap-y-4 gap-x-16 pb-6 sm:flex-row sm:items-center sm:justify-between">
Expand Down Expand Up @@ -52,21 +57,23 @@ export const NftsTab = <N extends NftCardInfo>({
{nfts.loading ? (
<Loader fill={false} />
) : (
<GridCardContainer className="pb-6">
{nfts.data.map((props) =>
props.OverrideNftCard ? (
<props.OverrideNftCard
{...(props as N)}
key={props.collection.address + props.tokenId}
/>
) : (
<NftCard
{...(props as N)}
key={props.collection.address + props.tokenId}
/>
)
)}
</GridCardContainer>
<>
<GridCardContainer className="pb-6">
{nfts.data
.slice((page - 1) * NFTS_PER_PAGE, page * NFTS_PER_PAGE)
.map((props) => (
<NftCard {...(props as N)} key={props.key} />
))}
</GridCardContainer>

<Pagination
className="mx-auto mt-12"
page={page}
pageSize={NFTS_PER_PAGE}
setPage={setPage}
total={nfts.data.length}
/>
</>
)}
</>
) : (
Expand Down