diff --git a/app/scripts/metamask-controller.js b/app/scripts/metamask-controller.js index ab747c86ec09..d015ace7a459 100644 --- a/app/scripts/metamask-controller.js +++ b/app/scripts/metamask-controller.js @@ -705,6 +705,10 @@ export default class MetamaskController extends EventEmitter { this.assetsContractController, ), addNft: this.nftController.addNft.bind(this.nftController), + updateNftFetchingProgressStatus: + this.nftController.updateNftFetchingProgressStatus.bind( + this.nftController, + ), getNftApi: this.nftController.getNftApi.bind(this.nftController), getNftState: () => this.nftController.state, // added this to track previous value of useNftDetection, should be true on very first initializing of controller[] @@ -3115,6 +3119,8 @@ export default class MetamaskController extends EventEmitter { nftController.checkAndUpdateSingleNftOwnershipStatus.bind( nftController, ), + updateNftFetchingProgressStatus: + nftController.updateNftFetchingProgressStatus.bind(nftController), isNftOwner: nftController.isNftOwner.bind(nftController), diff --git a/ui/components/app/nfts-tab/index.scss b/ui/components/app/nfts-tab/index.scss index 2f5e266c136a..44c20656d417 100644 --- a/ui/components/app/nfts-tab/index.scss +++ b/ui/components/app/nfts-tab/index.scss @@ -1,7 +1,15 @@ .nfts-tab { + &__fetching { + display: flex; + height: 100px; + align-items: center; + justify-content: center; + padding: 30px; + } + &__loading { display: flex; - height: 250px; + height: 200px; align-items: center; justify-content: center; padding: 30px; diff --git a/ui/components/app/nfts-tab/nfts-tab.js b/ui/components/app/nfts-tab/nfts-tab.js index f9f87ea0f8a1..44be21a5201f 100644 --- a/ui/components/app/nfts-tab/nfts-tab.js +++ b/ui/components/app/nfts-tab/nfts-tab.js @@ -1,4 +1,4 @@ -import React, { useContext, useEffect } from 'react'; +import React, { useContext, useEffect, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { @@ -48,6 +48,8 @@ import { RampsCard, } from '../../multichain/ramps-card/ramps-card'; import { useAccountTotalFiatBalance } from '../../../hooks/useAccountTotalFiatBalance'; +import { getIsStillNftsFetching } from '../../../ducks/metamask/metamask'; +import Spinner from '../../ui/spinner'; ///: END:ONLY_INCLUDE_IF export default function NftsTab() { @@ -63,11 +65,16 @@ export default function NftsTab() { const shouldHideZeroBalanceTokens = useSelector( getShouldHideZeroBalanceTokens, ); + + const isNftsStillFetched = useSelector(getIsStillNftsFetching); + const { totalFiatBalance } = useAccountTotalFiatBalance( selectedAddress, shouldHideZeroBalanceTokens, ); const balanceIsZero = Number(totalFiatBalance) === 0; + const [showLoader, setShowLoader] = useState(true); + const [showRefreshLoader, setShowRefreshLoader] = useState(false); const isBuyableChain = useSelector(getIsBuyableChain); const showRampsCard = isBuyableChain && balanceIsZero; ///: END:ONLY_INCLUDE_IF @@ -80,9 +87,15 @@ export default function NftsTab() { }; const onRefresh = () => { + setShowRefreshLoader(true); if (isMainnet) { - dispatch(detectNfts()); + detectNfts(); } + const timeoutForRefresh = setTimeout(() => { + setShowRefreshLoader(false); + clearTimeout(timeoutForRefresh); + }, 200); + checkAndUpdateAllNftsOwnershipStatus(); }; @@ -114,10 +127,34 @@ export default function NftsTab() { currentLocale, ]); - if (nftsLoading) { - return
{t('loadingNFTs')}
; + useEffect(() => { + const timeoutId = setTimeout(() => { + setShowLoader(false); + }, 2000); + return () => clearTimeout(timeoutId); + }, []); + + if (!hasAnyNfts && showLoader) { + return ( + + + + ); } + if (showRefreshLoader) { + return ( + + + + ); + } return ( <> { @@ -129,10 +166,21 @@ export default function NftsTab() { } {hasAnyNfts > 0 || previouslyOwnedCollection.nfts.length > 0 ? ( - + + + + {isNftsStillFetched.isFetchingInProgress ? ( + + + + ) : null} + ) : ( <> {isMainnet && !useNftDetection ? ( diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-nft-tab.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-nft-tab.tsx new file mode 100644 index 000000000000..e435b4bbbff7 --- /dev/null +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal-nft-tab.tsx @@ -0,0 +1,151 @@ +import React, { useEffect, useState } from 'react'; +import { useSelector } from 'react-redux'; +import NftsItems from '../../../app/nfts-items/nfts-items'; +import { + Box, + Text, + ButtonLink, + ButtonLinkSize, +} from '../../../component-library'; +import { + TextColor, + TextVariant, + TextAlign, + Display, + JustifyContent, + AlignItems, + FlexDirection, +} from '../../../../helpers/constants/design-system'; +import { useI18nContext } from '../../../../hooks/useI18nContext'; +import { TokenStandard } from '../../../../../shared/constants/transaction'; +import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; +import Spinner from '../../../ui/spinner'; +import { useScrollRequired } from '../../../../hooks/useScrollRequired'; +import { getIsStillNftsFetching } from '../../../../ducks/metamask/metamask'; +import PropTypes from 'prop-types'; + +type NFT = { + address: string; + description: string | null; + favorite: boolean; + image: string | null; + isCurrentlyOwned: boolean; + name: string | null; + standard: TokenStandard; + tokenId: string; + tokenURI?: string; +}; + +type Collection = { + collectionName: string; + collectionImage: string | null; + nfts: NFT[]; +}; + +type AssetPickerModalNftTabProps = { + collectionDataFiltered: Collection[]; + previouslyOwnedCollection: any; + onClose: () => void; +}; + +export function AssetPickerModalNftTab({ + collectionDataFiltered, + previouslyOwnedCollection, + onClose +}: AssetPickerModalNftTabProps) { + const t = useI18nContext(); + + const hasAnyNfts = Object.keys(collectionDataFiltered).length > 0; + const { isScrollable, isScrolledToBottom, ref, onScroll } = + useScrollRequired(); + const isNftsStillFetched = useSelector(getIsStillNftsFetching); + const [showLoader, setShowLoader] = useState(true); + + useEffect(() => { + // Use setTimeout to update the message after 2000 milliseconds (2 seconds) + const timeoutId = setTimeout(() => { + setShowLoader(false); + }, 2000); + + // Cleanup function to clear the timeout if the component unmounts + return () => clearTimeout(timeoutId); + }, []); // Empty dependency array ensures the effect runs only once + + if (!hasAnyNfts && showLoader) { + return ( + + + + ); + } + + if (hasAnyNfts) { + return ( + + onClose()} + showTokenId={true} + displayPreviouslyOwnedCollection={false} + /> + {isScrollable && + isScrolledToBottom && + isNftsStillFetched.isFetchingInProgress ? ( + + + + ) : null} + + ); + } + return ( + + + + + + + {t('noNFTs')} + + + {t('learnMoreUpperCase')} + + + + ); +} \ No newline at end of file diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx index 79e9ff299153..f2043635a6be 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/asset-picker-modal.tsx @@ -3,7 +3,6 @@ import { useSelector } from 'react-redux'; import classnames from 'classnames'; import { isEqual } from 'lodash'; import { Tab, Tabs } from '../../../ui/tabs'; -import NftsItems from '../../../app/nfts-items/nfts-items'; import { Modal, ModalContent, @@ -11,9 +10,6 @@ import { ModalHeader, TextFieldSearch, Box, - Text, - ButtonLink, - ButtonLinkSize, ButtonIconSize, TextFieldSearchSize, } from '../../../component-library'; @@ -21,13 +17,8 @@ import { BlockSize, BorderRadius, BackgroundColor, - TextColor, - TextVariant, - TextAlign, Display, - JustifyContent, AlignItems, - FlexDirection, FlexWrap, } from '../../../../helpers/constants/design-system'; import { useI18nContext } from '../../../../hooks/useI18nContext'; @@ -54,7 +45,7 @@ import { useCurrencyDisplay } from '../../../../hooks/useCurrencyDisplay'; import TokenCell from '../../../app/token-cell'; import { TokenListItem } from '../../token-list-item'; import { useNftsCollections } from '../../../../hooks/useNftsCollections'; -import ZENDESK_URLS from '../../../../helpers/constants/zendesk-url'; +import { AssetPickerModalNftTab } from './asset-picker-modal-nft-tab'; type AssetPickerModalProps = { isOpen: boolean; @@ -121,8 +112,6 @@ export function AssetPickerModal({ const { collections, previouslyOwnedCollection } = useNftsCollections(); - const hasAnyNfts = Object.keys(collections).length > 0; - const { currency: primaryCurrency, numberOfDecimals: primaryNumberOfDecimals, @@ -365,8 +354,17 @@ export function AssetPickerModal({ size={TextFieldSearchSize.Lg} /> - {hasAnyNfts ? ( - + + {/* {hasAnyNfts ? ( + + {isScrollable && + isScrolledToBottom && + isNftsStillFetched.isFetchingInProgress ? ( + + + + ) : null} ) : ( - )} + )} */} } diff --git a/ui/components/multichain/asset-picker-amount/asset-picker-modal/index.scss b/ui/components/multichain/asset-picker-amount/asset-picker-modal/index.scss index d7c0096ae8ef..23a39f6917c3 100644 --- a/ui/components/multichain/asset-picker-amount/asset-picker-modal/index.scss +++ b/ui/components/multichain/asset-picker-amount/asset-picker-modal/index.scss @@ -77,5 +77,13 @@ border-bottom: 0; padding: 16px; } + + &__loading { + display: flex; + height: 200px; + align-items: center; + justify-content: center; + padding: 15px; + } } } diff --git a/ui/components/ui/tabs/tabs.component.js b/ui/components/ui/tabs/tabs.component.js index 57e22994eb9d..fc7475310fee 100644 --- a/ui/components/ui/tabs/tabs.component.js +++ b/ui/components/ui/tabs/tabs.component.js @@ -1,14 +1,12 @@ import React, { useState } from 'react'; import PropTypes from 'prop-types'; import classnames from 'classnames'; -import { useDispatch } from 'react-redux'; import Box from '../box'; import { BackgroundColor, DISPLAY, JustifyContent, } from '../../../helpers/constants/design-system'; -import { useThrottle } from '../../../hooks/useThrottle'; import { detectNfts } from '../../../store/actions'; const Tabs = ({ @@ -34,21 +32,18 @@ const Tabs = ({ const _findChildByKey = (tabKey) => { return _getValidChildren().findIndex((c) => c?.props.tabKey === tabKey); }; - const dispatch = useDispatch(); const [activeTabIndex, setActiveTabIndex] = useState(() => Math.max(_findChildByKey(defaultActiveTabKey), 0), ); - const invokeDetectNfts = useThrottle(() => dispatch(detectNfts()), 60000); - const handleTabClick = (tabIndex, tabKey) => { if (tabIndex !== activeTabIndex) { setActiveTabIndex(tabIndex); onTabClick?.(tabKey); } if (tabKey === 'nfts') { - invokeDetectNfts(); + detectNfts(); } }; diff --git a/ui/ducks/metamask/metamask.js b/ui/ducks/metamask/metamask.js index ef45e19e7694..2aef24a71fd0 100644 --- a/ui/ducks/metamask/metamask.js +++ b/ui/ducks/metamask/metamask.js @@ -315,6 +315,17 @@ export const getNfts = (state) => { return allNfts?.[selectedAddress]?.[chainId] ?? []; }; +export const getIsStillNftsFetching = (state) => { + const { + metamask: { isNftFetchingInProgress }, + } = state; + const { address: selectedAddress } = getSelectedInternalAccount(state); + + const { chainId } = getProviderConfig(state); + + return isNftFetchingInProgress?.[selectedAddress]?.[chainId]; +}; + export const getNftContracts = (state) => { const { metamask: { allNftContracts }, diff --git a/ui/store/actions.ts b/ui/store/actions.ts index 0503a87afc09..68b53ef6b04b 100644 --- a/ui/store/actions.ts +++ b/ui/store/actions.ts @@ -3451,23 +3451,8 @@ export function detectTokens(): ThunkAction< }; } -export function detectNfts(): ThunkAction< - void, - MetaMaskReduxState, - unknown, - AnyAction -> { - return async (dispatch: MetaMaskReduxDispatch) => { - dispatch(showLoadingIndication()); - log.debug(`background.detectNfts`); - try { - await submitRequestToBackground('detectNfts'); - dispatch(hideLoadingIndication()); - await forceUpdateMetamaskState(dispatch); - } finally { - dispatch(hideLoadingIndication()); - } - }; +export async function detectNfts() { + await submitRequestToBackground('detectNfts'); } export function setAdvancedGasFee(