Skip to content

Commit

Permalink
feat: Rework hiddenCollections to handle more easily displays (#8457)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcayuelas-ledger authored Dec 2, 2024
1 parent edb3910 commit d45e871
Show file tree
Hide file tree
Showing 26 changed files with 322 additions and 120 deletions.
7 changes: 7 additions & 0 deletions .changeset/odd-cooks-glow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"ledger-live-desktop": minor
"@ledgerhq/live-nft-react": minor
"@ledgerhq/live-nft": minor
---

Rework Hiddencollections
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export function createContextMenuItems({
openModal("MODAL_HIDE_NFT_COLLECTION", {
collectionName: collectionName ?? collectionAddress,
collectionId: `${account.id}|${collectionAddress}`,
blockchain: account.currency.id,
onClose: () => {
if (goBackToAccount) {
setDrawer();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ export const useOpenHideCollectionModal = (
collectionName: collectionName as string,
collectionId: `${account.id}|${nft.contract}`,
onClose,
blockchain: account.currency.id,
}),
),
};
}, [account.id, dispatch, metadata, nft.contract, onClose, t]);
}, [account.currency.id, account.id, dispatch, metadata, nft.contract, onClose, t]);
};
3 changes: 3 additions & 0 deletions apps/ledger-live-desktop/src/renderer/actions/constants.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
// Action types

