diff --git a/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx b/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx new file mode 100644 index 00000000000..a23be055a48 --- /dev/null +++ b/src/components/asset-list/RecyclerAssetList2/NFTEmptyState.tsx @@ -0,0 +1,104 @@ +import React, { useCallback } from 'react'; +import Animated from 'react-native-reanimated'; +import { Box, Stack, Text, useColorMode } from '@/design-system'; +import * as i18n from '@/languages'; +import { TokenFamilyHeaderHeight } from './NFTLoadingSkeleton'; +import { MINTS, useExperimentalFlag } from '@/config'; +import { useRemoteConfig } from '@/model/remoteConfig'; +import { IS_TEST } from '@/env'; +import { useMints } from '@/resources/mints'; +import { useAccountSettings } from '@/hooks'; +import { GestureHandlerButton } from '@/__swaps__/screens/Swap/components/GestureHandlerButton'; +import { StyleSheet } from 'react-native'; +import { LIGHT_SEPARATOR_COLOR, SEPARATOR_COLOR } from '@/__swaps__/screens/Swap/constants'; +import { analyticsV2 } from '@/analytics'; +import { convertRawAmountToRoundedDecimal } from '@/helpers/utilities'; +import { ethereumUtils } from '@/utils'; +import { navigateToMintCollection } from '@/resources/reservoir/mints'; + +type LaunchFeaturedMintButtonProps = { + featuredMint: ReturnType['data']['featuredMint']; +}; + +const LaunchFeaturedMintButton = ({ featuredMint }: LaunchFeaturedMintButtonProps) => { + const { isDarkMode } = useColorMode(); + + const handlePress = useCallback(() => { + if (featuredMint) { + analyticsV2.track(analyticsV2.event.mintsPressedFeaturedMintCard, { + contractAddress: featuredMint.contractAddress, + chainId: featuredMint.chainId, + totalMints: featuredMint.totalMints, + mintsLastHour: featuredMint.totalMints, + priceInEth: convertRawAmountToRoundedDecimal(featuredMint.mintStatus.price, 18, 6), + }); + const network = ethereumUtils.getNetworkFromChainId(featuredMint.chainId); + navigateToMintCollection(featuredMint.contract, featuredMint.mintStatus.price, network); + } + }, [featuredMint]); + + return ( + + + + + + {i18n.t(i18n.l.nfts.collect_now)} + + + + + + ); +}; + +export function NFTEmptyState() { + const { mints_enabled } = useRemoteConfig(); + const { accountAddress } = useAccountSettings(); + + const { + data: { featuredMint }, + } = useMints({ walletAddress: accountAddress }); + + const mintsEnabled = (useExperimentalFlag(MINTS) || mints_enabled) && !IS_TEST; + + return ( + + + + + 🌟 + + + + {i18n.t(i18n.l.nfts.empty)} + + + + {i18n.t(i18n.l.nfts.will_appear_here)} + + + {mintsEnabled && featuredMint && } + + + + ); +} + +const styles = StyleSheet.create({ + buttonPadding: { + paddingHorizontal: 24, + paddingVertical: 12, + }, +}); diff --git a/src/components/asset-list/RecyclerAssetList2/NFTLoadingSkeleton.tsx b/src/components/asset-list/RecyclerAssetList2/NFTLoadingSkeleton.tsx new file mode 100644 index 00000000000..3e9788a0c0b --- /dev/null +++ b/src/components/asset-list/RecyclerAssetList2/NFTLoadingSkeleton.tsx @@ -0,0 +1,87 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { useForegroundColor } from '@/design-system'; +import { useTheme } from '@/theme'; +import { opacity } from '@/__swaps__/utils/swaps'; +import { deviceUtils } from '@/utils'; + +export const TokenFamilyHeaderHeight = 50; + +const getRandomBetween = (min: number, max: number) => { + return Math.random() * (max - min) + min; +}; + +const NFTItem = () => { + const { colors } = useTheme(); + const labelTertiary = useForegroundColor('labelTertiary'); + + return ( + + + + + + + ); +}; + +const NFTLoadingSkeleton = ({ items = 5 }) => { + return ( + + {[...Array(items)].map((_, index) => ( + + ))} + + ); +}; + +const sx = StyleSheet.create({ + container: { + flex: 1, + width: '100%', + gap: 5, + }, + image: { + height: 30, + width: 30, + borderRadius: 15, + marginRight: 12, + }, + textContainer: { + justifyContent: 'center', + }, + title: { + width: deviceUtils.dimensions.width / 2, + height: 14, + borderRadius: 7, + paddingRight: 9, + }, + content: { + flexDirection: 'row', + alignItems: 'center', + height: TokenFamilyHeaderHeight, + padding: 19, + width: '100%', + }, +}); + +export default NFTLoadingSkeleton; diff --git a/src/components/asset-list/RecyclerAssetList2/WrappedCollectiblesHeader.tsx b/src/components/asset-list/RecyclerAssetList2/WrappedCollectiblesHeader.tsx index 3ea62e41da6..bb233041e82 100644 --- a/src/components/asset-list/RecyclerAssetList2/WrappedCollectiblesHeader.tsx +++ b/src/components/asset-list/RecyclerAssetList2/WrappedCollectiblesHeader.tsx @@ -2,32 +2,31 @@ import React from 'react'; import { Box, Inline, Text } from '@/design-system'; import * as i18n from '@/languages'; import { ListHeaderMenu } from '@/components/list/ListHeaderMenu'; -import useNftSort, { CollectibleSortByOptions } from '@/hooks/useNFTsSortBy'; +import { NftCollectionSortCriterion } from '@/graphql/__generated__/arc'; +import useNftSort from '@/hooks/useNFTsSortBy'; const TokenFamilyHeaderHeight = 48; -const getIconForSortType = (selected: string) => { +const getIconForSortType = (selected: NftCollectionSortCriterion) => { switch (selected) { - case CollectibleSortByOptions.ABC: + case NftCollectionSortCriterion.Abc: return '􀋲'; - case CollectibleSortByOptions.FLOOR_PRICE: + case NftCollectionSortCriterion.FloorPrice: return '􀅺'; - case CollectibleSortByOptions.MOST_RECENT: + case NftCollectionSortCriterion.MostRecent: return '􀐫'; } - return ''; }; -const getMenuItemIcon = (value: CollectibleSortByOptions) => { +const getMenuItemIcon = (value: NftCollectionSortCriterion) => { switch (value) { - case CollectibleSortByOptions.ABC: + case NftCollectionSortCriterion.Abc: return 'list.bullet'; - case CollectibleSortByOptions.FLOOR_PRICE: + case NftCollectionSortCriterion.FloorPrice: return 'plus.forwardslash.minus'; - case CollectibleSortByOptions.MOST_RECENT: + case NftCollectionSortCriterion.MostRecent: return 'clock'; } - return ''; }; const CollectiblesHeader = () => { @@ -48,13 +47,13 @@ const CollectiblesHeader = () => { ({ + menuItems={Object.entries(NftCollectionSortCriterion).map(([key, value]) => ({ actionKey: value, actionTitle: i18n.t(i18n.l.nfts.sort[value]), icon: { iconType: 'SYSTEM', iconValue: getMenuItemIcon(value) }, menuState: nftSort === key ? 'on' : 'off', }))} - selectItem={string => updateNFTSort(string as CollectibleSortByOptions)} + selectItem={string => updateNFTSort(string as NftCollectionSortCriterion)} icon={getIconForSortType(nftSort)} text={i18n.t(i18n.l.nfts.sort[nftSort])} /> diff --git a/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx b/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx index afbe410d580..75f9844cdde 100644 --- a/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx +++ b/src/components/asset-list/RecyclerAssetList2/core/RowRenderer.tsx @@ -29,9 +29,10 @@ import { DiscoverMoreButton } from './DiscoverMoreButton'; import { RotatingLearnCard } from '@/components/cards/RotatingLearnCard'; import WrappedPosition from '../WrappedPosition'; import WrappedPositionsListHeader from '../WrappedPositionsListHeader'; -import * as lang from '@/languages'; import { RemoteCardCarousel } from '@/components/cards/remote-cards'; import WrappedCollectiblesHeader from '../WrappedCollectiblesHeader'; +import NFTLoadingSkeleton from '../NFTLoadingSkeleton'; +import { NFTEmptyState } from '../NFTEmptyState'; function rowRenderer(type: CellType, { uid }: { uid: string }, _: unknown, extendedState: ExtendedState) { const data = extendedState.additionalData[uid]; @@ -123,8 +124,13 @@ function rowRenderer(type: CellType, { uid }: { uid: string }, _: unknown, exten ); case CellType.NFTS_HEADER: return ; + case CellType.NFTS_LOADING: + return ; + case CellType.NFTS_EMPTY: + return ; case CellType.FAMILY_HEADER: { const { name, image, total } = data as NFTFamilyExtraData; + return ( = { [CellType.FAMILY_HEADER]: { height: TokenFamilyHeaderHeight }, [CellType.NFT]: { // @ts-expect-error - height: UniqueTokenRow.cardSize + UniqueTokenRow.cardMargin, + height: UniqueTokenRow.height, width: deviceUtils.dimensions.width / 2 - 0.1, }, + [CellType.NFTS_LOADING]: { + height: TokenFamilyHeaderHeight * 5, + }, + [CellType.NFTS_EMPTY]: { + height: TokenFamilyHeaderHeight * 5, + }, [CellType.NFT_SPACE_AFTER]: { height: 5 }, [CellType.LOADING_ASSETS]: { height: AssetListItemSkeletonHeight }, [CellType.POSITIONS_HEADER]: { height: AssetListHeaderHeight }, diff --git a/src/components/asset-list/RecyclerAssetList2/core/ViewTypes.ts b/src/components/asset-list/RecyclerAssetList2/core/ViewTypes.ts index ab6df6115e6..a38a6d1ab05 100644 --- a/src/components/asset-list/RecyclerAssetList2/core/ViewTypes.ts +++ b/src/components/asset-list/RecyclerAssetList2/core/ViewTypes.ts @@ -17,6 +17,8 @@ export enum CellType { PROFILE_NAME_ROW_SPACE_AFTER = 'PROFILE_NAME_ROW_SPACE_AFTER', PROFILE_STICKY_HEADER = 'PROFILE_STICKY_HEADER', NFTS_HEADER = 'NFTS_HEADER', + NFTS_LOADING = 'NFTS_LOADING', + NFTS_EMPTY = 'NFTS_EMPTY', NFTS_HEADER_SPACE_BEFORE = 'NFTS_HEADER_SPACE_BEFORE', NFTS_HEADER_SPACE_AFTER = 'NFTS_HEADER_SPACE_AFTER', FAMILY_HEADER = 'FAMILY_HEADER', diff --git a/src/components/asset-list/RecyclerAssetList2/core/getLayoutProvider.tsx b/src/components/asset-list/RecyclerAssetList2/core/getLayoutProvider.tsx index 82c89dd7dfe..13619bef984 100644 --- a/src/components/asset-list/RecyclerAssetList2/core/getLayoutProvider.tsx +++ b/src/components/asset-list/RecyclerAssetList2/core/getLayoutProvider.tsx @@ -2,7 +2,6 @@ import { Dimension, Layout, LayoutManager, LayoutProvider } from 'recyclerlistvi import ViewDimensions from './ViewDimensions'; import { BaseCellType, CellType } from './ViewTypes'; import { deviceUtils } from '@/utils'; -import { TrimmedCard } from '@/resources/cards/cardCollectionQuery'; const getStyleOverridesForIndex = (indices: number[]) => (index: number) => { if (indices.includes(index)) { diff --git a/src/components/asset-list/RecyclerAssetList2/core/useMemoBriefSectionData.ts b/src/components/asset-list/RecyclerAssetList2/core/useMemoBriefSectionData.ts index e3285e63fe6..875d7f91d90 100644 --- a/src/components/asset-list/RecyclerAssetList2/core/useMemoBriefSectionData.ts +++ b/src/components/asset-list/RecyclerAssetList2/core/useMemoBriefSectionData.ts @@ -73,11 +73,6 @@ export default function useMemoBriefSectionData({ return false; } - // removes NFTS_HEADER if wallet doesn't have NFTs - if (data.type === CellType.NFTS_HEADER && !arr[arrIndex + 2]) { - return false; - } - if (data.type === CellType.PROFILE_STICKY_HEADER) { stickyHeaders.push(index); } diff --git a/src/components/list/ListHeaderMenu.tsx b/src/components/list/ListHeaderMenu.tsx index 289498f1432..f39df68a365 100644 --- a/src/components/list/ListHeaderMenu.tsx +++ b/src/components/list/ListHeaderMenu.tsx @@ -3,7 +3,7 @@ import ContextMenuButton from '@/components/native-context-menu/contextMenu'; import { ButtonPressAnimation } from '@/components/animations'; import { Bleed, Box, Inline, Text, useForegroundColor } from '@/design-system'; import { haptics } from '@/utils'; -import { CollectibleSortByOptions } from '@/hooks/useNFTsSortBy'; +import { NftCollectionSortCriterion } from '@/graphql/__generated__/arc'; type MenuItem = { actionKey: string; @@ -12,7 +12,7 @@ type MenuItem = { }; type ListHeaderMenuProps = { - selected: CollectibleSortByOptions; + selected: NftCollectionSortCriterion; menuItems: MenuItem[]; selectItem: (item: string) => void; icon: string; diff --git a/src/components/skeleton/Skeleton.tsx b/src/components/skeleton/Skeleton.tsx index 69d39e7a1ed..628090fc8a6 100644 --- a/src/components/skeleton/Skeleton.tsx +++ b/src/components/skeleton/Skeleton.tsx @@ -24,6 +24,13 @@ export const FakeAvatar = styled.View({ borderRadius: 20, }); +// @ts-expect-error Property 'View' does not exist on type... +export const FakeNFT = styled.View({ + ...position.sizeAsObject(32), + backgroundColor: ({ theme: { colors }, color }: FakeItemProps) => color ?? colors.skeleton, + borderRadius: 16, +}); + export const FakeRow = styled(Row).attrs({ align: 'flex-end', flex: 0, diff --git a/src/graphql/queries/arc.graphql b/src/graphql/queries/arc.graphql index 24c89874153..4b8d55ebb58 100644 --- a/src/graphql/queries/arc.graphql +++ b/src/graphql/queries/arc.graphql @@ -298,8 +298,8 @@ fragment simpleHashPaymentToken on SimpleHashPaymentToken { decimals } -query getNFTs($walletAddress: String!) { - nfts(walletAddress: $walletAddress) { +query getNFTs($walletAddress: String!, $sortBy: NFTCollectionSortCriterion, $sortDirection: SortDirection) { + nftsV2(walletAddress: $walletAddress, sortBy: $sortBy, sortDirection: $sortDirection) { nft_id chain contract_address diff --git a/src/helpers/assets.ts b/src/helpers/assets.ts index 847e9d2f912..b6cf424d7e8 100644 --- a/src/helpers/assets.ts +++ b/src/helpers/assets.ts @@ -5,9 +5,8 @@ import { AssetListType } from '@/components/asset-list/RecyclerAssetList2'; import { supportedNativeCurrencies } from '@/references'; import { getUniqueTokenFormat, getUniqueTokenType } from '@/utils'; import * as i18n from '@/languages'; -import * as ls from '@/storage'; import { UniqueAsset } from '@/entities'; -import { CollectibleSortByOptions } from '@/hooks/useNFTsSortBy'; +import { NftCollectionSortCriterion } from '@/graphql/__generated__/arc'; const COINS_TO_SHOW = 5; @@ -235,39 +234,15 @@ export const buildUniqueTokenList = (uniqueTokens: any, selectedShowcaseTokens: return rows; }; -const regex = RegExp(/\s*(the)\s/, 'i'); - -const sortCollectibles = (assetsByName: Dictionary, collectibleSortBy: string) => { - const families = Object.keys(assetsByName); - - switch (collectibleSortBy) { - case CollectibleSortByOptions.MOST_RECENT: - return families.sort((a, b) => { - const maxDateA = Math.max(Number(...assetsByName[a].map(asset => asset.acquisition_date))); - const maxDateB = Math.max(Number(...assetsByName[b].map(asset => asset.acquisition_date))); - return maxDateB - maxDateA; - }); - case CollectibleSortByOptions.ABC: - return families.sort((a, b) => a.replace(regex, '').toLowerCase().localeCompare(b.replace(regex, '').toLowerCase())); - case CollectibleSortByOptions.FLOOR_PRICE: - return families.sort((a, b) => { - const minPriceA = Math.min(...assetsByName[a].map(asset => (asset.floorPriceEth !== undefined ? asset.floorPriceEth : -1))); - const minPriceB = Math.min(...assetsByName[b].map(asset => (asset.floorPriceEth !== undefined ? asset.floorPriceEth : -1))); - return minPriceB - minPriceA; - }); - default: - return families; - } -}; - export const buildBriefUniqueTokenList = ( - uniqueTokens: any, + uniqueTokens: UniqueAsset[], selectedShowcaseTokens: any, sellingTokens: any[] = [], hiddenTokens: string[] = [], listType: AssetListType = 'wallet', isReadOnlyWallet = false, - nftSort: string = CollectibleSortByOptions.MOST_RECENT + nftSort = NftCollectionSortCriterion.MostRecent, + isFetchingNfts = false ) => { const hiddenUniqueTokensIds = uniqueTokens .filter(({ fullUniqueId }: any) => hiddenTokens.includes(fullUniqueId)) @@ -277,7 +252,7 @@ export const buildBriefUniqueTokenList = ( .filter(({ uniqueId }: any) => selectedShowcaseTokens?.includes(uniqueId)) .map(({ uniqueId }: any) => uniqueId); - const filteredUniqueTokens = nonHiddenUniqueTokens.filter((token: any) => { + const filteredUniqueTokens = nonHiddenUniqueTokens.filter((token: UniqueAsset) => { if (listType === 'select-nft') { const format = getUniqueTokenFormat(token); const type = getUniqueTokenType(token); @@ -287,10 +262,7 @@ export const buildBriefUniqueTokenList = ( }); // group the assets by collection name - const assetsByName = groupBy(filteredUniqueTokens, token => token.familyName); - - // depending on the sort by option, sort the collections - const families2 = sortCollectibles(assetsByName, nftSort); + const assetsByName = groupBy(filteredUniqueTokens, token => token.familyName); const result = [ { @@ -342,25 +314,37 @@ export const buildBriefUniqueTokenList = ( } result.push({ type: 'NFT_SPACE_AFTER', uid: `showcase-space-after` }); } - for (const family of families2) { - result.push({ - // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. - image: assetsByName[family][0].familyImage, - name: family, - total: assetsByName[family].length, - type: 'FAMILY_HEADER', - uid: family, - }); - const tokens = assetsByName[family].map(({ uniqueId }) => uniqueId); - for (let index = 0; index < tokens.length; index++) { - const uniqueId = tokens[index]; - // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. - result.push({ index, type: 'NFT', uid: uniqueId, uniqueId }); + if (!Object.keys(assetsByName).length) { + if (!isFetchingNfts) { + // empty NFT section + result.push({ type: 'NFTS_EMPTY', uid: `nft-empty` }); + } else { + // loading NFTs section (most likely from a sortBy change) but initial load too + result.push({ type: 'NFTS_LOADING', uid: `nft-loading-${nftSort}` }); } + } else { + for (const family of Object.keys(assetsByName)) { + result.push({ + // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. + image: assetsByName[family][0].familyImage, + name: family, + total: assetsByName[family].length, + type: 'FAMILY_HEADER', + uid: family, + }); + const tokens = assetsByName[family].map(({ uniqueId }) => uniqueId); + for (let index = 0; index < tokens.length; index++) { + const uniqueId = tokens[index]; - result.push({ type: 'NFT_SPACE_AFTER', uid: `${family}-space-after` }); + // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call. + result.push({ index, type: 'NFT', uid: uniqueId, uniqueId }); + } + + result.push({ type: 'NFT_SPACE_AFTER', uid: `${family}-space-after` }); + } } + if (hiddenUniqueTokensIds.length > 0 && listType === 'wallet' && !isReadOnlyWallet) { result.push({ // @ts-expect-error "name" does not exist in type. @@ -382,6 +366,7 @@ export const buildBriefUniqueTokenList = ( result.push({ type: 'NFT_SPACE_AFTER', uid: `showcase-space-after` }); } + return result; }; diff --git a/src/helpers/buildWalletSections.tsx b/src/helpers/buildWalletSections.tsx index 255add920b8..8cc4dc1e504 100644 --- a/src/helpers/buildWalletSections.tsx +++ b/src/helpers/buildWalletSections.tsx @@ -50,6 +50,8 @@ const sellingTokensSelector = (state: any) => state.sellingTokens; const showcaseTokensSelector = (state: any) => state.showcaseTokens; const hiddenTokensSelector = (state: any) => state.hiddenTokens; const uniqueTokensSelector = (state: any) => state.uniqueTokens; +const nftSortSelector = (state: any) => state.nftSort; +const isFetchingNftsSelector = (state: any) => state.isFetchingNfts; const listTypeSelector = (state: any) => state.listType; const buildBriefWalletSections = (balanceSectionData: any, uniqueTokenFamiliesSection: any) => { @@ -209,7 +211,8 @@ const briefUniqueTokenDataSelector = createSelector( hiddenTokensSelector, listTypeSelector, isReadOnlyWalletSelector, - (state: any, nftSort: string) => nftSort, + nftSortSelector, + isFetchingNftsSelector, ], buildBriefUniqueTokenList ); @@ -230,6 +233,6 @@ const briefBalanceSectionSelector = createSelector( ); export const buildBriefWalletSectionsSelector = createSelector( - [briefBalanceSectionSelector, (state: any, nftSort: string) => briefUniqueTokenDataSelector(state, nftSort)], + [briefBalanceSectionSelector, (state: any) => briefUniqueTokenDataSelector(state)], buildBriefWalletSections ); diff --git a/src/hooks/useNFTsSortBy.ts b/src/hooks/useNFTsSortBy.ts index 544782ee1db..345f20132a7 100644 --- a/src/hooks/useNFTsSortBy.ts +++ b/src/hooks/useNFTsSortBy.ts @@ -1,29 +1,24 @@ import { useCallback } from 'react'; import { MMKV, useMMKVString } from 'react-native-mmkv'; import useAccountSettings from './useAccountSettings'; +import { NftCollectionSortCriterion } from '@/graphql/__generated__/arc'; const mmkv = new MMKV(); const getStorageKey = (accountAddress: string) => `nfts-sort-${accountAddress}`; -export enum CollectibleSortByOptions { - MOST_RECENT = 'most_recent', - ABC = 'abc', - FLOOR_PRICE = 'floor_price', -} - export const getNftSortForAddress = (accountAddress: string) => { - mmkv.getString(getStorageKey(accountAddress)); + return mmkv.getString(getStorageKey(accountAddress)) as NftCollectionSortCriterion; }; export default function useNftSort(): { - nftSort: CollectibleSortByOptions; - updateNFTSort: (sortBy: CollectibleSortByOptions) => void; + nftSort: NftCollectionSortCriterion; + updateNFTSort: (sortBy: NftCollectionSortCriterion) => void; } { const { accountAddress } = useAccountSettings(); const [nftSort, setNftSort] = useMMKVString(getStorageKey(accountAddress)); const updateNFTSort = useCallback( - (sortBy: CollectibleSortByOptions) => { + (sortBy: NftCollectionSortCriterion) => { setNftSort(sortBy); }, [setNftSort] @@ -31,6 +26,6 @@ export default function useNftSort(): { return { updateNFTSort, - nftSort: (nftSort as CollectibleSortByOptions) || CollectibleSortByOptions.MOST_RECENT, + nftSort: (nftSort as NftCollectionSortCriterion) || NftCollectionSortCriterion.MostRecent, }; } diff --git a/src/hooks/useRefreshAccountData.ts b/src/hooks/useRefreshAccountData.ts index df30a1a18e6..ad94660351a 100644 --- a/src/hooks/useRefreshAccountData.ts +++ b/src/hooks/useRefreshAccountData.ts @@ -13,6 +13,7 @@ import { userAssetsQueryKey } from '@/resources/assets/UserAssetsQuery'; import { userAssetsQueryKey as swapsUserAssetsQueryKey } from '@/__swaps__/screens/Swap/resources/assets/userAssets'; import { nftsQueryKey } from '@/resources/nfts'; import { positionsQueryKey } from '@/resources/defi/PositionsQuery'; +import useNftSort from './useNFTsSortBy'; import { Address } from 'viem'; import { addysSummaryQueryKey } from '@/resources/summary/summary'; import useWallets from './useWallets'; @@ -22,6 +23,7 @@ export default function useRefreshAccountData() { const { accountAddress, nativeCurrency } = useAccountSettings(); const [isRefreshing, setIsRefreshing] = useState(false); const profilesEnabled = useExperimentalFlag(PROFILES); + const { nftSort } = useNftSort(); const { wallets } = useWallets(); @@ -33,7 +35,7 @@ export default function useRefreshAccountData() { const fetchAccountData = useCallback(async () => { const connectedToHardhat = getIsHardhatConnected(); - queryClient.invalidateQueries(nftsQueryKey({ address: accountAddress })); + queryClient.invalidateQueries(nftsQueryKey({ address: accountAddress, sortBy: nftSort })); queryClient.invalidateQueries(positionsQueryKey({ address: accountAddress as Address, currency: nativeCurrency })); queryClient.invalidateQueries(addysSummaryQueryKey({ addresses: allAddresses, currency: nativeCurrency })); queryClient.invalidateQueries(userAssetsQueryKey({ address: accountAddress, currency: nativeCurrency, connectedToHardhat })); diff --git a/src/hooks/useWalletSectionsData.ts b/src/hooks/useWalletSectionsData.ts index 7fe57995040..72e89e3de55 100644 --- a/src/hooks/useWalletSectionsData.ts +++ b/src/hooks/useWalletSectionsData.ts @@ -22,12 +22,16 @@ export default function useWalletSectionsData({ const { isLoading: isLoadingUserAssets, data: sortedAssets = [] } = useSortedUserAssets(); const isWalletEthZero = useIsWalletEthZero(); + const { nftSort } = useNftSort(); + const { accountAddress, language, network, nativeCurrency } = useAccountSettings(); const { sendableUniqueTokens } = useSendableUniqueTokens(); const { data: { nfts: allUniqueTokens }, + isLoading: isFetchingNfts, } = useLegacyNFTs({ address: accountAddress, + sortBy: nftSort, }); const walletsWithBalancesAndNames = useWalletsWithBalancesAndNames(); @@ -43,8 +47,6 @@ export default function useWalletSectionsData({ const { isCoinListEdited } = useCoinListEdited(); - const { nftSort } = useNftSort(); - const walletSections = useMemo(() => { const accountInfo = { hiddenCoins, @@ -65,10 +67,11 @@ export default function useWalletSectionsData({ listType: type, showcaseTokens, uniqueTokens: allUniqueTokens, + isFetchingNfts, nftSort, }; - const { briefSectionsData, isEmpty } = buildBriefWalletSectionsSelector(accountInfo, nftSort); + const { briefSectionsData, isEmpty } = buildBriefWalletSectionsSelector(accountInfo); const hasNFTs = allUniqueTokens.length > 0; return { @@ -96,6 +99,7 @@ export default function useWalletSectionsData({ type, showcaseTokens, allUniqueTokens, + isFetchingNfts, nftSort, ]); return walletSections; diff --git a/src/hooks/useWatchPendingTxs.ts b/src/hooks/useWatchPendingTxs.ts index aed5c6fad25..51a710ff73d 100644 --- a/src/hooks/useWatchPendingTxs.ts +++ b/src/hooks/useWatchPendingTxs.ts @@ -15,6 +15,7 @@ import { usePendingTransactionsStore } from '@/state/pendingTransactions'; import { useNonceStore } from '@/state/nonces'; import { Address } from 'viem'; import { nftsQueryKey } from '@/resources/nfts'; +import { getNftSortForAddress } from './useNFTsSortBy'; export const useWatchPendingTransactions = ({ address }: { address: string }) => { const { storePendingTransactions, setPendingTransactions } = usePendingTransactionsStore(state => ({ @@ -46,7 +47,7 @@ export const useWatchPendingTransactions = ({ address }: { address: string }) => testnetMode: !!connectedToHardhat, }) ); - queryClient.invalidateQueries(nftsQueryKey({ address })); + queryClient.invalidateQueries(nftsQueryKey({ address, sortBy: getNftSortForAddress(address) })); }, [address, nativeCurrency] ); diff --git a/src/languages/en_US.json b/src/languages/en_US.json index c243830f84d..5130d449fc6 100644 --- a/src/languages/en_US.json +++ b/src/languages/en_US.json @@ -1315,10 +1315,16 @@ "nfts": { "selling": "Selling", "sort": { + "ABC": "Alphabetical", "abc": "Alphabetical", + "MOST_RECENT": "Recent", "most_recent": "Recent", + "FLOOR_PRICE": "Floor Price", "floor_price": "Floor Price" - } + }, + "empty": "Collectibles", + "collect_now": "Collect Now", + "will_appear_here": "Your Collectibles Will Appear Here" }, "nft_offers": { "card": { diff --git a/src/resources/nfts/index.ts b/src/resources/nfts/index.ts index d7b1c88c198..a39f3c6848d 100644 --- a/src/resources/nfts/index.ts +++ b/src/resources/nfts/index.ts @@ -9,12 +9,14 @@ import { Network } from '@/helpers'; import { UniqueAsset } from '@/entities'; import { arcClient } from '@/graphql'; import { createSelector } from 'reselect'; +import { NftCollectionSortCriterion } from '@/graphql/__generated__/arc'; const NFTS_STALE_TIME = 600000; // 10 minutes const NFTS_CACHE_TIME_EXTERNAL = 3600000; // 1 hour const NFTS_CACHE_TIME_INTERNAL = 604800000; // 1 week -export const nftsQueryKey = ({ address }: { address: string }) => createQueryKey('nfts', { address }, { persisterVersion: 3 }); +export const nftsQueryKey = ({ address, sortBy }: { address: string; sortBy: NftCollectionSortCriterion }) => + createQueryKey('nfts', { address, sortBy }, { persisterVersion: 4 }); export const nftListingQueryKey = ({ contractAddress, @@ -52,10 +54,10 @@ interface NFTData { type NFTQueryKey = ReturnType; const fetchNFTData: QueryFunction = async ({ queryKey }) => { - const [{ address }] = queryKey; - const queryResponse = await arcClient.getNFTs({ walletAddress: address }); + const [{ address, sortBy }] = queryKey; + const queryResponse = await arcClient.getNFTs({ walletAddress: address, sortBy }); - const nfts = queryResponse?.nfts?.map(nft => simpleHashNFTToUniqueAsset(nft, address)); + const nfts = queryResponse?.nftsV2?.map(nft => simpleHashNFTToUniqueAsset(nft, address)); // ⚠️ TODO: Delete this and rework the code that uses it const nftsMap = nfts?.reduce( @@ -75,14 +77,16 @@ const FALLBACK_DATA: NFTData = { nfts: [], nftsMap: {} }; export function useLegacyNFTs({ address, + sortBy = NftCollectionSortCriterion.MostRecent, config, }: { address: string; + sortBy?: NftCollectionSortCriterion; config?: QueryConfigWithSelect; }) { const isImportedWallet = useSelector((state: AppState) => isImportedWalletSelector(state, address)); - const { data, error, isFetching } = useQuery(nftsQueryKey({ address }), fetchNFTData, { + const { data, error, isLoading, isInitialLoading } = useQuery(nftsQueryKey({ address, sortBy }), fetchNFTData, { cacheTime: isImportedWallet ? NFTS_CACHE_TIME_INTERNAL : NFTS_CACHE_TIME_EXTERNAL, enabled: !!address, retry: 3, @@ -93,7 +97,8 @@ export function useLegacyNFTs({ return { data: (config?.select ? data ?? config.select(FALLBACK_DATA) : data ?? FALLBACK_DATA) as TSelected, error, - isInitialLoading: !data && isFetching, + isLoading, + isInitialLoading, }; }