From f0a34a06461331ec44fa02b8093699fa001f94a0 Mon Sep 17 00:00:00 2001 From: Martin CAYUELAS <112866305+mcayuelas-ledger@users.noreply.github.com> Date: Mon, 2 Dec 2024 14:44:48 +0100 Subject: [PATCH] feat: Rework hiddenCollections to handle more easily displays in LLM (#8495) --- .changeset/big-apricots-live.md | 8 + .../CustomImage/NFTGallerySelector.tsx | 10 +- .../__tests__/useHideSpamCollection.test.ts | 1 - .../hooks/nfts/useHideSpamCollection.ts | 5 +- .../hooks/nfts/useNftCollectionsStatus.ts | 26 ++-- .../hooks/nfts/useSyncNFTsWithAccounts.ts | 8 +- .../src/renderer/reducers/accounts.ts | 12 +- .../src/renderer/reducers/settings.ts | 6 +- .../src/actions/settings.ts | 20 +-- apps/ledger-live-mobile/src/actions/types.ts | 21 ++- .../src/components/Nft/HideNftDrawer.tsx | 20 +-- .../Nft/NftCollectionOptionsMenu.tsx | 22 +-- .../components/Nft/NftGallery/NftList.hook.ts | 20 +-- .../src/hooks/nfts/__tests__/shared.ts | 32 ++++ .../__tests__/useHideSpamCollection.test.ts | 60 +++++--- .../__tests__/useNftCollectionsStatus.test.ts | 141 ++++++++++++++++++ .../src/hooks/nfts/useHideSpamCollection.ts | 24 ++- .../src/hooks/nfts/useNftCollections.ts | 13 +- .../src/hooks/nfts/useNftCollectionsStatus.ts | 41 +++++ .../src/hooks/nfts/useSyncNFTsWithAccounts.ts | 8 +- .../src/reducers/accounts.ts | 25 +++- .../src/reducers/settings.ts | 60 +++----- apps/ledger-live-mobile/src/reducers/types.ts | 2 + .../src/screens/Analytics/Operations.tsx | 8 +- .../CustomImage/NFTGallerySelector.tsx | 10 +- .../screens/Nft/WalletNftGallery/index.tsx | 6 +- .../Accounts/HiddenNftCollections.tsx | 39 +++-- .../OperationsHistory.tsx | 8 +- 28 files changed, 456 insertions(+), 200 deletions(-) create mode 100644 .changeset/big-apricots-live.md create mode 100644 apps/ledger-live-mobile/src/hooks/nfts/__tests__/shared.ts create mode 100644 apps/ledger-live-mobile/src/hooks/nfts/__tests__/useNftCollectionsStatus.test.ts create mode 100644 apps/ledger-live-mobile/src/hooks/nfts/useNftCollectionsStatus.ts diff --git a/.changeset/big-apricots-live.md b/.changeset/big-apricots-live.md new file mode 100644 index 000000000000..af33a9f90a62 --- /dev/null +++ b/.changeset/big-apricots-live.md @@ -0,0 +1,8 @@ +--- +"ledger-live-desktop": minor +"live-mobile": minor +"@ledgerhq/live-nft-react": minor +"@ledgerhq/live-nft": minor +--- + +Rework Hiddencollections diff --git a/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx b/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx index 39b28538b99b..d4121d974656 100644 --- a/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx +++ b/apps/ledger-live-desktop/src/renderer/components/CustomImage/NFTGallerySelector.tsx @@ -11,6 +11,8 @@ import styled from "styled-components"; import { useOnScreen } from "~/renderer/screens/nft/useOnScreen"; import { getEnv } from "@ledgerhq/live-env"; import { useNftCollections } from "~/renderer/hooks/nfts/useNftCollections"; +import { State } from "~/renderer/reducers"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; const ScrollContainer = styled(Flex).attrs({ flexDirection: "column", @@ -31,9 +33,13 @@ type Props = { const NFTGallerySelector = ({ handlePickNft, selectedNftId }: Props) => { const SUPPORTED_NFT_CURRENCIES = getEnv("NFT_CURRENCIES"); + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); const accounts = useSelector(accountsSelector); - const nftsOrdered = useSelector(orderedVisibleNftsSelector, isEqual); - + const nftsOrdered = useSelector( + (state: State) => + orderedVisibleNftsSelector(state, Boolean(nftsFromSimplehashFeature?.enabled)), + isEqual, + ); const addresses = useMemo( () => [ diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts index 5c6da00ff3ec..46f0a721ded8 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/__tests__/useHideSpamCollection.test.ts @@ -47,7 +47,6 @@ describe("useHideSpamCollection", () => { collectionB: NftStatus.spam, }, }, - whitelistedNftCollections: ["collectionA", "collectionB"], }, }, }); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts index d185050f4681..3c50a64100d7 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useHideSpamCollection.ts @@ -16,9 +16,8 @@ export function useHideSpamCollection() { const hideSpamCollection = useCallback( (collection: string, blockchain: BlockchainsType) => { - const elem = Object.entries(nftCollectionsStatusByNetwork).find( - ([key]) => key === blockchain, - )?.[1]; + const blockchainToCheck = nftCollectionsStatusByNetwork[blockchain] || {}; + const elem = Object.entries(blockchainToCheck).find(([key, _]) => key === collection); if (!elem) { dispatch(updateNftStatus(blockchain, collection, NftStatus.spam)); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollectionsStatus.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollectionsStatus.ts index 72876c2d43c1..46ffbc3ff45b 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollectionsStatus.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useNftCollectionsStatus.ts @@ -5,27 +5,27 @@ import { nftCollectionsStatusByNetworkSelector } from "~/renderer/reducers/setti import { NftStatus } from "@ledgerhq/live-nft/types"; import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +export const nftCollectionParser = ( + nftCollection: Record>, + applyFilterFn: (arg0: [string, NftStatus]) => boolean, +) => + Object.values(nftCollection).flatMap(contracts => + Object.entries(contracts) + .filter(applyFilterFn) + .map(([contract]) => contract), + ); + export function useNftCollectionsStatus() { const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); const nftCollectionsStatusByNetwork = useSelector(nftCollectionsStatusByNetworkSelector); - const shouldDisplaySpams = !nftsFromSimplehashFeature?.enabled; - - const nftCollectionParser = ( - nftCollection: Record>, - applyFilterFn: (arg0: [string, NftStatus]) => boolean, - ) => - Object.values(nftCollection).flatMap(contracts => - Object.entries(contracts) - .filter(applyFilterFn) - .map(([contract]) => contract), - ); + const hideSpams = Boolean(nftsFromSimplehashFeature?.enabled); const list = useMemo(() => { return nftCollectionParser(nftCollectionsStatusByNetwork, ([_, status]) => - !shouldDisplaySpams ? status !== NftStatus.whitelisted : status === NftStatus.blacklisted, + hideSpams ? status !== NftStatus.whitelisted : status === NftStatus.blacklisted, ); - }, [nftCollectionsStatusByNetwork, shouldDisplaySpams]); + }, [nftCollectionsStatusByNetwork, hideSpams]); const whitelisted = useMemo(() => { return nftCollectionParser( diff --git a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts index e9f06ead91c2..39269f038231 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/nfts/useSyncNFTsWithAccounts.ts @@ -6,6 +6,7 @@ import { useSelector } from "react-redux"; import { accountsSelector, orderedVisibleNftsSelector } from "~/renderer/reducers/accounts"; import isEqual from "lodash/isEqual"; import { getEnv } from "@ledgerhq/live-env"; +import { State } from "~/renderer/reducers"; /** * Represents the size of groups for batching address fetching. @@ -41,9 +42,12 @@ export function useSyncNFTsWithAccounts() { const threshold = getThreshold(nftsFromSimplehashFeature?.params?.threshold); const { enabled, hideSpamCollection } = useHideSpamCollection(); - const accounts = useSelector(accountsSelector); - const nftsOwned = useSelector(orderedVisibleNftsSelector, isEqual); + const nftsOwned = useSelector( + (state: State) => + orderedVisibleNftsSelector(state, Boolean(nftsFromSimplehashFeature?.enabled)), + isEqual, + ); const addressGroups = useMemo(() => { const uniqueAddresses = [ diff --git a/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts b/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts index d0afae082b1e..6f6174f410c6 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/accounts.ts @@ -20,6 +20,7 @@ import { isStarredAccountSelector } from "@ledgerhq/live-wallet/store"; import { nestedSortAccounts, AccountComparator } from "@ledgerhq/live-wallet/ordering"; import { AddAccountsAction } from "@ledgerhq/live-wallet/addAccounts"; import { NftStatus } from "@ledgerhq/live-nft/types"; +import { nftCollectionParser } from "../hooks/nfts/useNftCollectionsStatus"; /* FIXME @@ -224,13 +225,14 @@ export const flattenAccountsSelector = createSelector(accountsSelector, flattenA export const orderedVisibleNftsSelector = createSelector( accountsSelector, nftCollectionsStatusByNetworkSelector, - (accounts, nftCollectionsStatusByNetwork) => { + (_: State, hideSpam: boolean) => hideSpam, + (accounts, nftCollectionsStatusByNetwork, hideSpams) => { const nfts = accounts.map(a => a.nfts ?? []).flat(); - const hiddenNftCollections = Object.values(nftCollectionsStatusByNetwork).flatMap(contracts => - Object.entries(contracts) - .filter(([_, status]) => status === NftStatus.blacklisted || status === NftStatus.spam) - .map(([contract]) => contract), + const hiddenNftCollections = nftCollectionParser( + nftCollectionsStatusByNetwork, + ([_, status]) => + hideSpams ? status !== NftStatus.whitelisted : status === NftStatus.blacklisted, ); const visibleNfts = nfts.filter( diff --git a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts index 81726e9967c3..83817274d815 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts @@ -37,7 +37,7 @@ import { TOGGLE_MEV, UPDATE_NFT_COLLECTION_STATUS, } from "../actions/constants"; -import { BlockchainsType } from "@ledgerhq/live-nft/supported"; +import { BlockchainsType, SupportedBlockchainsType } from "@ledgerhq/live-nft/supported"; import { NftStatus } from "@ledgerhq/live-nft/types"; /* Initial state */ @@ -203,7 +203,7 @@ export const INITIAL_STATE: SettingsState = { blacklistedTokenIds: [], hiddenNftCollections: [], whitelistedNftCollections: [], - nftCollectionsStatusByNetwork: {} as Record>, + nftCollectionsStatusByNetwork: {} as Record>, hiddenOrdinalsAsset: [], deepLinkUrl: null, firstTimeLend: false, @@ -255,7 +255,7 @@ type HandlersPayloads = { SHOW_TOKEN: string; BLACKLIST_TOKEN: string; [UPDATE_NFT_COLLECTION_STATUS]: { - blockchain: BlockchainsType; + blockchain: SupportedBlockchainsType; collectionId: string; status: NftStatus; }; diff --git a/apps/ledger-live-mobile/src/actions/settings.ts b/apps/ledger-live-mobile/src/actions/settings.ts index b4dacc6fc6c6..f7c46acbf502 100755 --- a/apps/ledger-live-mobile/src/actions/settings.ts +++ b/apps/ledger-live-mobile/src/actions/settings.ts @@ -10,7 +10,6 @@ import { DangerouslyOverrideStatePayload, SettingsDismissBannerPayload, SettingsHideEmptyTokenAccountsPayload, - SettingsHideNftCollectionPayload, SettingsImportDesktopPayload, SettingsImportPayload, SettingsSetHasInstalledAnyAppPayload, @@ -41,7 +40,6 @@ import { SettingsSetSwapSelectableCurrenciesPayload, SettingsSetThemePayload, SettingsShowTokenPayload, - SettingsUnhideNftCollectionPayload, SettingsUpdateCurrencyPayload, SettingsActionTypes, SettingsSetWalletTabNavigatorLastVisitedTabPayload, @@ -71,9 +69,8 @@ import { SettingsRemoveStarredMarketcoinsPayload, SettingsSetFromLedgerSyncOnboardingPayload, SettingsSetHasBeenRedirectedToPostOnboardingPayload, - SettingsWhitelistNftCollectionPayload, - SettingsUnwhitelistNftCollectionPayload, SettingsSetMevProtectionPayload, + SettingsUpdateNftCollectionStatus, } from "./types"; import { ImageType } from "~/components/CustomImage/types"; @@ -144,19 +141,8 @@ export const blacklistToken = createAction( SettingsActionTypes.BLACKLIST_TOKEN, ); export const showToken = createAction(SettingsActionTypes.SHOW_TOKEN); -export const hideNftCollection = createAction( - SettingsActionTypes.HIDE_NFT_COLLECTION, -); -export const unhideNftCollection = createAction( - SettingsActionTypes.UNHIDE_NFT_COLLECTION, -); - -export const whitelistNftCollection = createAction( - SettingsActionTypes.WHITELIST_NFT_COLLECTION, -); - -export const unwhitelistNftCollection = createAction( - SettingsActionTypes.UNWHITELIST_NFT_COLLECTION, +export const updateNftStatus = createAction( + SettingsActionTypes.UPDATE_NFT_COLLECTION_STATUS, ); export const dismissBanner = createAction( diff --git a/apps/ledger-live-mobile/src/actions/types.ts b/apps/ledger-live-mobile/src/actions/types.ts index 862298861625..0370bc76596c 100644 --- a/apps/ledger-live-mobile/src/actions/types.ts +++ b/apps/ledger-live-mobile/src/actions/types.ts @@ -37,6 +37,8 @@ import type { Unpacked } from "../types/helpers"; import { HandlersPayloads } from "@ledgerhq/live-wallet/store"; import { ImportAccountsReduceInput } from "@ledgerhq/live-wallet/liveqr/importAccounts"; import { Steps } from "LLM/features/WalletSync/types/Activation"; +import { NftStatus } from "@ledgerhq/live-nft/types"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; // === ACCOUNTS ACTIONS === @@ -242,10 +244,7 @@ export enum SettingsActionTypes { SETTINGS_FILTER_TOKEN_OPERATIONS_ZERO_AMOUNT = "SETTINGS_FILTER_TOKEN_OPERATIONS_ZERO_AMOUNT", SHOW_TOKEN = "SHOW_TOKEN", BLACKLIST_TOKEN = "BLACKLIST_TOKEN", - HIDE_NFT_COLLECTION = "HIDE_NFT_COLLECTION", - UNHIDE_NFT_COLLECTION = "UNHIDE_NFT_COLLECTION", - UNWHITELIST_NFT_COLLECTION = "UNWHITELIST_NFT_COLLECTION", - WHITELIST_NFT_COLLECTION = "WHITELIST_NFT_COLLECTION", + UPDATE_NFT_COLLECTION_STATUS = "UPDATE_NFT_COLLECTION_STATUS", SETTINGS_DISMISS_BANNER = "SETTINGS_DISMISS_BANNER", SETTINGS_SET_AVAILABLE_UPDATE = "SETTINGS_SET_AVAILABLE_UPDATE", DANGEROUSLY_OVERRIDE_STATE = "DANGEROUSLY_OVERRIDE_STATE", @@ -324,10 +323,11 @@ export type SettingsFilterTokenOperationsZeroAmountPayload = SettingsState["filterTokenOperationsZeroAmount"]; export type SettingsShowTokenPayload = string; export type SettingsBlacklistTokenPayload = string; -export type SettingsHideNftCollectionPayload = string; -export type SettingsUnhideNftCollectionPayload = string; -export type SettingsWhitelistNftCollectionPayload = string; -export type SettingsUnwhitelistNftCollectionPayload = string; +export type SettingsUpdateNftCollectionStatus = { + blockchain: BlockchainEVM; + collection: string; + status: NftStatus; +}; export type SettingsDismissBannerPayload = string; export type SettingsSetAvailableUpdatePayload = SettingsState["hasAvailableUpdate"]; export type SettingsSetThemePayload = SettingsState["theme"]; @@ -425,10 +425,7 @@ export type SettingsPayload = | SettingsHideEmptyTokenAccountsPayload | SettingsShowTokenPayload | SettingsBlacklistTokenPayload - | SettingsHideNftCollectionPayload - | SettingsUnhideNftCollectionPayload - | SettingsWhitelistNftCollectionPayload - | SettingsUnwhitelistNftCollectionPayload + | SettingsUpdateNftCollectionStatus | SettingsDismissBannerPayload | SettingsSetAvailableUpdatePayload | SettingsSetThemePayload diff --git a/apps/ledger-live-mobile/src/components/Nft/HideNftDrawer.tsx b/apps/ledger-live-mobile/src/components/Nft/HideNftDrawer.tsx index 8c059fe6d407..7cc31a6d40b8 100644 --- a/apps/ledger-live-mobile/src/components/Nft/HideNftDrawer.tsx +++ b/apps/ledger-live-mobile/src/components/Nft/HideNftDrawer.tsx @@ -7,11 +7,12 @@ import { useDispatch, useSelector } from "react-redux"; import { Account } from "@ledgerhq/types-live"; import { decodeNftId } from "@ledgerhq/coin-framework/nft/nftId"; import { track, TrackScreen } from "~/analytics"; -import { hideNftCollection, unwhitelistNftCollection } from "~/actions/settings"; import { accountSelector } from "~/reducers/accounts"; import { State } from "~/reducers/types"; import QueuedDrawer from "../QueuedDrawer"; -import { whitelistedNftCollectionsSelector } from "~/reducers/settings"; +import { updateNftStatus } from "~/actions/settings"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; type Props = { nftId?: string; @@ -25,8 +26,6 @@ const HideNftDrawer = ({ nftId, nftContract, collection, isOpened, onClose }: Pr const navigation = useNavigation(); const dispatch = useDispatch(); - const whitelistedNftCollections = useSelector(whitelistedNftCollectionsSelector); - const { accountId } = decodeNftId(nftId ?? ""); const account = useSelector(state => accountSelector(state, { accountId }), @@ -39,14 +38,17 @@ const HideNftDrawer = ({ nftId, nftContract, collection, isOpened, onClose }: Pr }); const collectionId = `${account?.id}|${nftContract}`; - if (whitelistedNftCollections.includes(collectionId)) { - dispatch(unwhitelistNftCollection(collectionId)); - } - dispatch(hideNftCollection(collectionId)); + dispatch( + updateNftStatus({ + collection: collectionId, + status: NftStatus.blacklisted, + blockchain: account?.currency.id as BlockchainEVM, + }), + ); onClose(); navigation.goBack(); - }, [account?.id, dispatch, navigation, nftContract, onClose, whitelistedNftCollections]); + }, [account?.currency.id, account?.id, dispatch, navigation, nftContract, onClose]); const onPressClose = useCallback(() => { track("button_clicked", { diff --git a/apps/ledger-live-mobile/src/components/Nft/NftCollectionOptionsMenu.tsx b/apps/ledger-live-mobile/src/components/Nft/NftCollectionOptionsMenu.tsx index 53ecf427db87..ce4cd590d3a5 100644 --- a/apps/ledger-live-mobile/src/components/Nft/NftCollectionOptionsMenu.tsx +++ b/apps/ledger-live-mobile/src/components/Nft/NftCollectionOptionsMenu.tsx @@ -1,12 +1,13 @@ import React, { useCallback } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import { Text, IconsLegacy, BoxedIcon, Button, Flex } from "@ledgerhq/native-ui"; import { Account, ProtoNFT } from "@ledgerhq/types-live"; import { useTranslation } from "react-i18next"; -import { hideNftCollection, unwhitelistNftCollection } from "~/actions/settings"; +import { updateNftStatus } from "~/actions/settings"; import QueuedDrawer from "../QueuedDrawer"; -import { whitelistedNftCollectionsSelector } from "~/reducers/settings"; +import { NftStatus } from "@ledgerhq/live-nft/types"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; type Props = { isOpen: boolean; @@ -19,17 +20,18 @@ const NftCollectionOptionsMenu = ({ isOpen, onClose, collection, account }: Prop const { t } = useTranslation(); const dispatch = useDispatch(); - const whitelistedNftCollections = useSelector(whitelistedNftCollectionsSelector); - const onConfirm = useCallback(() => { const collectionId = `${account.id}|${collection?.[0]?.contract}`; - if (whitelistedNftCollections.includes(collectionId)) { - dispatch(unwhitelistNftCollection(collectionId)); - } - dispatch(hideNftCollection(collectionId)); + dispatch( + updateNftStatus({ + collection: collectionId, + status: NftStatus.blacklisted, + blockchain: account.currency.id as BlockchainEVM, + }), + ); onClose(); - }, [account.id, collection, whitelistedNftCollections, dispatch, onClose]); + }, [account.id, account.currency.id, collection, dispatch, onClose]); return ( diff --git a/apps/ledger-live-mobile/src/components/Nft/NftGallery/NftList.hook.ts b/apps/ledger-live-mobile/src/components/Nft/NftGallery/NftList.hook.ts index 03b2a5a68400..dc2060c2d651 100644 --- a/apps/ledger-live-mobile/src/components/Nft/NftGallery/NftList.hook.ts +++ b/apps/ledger-live-mobile/src/components/Nft/NftGallery/NftList.hook.ts @@ -7,14 +7,15 @@ import { useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { BackHandler } from "react-native"; -import { hideNftCollection, unwhitelistNftCollection } from "~/actions/settings"; +import { updateNftStatus } from "~/actions/settings"; import { track } from "../../../analytics"; import { NavigatorName, ScreenName } from "~/const"; import { updateMainNavigatorVisibility } from "~/actions/appstate"; import { galleryFilterDrawerVisibleSelector, galleryChainFiltersSelector } from "~/reducers/nft"; import { setGalleryChainFilter, setGalleryFilterDrawerVisible } from "~/actions/nft"; import { NftGalleryChainFiltersState } from "../../../reducers/types"; -import { whitelistedNftCollectionsSelector } from "~/reducers/settings"; +import { NftStatus } from "@ledgerhq/live-nft/types"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; const TOAST_ID = "SUCCESS_HIDE"; @@ -28,8 +29,6 @@ export function useNftList({ nftList }: { nftList?: ProtoNFT[] }) { const isFilterDrawerVisible = useSelector(galleryFilterDrawerVisibleSelector); const chainFilters = useSelector(galleryChainFiltersSelector); - const whitelistedNftCollections = useSelector(whitelistedNftCollectionsSelector); - const [nftsToHide, setNftsToHide] = useState([]); // Multi Select ------------------------ @@ -67,11 +66,14 @@ export function useNftList({ nftList }: { nftList?: ProtoNFT[] }) { nftsToHide.forEach(nft => { const { accountId } = decodeNftId(nft.id ?? ""); const collectionId = `${accountId}|${nft.contract}`; - if (whitelistedNftCollections.includes(collectionId)) { - dispatch(unwhitelistNftCollection(collectionId)); - } - dispatch(hideNftCollection(collectionId)); + dispatch( + updateNftStatus({ + collection: collectionId, + status: NftStatus.blacklisted, + blockchain: nft.currencyId as BlockchainEVM, + }), + ); }); pushToast({ @@ -82,7 +84,7 @@ export function useNftList({ nftList }: { nftList?: ProtoNFT[] }) { count: nftsToHide.length, }), }); - }, [exitMultiSelectMode, nftsToHide, pushToast, t, whitelistedNftCollections, dispatch]); + }, [exitMultiSelectMode, nftsToHide, pushToast, t, dispatch]); const triggerMultiSelectMode = useCallback(() => { setNftsToHide([]); diff --git a/apps/ledger-live-mobile/src/hooks/nfts/__tests__/shared.ts b/apps/ledger-live-mobile/src/hooks/nfts/__tests__/shared.ts new file mode 100644 index 000000000000..1db7819aeef2 --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/nfts/__tests__/shared.ts @@ -0,0 +1,32 @@ +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; + +export const nftCollectionsStatusByNetwork = { + [BlockchainEVM.Ethereum]: {}, + [BlockchainEVM.Avalanche]: {}, + [BlockchainEVM.Polygon]: {}, + [BlockchainEVM.Arbitrum]: {}, + [BlockchainEVM.Base]: {}, + [BlockchainEVM.Blast]: {}, + [BlockchainEVM.Bsc]: {}, + [BlockchainEVM.Canto]: {}, + [BlockchainEVM.Celo]: {}, + [BlockchainEVM.Cyber]: {}, + [BlockchainEVM.Degen]: {}, + [BlockchainEVM.Fantom]: {}, + [BlockchainEVM.Gnosis]: {}, + [BlockchainEVM.Godwoken]: {}, + [BlockchainEVM.Linea]: {}, + [BlockchainEVM.Loot]: {}, + [BlockchainEVM.Manta]: {}, + [BlockchainEVM.Mode]: {}, + [BlockchainEVM.Moonbeam]: {}, + [BlockchainEVM.Opbnb]: {}, + [BlockchainEVM.Optimism]: {}, + [BlockchainEVM.Palm]: {}, + [BlockchainEVM.ProofOfPlay]: {}, + [BlockchainEVM.Rari]: {}, + [BlockchainEVM.Scroll]: {}, + [BlockchainEVM.Sei]: {}, + [BlockchainEVM.Xai]: {}, + [BlockchainEVM.Zora]: {}, +}; diff --git a/apps/ledger-live-mobile/src/hooks/nfts/__tests__/useHideSpamCollection.test.ts b/apps/ledger-live-mobile/src/hooks/nfts/__tests__/useHideSpamCollection.test.ts index ef78b77ad2ab..bd5007c4ef95 100644 --- a/apps/ledger-live-mobile/src/hooks/nfts/__tests__/useHideSpamCollection.test.ts +++ b/apps/ledger-live-mobile/src/hooks/nfts/__tests__/useHideSpamCollection.test.ts @@ -1,8 +1,12 @@ import { useHideSpamCollection } from "../useHideSpamCollection"; import { useDispatch } from "react-redux"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; import { renderHook } from "@tests/test-renderer"; -import { hideNftCollection } from "~/actions/settings"; +import { INITIAL_STATE } from "~/reducers/settings"; +import { updateNftStatus } from "~/actions/settings"; import { State } from "~/reducers/types"; +import { nftCollectionsStatusByNetwork } from "./shared"; jest.mock("react-redux", () => ({ ...jest.requireActual("react-redux"), @@ -17,38 +21,48 @@ describe("useHideSpamCollection", () => { mockDispatch.mockClear(); }); - it("should dispatch hideNftCollection action if collection is not whitelisted", () => { - const { result } = renderHook( - () => useHideSpamCollection(), - - { - overrideInitialState: (state: State) => ({ - ...state, - settings: { - ...state.settings, - whitelistedNftCollections: ["collectionA", "collectionB"], - hiddenNftCollections: [], - }, - }), - }, - ); - result.current.hideSpamCollection("collectionC"); + it("should dispatch updateNftStatus action if collection is not already marked with a status", () => { + const { result } = renderHook(() => useHideSpamCollection(), { + overrideInitialState: (state: State) => ({ + ...state, + settings: { + ...INITIAL_STATE, + nftCollectionsStatusByNetwork, + }, + }), + }); + result.current.hideSpamCollection("collectionC", BlockchainEVM.Ethereum); - expect(mockDispatch).toHaveBeenCalledWith(hideNftCollection("collectionC")); + expect(mockDispatch).toHaveBeenCalledWith( + updateNftStatus({ + blockchain: BlockchainEVM.Ethereum, + collection: "collectionC", + status: NftStatus.spam, + }), + ); }); - it("should not dispatch hideNftCollection action if collection is whitelisted", () => { + it("should not dispatch hideNftCollection action if collection is already marked with a status", () => { const { result } = renderHook(() => useHideSpamCollection(), { overrideInitialState: (state: State) => ({ ...state, settings: { - ...state.settings, - hiddenNftCollections: [], - whitelistedNftCollections: ["collectionA", "collectionB"], + ...INITIAL_STATE, + nftCollectionsStatusByNetwork: { + ...nftCollectionsStatusByNetwork, + [BlockchainEVM.Ethereum]: { + collectionA: NftStatus.spam, + }, + [BlockchainEVM.Avalanche]: { + collectionB: NftStatus.spam, + }, + }, }, }), }); - result.current.hideSpamCollection("collectionA"); + + result.current.hideSpamCollection("collectionA", BlockchainEVM.Ethereum); + result.current.hideSpamCollection("collectionB", BlockchainEVM.Avalanche); expect(mockDispatch).not.toHaveBeenCalled(); }); diff --git a/apps/ledger-live-mobile/src/hooks/nfts/__tests__/useNftCollectionsStatus.test.ts b/apps/ledger-live-mobile/src/hooks/nfts/__tests__/useNftCollectionsStatus.test.ts new file mode 100644 index 000000000000..b1a32e4634dd --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/nfts/__tests__/useNftCollectionsStatus.test.ts @@ -0,0 +1,141 @@ +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; +import { useNftCollectionsStatus } from "../useNftCollectionsStatus"; +import { renderHook } from "@tests/test-renderer"; +import { INITIAL_STATE } from "~/reducers/settings"; +import { State } from "~/reducers/types"; + +describe("useNftCollectionsStatus", () => { + it("should returns only NFTs (contract) with NftStatus !== whitelisted when FF is ON", () => { + const { result } = renderHook(() => useNftCollectionsStatus(), { + overrideInitialState: (state: State) => ({ + ...state, + settings: { + ...INITIAL_STATE, + overriddenFeatureFlags: { + nftsFromSimplehash: { + enabled: true, + }, + }, + nftCollectionsStatusByNetwork: { + [BlockchainEVM.Ethereum]: { + collectionETHA: NftStatus.whitelisted, + collectionETHB: NftStatus.blacklisted, + collectionETHC: NftStatus.spam, + collectionETHD: NftStatus.spam, + }, + [BlockchainEVM.Avalanche]: { + collectionAVAX1: NftStatus.blacklisted, + collectionAVAX2: NftStatus.spam, + collectionAVAX3: NftStatus.blacklisted, + }, + [BlockchainEVM.Polygon]: { + collectionP1: NftStatus.blacklisted, + collectionP2: NftStatus.whitelisted, + }, + [BlockchainEVM.Arbitrum]: {}, + [BlockchainEVM.Base]: {}, + [BlockchainEVM.Blast]: {}, + [BlockchainEVM.Bsc]: {}, + [BlockchainEVM.Canto]: {}, + [BlockchainEVM.Celo]: {}, + [BlockchainEVM.Cyber]: {}, + [BlockchainEVM.Degen]: {}, + [BlockchainEVM.Fantom]: {}, + [BlockchainEVM.Gnosis]: {}, + [BlockchainEVM.Godwoken]: {}, + [BlockchainEVM.Linea]: {}, + [BlockchainEVM.Loot]: {}, + [BlockchainEVM.Manta]: {}, + [BlockchainEVM.Mode]: {}, + [BlockchainEVM.Moonbeam]: {}, + [BlockchainEVM.Opbnb]: {}, + [BlockchainEVM.Optimism]: {}, + [BlockchainEVM.Palm]: {}, + [BlockchainEVM.ProofOfPlay]: {}, + [BlockchainEVM.Rari]: {}, + [BlockchainEVM.Scroll]: {}, + [BlockchainEVM.Sei]: {}, + [BlockchainEVM.Xai]: {}, + [BlockchainEVM.Zora]: {}, + }, + }, + }), + }); + + expect(result.current.hiddenNftCollections).toEqual([ + "collectionETHB", + "collectionETHC", + "collectionETHD", + "collectionAVAX1", + "collectionAVAX2", + "collectionAVAX3", + "collectionP1", + ]); + }); + + it("should returns only NFTs (contract) with NftStatus.blacklisted when FF is oFF ", () => { + const { result } = renderHook(() => useNftCollectionsStatus(), { + overrideInitialState: (state: State) => ({ + ...state, + settings: { + ...INITIAL_STATE, + overriddenFeatureFlags: { + nftsFromSimplehash: { + enabled: false, + }, + }, + nftCollectionsStatusByNetwork: { + [BlockchainEVM.Ethereum]: { + collectionETHA: NftStatus.whitelisted, + collectionETHB: NftStatus.blacklisted, + collectionETHC: NftStatus.spam, + collectionETHD: NftStatus.spam, + }, + [BlockchainEVM.Avalanche]: { + collectionAVAX1: NftStatus.blacklisted, + collectionAVAX2: NftStatus.spam, + collectionAVAX3: NftStatus.blacklisted, + }, + [BlockchainEVM.Polygon]: { + collectionP1: NftStatus.blacklisted, + collectionP2: NftStatus.whitelisted, + }, + [BlockchainEVM.Arbitrum]: {}, + [BlockchainEVM.Base]: {}, + [BlockchainEVM.Blast]: {}, + [BlockchainEVM.Bsc]: {}, + [BlockchainEVM.Canto]: {}, + [BlockchainEVM.Celo]: {}, + [BlockchainEVM.Cyber]: {}, + [BlockchainEVM.Degen]: {}, + [BlockchainEVM.Fantom]: {}, + [BlockchainEVM.Gnosis]: {}, + [BlockchainEVM.Godwoken]: {}, + [BlockchainEVM.Linea]: {}, + [BlockchainEVM.Loot]: {}, + [BlockchainEVM.Manta]: {}, + [BlockchainEVM.Mode]: {}, + [BlockchainEVM.Moonbeam]: {}, + [BlockchainEVM.Opbnb]: {}, + [BlockchainEVM.Optimism]: {}, + [BlockchainEVM.Palm]: {}, + [BlockchainEVM.ProofOfPlay]: {}, + [BlockchainEVM.Rari]: {}, + [BlockchainEVM.Scroll]: {}, + [BlockchainEVM.Sei]: {}, + [BlockchainEVM.Xai]: {}, + [BlockchainEVM.Zora]: {}, + }, + }, + }), + }); + + expect(result.current.hiddenNftCollections).toEqual([ + "collectionETHB", + "collectionAVAX1", + "collectionAVAX3", + "collectionP1", + ]); + }); +}); diff --git a/apps/ledger-live-mobile/src/hooks/nfts/useHideSpamCollection.ts b/apps/ledger-live-mobile/src/hooks/nfts/useHideSpamCollection.ts index 4974265b1240..24450cbebc41 100644 --- a/apps/ledger-live-mobile/src/hooks/nfts/useHideSpamCollection.ts +++ b/apps/ledger-live-mobile/src/hooks/nfts/useHideSpamCollection.ts @@ -1,25 +1,33 @@ import { useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; -import { hideNftCollection } from "~/actions/settings"; -import { whitelistedNftCollectionsSelector } from "~/reducers/settings"; +import { updateNftStatus } from "~/actions/settings"; +import { nftCollectionsStatusByNetworkSelector } from "~/reducers/settings"; +import { BlockchainsType } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; export function useHideSpamCollection() { const spamFilteringTxFeature = useFeature("spamFilteringTx"); - const whitelistedNftCollections = useSelector(whitelistedNftCollectionsSelector); + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); + + const nftCollectionsStatusByNetwork = useSelector(nftCollectionsStatusByNetworkSelector); const dispatch = useDispatch(); + const hideSpamCollection = useCallback( - (collection: string) => { - if (!whitelistedNftCollections.includes(collection)) { - dispatch(hideNftCollection(collection)); + (collection: string, blockchain: BlockchainsType) => { + const blockchainToCheck = nftCollectionsStatusByNetwork[blockchain] || {}; + const elem = Object.entries(blockchainToCheck).find(([key, _]) => key === collection); + + if (!elem) { + dispatch(updateNftStatus({ blockchain, collection, status: NftStatus.spam })); } }, - [dispatch, whitelistedNftCollections], + [dispatch, nftCollectionsStatusByNetwork], ); return { hideSpamCollection, - enabled: spamFilteringTxFeature?.enabled, + enabled: spamFilteringTxFeature?.enabled && nftsFromSimplehashFeature?.enabled, }; } diff --git a/apps/ledger-live-mobile/src/hooks/nfts/useNftCollections.ts b/apps/ledger-live-mobile/src/hooks/nfts/useNftCollections.ts index b97c94f00f70..2bfabb5e54ee 100644 --- a/apps/ledger-live-mobile/src/hooks/nfts/useNftCollections.ts +++ b/apps/ledger-live-mobile/src/hooks/nfts/useNftCollections.ts @@ -4,11 +4,7 @@ import { nftsByCollections } from "@ledgerhq/live-nft/index"; import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; import { Account, ProtoNFT } from "@ledgerhq/types-live"; import { useMemo } from "react"; -import { useSelector } from "react-redux"; -import { - hiddenNftCollectionsSelector, - whitelistedNftCollectionsSelector, -} from "~/reducers/settings"; +import { useNftCollectionsStatus } from "./useNftCollectionsStatus"; export function useNftCollections({ account, @@ -25,19 +21,18 @@ export function useNftCollections({ const threshold = nftsFromSimplehashFeature?.params?.threshold; const simplehashEnabled = nftsFromSimplehashFeature?.enabled; - const whitelistNft = useSelector(whitelistedNftCollectionsSelector); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + const { hiddenNftCollections, whitelistedNftCollections } = useNftCollectionsStatus(); const nftsOwnedToCheck = useMemo(() => account?.nfts ?? nftsOwned, [account?.nfts, nftsOwned]); const whitelistedNfts = useMemo( () => nftsOwnedToCheck?.filter(nft => - whitelistNft + whitelistedNftCollections .map(collection => decodeCollectionId(collection).contractAddress) .includes(nft.contract), ) ?? [], - [nftsOwnedToCheck, whitelistNft], + [nftsOwnedToCheck, whitelistedNftCollections], ); const { nfts, fetchNextPage, hasNextPage, ...rest } = useNftGalleryFilter({ diff --git a/apps/ledger-live-mobile/src/hooks/nfts/useNftCollectionsStatus.ts b/apps/ledger-live-mobile/src/hooks/nfts/useNftCollectionsStatus.ts new file mode 100644 index 000000000000..07cc76650a2c --- /dev/null +++ b/apps/ledger-live-mobile/src/hooks/nfts/useNftCollectionsStatus.ts @@ -0,0 +1,41 @@ +import { useMemo } from "react"; +import { useSelector } from "react-redux"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { NftStatus } from "@ledgerhq/live-nft/types"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; +import { nftCollectionsStatusByNetworkSelector } from "~/reducers/settings"; + +export const nftCollectionParser = ( + nftCollection: Record>, + applyFilterFn: (arg0: [string, NftStatus]) => boolean, +) => + Object.values(nftCollection).flatMap(contracts => + Object.entries(contracts) + .filter(applyFilterFn) + .map(([contract]) => contract), + ); + +export function useNftCollectionsStatus() { + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); + const nftCollectionsStatusByNetwork = useSelector(nftCollectionsStatusByNetworkSelector); + + const hideSpam = Boolean(nftsFromSimplehashFeature?.enabled); + + const list = useMemo(() => { + return nftCollectionParser(nftCollectionsStatusByNetwork, ([_, status]) => + hideSpam ? status !== NftStatus.whitelisted : status === NftStatus.blacklisted, + ); + }, [nftCollectionsStatusByNetwork, hideSpam]); + + const whitelisted = useMemo(() => { + return nftCollectionParser( + nftCollectionsStatusByNetwork, + ([_, status]) => status === NftStatus.whitelisted, + ); + }, [nftCollectionsStatusByNetwork]); + + return { + hiddenNftCollections: list, + whitelistedNftCollections: whitelisted, + }; +} diff --git a/apps/ledger-live-mobile/src/hooks/nfts/useSyncNFTsWithAccounts.ts b/apps/ledger-live-mobile/src/hooks/nfts/useSyncNFTsWithAccounts.ts index 2dd05c31e5f3..73c19e77c49b 100644 --- a/apps/ledger-live-mobile/src/hooks/nfts/useSyncNFTsWithAccounts.ts +++ b/apps/ledger-live-mobile/src/hooks/nfts/useSyncNFTsWithAccounts.ts @@ -6,6 +6,7 @@ import { getEnv } from "@ledgerhq/live-env"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { getThreshold, useCheckNftAccount } from "@ledgerhq/live-nft-react"; import { accountsSelector, orderedVisibleNftsSelector } from "~/reducers/accounts"; +import { State } from "~/reducers/types"; /** * Represents the size of groups for batching address fetching. @@ -43,7 +44,12 @@ export function useSyncNFTsWithAccounts() { const { enabled, hideSpamCollection } = useHideSpamCollection(); const accounts = useSelector(accountsSelector); - const nftsOwned = useSelector(orderedVisibleNftsSelector, isEqual); + + const nftsOwned = useSelector( + (state: State) => + orderedVisibleNftsSelector(state, Boolean(nftsFromSimplehashFeature?.enabled)), + isEqual, + ); const addressGroups = useMemo(() => { const uniqueAddresses = [ diff --git a/apps/ledger-live-mobile/src/reducers/accounts.ts b/apps/ledger-live-mobile/src/reducers/accounts.ts index dbe2665f30e7..50efb6c31767 100644 --- a/apps/ledger-live-mobile/src/reducers/accounts.ts +++ b/apps/ledger-live-mobile/src/reducers/accounts.ts @@ -39,7 +39,7 @@ import type { } from "../actions/types"; import { AccountsActionTypes } from "../actions/types"; import accountModel from "../logic/accountModel"; -import { blacklistedTokenIdsSelector, hiddenNftCollectionsSelector } from "./settings"; +import { blacklistedTokenIdsSelector, nftCollectionsStatusByNetworkSelector } from "./settings"; import { galleryChainFiltersSelector } from "./nft"; import { accountNameWithDefaultSelector, @@ -51,6 +51,8 @@ import { importAccountsReduce } from "@ledgerhq/live-wallet/liveqr/importAccount import { walletSelector } from "./wallet"; import { nestedSortAccounts } from "@ledgerhq/live-wallet/ordering"; import { AddAccountsAction } from "@ledgerhq/live-wallet/addAccounts"; +import { NftStatus } from "@ledgerhq/live-nft/types"; +import { nftCollectionParser } from "~/hooks/nfts/useNftCollectionsStatus"; export const INITIAL_STATE: AccountsState = { active: [], @@ -478,12 +480,23 @@ export const orderedNftsSelector = createSelector( * ``` * */ export const orderedVisibleNftsSelector = createSelector( - orderedNftsSelector, - hiddenNftCollectionsSelector, - (nfts, hiddenNftCollections) => - nfts.filter( + accountsSelector, + nftCollectionsStatusByNetworkSelector, + (_: State, hideSpam: boolean) => hideSpam, + (accounts, nftCollectionsStatusByNetwork, hideSpam) => { + const nfts = accounts.map(a => a.nfts ?? []).flat(); + + const hiddenNftCollections = nftCollectionParser( + nftCollectionsStatusByNetwork, + ([_, status]) => + hideSpam ? status !== NftStatus.whitelisted : status === NftStatus.blacklisted, + ); + + const visibleNfts = nfts.filter( nft => !hiddenNftCollections.includes(`${decodeNftId(nft.id).accountId}|${nft.contract}`), - ), + ); + return orderByLastReceived(accounts, visibleNfts); + }, ); export const hasNftsSelector = createSelector(nftsSelector, nfts => { diff --git a/apps/ledger-live-mobile/src/reducers/settings.ts b/apps/ledger-live-mobile/src/reducers/settings.ts index ced0b2eedd8b..ed95678e9300 100644 --- a/apps/ledger-live-mobile/src/reducers/settings.ts +++ b/apps/ledger-live-mobile/src/reducers/settings.ts @@ -20,7 +20,6 @@ import type { SettingsDismissBannerPayload, SettingsHideEmptyTokenAccountsPayload, SettingsFilterTokenOperationsZeroAmountPayload, - SettingsHideNftCollectionPayload, SettingsImportDesktopPayload, SettingsImportPayload, SettingsSetHasInstalledAnyAppPayload, @@ -51,7 +50,6 @@ import type { SettingsSetSensitiveAnalyticsPayload, SettingsSetThemePayload, SettingsShowTokenPayload, - SettingsUnhideNftCollectionPayload, SettingsUpdateCurrencyPayload, SettingsSetSwapSelectableCurrenciesPayload, SettingsSetDismissedDynamicCardsPayload, @@ -81,15 +79,16 @@ import type { SettingsRemoveStarredMarketcoinsPayload, SettingsSetFromLedgerSyncOnboardingPayload, SettingsSetHasBeenRedirectedToPostOnboardingPayload, - SettingsUnwhitelistNftCollectionPayload, - SettingsWhitelistNftCollectionPayload, SettingsSetMevProtectionPayload, + SettingsUpdateNftCollectionStatus, } from "../actions/types"; import { SettingsActionTypes, SettingsSetWalletTabNavigatorLastVisitedTabPayload, } from "../actions/types"; import { ScreenName } from "~/const"; +import { BlockchainsType } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; export const timeRangeDaysByKey = { day: 1, @@ -123,6 +122,7 @@ export const INITIAL_STATE: SettingsState = { blacklistedTokenIds: [], hiddenNftCollections: [], whitelistedNftCollections: [], + nftCollectionsStatusByNetwork: {} as Record>, dismissedBanners: [], hasAvailableUpdate: false, theme: "system", @@ -360,44 +360,18 @@ const handlers: ReducerMap = { }; }, - [SettingsActionTypes.HIDE_NFT_COLLECTION]: (state, action) => { - const ids = state.hiddenNftCollections; + [SettingsActionTypes.UPDATE_NFT_COLLECTION_STATUS]: (state, action) => { + const { blockchain, collection, status } = (action as Action) + .payload; return { ...state, - hiddenNftCollections: [ - ...new Set(ids.concat((action as Action).payload)), - ], - }; - }, - - [SettingsActionTypes.UNHIDE_NFT_COLLECTION]: (state, action) => { - const ids = state.hiddenNftCollections; - return { - ...state, - hiddenNftCollections: ids.filter( - id => id !== (action as Action).payload, - ), - }; - }, - - [SettingsActionTypes.UNWHITELIST_NFT_COLLECTION]: (state, action) => { - const ids = state.whitelistedNftCollections; - return { - ...state, - whitelistedNftCollections: ids.filter( - id => id !== (action as Action).payload, - ), - }; - }, - [SettingsActionTypes.WHITELIST_NFT_COLLECTION]: (state, action) => { - const collections = state.whitelistedNftCollections; - return { - ...state, - whitelistedNftCollections: [ - ...new Set( - collections.concat((action as Action).payload), - ), - ], + nftCollectionsStatusByNetwork: { + ...state.nftCollectionsStatusByNetwork, + [blockchain]: { + ...state.nftCollectionsStatusByNetwork[blockchain], + [collection]: status, + }, + }, }; }, @@ -836,9 +810,6 @@ export const hasInstalledAnyAppSelector = (state: State) => state.settings.hasIn export const countervalueFirstSelector = (state: State) => state.settings.graphCountervalueFirst; export const readOnlyModeEnabledSelector = (state: State) => state.settings.readOnlyModeEnabled; export const blacklistedTokenIdsSelector = (state: State) => state.settings.blacklistedTokenIds; -export const hiddenNftCollectionsSelector = (state: State) => state.settings.hiddenNftCollections; -export const whitelistedNftCollectionsSelector = (state: State) => - state.settings.whitelistedNftCollections; export const exportSettingsSelector = createSelector( counterValueCurrencySelector, () => getEnv("MANAGER_DEV_MODE"), @@ -934,3 +905,6 @@ export const isFromLedgerSyncOnboardingSelector = (state: State) => export const starredMarketCoinsSelector = (state: State) => state.settings.starredMarketCoins; export const mevProtectionSelector = (state: State) => state.settings.mevProtection; + +export const nftCollectionsStatusByNetworkSelector = (state: State) => + state.settings.nftCollectionsStatusByNetwork; diff --git a/apps/ledger-live-mobile/src/reducers/types.ts b/apps/ledger-live-mobile/src/reducers/types.ts index 74c9956d07b4..89334418e3e9 100644 --- a/apps/ledger-live-mobile/src/reducers/types.ts +++ b/apps/ledger-live-mobile/src/reducers/types.ts @@ -32,6 +32,7 @@ import { WalletState } from "@ledgerhq/live-wallet/store"; import { TrustchainStore } from "@ledgerhq/ledger-key-ring-protocol/store"; import { Steps } from "LLM/features/WalletSync/types/Activation"; import { SupportedBlockchainsType, BlockchainsType } from "@ledgerhq/live-nft/supported"; +import { NftStatus } from "@ledgerhq/live-nft/types"; // === ACCOUNT STATE === @@ -219,6 +220,7 @@ export type SettingsState = { blacklistedTokenIds: string[]; hiddenNftCollections: string[]; whitelistedNftCollections: string[]; + nftCollectionsStatusByNetwork: Record>; dismissedBanners: string[]; hasAvailableUpdate: boolean; theme: Theme; diff --git a/apps/ledger-live-mobile/src/screens/Analytics/Operations.tsx b/apps/ledger-live-mobile/src/screens/Analytics/Operations.tsx index 5130d4c3885c..c04ad788bd35 100644 --- a/apps/ledger-live-mobile/src/screens/Analytics/Operations.tsx +++ b/apps/ledger-live-mobile/src/screens/Analytics/Operations.tsx @@ -32,10 +32,8 @@ import { TrackScreen } from "~/analytics"; import { withDiscreetMode } from "~/context/DiscreetModeContext"; import type { BaseNavigatorStackParamList } from "~/components/RootNavigator/types/BaseNavigator"; import type { StackNavigatorProps } from "~/components/RootNavigator/types/helpers"; -import { - filterTokenOperationsZeroAmountEnabledSelector, - hiddenNftCollectionsSelector, -} from "~/reducers/settings"; +import { filterTokenOperationsZeroAmountEnabledSelector } from "~/reducers/settings"; +import { useNftCollectionsStatus } from "~/hooks/nfts/useNftCollectionsStatus"; type Props = StackNavigatorProps; @@ -56,7 +54,7 @@ export function Operations({ navigation, route }: Props) { [accountsFromState, accountsIds], ); const allAccounts: AccountLikeArray = useSelector(flattenAccountsSelector); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + const { hiddenNftCollections } = useNftCollectionsStatus(); const refreshAccountsOrdering = useRefreshAccountsOrdering(); useFocusEffect(refreshAccountsOrdering); diff --git a/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx b/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx index cd118e546ffe..151e6532a0c4 100644 --- a/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx +++ b/apps/ledger-live-mobile/src/screens/CustomImage/NFTGallerySelector.tsx @@ -15,6 +15,8 @@ import { CustomImageNavigatorParamList } from "~/components/RootNavigator/types/ import { TrackScreen } from "~/analytics"; import { getEnv } from "@ledgerhq/live-env"; import { useNftCollections } from "~/hooks/nfts/useNftCollections"; +import { State } from "~/reducers/types"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; const NB_COLUMNS = 2; @@ -27,10 +29,14 @@ const keyExtractor = (item: ProtoNFT) => item.id; const NFTGallerySelector = ({ navigation, route }: NavigationProps) => { const { params } = route; const { device, deviceModelId } = params; - + const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); const SUPPORTED_NFT_CURRENCIES = getEnv("NFT_CURRENCIES"); - const nftsOrdered = useSelector(orderedVisibleNftsSelector, isEqual); + const nftsOrdered = useSelector( + (state: State) => + orderedVisibleNftsSelector(state, Boolean(nftsFromSimplehashFeature?.enabled)), + isEqual, + ); const accounts = useSelector(accountsSelector); diff --git a/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx b/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx index 82fa971d1bf0..d87baeb3fcf3 100644 --- a/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx +++ b/apps/ledger-live-mobile/src/screens/Nft/WalletNftGallery/index.tsx @@ -11,6 +11,7 @@ import isEqual from "lodash/isEqual"; import { galleryChainFiltersSelector } from "~/reducers/nft"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; import { useNftCollections } from "~/hooks/nfts/useNftCollections"; +import { State } from "~/reducers/types"; const WalletNftGallery = () => { const { space } = useTheme(); @@ -19,7 +20,10 @@ const WalletNftGallery = () => { const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); const chainFilters = useSelector(galleryChainFiltersSelector); - const nftsOwned = useSelector(filteredNftsSelector, isEqual); + const nftsOwned = useSelector( + (state: State) => filteredNftsSelector(state, Boolean(nftsFromSimplehashFeature?.enabled)), + isEqual, + ); const addresses = useMemo( () => diff --git a/apps/ledger-live-mobile/src/screens/Settings/Accounts/HiddenNftCollections.tsx b/apps/ledger-live-mobile/src/screens/Settings/Accounts/HiddenNftCollections.tsx index 750186db4358..4c259d78a735 100644 --- a/apps/ledger-live-mobile/src/screens/Settings/Accounts/HiddenNftCollections.tsx +++ b/apps/ledger-live-mobile/src/screens/Settings/Accounts/HiddenNftCollections.tsx @@ -6,13 +6,14 @@ import { Account, NFTMetadata } from "@ledgerhq/types-live"; import { useNftCollectionMetadata, useNftMetadata } from "@ledgerhq/live-nft-react"; import { TouchableOpacity } from "react-native-gesture-handler"; import styled from "styled-components/native"; -import { NFTResource, NFTResourceLoaded } from "@ledgerhq/live-nft/types"; -import { hiddenNftCollectionsSelector } from "~/reducers/settings"; +import { NFTResource, NFTResourceLoaded, NftStatus } from "@ledgerhq/live-nft/types"; import { accountSelector } from "~/reducers/accounts"; import NftMedia from "~/components/Nft/NftMedia"; import Skeleton from "~/components/Skeleton"; -import { unhideNftCollection, whitelistNftCollection } from "~/actions/settings"; +import { updateNftStatus } from "~/actions/settings"; import { State } from "~/reducers/types"; +import { nftCollectionsStatusByNetworkSelector } from "~/reducers/settings"; +import { BlockchainEVM, BlockchainsType } from "@ledgerhq/live-nft/supported"; const MAX_COLLECTIONS_FIRST_RENDER = 10; const COLLECTIONS_TO_ADD_ON_LIST_END_REACHED = 6; @@ -88,15 +89,17 @@ const HiddenNftCollectionRow = ({ }; const HiddenNftCollections = () => { - const hiddenCollections = useSelector(hiddenNftCollectionsSelector); + const collections = useSelector(nftCollectionsStatusByNetworkSelector); + const dispatch = useDispatch(); const [collectionsCount, setCollectionsCount] = useState(MAX_COLLECTIONS_FIRST_RENDER); const onUnhideCollection = useCallback( - (collectionId: string) => { - dispatch(unhideNftCollection(collectionId)); - dispatch(whitelistNftCollection(collectionId)); + (collectionId: string, blockchain: BlockchainsType) => { + dispatch( + updateNftStatus({ blockchain, collection: collectionId, status: NftStatus.whitelisted }), + ); }, [dispatch], ); @@ -104,22 +107,36 @@ const HiddenNftCollections = () => { const renderItem = useCallback( ({ item }: { item: string }) => { const [accountId, contractAddress] = item.split("|"); + const network = (Object.keys(collections).find( + key => collections[key as BlockchainEVM][item], + ) ?? BlockchainEVM.Ethereum) as BlockchainsType; return ( onUnhideCollection(item)} + onUnhide={() => onUnhideCollection(item, network)} /> ); }, - [onUnhideCollection], + [collections, onUnhideCollection], ); const keyExtractor = useCallback((item: string) => item, []); + const hiddenNftCollections = useMemo( + () => + Object.values(collections).flatMap(network => + Object.keys(network).filter( + collection => + network[collection] === NftStatus.blacklisted || network[collection] === NftStatus.spam, + ), + ), + [collections], + ); + const collectionsSliced: string[] = useMemo( - () => hiddenCollections.slice(0, collectionsCount), - [collectionsCount, hiddenCollections], + () => hiddenNftCollections.slice(0, collectionsCount), + [collectionsCount, hiddenNftCollections], ); const onEndReached = useCallback(() => { diff --git a/apps/ledger-live-mobile/src/screens/WalletCentricSections/OperationsHistory.tsx b/apps/ledger-live-mobile/src/screens/WalletCentricSections/OperationsHistory.tsx index cf40447d0523..e6948ff8e619 100644 --- a/apps/ledger-live-mobile/src/screens/WalletCentricSections/OperationsHistory.tsx +++ b/apps/ledger-live-mobile/src/screens/WalletCentricSections/OperationsHistory.tsx @@ -20,10 +20,8 @@ import { ScreenName } from "~/const"; import { parentAccountSelector } from "~/reducers/accounts"; import { track } from "~/analytics"; import { State } from "~/reducers/types"; -import { - filterTokenOperationsZeroAmountEnabledSelector, - hiddenNftCollectionsSelector, -} from "~/reducers/settings"; +import { filterTokenOperationsZeroAmountEnabledSelector } from "~/reducers/settings"; +import { useNftCollectionsStatus } from "~/hooks/nfts/useNftCollectionsStatus"; type Props = { accounts: AccountLikeArray; @@ -44,7 +42,7 @@ const OperationsHistory = ({ accounts }: Props) => { const shouldFilterTokenOpsZeroAmount = useSelector( filterTokenOperationsZeroAmountEnabledSelector, ); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + const { hiddenNftCollections } = useNftCollectionsStatus(); const filterOperation = useCallback( (operation: Operation, account: AccountLike) => {