Fix(wallet): paginate portfolio NFTs page (#23816)

* fix: paginate portfolio NFTs page

* feat(wallet): allow hiding unowned NFTs

* fix(wallet): remove group-by filter from NFTs portfolio view

* fix(wallet): reset nfts portfolio page number when saving new filters

* fix(wallet): update found portfolio NFTs counting and loading logic
This commit is contained in:
Josh Leonard
2024-06-05 13:07:47 -06:00
committed by GitHub
parent f73de30870
commit 407295f1ae
20 changed files with 757 additions and 428 deletions
@@ -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},
@@ -90,6 +90,10 @@ export class BaseQueryCache {
private _nftMetadataRegistry: Record<string, NFTMetadataReturnType> = {}
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) {
@@ -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 = {
@@ -16,7 +16,7 @@ import {
GetTokenBalancesRegistryArg //
} from '../slices/endpoints/token_balances.endpoints'
type Arg = Pick<GetTokenBalancesRegistryArg, 'networks'> & {
type Arg = Pick<GetTokenBalancesRegistryArg, 'networks' | 'isSpamRegistry'> & {
accounts: BraveWallet.AccountInfo[]
}
@@ -45,7 +45,8 @@ export const useBalancesFetcher = (arg: Arg | typeof skipToken) => {
supportedKeyrings
})
),
useAnkrBalancesFeature
useAnkrBalancesFeature,
isSpamRegistry: arg.isSpamRegistry
}
: skipToken,
{
@@ -206,46 +206,25 @@ export const nftsEndpoints = ({
}
}
}),
getSimpleHashSpamNfts: query<BraveWallet.BlockchainToken[], void>({
queryFn: async (_arg, { endpoint }, _extraOptions, baseQuery) => {
/** 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 { data: api, cache } = baseQuery(undefined)
const { braveWalletService } = api
const { cache } = baseQuery(undefined)
const networksRegistry = await cache.getNetworksRegistry()
const lookupAccounts =
arg?.accounts ?? (await cache.getAllAccounts()).accounts
const chainIds = networksRegistry.ids.map(
(network) => networksRegistry.entities[network]!.chainId
)
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)
@@ -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',
@@ -66,11 +66,10 @@ import { ContentWrapper, ButtonRow } from './portfolio-filters-modal.style'
interface Props {
onClose: () => void
onSave?: () => void
}
export const PortfolioFiltersModal = (props: Props) => {
const { onClose } = props
export const PortfolioFiltersModal = ({ onClose, onSave }: Props) => {
// routing
const { pathname: currentRoute } = useLocation()
@@ -107,6 +106,10 @@ export const PortfolioFiltersModal = (props: Props) => {
LOCAL_STORAGE_KEYS.SHOW_NETWORK_LOGO_ON_NFTS,
false
)
const [hideUnownedNfts, setHideUnownedNfts] = useSyncedLocalStorage<boolean>(
LOCAL_STORAGE_KEYS.HIDE_UNOWNED_NFTS,
false
)
// queries
const { data: defaultFiatCurrency = 'usd' } = useGetDefaultFiatCurrencyQuery()
@@ -128,6 +131,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 +156,8 @@ export const PortfolioFiltersModal = (props: Props) => {
setSelectedAssetFilter(selectedAssetFilterOption)
setHidePortfolioSmallBalances(hideSmallBalances)
setShowNetworkLogoOnNfts(showNetworkLogo)
setHideUnownedNfts(hideUnownedNftsToggle)
onSave?.()
onClose()
}, [
setFilteredOutPortfolioNetworkKeys,
@@ -165,6 +172,9 @@ export const PortfolioFiltersModal = (props: Props) => {
hideSmallBalances,
setShowNetworkLogoOnNfts,
showNetworkLogo,
setHideUnownedNfts,
hideUnownedNftsToggle,
onSave,
onClose
])
@@ -185,7 +195,7 @@ export const PortfolioFiltersModal = (props: Props) => {
fullWidth={true}
alignItems='flex-start'
>
{showNftFilters && (
{showNftFilters ? (
<>
<FilterToggleSection
title={getLocale('braveWalletShowNetworkLogoOnNftsTitle')}
@@ -197,6 +207,14 @@ export const PortfolioFiltersModal = (props: Props) => {
setIsSelected={() => setShowNetworkLogo((prev) => !prev)}
/>
<FilterToggleSection
title={getLocale('braveWalletHideNotOwnedNfTs')}
description={''}
icon='web3'
isSelected={hideUnownedNftsToggle}
setIsSelected={() => setHideUnownedNftsToggle((prev) => !prev)}
/>
{/* Disabled until Spam NFTs feature is implemented in core */}
{/* <FilterToggleSection
title={getLocale('braveWalletShowSpamNftsTitle')}
@@ -208,19 +226,18 @@ export const PortfolioFiltersModal = (props: Props) => {
}
/> */}
</>
)}
<FilterDropdownSection
title={getLocale('braveWalletPortfolioGroupByTitle')}
description={getLocale('braveWalletPortfolioGroupByDescription')}
icon='stack'
dropdownOptions={GroupAssetsByOptions}
selectedOptionId={selectedGroupAssetsByOption}
onSelectOption={setSelectedGroupAssetsByOption}
/>
{!showNftFilters && (
) : (
<>
<FilterDropdownSection
title={getLocale('braveWalletPortfolioGroupByTitle')}
description={getLocale(
'braveWalletPortfolioGroupByDescription'
)}
icon='stack'
dropdownOptions={GroupAssetsByOptions}
selectedOptionId={selectedGroupAssetsByOption}
onSelectOption={setSelectedGroupAssetsByOption}
/>
<FilterDropdownSection
title={getLocale('braveWalletSortAssets')}
description={getLocale('braveWalletSortAssetsDescription')}
@@ -5,6 +5,9 @@
import * as React from 'react'
// types
import { NftDropdownOptionId } from '../../../../../../constants/types'
// styles
import {
DropdownButton,
@@ -16,8 +19,6 @@ import {
DropdownContainer
} from './nft-group-selector.styles'
export type NftDropdownOptionId = 'collected' | 'hidden'
export interface NftDropdownOption {
id: NftDropdownOptionId
label: string
@@ -5,13 +5,26 @@
import * as React from 'react'
import { useHistory } from 'react-router-dom'
import { useDispatch } from 'react-redux'
import { skipToken } from '@reduxjs/toolkit/query'
// types
import { BraveWallet, WalletRoutes } from '../../../../../constants/types'
import {
BraveWallet,
NftDropdownOptionId
} from '../../../../../constants/types'
import {
TokenBalancesRegistry //
} from '../../../../../common/slices/entities/token-balance.entity'
// hooks
import { useNftPin } from '../../../../../common/hooks/nft-pin'
import { useLocalStorage } from '../../../../../common/hooks/use_local_storage'
import { useAccountsQuery } from '../../../../../common/slices/api.slice.extra'
import {
useBalancesFetcher //
} from '../../../../../common/hooks/use-balances-fetcher'
import {
useSyncedLocalStorage //
} from '../../../../../common/hooks/use_local_storage'
// selectors
import {
@@ -36,20 +49,18 @@ import {
useGetUserTokensRegistryQuery,
useSetNftDiscoveryEnabledMutation
} from '../../../../../common/slices/api.slice'
import { getBalance } from '../../../../../utils/balance-utils'
import {
AccountsGroupByOption,
NetworksGroupByOption,
NoneGroupByOption
} from '../../../../../options/group-assets-by-options'
import { useApiProxy } from '../../../../../common/hooks/use-api-proxy'
import { getAssetIdKey } from '../../../../../utils/asset-utils'
import { useQuery } from '../../../../../common/hooks/use-query'
import { makePortfolioAssetRoute } from '../../../../../utils/routes-utils'
import {
makePortfolioAssetRoute,
makePortfolioNftsRoute
} from '../../../../../utils/routes-utils'
import {
selectAllVisibleUserNFTsFromQueryResult,
selectHiddenNftsFromQueryResult //
} from '../../../../../common/slices/entities/blockchain-token.entity'
import { getBalance } from '../../../../../utils/balance-utils'
// components
import SearchBar from '../../../../shared/search-bar'
@@ -57,10 +68,18 @@ import { NFTGridViewItem } from '../../portfolio/components/nft-grid-view/nft-gr
import { EnableNftDiscoveryModal } from '../../../popup-modals/enable-nft-discovery-modal/enable-nft-discovery-modal'
import { AutoDiscoveryEmptyState } from './auto-discovery-empty-state/auto-discovery-empty-state'
import { NftIpfsBanner } from '../../../nft-ipfs-banner/nft-ipfs-banner'
import {
NftGridViewItemSkeleton //
} from '../../portfolio/components/nft-grid-view/nft-grid-view-item-skeleton'
import { Pagination } from '../../../../shared/pagination/pagination'
import {
NftDropdown,
NftDropdownOption
} from './nft-group-selector/nft-group-selector'
// styles
import { BannerWrapper, NFTListWrapper, NftGrid } from './nfts.styles'
import { Row } from '../../../../shared/style'
import { Column, Row } from '../../../../shared/style'
import { AddOrEditNftModal } from '../../../popup-modals/add-edit-nft-modal/add-edit-nft-modal'
import { NftsEmptyState } from './nfts-empty-state/nfts-empty-state'
import {
@@ -70,24 +89,12 @@ import {
ControlBarWrapper,
ContentWrapper
} from '../../portfolio/style'
import { AssetGroupContainer } from '../../../asset-group-container/asset-group-container'
import { networkEntityAdapter } from '../../../../../common/slices/entities/network.entity'
import {
TokenBalancesRegistry //
} from '../../../../../common/slices/entities/token-balance.entity'
import { NftGridViewItemSkeleton } from '../../portfolio/components/nft-grid-view/nft-grid-view-item-skeleton'
import {
NftDropdown,
NftDropdownOption,
NftDropdownOptionId
} from './nft-group-selector/nft-group-selector'
interface Props {
networks: BraveWallet.NetworkInfo[]
nftList: BraveWallet.BlockchainToken[]
accounts: BraveWallet.AccountInfo[]
onShowPortfolioSettings?: () => void
tokenBalancesRegistry: TokenBalancesRegistry | undefined | null
accounts: BraveWallet.AccountInfo[]
tokenBalancesRegistry: TokenBalancesRegistry | null | undefined
networks: BraveWallet.NetworkInfo[]
}
const compareFn = (
@@ -95,24 +102,50 @@ const compareFn = (
b: BraveWallet.BlockchainToken
) => a.name.localeCompare(b.name)
export const Nfts = (props: Props) => {
const {
nftList,
accounts,
networks,
onShowPortfolioSettings,
tokenBalancesRegistry
} = props
const searchNfts = (
searchValue: string,
items: BraveWallet.BlockchainToken[]
) => {
if (searchValue === '') {
return items
}
const { braveWalletP3A } = useApiProxy()
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)
)
})
}
// local-storage
const [selectedGroupAssetsByItem] = useLocalStorage<string>(
LOCAL_STORAGE_KEYS.GROUP_PORTFOLIO_ASSETS_BY,
NoneGroupByOption.id
)
const LIST_PAGE_ITEM_COUNT = 15
const scrollOptions: ScrollIntoViewOptions = { block: 'start' }
const emptyTokenIdsList: string[] = []
export const Nfts = ({
networks,
accounts,
onShowPortfolioSettings,
tokenBalancesRegistry
}: Props) => {
// routing
const history = useHistory()
const urlSearchParams = useQuery()
const tab = urlSearchParams.get('tab')
const currentPageNumber = Number(urlSearchParams.get('page')) || 1
const selectedTab: NftDropdownOptionId =
tab === 'collected' || tab === 'hidden' ? tab : 'collected'
// refs
const listScrollContainerRef = React.useRef<HTMLDivElement>(null)
// redux
const dispatch = useDispatch()
const isNftPinningFeatureEnabled = useSafeWalletSelector(
WalletSelectors.isNftPinningFeatureEnabled
)
@@ -124,6 +157,12 @@ export const Nfts = (props: Props) => {
)
const isPanel = useSafeUISelector(UISelectors.isPanel)
// local-storage
const [hideUnownedNfts] = useSyncedLocalStorage<boolean>(
LOCAL_STORAGE_KEYS.HIDE_UNOWNED_NFTS,
false
)
// state
const [searchValue, setSearchValue] = React.useState<string>('')
const [showAddNftModal, setShowAddNftModal] = React.useState<boolean>(false)
@@ -135,19 +174,18 @@ export const Nfts = (props: Props) => {
)
const [showSearchBar, setShowSearchBar] = React.useState<boolean>(false)
// hooks
const history = useHistory()
const dispatch = useDispatch()
// custom hooks
const { braveWalletP3A } = useApiProxy()
const { isIpfsBannerVisible, onToggleShowIpfsBanner } = useNftPin()
const urlSearchParams = useQuery()
const tab = urlSearchParams.get('tab')
const selectedTab: NftDropdownOptionId =
tab === 'collected' || tab === 'hidden' ? tab : 'collected'
// queries
const { data: isNftAutoDiscoveryEnabled } =
useGetNftDiscoveryEnabledStatusQuery()
const { data: simpleHashSpamNfts = [] } = useGetSimpleHashSpamNftsQuery()
const { data: simpleHashSpamNfts = [], isFetching: isLoadingSpamNfts } =
useGetSimpleHashSpamNftsQuery(
selectedTab === 'collected' || !accounts.length ? skipToken : { accounts }
)
const { accounts: allAccounts } = useAccountsQuery()
const { userTokensRegistry, hiddenNfts, visibleNfts } =
useGetUserTokensRegistryQuery(undefined, {
selectFromResult: (result) => ({
@@ -157,15 +195,227 @@ export const Nfts = (props: Props) => {
})
})
const shouldFetchSpamNftBalances =
selectedTab === 'hidden' &&
!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 { searchResults, totalNftsFound } = React.useMemo(() => {
const searchResults = searchNfts(
searchValue,
sortedSelectedNftListForChainsAndAccounts
)
return {
searchResults,
totalNftsFound: searchResults.length
}
}, [searchValue, sortedSelectedNftListForChainsAndAccounts])
const lastPageNumber =
Math.floor(searchResults.length / LIST_PAGE_ITEM_COUNT) + 1
/** label summary is shown only on the selected tab */
const dropDownOptions: NftDropdownOption[] = React.useMemo(() => {
return [
{
id: 'collected',
label: getLocale('braveNftsTabCollected'),
labelSummary: totalNftsFound
},
{
id: 'hidden',
label: getLocale('braveNftsTabHidden'),
labelSummary: totalNftsFound
}
]
}, [totalNftsFound])
const renderedListPage = React.useMemo(() => {
const pageStartItemIndex =
currentPageNumber * LIST_PAGE_ITEM_COUNT - LIST_PAGE_ITEM_COUNT
return searchResults.slice(
pageStartItemIndex,
pageStartItemIndex + LIST_PAGE_ITEM_COUNT
)
}, [searchResults, currentPageNumber])
const isLoadingAssets =
!assetAutoDiscoveryCompleted ||
(selectedTab === 'hidden' &&
(isLoadingSpamNfts ||
(shouldFetchSpamNftBalances && !spamTokenBalancesRegistry)))
// methods
const onSearchValueChange = React.useCallback(
(event: React.ChangeEvent<HTMLInputElement>) => {
setSearchValue(event.target.value)
if (currentPageNumber !== 1) {
history.push(makePortfolioNftsRoute(selectedTab, 1))
}
},
[]
[currentPageNumber, history, selectedTab]
)
const onSelectAsset = React.useCallback(
@@ -206,266 +456,28 @@ export const Nfts = (props: Props) => {
const onSelectOption = React.useCallback(
(selectedOption: NftDropdownOption) => {
history.push({
pathname: WalletRoutes.PortfolioNFTs,
search: `?tab=${selectedOption.id}`
})
history.push(makePortfolioNftsRoute(selectedOption.id, 1))
},
[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('')
}, [])
React.useEffect(() => {
braveWalletP3A.recordNFTGalleryView(nftList.length)
}, [braveWalletP3A, nftList])
// memos
const { userNonSpamNfts, userMarkedSpamNfts } = React.useMemo(() => {
const results: {
userNonSpamNfts: BraveWallet.BlockchainToken[]
userMarkedSpamNfts: BraveWallet.BlockchainToken[]
} = {
userNonSpamNfts: [],
userMarkedSpamNfts: []
}
for (const nft of nftList) {
if (!nft.isSpam) {
if (nft.visible) {
results.userNonSpamNfts.push(nft)
}
} else {
results.userMarkedSpamNfts.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])
// Returns a list of assets based on provided account
const getFilteredNftsByAccount = React.useCallback(
(account: BraveWallet.AccountInfo) => {
return renderedList.filter(
(nft) =>
nft.coin === account.accountId.coin &&
new Amount(
getBalance(account.accountId, nft, tokenBalancesRegistry)
).gte('1')
)
const navigateToPage = React.useCallback(
(pageNumber: number) => {
history.push(makePortfolioNftsRoute(selectedTab, pageNumber))
listScrollContainerRef.current?.scrollIntoView(scrollOptions)
},
[renderedList, tokenBalancesRegistry]
[history, selectedTab]
)
// Returns a list of assets based on provided network
const getAssetsByNetwork = React.useCallback(
(network: BraveWallet.NetworkInfo) => {
return renderedList.filter(
(asset) =>
networkEntityAdapter
.selectId({
chainId: asset.chainId,
coin: asset.coin
})
.toString() === networkEntityAdapter.selectId(network).toString()
)
},
[renderedList]
)
const renderGridViewItem = React.useCallback(
(nft: BraveWallet.BlockchainToken) => {
const assetId = getAssetIdKey(nft)
const isSpam = allSpamNftsIds.includes(assetId)
return (
<NFTGridViewItem
key={assetId}
token={nft}
onSelectAsset={() => onSelectAsset(nft)}
isTokenHidden={
userTokensRegistry?.nonFungibleHiddenTokenIds.includes(assetId) ||
isSpam
}
isTokenSpam={isSpam}
/>
)
},
[userTokensRegistry, allSpamNftsIds, onSelectAsset]
)
const listUiByAccounts = React.useMemo(() => {
return accounts.map((account) => (
<React.Fragment key={account.accountId.uniqueKey}>
{getFilteredNftsByAccount(account).length !== 0 && (
<AssetGroupContainer
balance=''
hideBalance={true}
account={account}
isDisabled={getFilteredNftsByAccount(account).length === 0}
>
<NftGrid>
{getFilteredNftsByAccount(account).map(renderGridViewItem)}
{!assetAutoDiscoveryCompleted && <NftGridViewItemSkeleton />}
</NftGrid>
</AssetGroupContainer>
)}
</React.Fragment>
))
}, [
accounts,
assetAutoDiscoveryCompleted,
getFilteredNftsByAccount,
renderGridViewItem
])
const listUiByNetworks = React.useMemo(() => {
return networks?.map((network) => (
<React.Fragment key={networkEntityAdapter.selectId(network).toString()}>
{getAssetsByNetwork(network).length !== 0 && (
<AssetGroupContainer
balance=''
hideBalance={true}
network={network}
isDisabled={getAssetsByNetwork(network).length === 0}
>
<NftGrid>
{getAssetsByNetwork(network).map(renderGridViewItem)}
{!assetAutoDiscoveryCompleted && <NftGridViewItemSkeleton />}
</NftGrid>
</AssetGroupContainer>
)}
</React.Fragment>
))
}, [
assetAutoDiscoveryCompleted,
getAssetsByNetwork,
networks,
renderGridViewItem
])
const listUi = React.useMemo(() => {
return selectedGroupAssetsByItem === NetworksGroupByOption.id ? (
listUiByNetworks
) : selectedGroupAssetsByItem === AccountsGroupByOption.id ? (
listUiByAccounts
) : (
<NftGrid padding='0px'>
{renderedList.map(renderGridViewItem)}
{!assetAutoDiscoveryCompleted && <NftGridViewItemSkeleton />}
</NftGrid>
)
}, [
selectedGroupAssetsByItem,
listUiByNetworks,
listUiByAccounts,
renderedList,
renderGridViewItem,
assetAutoDiscoveryCompleted
])
// effects
React.useEffect(() => {
dispatch(WalletActions.refreshNetworksAndTokens({}))
}, [assetAutoDiscoveryCompleted, dispatch])
braveWalletP3A.recordNFTGalleryView(visibleNfts.length)
}, [braveWalletP3A, visibleNfts.length])
return (
<ContentWrapper
@@ -541,8 +553,11 @@ export const Nfts = (props: Props) => {
)}
</Row>
</ControlBarWrapper>
<NFTListWrapper>
{nftList.length === 0 &&
<NFTListWrapper
ref={listScrollContainerRef}
fullHeight
>
{visibleNfts.length === 0 &&
userTokensRegistry?.hiddenTokenIds.length === 0 ? (
isNftAutoDiscoveryEnabled ? (
<AutoDiscoveryEmptyState
@@ -554,7 +569,45 @@ export const Nfts = (props: Props) => {
<NftsEmptyState onImportNft={toggleShowAddNftModal} />
)
) : (
listUi
<>
<NftGrid padding='0px'>
{renderedListPage.map((nft) => {
const assetId = getAssetIdKey(nft)
const isSpam = allSpamNftsIds.includes(assetId)
return (
<NFTGridViewItem
key={assetId}
token={nft}
onSelectAsset={onSelectAsset}
isTokenHidden={
isSpam ||
Boolean(
userTokensRegistry?.nonFungibleHiddenTokenIds.includes(
assetId
)
)
}
isTokenSpam={isSpam}
/>
)
})}
{isLoadingAssets && <NftGridViewItemSkeleton />}
</NftGrid>
<Row
width='100%'
margin={'24px 0px 0px 0px'}
>
<Column width='50%'>
<Pagination
onSelectPageNumber={navigateToPage}
currentPageNumber={currentPageNumber}
lastPageNumber={lastPageNumber}
/>
</Column>
</Row>
</>
)}
</NFTListWrapper>
{showAddNftModal && (
@@ -56,7 +56,7 @@ interface Props {
token: BraveWallet.BlockchainToken
isTokenHidden: boolean
isTokenSpam: boolean
onSelectAsset: () => void
onSelectAsset: (token: BraveWallet.BlockchainToken) => void
}
export const NFTGridViewItem = (props: Props) => {
@@ -165,7 +165,7 @@ export const NFTGridViewItem = (props: Props) => {
}}
onClose={() => setShowMore(false)}
/>
<DIVForClickableArea onClick={onSelectAsset} />
<DIVForClickableArea onClick={() => onSelectAsset(token)} />
{isTokenSpam && (
<JunkMarker>
{getLocale('braveWalletNftJunk')}
@@ -5,17 +5,12 @@
import * as React from 'react'
import { skipToken } from '@reduxjs/toolkit/query/react'
import { useDispatch } from 'react-redux'
import { useHistory } from 'react-router'
import { useHistory, useLocation } from 'react-router'
import { Route, Switch, Redirect } from 'react-router-dom'
// selectors
import {
useUnsafePageSelector,
useSafeUISelector
} from '../../../../common/hooks/use-safe-selector'
import { useSafeUISelector } from '../../../../common/hooks/use-safe-selector'
import { UISelectors } from '../../../../common/selectors'
import { PageSelectors } from '../../../../page/selectors'
// hooks
import {
@@ -38,9 +33,6 @@ import {
} from '../../../../constants/types'
import { emptyRewardsInfo } from '../../../../common/async/base-query-cache'
// actions
import { WalletPageActions } from '../../../../page/actions'
// Utils
import Amount from '../../../../utils/amount'
import {
@@ -115,12 +107,13 @@ import {
querySubscriptionOptions60s //
} from '../../../../common/slices/constants'
import {
selectAllVisibleUserAssetsFromQueryResult //
selectAllVisibleFungibleUserAssetsFromQueryResult //
} from '../../../../common/slices/entities/blockchain-token.entity'
export const PortfolioOverview = () => {
// routing
const history = useHistory()
const location = useLocation()
// local-storage
const [filteredOutPortfolioNetworkKeys] = useLocalStorage(
@@ -153,8 +146,6 @@ export const PortfolioOverview = () => {
)
// redux
const dispatch = useDispatch()
const nftMetadata = useUnsafePageSelector(PageSelectors.nftMetadata)
const isPanel = useSafeUISelector(UISelectors.isPanel)
// queries
@@ -164,7 +155,8 @@ export const PortfolioOverview = () => {
useGetUserTokensRegistryQuery(undefined, {
selectFromResult: (result) => ({
isLoadingUserTokens: result.isLoading,
userVisibleTokensInfo: selectAllVisibleUserAssetsFromQueryResult(result)
userVisibleTokensInfo:
selectAllVisibleFungibleUserAssetsFromQueryResult(result)
})
})
const { data: defaultFiat } = useGetDefaultFiatCurrencyQuery()
@@ -261,22 +253,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 +320,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 +470,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,8 +642,7 @@ export const PortfolioOverview = () => {
exact
>
<Nfts
networks={networks}
nftList={userVisibleNfts}
networks={visiblePortfolioNetworks}
accounts={usersFilteredAccounts}
onShowPortfolioSettings={() => setShowPortfolioSettings(true)}
tokenBalancesRegistry={tokenBalancesRegistry}
@@ -692,7 +658,18 @@ export const PortfolioOverview = () => {
{showPortfolioSettings && (
<PortfolioFiltersModal
onClose={() => setShowPortfolioSettings(false)}
onSave={() => {
// reset to first page after filters change
const newParams = new URLSearchParams(location.search)
newParams.delete('page')
history.push({
...location,
search: `?${newParams.toString()}`
})
}}
onClose={() => {
setShowPortfolioSettings(false)
}}
/>
)}
</>
@@ -0,0 +1,31 @@
// Copyright (c) 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import * as React from 'react'
import {
WalletPanelStory //
} from '../../../stories/wrappers/wallet-panel-story-wrapper'
import { Pagination } from './pagination'
export const _Pagination = () => {
// state
const [currentPageNumber, onSelectPageNumber] = React.useState(1)
// render
return (
<WalletPanelStory>
<Pagination
currentPageNumber={currentPageNumber}
onSelectPageNumber={onSelectPageNumber}
lastPageNumber={99999}
/>
</WalletPanelStory>
)
}
_Pagination.storyName = 'Pagination'
export default _Pagination
@@ -0,0 +1,20 @@
// Copyright (c) 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import styled from 'styled-components'
import * as leo from '@brave/leo/tokens/css/variables'
import Button from '@brave/leo/react/button'
import { Row } from '../style'
export const PaginationRow = styled(Row)`
gap: ${leo.spacing.s};
`
export const PaginationButton = styled(Button)`
--leo-button-padding: ${leo.spacing.s};
min-width: 36px;
flex: 1;
`
@@ -0,0 +1,128 @@
// Copyright (c) 2024 The Brave Authors. All rights reserved.
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at https://mozilla.org/MPL/2.0/.
import * as React from 'react'
import Icon from '@brave/leo/react/icon'
// styles
import { PaginationButton, PaginationRow } from './pagination.styles'
function createNavPageNumbers(currentPage: number, totalPages: number) {
const pageOffsets = []
const canGoBackMultiple = currentPage - 2 >= 1
const canGoBack = currentPage - 1 >= 1
const canGoForward = currentPage + 1 <= totalPages
const canGoForwardMultiple = currentPage + 2 <= totalPages
// back
if (!canGoForwardMultiple && currentPage - 4 >= 1) {
pageOffsets.push(currentPage - 4)
}
if (!canGoForward && currentPage - 3 >= 1) {
pageOffsets.push(currentPage - 3)
}
if (canGoBackMultiple) {
pageOffsets.push(currentPage - 2)
}
if (canGoBack) {
pageOffsets.push(currentPage - 1)
}
// current
pageOffsets.push(currentPage)
// forward
if (canGoForward) {
pageOffsets.push(currentPage + 1)
}
if (canGoForwardMultiple) {
pageOffsets.push(currentPage + 2)
}
if (!canGoBackMultiple && currentPage + 3 <= totalPages) {
pageOffsets.push(currentPage + 3)
}
if (!canGoBack && currentPage + 4 <= totalPages) {
pageOffsets.push(currentPage + 4)
}
return pageOffsets
}
export function Pagination({
onSelectPageNumber,
currentPageNumber,
lastPageNumber
}: {
onSelectPageNumber: (pageNumber: number) => void
currentPageNumber: number
lastPageNumber: number
}) {
// computed
const canNavigateBack = currentPageNumber > 1
const canNavigateForward = currentPageNumber < lastPageNumber
// render
return (
<PaginationRow>
{/* First Page */}
<PaginationButton
size='small'
isDisabled={!canNavigateBack}
kind='plain-faint'
onClick={() => onSelectPageNumber(1)}
>
<Icon name='carat-first' />
</PaginationButton>
{/* Back */}
<PaginationButton
size='small'
isDisabled={!canNavigateBack}
kind='plain-faint'
onClick={() => onSelectPageNumber(currentPageNumber - 1)}
>
<Icon name='carat-left' />
</PaginationButton>
{/* Numbers for navigating pages (up to 5 numbered buttons) */}
{createNavPageNumbers(currentPageNumber, lastPageNumber).map(
(newPageNumber) => {
const isCurrentPage = newPageNumber === currentPageNumber
return (
<PaginationButton
size='small'
key={newPageNumber}
kind={isCurrentPage ? 'outline' : 'plain-faint'}
onClick={() => onSelectPageNumber(newPageNumber)}
>
{newPageNumber}
</PaginationButton>
)
}
)}
{/* Forward */}
<PaginationButton
size='small'
isDisabled={!canNavigateForward}
kind='plain-faint'
onClick={() => onSelectPageNumber(currentPageNumber + 1)}
>
<Icon name='carat-right' />
</PaginationButton>
{/* Last */}
<PaginationButton
size='small'
isDisabled={!canNavigateForward}
kind='plain-faint'
onClick={() => onSelectPageNumber(lastPageNumber)}
>
<Icon name='carat-last' />
</PaginationButton>
</PaginationRow>
)
}
@@ -17,6 +17,8 @@ import {
// path of generated mojom files.
export { BraveWallet }
export { Url } from 'gen/url/mojom/url.mojom.m.js'
export type NftDropdownOptionId = 'collected' | 'hidden'
export { Origin } from 'gen/url/mojom/origin.mojom.m.js'
export { TimeDelta }
@@ -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',
@@ -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)
}
}
@@ -10,7 +10,8 @@ import {
SendPageTabHashes,
WalletOrigin,
WalletCreationMode,
WalletImportMode
WalletImportMode,
NftDropdownOptionId
} from '../constants/types'
import { LOCAL_STORAGE_KEYS } from '../common/constants/local-storage-keys'
@@ -271,6 +272,17 @@ export const makePortfolioAssetRoute = (isNft: boolean, assetId: string) => {
).replace(':assetId', assetId)
}
export const makePortfolioNftsRoute = (
tab: NftDropdownOptionId,
page?: number
) => {
const params = new URLSearchParams({
tab: tab,
page: page?.toString() || '0'
})
return `${WalletRoutes.PortfolioNFTs}?${params.toString()}`
}
// Tabs
export function openTab(url: string) {
if (chrome.tabs !== undefined) {
+1
View File
@@ -1082,4 +1082,5 @@
<message name="IDS_BRAVE_WALLET_ACCOUNT_NAME_TOO_LONG_ERROR" desc="An error that appears when the user enters an account name that is too long">Account name must be <ph name="VALUE">$1<ex>30</ex></ph> characters or less</message>
<message name="IDS_BRAVE_WALLET_ENTER_PASSWORD_IF_APPLICABLE" desc="A label for the input field for entering the password for an imported file">Enter password (if applicable)</message>
<message name="IDS_BRAVE_WALLET_PRIVATE_KEY_IMPORT_TYPE" desc="Label for the dropdown to select the format to use for importing a private key">Import type</message>
<message name="IDS_BRAVE_WALLET_HIDE_NOT_OWNED_NF_TS" desc="Toggle label to enable or disable the display of NFTs that are not owned by any wallet account">Hide not owned NFTs</message>
</grit-part>
+2
View File
@@ -176,6 +176,8 @@ leo_icons = [
"browser-extensions.svg",
"browser-ntp-widget.svg",
"carat-down.svg",
"carat-first.svg",
"carat-last.svg",
"carat-left.svg",
"carat-right.svg",
"check-circle-filled.svg",