diff --git a/.changeset/short-hotels-invite.md b/.changeset/short-hotels-invite.md new file mode 100644 index 000000000000..81c53a5bd550 --- /dev/null +++ b/.changeset/short-hotels-invite.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +UX/UI Improvements on HiddenCollections section diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx index 8dfbd271f98d..00f3910aac3a 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Nfts/screens/Gallery/useNftGalleryModel.tsx @@ -3,12 +3,13 @@ import { useDispatch, useSelector } from "react-redux"; import { State } from "~/renderer/reducers"; import { accountSelector } from "~/renderer/reducers/accounts"; import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; -import { useNftGalleryFilter, isThresholdValid, Chain } from "@ledgerhq/live-nft-react"; +import { useNftGalleryFilter, isThresholdValid } from "@ledgerhq/live-nft-react"; import { nftsByCollections } from "@ledgerhq/live-nft"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { openModal } from "~/renderer/actions/modals"; import { useOnScreen } from "LLD/hooks/useOnScreen"; import useFeature from "@ledgerhq/live-common/featureFlags/useFeature"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; const defaultNumberOfVisibleNfts = 10; @@ -31,7 +32,7 @@ const useNftGalleryModel = () => { const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ nftsOwned: account?.nfts || [], addresses: account?.freshAddress || "", - chains: [account?.currency.id ?? Chain.ETHEREUM], + chains: [account?.currency.id ?? BlockchainEVM.Ethereum], threshold: isThresholdValid(threshold) ? Number(threshold) : 75, }); diff --git a/apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts b/apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts index 695c509772cd..622b05852077 100644 --- a/apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts +++ b/apps/ledger-live-desktop/src/renderer/hooks/useSyncNFTsWithAccounts.ts @@ -1,10 +1,11 @@ import { useEffect, useMemo, useState } from "react"; import { useHideSpamCollection } from "./useHideSpamCollection"; -import { isThresholdValid, supportedChains, useCheckNftAccount } from "@ledgerhq/live-nft-react"; +import { isThresholdValid, useCheckNftAccount } from "@ledgerhq/live-nft-react"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; import { useSelector } from "react-redux"; import { accountsSelector, orderedVisibleNftsSelector } from "../reducers/accounts"; import isEqual from "lodash/isEqual"; +import { getEnv } from "@ledgerhq/live-env"; /** * Represents the size of groups for batching address fetching. @@ -35,6 +36,7 @@ const TIMER = 5 * 60 * 60 * 1000; // 5 hours = 18000000 ms */ export function useSyncNFTsWithAccounts() { + const SUPPORTED_NFT_CURRENCIES = getEnv("NFT_CURRENCIES"); const nftsFromSimplehashFeature = useFeature("nftsFromSimplehash"); const threshold = isThresholdValid(Number(nftsFromSimplehashFeature?.params?.threshold)) ? Number(nftsFromSimplehashFeature?.params?.threshold) @@ -80,7 +82,7 @@ export function useSyncNFTsWithAccounts() { useCheckNftAccount({ addresses: groupToFetch.join(","), nftsOwned, - chains: supportedChains, + chains: SUPPORTED_NFT_CURRENCIES, threshold, action: hideSpamCollection, enabled, diff --git a/apps/ledger-live-desktop/src/renderer/modals/SimpleHashTools/index.tsx b/apps/ledger-live-desktop/src/renderer/modals/SimpleHashTools/index.tsx index ec88031582f3..0ab69a72fea4 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/SimpleHashTools/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/modals/SimpleHashTools/index.tsx @@ -11,7 +11,7 @@ import RefreshMetadata, { HookResult as RefreshHookResult, useHook as useHookRefresh, } from "~/renderer/screens/settings/sections/Developer/SimpleHashTools/RefreshMetadata"; -import { Flex } from "@ledgerhq/react-ui"; +import { Flex, InfiniteLoader } from "@ledgerhq/react-ui"; import Button from "~/renderer/components/ButtonV3"; import SpamScore, { HookResult as SpamScoreHookResult, @@ -35,6 +35,7 @@ const getItems = ( cta: t("settings.developer.debugSimpleHash.debugSpamNft.report"), closeInfo: hooks.spam.closeInfo, displayInfo: hooks.spam.displayInfo, + isLoading: hooks.spam.spamReportMutation.isPending, }, { key: "refresh", @@ -44,6 +45,7 @@ const getItems = ( cta: t("settings.developer.debugSimpleHash.debugRefreshMetadata.refresh"), closeInfo: hooks.refresh.closeInfo, displayInfo: hooks.refresh.displayInfo, + isLoading: hooks.refresh.refreshMutation.isPending, }, { key: "check", @@ -53,6 +55,7 @@ const getItems = ( cta: t("settings.developer.debugSimpleHash.debugCheckSpamScore.check"), closeInfo: hooks.check.closeInfo, displayInfo: hooks.check.displayInfo, + isLoading: hooks.check.checkSpamScore.isLoading, }, ]; @@ -80,6 +83,7 @@ const SimpleHashToolsDebugger = () => { ( { height={15} /> - + {activeItem.value} @@ -109,7 +113,9 @@ const SimpleHashToolsDebugger = () => { )} renderFooter={() => ( <> - {displayInfo ? ( + {activeItem.isLoading ? ( + + ) : displayInfo ? ( diff --git a/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx b/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx index 6c2fd6ba14b1..67a85d695cdc 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/nft/Gallery/Gallery.tsx @@ -20,8 +20,9 @@ import { State } from "~/renderer/reducers"; import { ProtoNFT } from "@ledgerhq/types-live"; import theme from "@ledgerhq/react-ui/styles/theme"; import { useOnScreen } from "../useOnScreen"; -import { Chain, isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; +import { isThresholdValid, useNftGalleryFilter } from "@ledgerhq/live-nft-react"; import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; +import { BlockchainEVM } from "@ledgerhq/live-nft/supported"; const SpinnerContainer = styled.div` display: flex; @@ -69,7 +70,7 @@ const Gallery = () => { const { nfts, fetchNextPage, hasNextPage } = useNftGalleryFilter({ nftsOwned: account?.nfts || [], addresses: account?.freshAddress || "", - chains: [account?.currency.id ?? Chain.ETHEREUM], + chains: [account?.currency.id ?? BlockchainEVM.Ethereum], threshold: isThresholdValid(thresold) ? Number(thresold) : 75, }); diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections.tsx deleted file mode 100644 index 31c56de3dc5c..000000000000 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections.tsx +++ /dev/null @@ -1,169 +0,0 @@ -import React, { useCallback, useState, useMemo } from "react"; -import styled from "styled-components"; -import { useTranslation } from "react-i18next"; -import { useDispatch, useSelector } from "react-redux"; -import { useNftMetadata, useNftCollectionMetadata } from "@ledgerhq/live-nft-react"; -import { SettingsSection as Section, SettingsSectionRow as Row } from "../../SettingsSection"; -import Text from "~/renderer/components/Text"; -import Box from "~/renderer/components/Box"; -import Media from "~/renderer/components/Nft/Media"; -import Skeleton from "~/renderer/components/Nft/Skeleton"; -import { unhideNftCollection } from "~/renderer/actions/settings"; -import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; -import { accountSelector } from "~/renderer/reducers/accounts"; -import Track from "~/renderer/analytics/Track"; -import { State } from "~/renderer/reducers"; -import { NFTMetadata } from "@ledgerhq/types-live"; -import { Icons } from "@ledgerhq/react-ui"; - -const HiddenNftCollectionRow = ({ - contractAddress, - accountId, - onUnhide, -}: { - contractAddress: string; - accountId: string; - onUnhide: () => void; -}) => { - const account = useSelector((state: State) => - accountSelector(state, { - accountId, - }), - ); - const firstNft = account?.nfts?.find(nft => nft.contract === contractAddress); - const { metadata: nftMetadata, status: nftStatus } = useNftMetadata( - contractAddress, - firstNft?.tokenId, - firstNft?.currencyId, - ); - const { metadata: collectionMetadata, status: collectionStatus } = useNftCollectionMetadata( - contractAddress, - firstNft?.currencyId, - ); - const loading = useMemo( - () => nftStatus === "loading" || collectionStatus === "loading", - [collectionStatus, nftStatus], - ); - - return ( - - - {nftMetadata && firstNft && ( - - )} - - - {collectionMetadata?.tokenName || contractAddress} - - - - - - ); -}; -export default function HiddenNftCollections() { - const { t } = useTranslation(); - const dispatch = useDispatch(); - const [sectionVisible, setSectionVisible] = useState(false); - const onUnhideCollection = useCallback( - (collectionId: string) => { - dispatch(unhideNftCollection(collectionId)); - }, - [dispatch], - ); - const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); - const toggleCurrencySection = useCallback(() => { - setSectionVisible(prevState => !prevState); - }, [setSectionVisible]); - return ( -
- - - {hiddenNftCollections.length ? ( - - - {t("settings.accounts.hiddenNftCollections.count", { - count: hiddenNftCollections.length, - })} - - - - - - ) : null} - - - {sectionVisible && ( - - {hiddenNftCollections.map(collectionId => { - const [accountId, contractAddress] = collectionId.split("|"); - return ( - onUnhideCollection(collectionId)} - /> - ); - })} - - )} -
- ); -} -const IconContainer = styled.div` - color: ${p => p.theme.colors.palette.text.shade60}; - text-align: center; - &:hover { - cursor: pointer; - color: ${p => p.theme.colors.palette.text.shade40}; - } -`; -const HiddenNftCollectionRowContainer = styled(Box).attrs({ - alignItems: "center", - horizontal: true, - flow: 1, - py: 1, -})` - margin: 0px; - &:not(:last-child) { - border-bottom: 1px solid ${p => p.theme.colors.palette.text.shade10}; - } - padding: 14px 6px; -`; -const Body = styled(Box)` - &:not(:empty) { - padding: 0 20px; - } -`; - -const Show = styled(Box).attrs<{ visible?: boolean }>({})<{ visible?: boolean }>` - transform: rotate(${p => (p.visible ? 0 : 270)}deg); -`; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/helpers.ts b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/helpers.ts new file mode 100644 index 000000000000..161739c68aaa --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/helpers.ts @@ -0,0 +1,2 @@ +export const splitAddress = (str: string, length: number) => + `${str.slice(0, length)}...${str.slice(-length)}`; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx new file mode 100644 index 000000000000..e046da17d952 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/index.tsx @@ -0,0 +1,91 @@ +import React, { useState, useCallback } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; +import styled from "styled-components"; +import { unhideNftCollection } from "~/renderer/actions/settings"; +import { hiddenNftCollectionsSelector } from "~/renderer/reducers/settings"; + +import { SettingsSection as Section, SettingsSectionRow as Row } from "../../../SettingsSection"; +import Box from "~/renderer/components/Box"; +import Track from "~/renderer/analytics/Track"; + +import IconAngleDown from "~/renderer/icons/AngleDown"; +import { HiddenNftCollectionRow } from "./row"; + +export default function HiddenNftCollections() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const [sectionVisible, setSectionVisible] = useState(false); + + const hiddenNftCollections = useSelector(hiddenNftCollectionsSelector); + + const onUnhideCollection = useCallback( + (collectionId: string) => { + dispatch(unhideNftCollection(collectionId)); + }, + [dispatch], + ); + + const toggleHiddenCollectionsSection = useCallback(() => { + setSectionVisible(prevState => !prevState); + }, []); + + return ( +
+ + + {hiddenNftCollections.length ? ( + + + {t("settings.accounts.hiddenNftCollections.count", { + count: hiddenNftCollections.length, + })} + + + + + + ) : null} + + + {sectionVisible && ( + + {hiddenNftCollections.map(collectionId => { + const [accountId, contractAddress] = collectionId.split("|"); + return ( + onUnhideCollection(collectionId)} + /> + ); + })} + + )} +
+ ); +} + +// Styled components and layout + +const Body = styled(Box)` + &:not(:empty) { + padding: 0 20px; + } +`; + +const Show = styled(Box).attrs<{ visible?: boolean }>({})<{ visible?: boolean }>` + transform: rotate(${p => (p.visible ? 0 : 270)}deg); +`; + +const Collections = styled(Box)` + &:hover { + cursor: pointer; + } +`; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/row.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/row.tsx new file mode 100644 index 000000000000..477f1958481f --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenNFTCollections/row.tsx @@ -0,0 +1,127 @@ +import React, { useState, useMemo, useCallback } from "react"; +import { useSelector } from "react-redux"; +import { useTranslation } from "react-i18next"; +import { useNftMetadata, useNftCollectionMetadata } from "@ledgerhq/live-nft-react"; +import { accountSelector } from "~/renderer/reducers/accounts"; +import { State } from "~/renderer/reducers"; +import { NFTMetadata } from "@ledgerhq/types-live"; +import { clipboard } from "electron"; + +import Text from "~/renderer/components/Text"; +import Media from "~/renderer/components/Nft/Media"; +import Skeleton from "~/renderer/components/Nft/Skeleton"; +import { Flex, Icons } from "@ledgerhq/react-ui"; + +import IconCross from "~/renderer/icons/Cross"; +import styled from "styled-components"; +import { splitAddress } from "./helpers"; + +export const HiddenNftCollectionRow = ({ + contractAddress, + accountId, + onUnhide, +}: { + contractAddress: string; + accountId: string; + onUnhide: () => void; +}) => { + const { t } = useTranslation(); + const account = useSelector((state: State) => accountSelector(state, { accountId })); + const firstNft = useMemo( + () => account?.nfts?.find(nft => nft.contract === contractAddress), + [account, contractAddress], + ); + + const { metadata: nftMetadata, status: nftStatus } = useNftMetadata( + contractAddress, + firstNft?.tokenId, + firstNft?.currencyId, + ); + const { metadata: collectionMetadata, status: collectionStatus } = useNftCollectionMetadata( + contractAddress, + firstNft?.currencyId, + ); + + const loading = useMemo( + () => nftStatus === "loading" || collectionStatus === "loading", + [nftStatus, collectionStatus], + ); + + const [copyFeedback, setCopyFeedback] = useState(false); + const onCopy = useCallback(() => { + clipboard.writeText(contractAddress); + setCopyFeedback(true); + + setTimeout(() => setCopyFeedback(false), 1e3); + }, [contractAddress]); + + return ( + + + + {nftMetadata && firstNft && ( + + )} + + + + + {collectionMetadata?.tokenName || contractAddress} + + + + + {splitAddress(contractAddress, 8)} + + + {!copyFeedback ? ( + + ) : ( + <> + + + {t("common.addressCopied")} + + + )} + + + + + + + + + ); +}; + +const IconContainer = styled(Flex)` + color: ${p => p.theme.colors.palette.text.shade60}; + &:hover { + cursor: pointer; + color: ${p => p.theme.colors.palette.text.shade40}; + } +`; + +const HiddenNftCollectionRowContainer = styled(Flex).attrs({ + alignItems: "center", + justifyContent: "space-between", + py: 1, +})` + margin: 0px; + &:not(:last-child) { + border-bottom: 1px solid ${p => p.theme.colors.palette.text.shade10}; + } + padding: 14px 6px; +`; + +const StyledFlex = styled(Flex)` + &:hover { + cursor: pointer; + color: ${p => p.theme.colors.primary.c80}; + } +`; diff --git a/libs/live-nft-react/src/index.ts b/libs/live-nft-react/src/index.ts index 09f4ee858db1..bbe9948fc0b9 100644 --- a/libs/live-nft-react/src/index.ts +++ b/libs/live-nft-react/src/index.ts @@ -10,4 +10,3 @@ export * from "./hooks/helpers/ordinals"; export * from "./hooks/useCheckNftAccount"; export * from "./hooks/helpers/index"; export * from "./hooks/types"; -export * from "./supported"; diff --git a/libs/live-nft-react/src/supported.ts b/libs/live-nft-react/src/supported.ts deleted file mode 100644 index 309d048b39dd..000000000000 --- a/libs/live-nft-react/src/supported.ts +++ /dev/null @@ -1,6 +0,0 @@ -export enum Chain { - ETHEREUM = "ethereum", - POLYGON = "polygon", -} - -export const supportedChains = [Chain.ETHEREUM, Chain.POLYGON];