/** Settings --------- */
export const TOGGLE_MEMOTAG_INFO = "settings/toggleShouldDisplayMemoTagInfo";
export const TOGGLE_MEV = "settings/toggleMEV";
export const TOGGLE_MARKET_WIDGET = "settings/toggleMarketWidget";
export const UPDATE_NFT_COLLECTION_STATUS = "settings/updateNftCollectionStatus";
34 changes: 17 additions & 17 deletions apps/ledger-live-desktop/src/renderer/actions/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,14 @@ import {
import { useRefreshAccountsOrdering } from "~/renderer/actions/general";
import { Language, Locale } from "~/config/languages";
import { Layout } from "LLD/features/Collectibles/types/Layouts";
import { TOGGLE_MARKET_WIDGET, TOGGLE_MEMOTAG_INFO, TOGGLE_MEV } from "./constants";
import {
TOGGLE_MARKET_WIDGET,
TOGGLE_MEMOTAG_INFO,
TOGGLE_MEV,
UPDATE_NFT_COLLECTION_STATUS,
} from "./constants";
import { BlockchainsType } from "@ledgerhq/live-nft/supported";
import { NftStatus } from "@ledgerhq/live-nft/types";
export type SaveSettings = (a: Partial<Settings>) => {
type: string;
payload: Partial<Settings>;
Expand Down Expand Up @@ -215,15 +222,6 @@ export const blacklistToken = (tokenId: string) => ({
type: "BLACKLIST_TOKEN",
payload: tokenId,
});
export const hideNftCollection = (collectionId: string) => ({
type: "HIDE_NFT_COLLECTION",
payload: collectionId,
});

export const whitelistNftCollection = (collectionId: string) => ({
type: "WHITELIST_NFT_COLLECTION",
payload: collectionId,
});

export const hideOrdinalsAsset = (inscriptionId: string) => ({
type: "HIDE_ORDINALS_ASSET",
Expand Down Expand Up @@ -255,14 +253,16 @@ export const showToken = (tokenId: string) => ({
type: "SHOW_TOKEN",
payload: tokenId,
});
export const unhideNftCollection = (collectionId: string) => ({
type: "UNHIDE_NFT_COLLECTION",
payload: collectionId,
});
export const unwhitelistNftCollection = (collectionId: string) => ({
type: "UNWHITELIST_NFT_COLLECTION",
payload: collectionId,

export const updateNftStatus = (
blockchain: BlockchainsType,
collectionId: string,
status: NftStatus,
) => ({
type: UPDATE_NFT_COLLECTION_STATUS,
payload: { blockchain, collectionId, status },
});

export const unhideOrdinalsAsset = (inscriptionId: string) => ({
type: "UNHIDE_ORDINALS_ASSET",
payload: inscriptionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export default function NFTCollectionContextMenu({
history.replace(`account/${account.id}`);
}
},
blockchain: account.currency.id,
}),
),
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { hideNftCollection } from "~/renderer/actions/settings";
import { updateNftStatus } from "~/renderer/actions/settings";
import { useHideSpamCollection } from "../useHideSpamCollection";
import { renderHook } from "tests/testUtils";
import { INITIAL_STATE } from "~/renderer/reducers/settings";
import { useDispatch } from "react-redux";
import { BlockchainEVM } from "@ledgerhq/live-nft/supported";
import { NftStatus } from "@ledgerhq/live-nft/types";

jest.mock("react-redux", () => ({
...jest.requireActual("react-redux"),
Expand All @@ -17,31 +19,41 @@ describe("useHideSpamCollection", () => {
mockDispatch.mockClear();
});

it("should dispatch hideNftCollection action if collection is not whitelisted", () => {
it("should dispatch updateNftStatus action if collection is not already marked with a status", () => {
const { result } = renderHook(() => useHideSpamCollection(), {
initialState: {
settings: {
...INITIAL_STATE,
whitelistedNftCollections: ["collectionA", "collectionB"],
hiddenNftCollections: [],
nftCollectionsStatusByNetwork: {},
},
},
});
result.current.hideSpamCollection("collectionC");
result.current.hideSpamCollection("collectionC", BlockchainEVM.Ethereum);

expect(mockDispatch).toHaveBeenCalledWith(hideNftCollection("collectionC"));
expect(mockDispatch).toHaveBeenCalledWith(
updateNftStatus(BlockchainEVM.Ethereum, "collectionC", 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(), {
initialState: {
settings: {
hiddenNftCollections: [],
nftCollectionsStatusByNetwork: {
[BlockchainEVM.Ethereum]: {
collectionA: NftStatus.spam,
},
[BlockchainEVM.Avalanche]: {
collectionB: NftStatus.spam,
},
},
whitelistedNftCollections: ["collectionA", "collectionB"],
},
},
});
result.current.hideSpamCollection("collectionA");

result.current.hideSpamCollection("collectionA", BlockchainEVM.Ethereum);
result.current.hideSpamCollection("collectionB", BlockchainEVM.Avalanche);

expect(mockDispatch).not.toHaveBeenCalled();
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { renderHook } from "tests/testUtils";
import { INITIAL_STATE } from "~/renderer/reducers/settings";
import { BlockchainEVM } from "@ledgerhq/live-nft/supported";
import { NftStatus } from "@ledgerhq/live-nft/types";
import { useNftCollectionsStatus } from "../useNftCollectionsStatus";

describe("useNftCollectionsStatus", () => {
it("should returns only NFTs (contract) with NftStatus !== whitelisted when FF is ON", () => {
const { result } = renderHook(() => useNftCollectionsStatus(), {
initialState: {
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,
},
},
},
},
});

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(), {
initialState: {
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,
},
},
},
},
});

expect(result.current.hiddenNftCollections).toEqual([
"collectionETHB",
"collectionAVAX1",
"collectionAVAX3",
"collectionP1",
]);
});
});
Original file line number Diff line number Diff line change
@@ -1,25 +1,34 @@
import { useCallback } from "react";
import { useDispatch, useSelector } from "react-redux";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import { whitelistedNftCollectionsSelector } from "~/renderer/reducers/settings";
import { hideNftCollection } from "~/renderer/actions/settings";
import { nftCollectionsStatusByNetworkSelector } from "~/renderer/reducers/settings";
import { updateNftStatus } from "~/renderer/actions/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 elem = Object.entries(nftCollectionsStatusByNetwork).find(
([key]) => key === blockchain,
)?.[1];

if (!elem) {
dispatch(updateNftStatus(blockchain, collection, NftStatus.spam));
}
},
[dispatch, whitelistedNftCollections],
[dispatch, nftCollectionsStatusByNetwork],
);

return {
hideSpamCollection,
enabled: spamFilteringTxFeature?.enabled,
enabled: spamFilteringTxFeature?.enabled && nftsFromSimplehashFeature?.enabled,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 "~/renderer/reducers/settings";
import { useNftCollectionsStatus } from "./useNftCollectionsStatus";

export function useNftCollections({
account,
Expand All @@ -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 } = useNftGalleryFilter({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { useMemo } from "react";
import { useSelector } from "react-redux";
import { useFeature } from "@ledgerhq/live-common/featureFlags/index";
import { nftCollectionsStatusByNetworkSelector } from "~/renderer/reducers/settings";
import { NftStatus } from "@ledgerhq/live-nft/types";
import { BlockchainEVM } from "@ledgerhq/live-nft/supported";

export function useNftCollectionsStatus() {
const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash");
const nftCollectionsStatusByNetwork = useSelector(nftCollectionsStatusByNetworkSelector);

const shouldDisplaySpams = !nftsFromSimplehashFeature?.enabled;

const nftCollectionParser = (
nftCollection: Record<BlockchainEVM, Record<string, NftStatus>>,
applyFilterFn: (arg0: [string, NftStatus]) => boolean,
) =>
Object.values(nftCollection).flatMap(contracts =>
Object.entries(contracts)
.filter(applyFilterFn)
.map(([contract]) => contract),
);

const list = useMemo(() => {
return nftCollectionParser(nftCollectionsStatusByNetwork, ([_, status]) =>
!shouldDisplaySpams ? status !== NftStatus.whitelisted : status === NftStatus.blacklisted,
);
}, [nftCollectionsStatusByNetwork, shouldDisplaySpams]);

const whitelisted = useMemo(() => {
return nftCollectionParser(
nftCollectionsStatusByNetwork,
([_, status]) => status === NftStatus.whitelisted,
);
}, [nftCollectionsStatusByNetwork]);

return {
hiddenNftCollections: list,
whitelistedNftCollections: whitelisted,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,12 @@ export default (
collectionName: metadata?.tokenName ?? nft.contract,
collectionId: `${account.id}|${nft.contract}`,
onClose,
blockchain: account.currency.id,
}),
);
},
}),
[account.id, dispatch, metadata?.tokenName, nft.contract, onClose, t],
[account.currency.id, account.id, dispatch, metadata?.tokenName, nft.contract, onClose, t],
);
const customImageUri = useMemo(() => {
const mediaTypes = metadata ? getMetadataMediaTypes(metadata) : null;
Expand Down
Loading

0 comments on commit d45e871

Please sign in to comment.