From ff57232824d1248df24472458c054650efc49a5f Mon Sep 17 00:00:00 2001 From: Josh Leonard <30185185+josheleonard@users.noreply.github.com> Date: Thu, 23 May 2024 16:05:32 -0600 Subject: [PATCH] feat(wallet): allow hiding unowned NFTs --- .../browser/brave_wallet_constants.h | 1 + .../common/async/base-query-cache.ts | 41 ++ .../common/constants/local-storage-keys.ts | 4 +- .../common/hooks/use-balances-fetcher.tsx | 5 +- .../common/slices/endpoints/nfts.endpoints.ts | 45 +- .../endpoints/token_balances.endpoints.ts | 66 ++- .../filter-modals/portfolio-filters-modal.tsx | 17 + .../desktop/views/nfts/components/nfts.tsx | 416 ++++++++++++------ .../views/portfolio/portfolio-overview.tsx | 76 +--- components/brave_wallet_ui/stories/locale.ts | 1 + .../utils/local-storage-utils.ts | 30 ++ components/resources/wallet_strings.grdp | 1 + 12 files changed, 451 insertions(+), 252 deletions(-) diff --git a/components/brave_wallet/browser/brave_wallet_constants.h b/components/brave_wallet/browser/brave_wallet_constants.h index 3bb3eb3ffe9ee..f9920b9a11194 100644 --- a/components/brave_wallet/browser/brave_wallet_constants.h +++ b/components/brave_wallet/browser/brave_wallet_constants.h @@ -46,6 +46,7 @@ inline constexpr char kSimpleHashBraveProxyUrl[] = "https://simplehash.wallet.brave.com"; inline constexpr webui::LocalizedString kLocalizedStrings[] = { + {"braveWalletHideNotOwnedNfTs", IDS_BRAVE_WALLET_HIDE_NOT_OWNED_NF_TS}, {"braveWalletNoRoutesFound", IDS_BRAVE_WALLET_NO_ROUTES_FOUND}, {"braveWalletPrivateKeyImportType", IDS_BRAVE_WALLET_PRIVATE_KEY_IMPORT_TYPE}, diff --git a/components/brave_wallet_ui/common/async/base-query-cache.ts b/components/brave_wallet_ui/common/async/base-query-cache.ts index 0237b82edeb02..0bcdbb1c5bb76 100644 --- a/components/brave_wallet_ui/common/async/base-query-cache.ts +++ b/components/brave_wallet_ui/common/async/base-query-cache.ts @@ -90,6 +90,10 @@ export class BaseQueryCache { private _nftMetadataRegistry: Record = {} public rewardsInfo: BraveRewardsInfo | undefined = undefined public balanceScannerSupportedChains: string[] | undefined = undefined + public spamNftsForAccountRegistry: Record< + string, // accountUniqueId + BraveWallet.BlockchainToken[] + > = {} getWalletInfo = async () => { if (!this.walletInfo) { @@ -483,6 +487,43 @@ export class BaseQueryCache { return this._nftMetadataRegistry[tokenId] } + getSpamNftsForAccountId = async (accountId: BraveWallet.AccountId) => { + if (!this.spamNftsForAccountRegistry[accountId.uniqueKey]) { + const { braveWalletService } = getAPIProxy() + const { address, coin } = accountId + const networksRegistry = await cache.getNetworksRegistry() + + const chainIds = networksRegistry.ids.map( + (network) => networksRegistry.entities[network]!.chainId + ) + + let currentCursor: string | null = null + const accountSpamNfts = [] + + do { + const { + tokens, + cursor + }: { + tokens: BraveWallet.BlockchainToken[] + cursor: string | null + } = await braveWalletService.getSimpleHashSpamNFTs( + address, + chainIds, + coin, + currentCursor + ) + + accountSpamNfts.push(...tokens) + currentCursor = cursor + } while (currentCursor) + + this.spamNftsForAccountRegistry[accountId.uniqueKey] = accountSpamNfts + } + + return this.spamNftsForAccountRegistry[accountId.uniqueKey] + } + // Brave Rewards getBraveRewardsInfo = async () => { if (!this.rewardsInfo) { diff --git a/components/brave_wallet_ui/common/constants/local-storage-keys.ts b/components/brave_wallet_ui/common/constants/local-storage-keys.ts index c849257b58e1f..8e2be3c3520da 100644 --- a/components/brave_wallet_ui/common/constants/local-storage-keys.ts +++ b/components/brave_wallet_ui/common/constants/local-storage-keys.ts @@ -27,9 +27,11 @@ export const LOCAL_STORAGE_KEYS = { CURRENT_PANEL: 'BRAVE_WALLET_CURRENT_PANEL', LAST_VISITED_PANEL: 'BRAVE_WALLET_LAST_VISITED_PANEL', TOKEN_BALANCES: 'BRAVE_WALLET_TOKEN_BALANCES2', + SPAM_TOKEN_BALANCES: 'SPAM_TOKEN_BALANCES', SAVED_SESSION_ROUTE: 'BRAVE_WALLET_SAVED_SESSION_ROUTE', USER_HIDDEN_TOKEN_IDS: 'BRAVE_WALLET_USER_HIDDEN_TOKEN_IDS', - USER_DELETED_TOKEN_IDS: 'BRAVE_WALLET_USER_DELETED_TOKEN_IDS' + USER_DELETED_TOKEN_IDS: 'BRAVE_WALLET_USER_DELETED_TOKEN_IDS', + HIDE_UNOWNED_NFTS: 'HIDE_UNOWNED_NFTS' } as const const LOCAL_STORAGE_KEYS_DEPRECATED = { diff --git a/components/brave_wallet_ui/common/hooks/use-balances-fetcher.tsx b/components/brave_wallet_ui/common/hooks/use-balances-fetcher.tsx index 3f184aad14045..dd355f56d22db 100644 --- a/components/brave_wallet_ui/common/hooks/use-balances-fetcher.tsx +++ b/components/brave_wallet_ui/common/hooks/use-balances-fetcher.tsx @@ -16,7 +16,7 @@ import { GetTokenBalancesRegistryArg // } from '../slices/endpoints/token_balances.endpoints' -type Arg = Pick & { +type Arg = Pick & { accounts: BraveWallet.AccountInfo[] } @@ -45,7 +45,8 @@ export const useBalancesFetcher = (arg: Arg | typeof skipToken) => { supportedKeyrings }) ), - useAnkrBalancesFeature + useAnkrBalancesFeature, + isSpamRegistry: arg.isSpamRegistry } : skipToken, { diff --git a/components/brave_wallet_ui/common/slices/endpoints/nfts.endpoints.ts b/components/brave_wallet_ui/common/slices/endpoints/nfts.endpoints.ts index ab83961c1307b..566f2562b54c5 100644 --- a/components/brave_wallet_ui/common/slices/endpoints/nfts.endpoints.ts +++ b/components/brave_wallet_ui/common/slices/endpoints/nfts.endpoints.ts @@ -206,46 +206,25 @@ export const nftsEndpoints = ({ } } }), - getSimpleHashSpamNfts: query({ - queryFn: async (_arg, { endpoint }, _extraOptions, baseQuery) => { - try { - const { data: api, cache } = baseQuery(undefined) - const { braveWalletService } = api - const networksRegistry = await cache.getNetworksRegistry() + /** will get spam for all accounts if accounts arg is not provided */ + getSimpleHashSpamNfts: query< + BraveWallet.BlockchainToken[], + void | undefined | { accounts: BraveWallet.AccountInfo[] } + >({ + queryFn: async (arg, { endpoint }, _extraOptions, baseQuery) => { + try { + const { cache } = baseQuery(undefined) - const chainIds = networksRegistry.ids.map( - (network) => networksRegistry.entities[network]!.chainId - ) + const lookupAccounts = + arg?.accounts ?? (await cache.getAllAccounts()).accounts - const { accounts } = await cache.getAllAccounts() const spamNfts = ( await mapLimit( - accounts, + lookupAccounts, 10, async (account: BraveWallet.AccountInfo) => { - let currentCursor: string | null = null - const accountSpamNfts = [] - - do { - const { - tokens, - cursor - }: { - tokens: BraveWallet.BlockchainToken[] - cursor: string | null - } = await braveWalletService.getSimpleHashSpamNFTs( - account.address, - chainIds, - account.accountId.coin, - currentCursor - ) - - accountSpamNfts.push(...tokens) - currentCursor = cursor - } while (currentCursor) - - return accountSpamNfts + return await cache.getSpamNftsForAccountId(account.accountId) } ) ).flat(1) diff --git a/components/brave_wallet_ui/common/slices/endpoints/token_balances.endpoints.ts b/components/brave_wallet_ui/common/slices/endpoints/token_balances.endpoints.ts index 4f19383243b27..6260f0bc34704 100644 --- a/components/brave_wallet_ui/common/slices/endpoints/token_balances.endpoints.ts +++ b/components/brave_wallet_ui/common/slices/endpoints/token_balances.endpoints.ts @@ -40,10 +40,15 @@ import { baseQueryFunction } from '../../async/base-query-cache' import { + getPersistedPortfolioSpamTokenBalances, getPersistedPortfolioTokenBalances, + setPersistedPortfolioSpamTokenBalances, setPersistedPortfolioTokenBalances } from '../../../utils/local-storage-utils' import { getIsRewardsNetwork } from '../../../utils/rewards_utils' +import { + blockchainTokenEntityAdaptorInitialState // +} from '../entities/blockchain-token.entity' type BalanceNetwork = Pick< BraveWallet.NetworkInfo, @@ -92,6 +97,9 @@ export type GetTokenBalancesRegistryArg = { accountIds: BraveWallet.AccountId[] networks: BalanceNetwork[] useAnkrBalancesFeature: boolean + /** if true, only spam NFT balances will be fetched, if falsey, only user + * token balances will be fetched */ + isSpamRegistry?: boolean } function mergeTokenBalancesRegistry( @@ -222,8 +230,10 @@ export const tokenBalancesEndpoints = ({ TokenBalancesRegistry | null, GetTokenBalancesRegistryArg >({ - queryFn: function () { - const persistedBalances = getPersistedPortfolioTokenBalances() + queryFn: function (arg) { + const persistedBalances = arg.isSpamRegistry + ? getPersistedPortfolioSpamTokenBalances() + : getPersistedPortfolioTokenBalances() // return null so we can tell if we have data or not to start with return { @@ -357,7 +367,13 @@ export const tokenBalancesEndpoints = ({ networkSupportsAccount(network, accountId) ) - const userTokens = await cache.getUserTokensRegistry() + const userTokensRegistry = arg.isSpamRegistry + ? blockchainTokenEntityAdaptorInitialState + : await cache.getUserTokensRegistry() + + const spamTokens = arg.isSpamRegistry + ? await cache.getSpamNftsForAccountId(accountId) + : [] if (nonAnkrSupportedAccountNetworks.length) { await eachLimit( @@ -366,6 +382,22 @@ export const tokenBalancesEndpoints = ({ async (network: BraveWallet.NetworkInfo) => { assert(coinTypesMapping[network.coin] !== undefined) try { + const tokens = arg.isSpamRegistry + ? spamTokens.filter( + (token) => + token.coin === network.coin && + token.chainId === network.chainId + ) + : getEntitiesListFromEntityState( + userTokensRegistry, + userTokensRegistry.idsByChainId[ + networkEntityAdapter.selectId({ + coin: network.coin, + chainId: network.chainId + }) + ] + ) + await fetchTokenBalanceRegistryForAccountsAndChainIds({ args: network.coin === CoinTypes.SOL @@ -381,15 +413,7 @@ export const tokenBalancesEndpoints = ({ accountId, coin: coinTypesMapping[network.coin], chainId: network.chainId, - tokens: getEntitiesListFromEntityState( - userTokens, - userTokens.idsByChainId[ - networkEntityAdapter.selectId({ - coin: network.coin, - chainId: network.chainId - }) - ] - ) + tokens: tokens } ], cache, @@ -416,13 +440,19 @@ export const tokenBalancesEndpoints = ({ return tokenBalancesRegistry }) - const persistedBalances = getPersistedPortfolioTokenBalances() - setPersistedPortfolioTokenBalances( - mergeTokenBalancesRegistry( - persistedBalances, - tokenBalancesRegistry - ) + const persistedBalances = arg.isSpamRegistry + ? getPersistedPortfolioSpamTokenBalances() + : getPersistedPortfolioTokenBalances() + + const mergedRegistry = mergeTokenBalancesRegistry( + persistedBalances, + tokenBalancesRegistry ) + if (arg.isSpamRegistry) { + setPersistedPortfolioSpamTokenBalances(mergedRegistry) + } else { + setPersistedPortfolioTokenBalances(mergedRegistry) + } } catch (error) { handleEndpointError( 'getTokenBalancesRegistry.onCacheEntryAdded', diff --git a/components/brave_wallet_ui/components/desktop/popup-modals/filter-modals/portfolio-filters-modal.tsx b/components/brave_wallet_ui/components/desktop/popup-modals/filter-modals/portfolio-filters-modal.tsx index b2aa75c33cbac..fb3d9aa22ba9e 100644 --- a/components/brave_wallet_ui/components/desktop/popup-modals/filter-modals/portfolio-filters-modal.tsx +++ b/components/brave_wallet_ui/components/desktop/popup-modals/filter-modals/portfolio-filters-modal.tsx @@ -107,6 +107,10 @@ export const PortfolioFiltersModal = (props: Props) => { LOCAL_STORAGE_KEYS.SHOW_NETWORK_LOGO_ON_NFTS, false ) + const [hideUnownedNfts, setHideUnownedNfts] = useSyncedLocalStorage( + LOCAL_STORAGE_KEYS.HIDE_UNOWNED_NFTS, + false + ) // queries const { data: defaultFiatCurrency = 'usd' } = useGetDefaultFiatCurrencyQuery() @@ -128,6 +132,8 @@ export const PortfolioFiltersModal = (props: Props) => { const [showNetworkLogo, setShowNetworkLogo] = React.useState( showNetworkLogoOnNfts ) + const [hideUnownedNftsToggle, setHideUnownedNftsToggle] = + React.useState(hideUnownedNfts) // Memos const hideSmallBalancesDescription = React.useMemo(() => { @@ -151,6 +157,7 @@ export const PortfolioFiltersModal = (props: Props) => { setSelectedAssetFilter(selectedAssetFilterOption) setHidePortfolioSmallBalances(hideSmallBalances) setShowNetworkLogoOnNfts(showNetworkLogo) + setHideUnownedNfts(hideUnownedNftsToggle) onClose() }, [ setFilteredOutPortfolioNetworkKeys, @@ -165,6 +172,8 @@ export const PortfolioFiltersModal = (props: Props) => { hideSmallBalances, setShowNetworkLogoOnNfts, showNetworkLogo, + setHideUnownedNfts, + hideUnownedNftsToggle, onClose ]) @@ -197,6 +206,14 @@ export const PortfolioFiltersModal = (props: Props) => { setIsSelected={() => setShowNetworkLogo((prev) => !prev)} /> + setHideUnownedNftsToggle((prev) => !prev)} + /> + {/* Disabled until Spam NFTs feature is implemented in core */} {/* void + accounts: BraveWallet.AccountInfo[] + tokenBalancesRegistry: TokenBalancesRegistry | null | undefined + networks: BraveWallet.NetworkInfo[] } const compareFn = ( @@ -87,11 +103,37 @@ const compareFn = ( b: BraveWallet.BlockchainToken ) => a.name.localeCompare(b.name) +const searchNfts = ( + searchValue: string, + items: BraveWallet.BlockchainToken[] +) => { + if (searchValue === '') { + return items.slice() + } + + return items.filter((item) => { + const tokenId = new Amount(item.tokenId).toNumber().toString() + const searchValueLower = searchValue.toLowerCase() + return ( + item.name.toLocaleLowerCase().includes(searchValueLower) || + item.symbol.toLocaleLowerCase().includes(searchValueLower) || + tokenId.includes(searchValueLower) + ) + }) +} + const LIST_PAGE_ITEM_COUNT = 15 const scrollOptions: ScrollIntoViewOptions = { block: 'start' } -export const Nfts = ({ nftList, onShowPortfolioSettings }: Props) => { +const emptyTokenIdsList: string[] = [] + +export const Nfts = ({ + networks, + accounts, + onShowPortfolioSettings, + tokenBalancesRegistry +}: Props) => { // routing const history = useHistory() const urlSearchParams = useQuery() @@ -116,6 +158,12 @@ export const Nfts = ({ nftList, onShowPortfolioSettings }: Props) => { ) const isPanel = useSafeUISelector(UISelectors.isPanel) + // local-storage + const [hideUnownedNfts] = useSyncedLocalStorage( + LOCAL_STORAGE_KEYS.HIDE_UNOWNED_NFTS, + false + ) + // state const [searchValue, setSearchValue] = React.useState('') const [showAddNftModal, setShowAddNftModal] = React.useState(false) @@ -134,7 +182,11 @@ export const Nfts = ({ nftList, onShowPortfolioSettings }: Props) => { // queries const { data: isNftAutoDiscoveryEnabled } = useGetNftDiscoveryEnabledStatusQuery() - const { data: simpleHashSpamNfts = [] } = useGetSimpleHashSpamNftsQuery() + const { data: simpleHashSpamNfts = [], isLoading: isLoadingSpamNfts } = + useGetSimpleHashSpamNftsQuery( + tab === 'collected' ? skipToken : { accounts } + ) + const { accounts: allAccounts } = useAccountsQuery() const { userTokensRegistry, hiddenNfts, visibleNfts } = useGetUserTokensRegistryQuery(undefined, { selectFromResult: (result) => ({ @@ -144,9 +196,213 @@ export const Nfts = ({ nftList, onShowPortfolioSettings }: Props) => { }) }) + const shouldFetchSpamNftBalances = !( + tab === 'collected' || + isLoadingSpamNfts || + (!hideUnownedNfts && accounts.length === 0) || + networks.length === 0 + ) + + const { data: spamTokenBalancesRegistry } = useBalancesFetcher( + shouldFetchSpamNftBalances + ? { + accounts, + networks, + isSpamRegistry: true + } + : skipToken + ) + // mutations const [setNftDiscovery] = useSetNftDiscoveryEnabledMutation() + // memos & computed + const { visibleUserNonSpamNfts, visibleUserMarkedSpamNfts } = + React.useMemo(() => { + const results: { + visibleUserNonSpamNfts: BraveWallet.BlockchainToken[] + visibleUserMarkedSpamNfts: BraveWallet.BlockchainToken[] + } = { + visibleUserNonSpamNfts: [], + visibleUserMarkedSpamNfts: [] + } + for (const nft of visibleNfts) { + if (nft.isSpam) { + results.visibleUserMarkedSpamNfts.push(nft) + } else { + if (nft.visible) { + results.visibleUserNonSpamNfts.push(nft) + } + } + } + return results + }, [visibleNfts]) + + const hiddenNftsIds = + userTokensRegistry?.nonFungibleHiddenTokenIds ?? emptyTokenIdsList + const userNonSpamNftIds = + userTokensRegistry?.nonSpamTokenIds ?? emptyTokenIdsList + + const [allSpamNfts, allSpamNftsIds] = React.useMemo(() => { + // filter out NFTs user has marked not spam + // hidden NFTs, and deleted NFTs + const excludedNftIds = userNonSpamNftIds + .concat(hiddenNftsIds) + .concat(userTokensRegistry?.deletedTokenIds || []) + const simpleHashList = simpleHashSpamNfts.filter( + (nft) => !excludedNftIds.includes(getAssetIdKey(nft)) + ) + const simpleHashListIds = simpleHashList.map((nft) => getAssetIdKey(nft)) + // add NFTs user has marked as NFT if they are not in the list + // to avoid duplicates + const fullSpamList = [ + ...simpleHashList, + ...visibleUserMarkedSpamNfts.filter( + (nft) => !simpleHashListIds.includes(getAssetIdKey(nft)) + ) + ] + + return [fullSpamList, fullSpamList.map((nft) => getAssetIdKey(nft))] + }, [ + visibleUserMarkedSpamNfts, + simpleHashSpamNfts, + hiddenNftsIds, + userNonSpamNftIds, + userTokensRegistry + ]) + + const hiddenAndSpamNfts = React.useMemo(() => { + return hiddenNfts.concat(allSpamNfts) + }, [allSpamNfts, hiddenNfts]) + + const selectedNftList = + selectedTab === 'collected' ? visibleUserNonSpamNfts : hiddenAndSpamNfts + + const sortedSelectedNftList = React.useMemo(() => { + return selectedNftList.slice().sort(compareFn) + }, [selectedNftList]) + + // Filters the user's tokens based on the users + // filteredOutPortfolioNetworkKeys pref and visible networks. + const sortedSelectedNftListForChains = React.useMemo(() => { + return sortedSelectedNftList.filter((token) => + networks.some( + (net) => net.chainId === token.chainId && net.coin === token.coin + ) + ) + }, [sortedSelectedNftList, networks]) + + // apply accounts filter to selected nfts list + const sortedSelectedNftListForChainsAndAccounts = React.useMemo(() => { + if (hideUnownedNfts) { + return sortedSelectedNftListForChains.filter((token) => { + return accounts.some((account) => { + const balance = getBalance( + account.accountId, + token, + tokenBalancesRegistry + ) + const spamBalance = getBalance( + account.accountId, + token, + spamTokenBalancesRegistry + ) + return ( + (balance && balance !== '0') || (spamBalance && spamBalance !== '0') + ) + }) + }) + } + + // skip balance checks if all accounts are selected + if (accounts.length === allAccounts.length) { + return sortedSelectedNftListForChains + } + + return sortedSelectedNftListForChains.filter((token) => { + return ( + accounts.some((account) => { + const balance = getBalance( + account.accountId, + token, + tokenBalancesRegistry + ) + const spamBalance = getBalance( + account.accountId, + token, + spamTokenBalancesRegistry + ) + return ( + (balance && balance !== '0') || (spamBalance && spamBalance !== '0') + ) + }) || + // not owned by any account + !allAccounts.some((account) => { + const balance = getBalance( + account.accountId, + token, + tokenBalancesRegistry + ) + const spamBalance = getBalance( + account.accountId, + token, + spamTokenBalancesRegistry + ) + return ( + (balance && balance !== '0') || (spamBalance && spamBalance !== '0') + ) + }) + ) + }) + }, [ + accounts, + allAccounts, + hideUnownedNfts, + sortedSelectedNftListForChains, + spamTokenBalancesRegistry, + tokenBalancesRegistry + ]) + + const searchedNfts = React.useMemo(() => { + return searchNfts( + searchValue, + sortedSelectedNftListForChainsAndAccounts + ).sort(compareFn) + }, [searchValue, sortedSelectedNftListForChainsAndAccounts]) + + const lastPageNumber = + Math.floor(searchedNfts.length / LIST_PAGE_ITEM_COUNT) + 1 + + const dropDownOptions: NftDropdownOption[] = React.useMemo(() => { + return [ + { + id: 'collected', + label: getLocale('braveNftsTabCollected'), + labelSummary: visibleUserNonSpamNfts.length + }, + { + id: 'hidden', + label: getLocale('braveNftsTabHidden'), + labelSummary: hiddenAndSpamNfts.length + } + ] + }, [visibleUserNonSpamNfts.length, hiddenAndSpamNfts.length]) + + const renderedListPage = React.useMemo(() => { + const pageStartItemIndex = + currentPageNumber * LIST_PAGE_ITEM_COUNT - LIST_PAGE_ITEM_COUNT + return searchedNfts.slice( + pageStartItemIndex, + pageStartItemIndex + LIST_PAGE_ITEM_COUNT + ) + }, [searchedNfts, currentPageNumber]) + + const isLoadingAssets = + !assetAutoDiscoveryCompleted || + (tab === 'hidden' && + (isLoadingSpamNfts || + (shouldFetchSpamNftBalances && !spamTokenBalancesRegistry))) + // methods const onSearchValueChange = React.useCallback( (event: React.ChangeEvent) => { @@ -204,22 +460,6 @@ export const Nfts = ({ nftList, onShowPortfolioSettings }: Props) => { [history] ) - const searchNfts = React.useCallback( - (item: BraveWallet.BlockchainToken) => { - const tokenId = new Amount(item.tokenId).toNumber().toString() - - return ( - item.name.toLowerCase() === searchValue.toLowerCase() || - item.name.toLowerCase().includes(searchValue.toLowerCase()) || - item.symbol.toLocaleLowerCase() === searchValue.toLowerCase() || - item.symbol.toLowerCase().includes(searchValue.toLowerCase()) || - tokenId === searchValue.toLowerCase() || - tokenId.includes(searchValue.toLowerCase()) - ) - }, - [searchValue] - ) - const onCloseSearchBar = React.useCallback(() => { setShowSearchBar(false) setSearchValue('') @@ -233,130 +473,20 @@ export const Nfts = ({ nftList, onShowPortfolioSettings }: Props) => { [history, selectedTab] ) - // memos - const { userNonSpamNfts, userMarkedSpamNfts } = React.useMemo(() => { - const results: { - userNonSpamNfts: BraveWallet.BlockchainToken[] - userMarkedSpamNfts: BraveWallet.BlockchainToken[] - } = { - userNonSpamNfts: [], - userMarkedSpamNfts: [] - } - for (const nft of nftList) { - if (nft.isSpam) { - results.userMarkedSpamNfts.push(nft) - } else { - if (nft.visible) { - results.userNonSpamNfts.push(nft) - } - } - } - return results - }, [nftList]) - - const [hiddenNftsIds, userNonSpamNftIds] = React.useMemo(() => { - if (!userTokensRegistry) { - return [[], []] - } - return [ - userTokensRegistry.nonFungibleHiddenTokenIds, - userTokensRegistry.nonSpamTokenIds - ] - }, [userTokensRegistry]) - - const [allSpamNfts, allSpamNftsIds] = React.useMemo(() => { - // filter out NFTs user has marked not spam - // hidden NFTs, - // and deleted NFTs - const excludedNftIds = userNonSpamNftIds - .concat(hiddenNftsIds) - .concat(userTokensRegistry?.deletedTokenIds || []) - const simpleHashList = simpleHashSpamNfts.filter( - (nft) => !excludedNftIds.includes(getAssetIdKey(nft)) - ) - const simpleHashListIds = simpleHashList.map((nft) => getAssetIdKey(nft)) - // add NFTs user has marked as NFT if they are not in the list - // to avoid duplicates - const fullSpamList = [ - ...simpleHashList, - ...userMarkedSpamNfts.filter( - (nft) => !simpleHashListIds.includes(getAssetIdKey(nft)) - ) - ] - - return [fullSpamList, fullSpamList.map((nft) => getAssetIdKey(nft))] - }, [ - userMarkedSpamNfts, - simpleHashSpamNfts, - hiddenNftsIds, - userNonSpamNftIds, - userTokensRegistry - ]) - - const [sortedNfts, sortedHiddenNfts, sortedSpamNfts] = React.useMemo(() => { - if (searchValue === '') { - return [ - userNonSpamNfts.slice().sort(compareFn), - hiddenNfts.slice().sort(compareFn), - allSpamNfts.slice().sort(compareFn) - ] - } - - return [ - userNonSpamNfts.filter(searchNfts).sort(compareFn), - hiddenNfts.filter(searchNfts).sort(compareFn), - allSpamNfts.filter(searchNfts).sort(compareFn) - ] - }, [searchValue, userNonSpamNfts, hiddenNfts, allSpamNfts, searchNfts]) - - const dropDownOptions: NftDropdownOption[] = React.useMemo(() => { - return [ - { - id: 'collected', - label: getLocale('braveNftsTabCollected'), - labelSummary: sortedNfts.length - }, - { - id: 'hidden', - label: getLocale('braveNftsTabHidden'), - labelSummary: sortedHiddenNfts.concat(sortedSpamNfts).length - } - ] - }, [sortedHiddenNfts, sortedSpamNfts, sortedNfts]) - - const renderedList = React.useMemo(() => { - switch (selectedTab) { - case 'collected': - return sortedNfts - case 'hidden': - return sortedHiddenNfts.concat(sortedSpamNfts) - default: - return sortedNfts - } - }, [selectedTab, sortedNfts, sortedHiddenNfts, sortedSpamNfts]) - - const renderedListPage = React.useMemo(() => { - const pageStartItemIndex = - currentPageNumber * LIST_PAGE_ITEM_COUNT - LIST_PAGE_ITEM_COUNT - return renderedList.slice( - pageStartItemIndex, - pageStartItemIndex + LIST_PAGE_ITEM_COUNT - ) - }, [renderedList, currentPageNumber]) - - // computed - const lastPageNumber = - Math.floor(renderedList.length / LIST_PAGE_ITEM_COUNT) + 1 - // effects React.useEffect(() => { - braveWalletP3A.recordNFTGalleryView(nftList.length) - }, [braveWalletP3A, nftList]) + braveWalletP3A.recordNFTGalleryView(visibleNfts.length) + }, [braveWalletP3A, visibleNfts.length]) React.useEffect(() => { dispatch(WalletActions.refreshNetworksAndTokens({})) }, [assetAutoDiscoveryCompleted, dispatch]) + React.useEffect(() => { + // redirect to first page when networks or accounts change + history.push(makePortfolioNftsRoute(selectedTab, 1)) + }, [history, networks, accounts, selectedTab]) + return ( { ref={listScrollContainerRef} fullHeight > - {nftList.length === 0 && + {visibleNfts.length === 0 && userTokensRegistry?.hiddenTokenIds.length === 0 ? ( isNftAutoDiscoveryEnabled ? ( { /> ) })} - {!assetAutoDiscoveryCompleted && } + {isLoadingAssets && } { @@ -153,8 +145,6 @@ export const PortfolioOverview = () => { ) // redux - const dispatch = useDispatch() - const nftMetadata = useUnsafePageSelector(PageSelectors.nftMetadata) const isPanel = useSafeUISelector(UISelectors.isPanel) // queries @@ -164,7 +154,8 @@ export const PortfolioOverview = () => { useGetUserTokensRegistryQuery(undefined, { selectFromResult: (result) => ({ isLoadingUserTokens: result.isLoading, - userVisibleTokensInfo: selectAllVisibleUserAssetsFromQueryResult(result) + userVisibleTokensInfo: + selectAllVisibleFungibleUserAssetsFromQueryResult(result) }) }) const { data: defaultFiat } = useGetDefaultFiatCurrencyQuery() @@ -261,22 +252,11 @@ export const PortfolioOverview = () => { const visibleTokensForFilteredChains = React.useMemo(() => { return userTokensWithRewards.filter((token) => visiblePortfolioNetworkIds.includes( - networkEntityAdapter - .selectId({ - chainId: token.chainId, - coin: token.coin - }) - .toString() + networkEntityAdapter.selectId(token).toString() ) ) }, [userTokensWithRewards, visiblePortfolioNetworkIds]) - const userVisibleNfts = React.useMemo(() => { - return visibleTokensForFilteredChains.filter( - (token) => token.isErc721 || token.isNft - ) - }, [visibleTokensForFilteredChains]) - const { data: tokenBalancesRegistry } = // wait to see if we need rewards before fetching useBalancesFetcher( @@ -339,22 +319,17 @@ export const PortfolioOverview = () => { // wait for balances before computing this list return [] } - return visibleTokensForFilteredChains - .filter( - (asset) => - asset.visible && !asset.isErc721 && !asset.isErc1155 && !asset.isNft - ) - .map((asset) => { - return { - asset, - assetBalance: - getIsRewardsToken(asset) && rewardsBalance - ? new Amount(rewardsBalance) - .multiplyByDecimals(asset.decimals) - .format() - : fullAssetBalance(asset) - } - }) + return visibleTokensForFilteredChains.map((asset) => { + return { + asset, + assetBalance: + getIsRewardsToken(asset) && rewardsBalance + ? new Amount(rewardsBalance) + .multiplyByDecimals(asset.decimals) + .format() + : fullAssetBalance(asset) + } + }) }, [ visibleTokensForFilteredChains, fullAssetBalance, @@ -494,18 +469,9 @@ export const PortfolioOverview = () => { // methods const onSelectAsset = React.useCallback( (asset: BraveWallet.BlockchainToken) => { - if ((asset.isErc721 || asset.isNft) && nftMetadata) { - // reset nft metadata - dispatch(WalletPageActions.updateNFTMetadata(undefined)) - } - history.push( - makePortfolioAssetRoute( - asset.isErc721 || asset.isNft || asset.isErc1155, - getAssetIdKey(asset) - ) - ) + history.push(makePortfolioAssetRoute(false, getAssetIdKey(asset))) }, - [dispatch, history, nftMetadata] + [history] ) const tokenLists = React.useMemo(() => { @@ -675,10 +641,10 @@ export const PortfolioOverview = () => { exact > setShowPortfolioSettings(true)} - // tokenBalancesRegistry={tokenBalancesRegistry} + tokenBalancesRegistry={tokenBalancesRegistry} /> diff --git a/components/brave_wallet_ui/stories/locale.ts b/components/brave_wallet_ui/stories/locale.ts index faeb2dade4776..8d96185bcf895 100644 --- a/components/brave_wallet_ui/stories/locale.ts +++ b/components/brave_wallet_ui/stories/locale.ts @@ -1043,6 +1043,7 @@ provideStrings({ braveWalletShowSpamNftsTitle: 'Spam NFTs', braveWalletShowSpamNftsDescription: 'Show Spam NFTs', braveWalletPortfolioSettings: 'Portfolio Settings', + braveWalletHideNotOwnedNfTs: 'Hide not owned NFTs', // Account Filter braveWalletAccountFilterAllAccounts: 'All accounts', diff --git a/components/brave_wallet_ui/utils/local-storage-utils.ts b/components/brave_wallet_ui/utils/local-storage-utils.ts index 362512edc45b6..0588abfd447a6 100644 --- a/components/brave_wallet_ui/utils/local-storage-utils.ts +++ b/components/brave_wallet_ui/utils/local-storage-utils.ts @@ -144,6 +144,23 @@ export const getPersistedPortfolioTokenBalances = (): TokenBalancesRegistry => { } } +export const getPersistedPortfolioSpamTokenBalances = + (): TokenBalancesRegistry => { + try { + const registry: TokenBalancesRegistry = JSON.parse( + window.localStorage.getItem(LOCAL_STORAGE_KEYS.SPAM_TOKEN_BALANCES) || + JSON.stringify(createEmptyTokenBalancesRegistry()) + ) + if (registry.accounts) { + return registry + } + return createEmptyTokenBalancesRegistry() + } catch (error) { + console.error(error) + return createEmptyTokenBalancesRegistry() + } + } + export const setPersistedPortfolioTokenBalances = ( registry: TokenBalancesRegistry ) => { @@ -156,3 +173,16 @@ export const setPersistedPortfolioTokenBalances = ( console.error(error) } } + +export const setPersistedPortfolioSpamTokenBalances = ( + registry: TokenBalancesRegistry +) => { + try { + window.localStorage.setItem( + LOCAL_STORAGE_KEYS.SPAM_TOKEN_BALANCES, + JSON.stringify(registry) + ) + } catch (error) { + console.error(error) + } +} diff --git a/components/resources/wallet_strings.grdp b/components/resources/wallet_strings.grdp index 60fead02826ba..bcef98b025c13 100644 --- a/components/resources/wallet_strings.grdp +++ b/components/resources/wallet_strings.grdp @@ -1081,4 +1081,5 @@ Account name must be $130 characters or less Enter password (if applicable) Import type + Hide not owned NFTs