From 63ddbdc70e7202df3d20ecec487cff151937f8bd Mon Sep 17 00:00:00 2001 From: noah Date: Thu, 29 Jun 2023 12:49:24 -0700 Subject: [PATCH] Paginate NFT collection tab and lazy load NFT info. (#1296) --- packages/stateful/components/NftCard.tsx | 73 +++++++++- packages/stateful/recoil/selectors/nft.ts | 43 ++++++ .../components/NftCollectionTab.tsx | 133 ++---------------- .../components/dao/tabs/NftsTab.stories.tsx | 2 +- .../stateless/components/dao/tabs/NftsTab.tsx | 47 ++++--- 5 files changed, 156 insertions(+), 142 deletions(-) diff --git a/packages/stateful/components/NftCard.tsx b/packages/stateful/components/NftCard.tsx index 611808eb04..21b14b385d 100644 --- a/packages/stateful/components/NftCard.tsx +++ b/packages/stateful/components/NftCard.tsx @@ -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) => ( @@ -17,3 +27,64 @@ export const StakedNftCard = (props: ComponentProps) => { const { t } = useTranslation() return } + +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 ? ( + + ) : ( + + ) +} diff --git a/packages/stateful/recoil/selectors/nft.ts b/packages/stateful/recoil/selectors/nft.ts index 8946757537..eea8f8c689 100644 --- a/packages/stateful/recoil/selectors/nft.ts +++ b/packages/stateful/recoil/selectors/nft.ts @@ -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 { @@ -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, + } + }, +}) diff --git a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/NftCollectionTab.tsx b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/NftCollectionTab.tsx index 9561e1eb06..ad1a604fb1 100644 --- a/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/NftCollectionTab.tsx +++ b/packages/stateful/voting-module-adapter/adapters/DaoVotingCw721Staked/components/NftCollectionTab.tsx @@ -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 ( 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, + })), } } /> diff --git a/packages/stateless/components/dao/tabs/NftsTab.stories.tsx b/packages/stateless/components/dao/tabs/NftsTab.stories.tsx index 9d5d42c9ed..92f5b63f09 100644 --- a/packages/stateless/components/dao/tabs/NftsTab.stories.tsx +++ b/packages/stateless/components/dao/tabs/NftsTab.stories.tsx @@ -26,7 +26,7 @@ Default.args = { makeNftCardProps(), makeNftCardProps(), makeNftCardProps(), - ], + ].map((props) => ({ ...props, key: props.tokenId })), }, NftCard, description: 'This is the NFTs tab.', diff --git a/packages/stateless/components/dao/tabs/NftsTab.tsx b/packages/stateless/components/dao/tabs/NftsTab.tsx index 42af82200e..1488fe9b18 100644 --- a/packages/stateless/components/dao/tabs/NftsTab.tsx +++ b/packages/stateless/components/dao/tabs/NftsTab.tsx @@ -1,23 +1,26 @@ 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 { - nfts: LoadingData<(N & { OverrideNftCard?: ComponentType })[]> +export interface NftsTabProps { + nfts: LoadingData<(N & { key: string })[]> NftCard: ComponentType description?: string // If present, will show a dropdown to filter the NFTs. filterDropdownProps?: DropdownProps } -export const NftsTab = ({ +const NFTS_PER_PAGE = 30 + +export const NftsTab = ({ nfts, NftCard, description, @@ -25,6 +28,8 @@ export const NftsTab = ({ }: NftsTabProps) => { const { t } = useTranslation() + const [page, setPage] = useState(PAGINATION_MIN_PAGE) + return nfts.loading || nfts.data.length > 0 ? ( <>
@@ -52,21 +57,23 @@ export const NftsTab = ({ {nfts.loading ? ( ) : ( - - {nfts.data.map((props) => - props.OverrideNftCard ? ( - - ) : ( - - ) - )} - + <> + + {nfts.data + .slice((page - 1) * NFTS_PER_PAGE, page * NFTS_PER_PAGE) + .map((props) => ( + + ))} + + + + )} ) : (