From ef1e14ab91b2e672d68429e49eae6127404f2f12 Mon Sep 17 00:00:00 2001 From: Lucas Werey Date: Tue, 1 Oct 2024 17:33:25 +0200 Subject: [PATCH] :sparkles:(lld): add hide inscription feature --- .changeset/breezy-cycles-design.md | 5 + .../ContextMenu/CollectibleContextMenu.tsx | 6 + .../ContextMenu/createContextMenuItems.ts | 26 +++ .../Inscriptions/DetailsDrawer/Actions.tsx | 21 ++- .../Inscriptions/DetailsDrawer/index.tsx | 2 +- .../Inscriptions/HideModal/Body.tsx | 60 +++++++ .../Inscriptions/HideModal/index.tsx | 21 +++ .../components/Inscriptions/Item/index.tsx | 31 +++- .../components/Inscriptions/helpers.ts | 2 +- .../components/Inscriptions/index.tsx | 7 + .../Inscriptions/useInscriptionsModel.tsx | 11 +- .../Ordinals/screens/Account/index.tsx | 1 + .../screens/Account/useBitcoinAccountModel.ts | 14 +- .../__integration__/bitcoinPage.test.tsx | 35 +++- .../src/renderer/actions/settings.ts | 9 + .../src/renderer/modals/index.ts | 2 + .../src/renderer/modals/types.ts | 5 + .../src/renderer/reducers/settings.ts | 19 +++ .../Accounts/HiddenOrdinalsAssets.tsx | 156 ++++++++++++++++++ .../settings/sections/Accounts/index.tsx | 5 + .../static/i18n/en/app.json | 19 ++- apps/ledger-live-desktop/tests/testUtils.tsx | 5 +- .../useFetchOrdinalByTokenId.test.tsx | 39 +++++ .../src/hooks/useFetchOrdinalByTokenId.ts | 21 +++ libs/live-nft-react/src/index.ts | 1 + libs/live-nft-react/src/queryKeys.ts | 1 + libs/live-nft/src/api/simplehash.ts | 26 ++- 27 files changed, 516 insertions(+), 34 deletions(-) create mode 100644 .changeset/breezy-cycles-design.md create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/HideModal/Body.tsx create mode 100644 apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/HideModal/index.tsx create mode 100644 apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenOrdinalsAssets.tsx create mode 100644 libs/live-nft-react/src/hooks/__tests__/useFetchOrdinalByTokenId.test.tsx create mode 100644 libs/live-nft-react/src/hooks/useFetchOrdinalByTokenId.ts diff --git a/.changeset/breezy-cycles-design.md b/.changeset/breezy-cycles-design.md new file mode 100644 index 000000000000..d8c9eb79ebbc --- /dev/null +++ b/.changeset/breezy-cycles-design.md @@ -0,0 +1,5 @@ +--- +"ledger-live-desktop": patch +--- + +Add the hide inscription feature for ordinals diff --git a/apps/ledger-live-desktop/src/newArch/components/ContextMenu/CollectibleContextMenu.tsx b/apps/ledger-live-desktop/src/newArch/components/ContextMenu/CollectibleContextMenu.tsx index 0791c9c545a6..526d3d2f2067 100644 --- a/apps/ledger-live-desktop/src/newArch/components/ContextMenu/CollectibleContextMenu.tsx +++ b/apps/ledger-live-desktop/src/newArch/components/ContextMenu/CollectibleContextMenu.tsx @@ -16,6 +16,8 @@ type Props = { leftClick?: boolean; goBackToAccount?: boolean; typeOfCollectible: CollectibleType; + inscriptionId?: string; + inscriptionName?: string; }; export default function CollectionContextMenu({ @@ -26,6 +28,8 @@ export default function CollectionContextMenu({ leftClick, goBackToAccount = false, typeOfCollectible, + inscriptionId, + inscriptionName, }: Props) { const { t } = useTranslation(); const dispatch = useDispatch(); @@ -36,6 +40,8 @@ export default function CollectionContextMenu({ collectionName, collectionAddress, account, + inscriptionId, + inscriptionName, dispatch, setDrawer, history, diff --git a/apps/ledger-live-desktop/src/newArch/components/ContextMenu/createContextMenuItems.ts b/apps/ledger-live-desktop/src/newArch/components/ContextMenu/createContextMenuItems.ts index befe1ce2f99a..ff1d8206cd96 100644 --- a/apps/ledger-live-desktop/src/newArch/components/ContextMenu/createContextMenuItems.ts +++ b/apps/ledger-live-desktop/src/newArch/components/ContextMenu/createContextMenuItems.ts @@ -12,6 +12,8 @@ type Props = { collectionAddress: string; collectionName?: string | null; goBackToAccount?: boolean; + inscriptionId?: string; + inscriptionName?: string; typeOfCollectible: CollectibleType; dispatch: Dispatch; setDrawer: () => void; @@ -24,6 +26,8 @@ export function createContextMenuItems({ collectionName, collectionAddress, account, + inscriptionId, + inscriptionName, dispatch, setDrawer, history, @@ -52,5 +56,27 @@ export function createContextMenuItems({ }, ]; } + if (typeOfCollectible === CollectibleTypeEnum.Inscriptions) { + return [ + { + key: "hide", + label: t("ordinals.inscriptions.hide"), + Icon: IconsLegacy.NoneMedium, + callback: () => + dispatch( + openModal("MODAL_HIDE_INSCRIPTION", { + inscriptionName: String(inscriptionName), + inscriptionId: String(inscriptionId), + onClose: () => { + if (goBackToAccount) { + setDrawer(); + history.replace(`account/${account.id}`); + } + }, + }), + ), + }, + ]; + } return []; } diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/Actions.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/Actions.tsx index 6f84b66e4955..12328c76cb48 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/Actions.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/DetailsDrawer/Actions.tsx @@ -5,23 +5,36 @@ import { useTranslation } from "react-i18next"; import { ExternalViewerButton } from "LLD/features/Collectibles/components/DetailDrawer/components"; import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; import { Account } from "@ledgerhq/types-live"; +import { useDispatch } from "react-redux"; +import { openModal } from "~/renderer/actions/modals"; type ActionsProps = { inscription: SimpleHashNft; account: Account; + onModalClose: () => void; }; -const Actions: React.FC = ({ inscription, account }) => { +const Actions: React.FC = ({ inscription, account, onModalClose }) => { const { t } = useTranslation(); + const dispatch = useDispatch(); + + const onClick = () => { + dispatch( + openModal("MODAL_HIDE_INSCRIPTION", { + inscriptionName: inscription.name || inscription.contract.name || "", + inscriptionId: String(inscription.extra_metadata?.ordinal_details?.inscription_id), + onClose: () => onModalClose(), + }), + ); + }; + return ( + + + )} + /> + ); +}; +export default Body; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/HideModal/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/HideModal/index.tsx new file mode 100644 index 000000000000..745d4df848d7 --- /dev/null +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/HideModal/index.tsx @@ -0,0 +1,21 @@ +import React from "react"; +import Modal from "~/renderer/components/Modal"; +import Body from "./Body"; + +const HideNftCollectionModal = () => ( + ( + { + onClose?.(); + data?.onClose?.(); + }} + /> + )} + /> +); +export default HideNftCollectionModal; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item/index.tsx index b9c404527346..d0b946e5fd8b 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/Item/index.tsx @@ -4,10 +4,14 @@ import TableRow from "LLD/features/Collectibles/components/Collection/TableRow"; import { GroupedNftOrdinals } from "@ledgerhq/live-nft-react/index"; import { findCorrespondingSat } from "LLD/features/Collectibles/utils/findCorrespondingSat"; import { processRareSat } from "../helpers"; +import { CollectibleTypeEnum } from "LLD/features/Collectibles/types/enum/Collectibles"; +import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types"; +import CollectionContextMenu from "LLD/components/ContextMenu/CollectibleContextMenu"; type ItemProps = { isLoading: boolean; inscriptionsGroupedWithRareSats: GroupedNftOrdinals[]; + account: BitcoinAccount; } & InscriptionsItemProps; const Item: React.FC = ({ @@ -17,6 +21,7 @@ const Item: React.FC = ({ media, nftId, inscriptionsGroupedWithRareSats, + account, onClick, }) => { const correspondingRareSat = findCorrespondingSat(inscriptionsGroupedWithRareSats, nftId); @@ -25,15 +30,23 @@ const Item: React.FC = ({ }, [correspondingRareSat]); return ( - + + + ); }; diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts index 695f0a214b1d..775becabc731 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/helpers.ts @@ -7,7 +7,7 @@ export function getInscriptionsData( ) { return inscriptions.map(item => ({ tokenName: item.name || item.contract.name || "", - nftId: item.nft_id, + nftId: String(item.extra_metadata?.ordinal_details?.inscription_id), collectionName: item.collection.name, media: { uri: item.image_url || item.previews?.image_small_url, diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx index e6269cb4034b..96a42869d625 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/index.tsx @@ -14,12 +14,14 @@ import { CollectibleTypeEnum } from "LLD/features/Collectibles/types/enum/Collec import Button from "~/renderer/components/Button"; import { useTranslation } from "react-i18next"; import { GroupedNftOrdinals } from "@ledgerhq/live-nft-react/index"; +import { BitcoinAccount } from "@ledgerhq/coin-bitcoin/lib/types"; type ViewProps = ReturnType & { isLoading: boolean; isError: boolean; error: Error | null; inscriptionsGroupedWithRareSats: GroupedNftOrdinals[]; + account: BitcoinAccount; onReceive: () => void; }; @@ -29,6 +31,7 @@ type Props = { isError: boolean; error: Error | null; inscriptionsGroupedWithRareSats: GroupedNftOrdinals[]; + account: BitcoinAccount; onReceive: () => void; onInscriptionClick: (inscription: SimpleHashNft) => void; }; @@ -40,6 +43,7 @@ const View: React.FC = ({ inscriptions, error, inscriptionsGroupedWithRareSats, + account, onShowMore, onReceive, }) => { @@ -57,6 +61,7 @@ const View: React.FC = ({ {hasInscriptions && inscriptions.map((item, index) => ( = ({ isError, error, inscriptionsGroupedWithRareSats, + account, onReceive, onInscriptionClick, }) => ( @@ -92,6 +98,7 @@ const Inscriptions: React.FC = ({ isLoading={isLoading} isError={isError} error={error} + account={account} onReceive={onReceive} inscriptionsGroupedWithRareSats={inscriptionsGroupedWithRareSats} {...useInscriptionsModel({ inscriptions, onInscriptionClick })} diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/useInscriptionsModel.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/useInscriptionsModel.tsx index 431e3234b68f..83d5c472a27f 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/useInscriptionsModel.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/components/Inscriptions/useInscriptionsModel.tsx @@ -22,11 +22,16 @@ export const useInscriptionsModel = ({ inscriptions, onInscriptionClick }: Props useState(initialDisplayedObjects); useEffect(() => { - if (displayedObjects.length === 0) { - if (items.length > 3) setDisplayShowMore(true); + const filteredDisplayedObjects = displayedObjects.filter(displayedObject => + items.some(item => item.nftId === displayedObject.nftId), + ); + if (filteredDisplayedObjects.length !== displayedObjects.length) { + setDisplayedObjects(filteredDisplayedObjects); + } else if (displayedObjects.length === 0 && items.length > 3) { + setDisplayShowMore(true); setDisplayedObjects(items.slice(0, 3)); } - }, [items, displayedObjects.length]); + }, [items, displayedObjects]); const onShowMore = () => { setDisplayedObjects(prevDisplayedObjects => { diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx index 372d2c1a5e18..5664ebebbc9e 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/index.tsx @@ -36,6 +36,7 @@ const View: React.FC = ({ onReceive={onReceive} onInscriptionClick={onInscriptionClick} inscriptionsGroupedWithRareSats={inscriptionsGroupedWithRareSats} + account={account} /> diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/useBitcoinAccountModel.ts b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/useBitcoinAccountModel.ts index c1c8d0ad620b..0bd02c538c4e 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/useBitcoinAccountModel.ts +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/Ordinals/screens/Account/useBitcoinAccountModel.ts @@ -5,7 +5,10 @@ import { useState, useEffect, useCallback } from "react"; import { useDispatch, useSelector } from "react-redux"; import { openModal } from "~/renderer/actions/modals"; import { setHasSeenOrdinalsDiscoveryDrawer } from "~/renderer/actions/settings"; -import { hasSeenOrdinalsDiscoveryDrawerSelector } from "~/renderer/reducers/settings"; +import { + hasSeenOrdinalsDiscoveryDrawerSelector, + hiddenOrdinalsAssetSelector, +} from "~/renderer/reducers/settings"; import { findCorrespondingSat } from "LLD/features/Collectibles/utils/findCorrespondingSat"; interface Props { @@ -14,6 +17,7 @@ interface Props { export const useBitcoinAccountModel = ({ account }: Props) => { const dispatch = useDispatch(); + const hiddenOrdinalAssets = useSelector(hiddenOrdinalsAssetSelector); const hasSeenDiscoveryDrawer = useSelector(hasSeenOrdinalsDiscoveryDrawerSelector); const [selectedInscription, setSelectedInscription] = useState(null); const [correspondingRareSat, setCorrespondingRareSat] = useState< @@ -24,6 +28,12 @@ export const useBitcoinAccountModel = ({ account }: Props) => { account, }); + const filteredInscriptions = inscriptions.filter(inscription => { + return !hiddenOrdinalAssets.includes( + String(inscription.extra_metadata?.ordinal_details?.inscription_id), + ); + }); + const [isDrawerOpen, setIsDrawerOpen] = useState(!hasSeenDiscoveryDrawer); useEffect(() => { @@ -56,7 +66,7 @@ export const useBitcoinAccountModel = ({ account }: Props) => { return { rareSats, - inscriptions, + inscriptions: filteredInscriptions, rest, isDrawerOpen, selectedInscription, diff --git a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx index a34984d96302..e43ff0cae9a2 100644 --- a/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx +++ b/apps/ledger-live-desktop/src/newArch/features/Collectibles/__integration__/bitcoinPage.test.tsx @@ -6,6 +6,7 @@ import { render, screen, waitFor } from "tests/testUtils"; import { BitcoinPage } from "./shared"; import { openURL } from "~/renderer/linking"; import { DeviceModelId } from "@ledgerhq/devices"; +import { INITIAL_STATE as INITIAL_STATE_SETTINGS } from "~/renderer/reducers/settings"; jest.mock( "electron", @@ -22,20 +23,22 @@ describe("displayBitcoinPage", () => { const { user } = render(, { initialState: { settings: { + ...INITIAL_STATE_SETTINGS, hasSeenOrdinalsDiscoveryDrawer: true, devicesModelList: [DeviceModelId.stax, DeviceModelId.europa], + hiddenOrdinalsAsset: [], }, }, }); await waitFor(() => expect(screen.getByText(/the great war #3695/i)).toBeVisible()); - await waitFor(() => expect(screen.getByText(/see more inscriptions/i)).toBeVisible()); + expect(screen.getByText(/see more inscriptions/i)).toBeVisible(); await user.click(screen.getByText(/see more inscriptions/i)); await user.click(screen.getByText(/see more inscriptions/i)); - await waitFor(() => expect(screen.getByText(/bitcoin puppet #71/i)).toBeVisible()); - await waitFor(() => expect(screen.queryAllByTestId(/raresaticon-pizza-0/i)).toHaveLength(2)); + expect(screen.getByText(/bitcoin puppet #71/i)).toBeVisible(); + expect(screen.queryAllByTestId(/raresaticon-pizza-0/i)).toHaveLength(2); await user.hover(screen.queryAllByTestId(/raresaticon-pizza-0/i)[0]); - await waitFor(() => expect(screen.getByText(/papa john's pizza/i)).toBeVisible()); + expect(screen.getByText(/papa john's pizza/i)).toBeVisible(); }); it("should open discovery drawer when it is the first time feature is activated", async () => { @@ -50,16 +53,34 @@ describe("displayBitcoinPage", () => { const { user } = render(, { initialState: { settings: { + ...INITIAL_STATE_SETTINGS, hasSeenOrdinalsDiscoveryDrawer: true, devicesModelList: [DeviceModelId.stax, DeviceModelId.europa], + hiddenOrdinalsAsset: [], }, }, }); await waitFor(() => expect(screen.getByText(/the great war #3695/i)).toBeVisible()); await user.click(screen.getByText(/the great war #3695/i)); - await expect(screen.getByText(/hide/i)).toBeVisible(); - // sat name - await expect(screen.getByText(/dlngbapxjdv/i)).toBeVisible(); + expect(screen.getByText(/hide/i)).toBeVisible(); + expect(screen.getByText(/dlngbapxjdv/i)).toBeVisible(); + }); + + it("should open context menu", async () => { + const { user } = render(, { + initialState: { + settings: { + ...INITIAL_STATE_SETTINGS, + hasSeenOrdinalsDiscoveryDrawer: true, + devicesModelList: [DeviceModelId.stax, DeviceModelId.europa], + hiddenOrdinalsAsset: [], + }, + }, + }); + + await waitFor(() => expect(screen.getByText(/the great war #3695/i)).toBeVisible()); + await user.pointer({ keys: "[MouseRight>]", target: screen.getByText(/the great war #3695/i) }); + expect(screen.getByText(/hide inscription/i)); }); }); diff --git a/apps/ledger-live-desktop/src/renderer/actions/settings.ts b/apps/ledger-live-desktop/src/renderer/actions/settings.ts index ad3a94577590..e90520966f32 100644 --- a/apps/ledger-live-desktop/src/renderer/actions/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/actions/settings.ts @@ -218,6 +218,11 @@ export const hideNftCollection = (collectionId: string) => ({ type: "HIDE_NFT_COLLECTION", payload: collectionId, }); +export const hideOrdinalsAsset = (inscriptionId: string) => ({ + type: "HIDE_ORDINALS_ASSET", + payload: inscriptionId, +}); + export const setLastSeenCustomImage = (lastSeenCustomImage: { imageSize: number; imageHash: string; @@ -247,6 +252,10 @@ export const unhideNftCollection = (collectionId: string) => ({ type: "UNHIDE_NFT_COLLECTION", payload: collectionId, }); +export const unhideOrdinalsAsset = (inscriptionId: string) => ({ + type: "UNHIDE_ORDINALS_ASSET", + payload: inscriptionId, +}); type FetchSettings = (a: SettingsState) => (a: Dispatch>) => void; export const fetchSettings: FetchSettings = (settings: SettingsState) => dispatch => { dispatch({ diff --git a/apps/ledger-live-desktop/src/renderer/modals/index.ts b/apps/ledger-live-desktop/src/renderer/modals/index.ts index b9a57c49123c..5a8e78f49503 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/index.ts +++ b/apps/ledger-live-desktop/src/renderer/modals/index.ts @@ -30,6 +30,7 @@ import MODAL_PROTECT_DISCOVER from "./ProtectDiscover"; import MODAL_CONFIRM from "./ConfirmModal"; import MODAL_ERROR from "./ErrorModal"; import MODAL_VAULT_SIGNER from "./VaultSigner"; +import MODAL_HIDE_INSCRIPTION from "LLD/features/Collectibles/Ordinals/components/Inscriptions/HideModal"; import MODAL_WALLET_SYNC_DEBUGGER from "./WalletSyncDebugger"; import MODAL_SIMPLEHASH_TOOLS from "./SimpleHashTools"; @@ -63,6 +64,7 @@ const globalModals: GlobalModals = { MODAL_CREATE_LOCAL_APP, MODAL_WALLET_SYNC_DEBUGGER, MODAL_SIMPLEHASH_TOOLS, + MODAL_HIDE_INSCRIPTION, // Platform MODAL_PLATFORM_EXCHANGE_START, diff --git a/apps/ledger-live-desktop/src/renderer/modals/types.ts b/apps/ledger-live-desktop/src/renderer/modals/types.ts index 1d86c6af1fbb..5b4a110dffca 100644 --- a/apps/ledger-live-desktop/src/renderer/modals/types.ts +++ b/apps/ledger-live-desktop/src/renderer/modals/types.ts @@ -88,6 +88,11 @@ export type GlobalModalData = { MODAL_CONFIRM: ConfirmProps; MODAL_ERROR: ErrorProps; MODAL_VAULT_SIGNER: undefined; + MODAL_HIDE_INSCRIPTION: { + inscriptionName: string; + inscriptionId: string; + onClose?: () => void; + }; }; /** diff --git a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts index 24f033dc2cba..38cabc8fb56e 100644 --- a/apps/ledger-live-desktop/src/renderer/reducers/settings.ts +++ b/apps/ledger-live-desktop/src/renderer/reducers/settings.ts @@ -82,6 +82,7 @@ export type SettingsState = { starredAccountIds?: string[]; blacklistedTokenIds: string[]; hiddenNftCollections: string[]; + hiddenOrdinalsAsset: string[]; deepLinkUrl: string | undefined | null; lastSeenCustomImage: { size: number; @@ -181,6 +182,7 @@ export const INITIAL_STATE: SettingsState = { latestFirmware: null, blacklistedTokenIds: [], hiddenNftCollections: [], + hiddenOrdinalsAsset: [], deepLinkUrl: null, firstTimeLend: false, showClearCacheBanner: false, @@ -226,6 +228,8 @@ type HandlersPayloads = { BLACKLIST_TOKEN: string; UNHIDE_NFT_COLLECTION: string; HIDE_NFT_COLLECTION: string; + UNHIDE_ORDINALS_ASSET: string; + HIDE_ORDINALS_ASSET: string; LAST_SEEN_DEVICE_INFO: { lastSeenDevice: DeviceModelInfo; latestFirmware: FirmwareUpdateContext; @@ -333,6 +337,20 @@ const handlers: SettingsHandlers = { hiddenNftCollections: [...collections, collectionId], }; }, + UNHIDE_ORDINALS_ASSET: (state, { payload: inscriptionId }) => { + const ids = state.hiddenOrdinalsAsset; + return { + ...state, + hiddenOrdinalsAsset: ids.filter(id => id !== inscriptionId), + }; + }, + HIDE_ORDINALS_ASSET: (state, { payload: inscriptionId }) => { + const collections = state.hiddenOrdinalsAsset; + return { + ...state, + hiddenOrdinalsAsset: [...collections, inscriptionId], + }; + }, LAST_SEEN_DEVICE_INFO: (state, { payload }) => ({ ...state, lastSeenDevice: Object.assign({}, state.lastSeenDevice, payload.lastSeenDevice), @@ -743,6 +761,7 @@ export const enableLearnPageStagingUrlSelector = (state: State) => state.settings.enableLearnPageStagingUrl; export const blacklistedTokenIdsSelector = (state: State) => state.settings.blacklistedTokenIds; export const hiddenNftCollectionsSelector = (state: State) => state.settings.hiddenNftCollections; +export const hiddenOrdinalsAssetSelector = (state: State) => state.settings.hiddenOrdinalsAsset; export const hasCompletedOnboardingSelector = (state: State) => state.settings.hasCompletedOnboarding || getEnv("SKIP_ONBOARDING"); export const dismissedBannersSelector = (state: State) => state.settings.dismissedBanners || []; diff --git a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenOrdinalsAssets.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenOrdinalsAssets.tsx new file mode 100644 index 000000000000..624f1bc1f8d8 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/HiddenOrdinalsAssets.tsx @@ -0,0 +1,156 @@ +import React, { useCallback, useState } from "react"; +import styled from "styled-components"; +import { useTranslation } from "react-i18next"; +import { useDispatch, useSelector } from "react-redux"; +import { SettingsSection as Section, SettingsSectionRow as Row } from "../../SettingsSection"; +import Text from "~/renderer/components/Text"; +import Box from "~/renderer/components/Box"; +import IconCross from "~/renderer/icons/Cross"; +import { unhideOrdinalsAsset } from "~/renderer/actions/settings"; +import { hiddenOrdinalsAssetSelector } from "~/renderer/reducers/settings"; +import Track from "~/renderer/analytics/Track"; +import IconAngleDown from "~/renderer/icons/AngleDown"; +import { useFetchOrdinalByTokenId } from "@ledgerhq/live-nft-react"; +import Skeleton from "~/renderer/components/Nft/Skeleton"; +import { Flex } from "@ledgerhq/react-ui"; +import { Media } from "LLD/features/Collectibles/components"; + +const HiddenOrdinalsAssetRow = ({ + contractAddress, + onUnhide, +}: { + contractAddress: string; + onUnhide: () => void; +}) => { + const { data, isLoading } = useFetchOrdinalByTokenId(contractAddress); + + const inscriptionName = data?.name || data?.contract.name || contractAddress; + const previewUri = data?.previews?.image_large_url || data?.image_url; + + return ( + + + + + + + {inscriptionName} + + + + + + + + + ); +}; +export default function HiddenOrdinalsAssets() { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const [sectionVisible, setSectionVisible] = useState(false); + const onUnhideCollection = useCallback( + (collectionId: string) => { + dispatch(unhideOrdinalsAsset(collectionId)); + }, + [dispatch], + ); + const hiddenOrdinalAssets = useSelector(hiddenOrdinalsAssetSelector); + const toggleCurrencySection = useCallback(() => { + setSectionVisible(prevState => !prevState); + }, [setSectionVisible]); + + return ( +
+ + + {hiddenOrdinalAssets.length ? ( + + + {t("settings.accounts.hiddenOrdinalsAsset.count", { + count: hiddenOrdinalAssets.length, + })} + + + + + + ) : null} + + + {sectionVisible && ( + + {hiddenOrdinalAssets.map(inscriptionId => { + return ( + onUnhideCollection(inscriptionId)} + /> + ); + })} + + )} +
+ ); +} +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/index.tsx b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/index.tsx index cd5aff1b899c..568d6637b4f8 100644 --- a/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/index.tsx +++ b/apps/ledger-live-desktop/src/renderer/screens/settings/sections/Accounts/index.tsx @@ -8,8 +8,12 @@ import SectionExport from "./Export"; import Currencies from "./Currencies"; import BlacklistedTokens from "./BlacklistedTokens"; import HiddenNftCollections from "./HiddenNFTCollections"; +import HiddenOrdinalsAssets from "./HiddenOrdinalsAssets"; +import { useFeature } from "@ledgerhq/live-common/featureFlags/index"; export default function SectionAccounts() { const { t } = useTranslation(); + const ordinalsFeatureFlag = useFeature("lldnewArchOrdinals"); + const isOrdinalsEnabled = ordinalsFeatureFlag?.enabled; return ( @@ -25,6 +29,7 @@ export default function SectionAccounts() { + {isOrdinalsEnabled && } ); diff --git a/apps/ledger-live-desktop/static/i18n/en/app.json b/apps/ledger-live-desktop/static/i18n/en/app.json index f3239d24d7f8..4b27fa2e163f 100644 --- a/apps/ledger-live-desktop/static/i18n/en/app.json +++ b/apps/ledger-live-desktop/static/i18n/en/app.json @@ -4354,14 +4354,20 @@ "tokenBlacklist": { "title": "Hidden tokens", "desc": "You can hide tokens by going to the parent account then right-clicking on the token and selecting 'Hide token'.", - "count": "{{count}} token", - "count_plural": "{{count}} tokens" + "count": "1 token", + "count_other": "{{count}} tokens" }, "hiddenNftCollections": { "title": "Hidden NFT Collections", "desc": "You can hide NFT Collections by right-clicking on the collection name and selecting 'Hide NFT Collection'.", - "count": "{{count}} collection", - "count_plural": "{{count}} collections" + "count": "1 collection", + "count_other": "{{count}} collections" + }, + "hiddenOrdinalsAsset": { + "title": "Hidden Inscriptions", + "desc": "You can hide Inscription by right-clicking on the inscription and selecting 'Hide Inscription'.", + "count": "1 inscription", + "count_other": "{{count}} inscriptions" }, "fullNode": { "title": "Connect Bitcoin full node", @@ -6596,6 +6602,11 @@ "seeMore": "See more inscriptions", "empty": "To add Inscriptions, simply send them to your Bitcoin address.", "receive": "Receive Inscription", + "hide": "Hide Inscription", + "modal": { + "title": "Hide Inscription", + "desc": "This action will hide the following inscription <0>{{inscriptionName}} from your account. You can unhide it at any time from the settings." + }, "discoveryDrawer": { "title": "Discover Ordinals", "description": "Do you know that you may own valuable rare sats and inscriptions?", diff --git a/apps/ledger-live-desktop/tests/testUtils.tsx b/apps/ledger-live-desktop/tests/testUtils.tsx index 3c7d407da406..a516d67977af 100644 --- a/apps/ledger-live-desktop/tests/testUtils.tsx +++ b/apps/ledger-live-desktop/tests/testUtils.tsx @@ -16,6 +16,7 @@ import { getCurrencyBridge } from "@ledgerhq/live-common/bridge/index"; import DrawerProvider from "~/renderer/drawers/Provider"; import { FirebaseFeatureFlagsProvider } from "~/renderer/components/FirebaseFeatureFlags"; import { getFeature } from "./featureFlags"; +import ContextMenuWrapper from "~/renderer/components/ContextMenu/ContextMenuWrapper"; config.disabled = true; @@ -58,7 +59,9 @@ function render( - {children} + + {children} + diff --git a/libs/live-nft-react/src/hooks/__tests__/useFetchOrdinalByTokenId.test.tsx b/libs/live-nft-react/src/hooks/__tests__/useFetchOrdinalByTokenId.test.tsx new file mode 100644 index 000000000000..28c597507c36 --- /dev/null +++ b/libs/live-nft-react/src/hooks/__tests__/useFetchOrdinalByTokenId.test.tsx @@ -0,0 +1,39 @@ +import { renderHook } from "@testing-library/react"; +import { wrapper } from "../../tools/helperTests"; +import { useQuery } from "@tanstack/react-query"; +import { useFetchOrdinalByTokenId } from "../useFetchOrdinalByTokenId"; + +jest.mock("@tanstack/react-query", () => ({ + ...jest.requireActual("@tanstack/react-query"), + useQuery: jest.fn(), +})); + +const mockedInscriptionID = "51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6i0"; + +const mockQueryResult = { + data: {}, + isLoading: false, + isError: false, + fetchNextPage: jest.fn(), + hasNextPage: false, +}; + +describe("useFetchOrdinals", () => { + it("calls useInfiniteQuery with correct arguments", async () => { + (useQuery as jest.Mock).mockReturnValue(mockQueryResult); + + renderHook(() => useFetchOrdinalByTokenId(mockedInscriptionID), { + wrapper, + }); + + expect(useQuery).toHaveBeenCalledWith({ + queryKey: [ + "FectchOrdinalsByTokenId", + "51fb634f0fefa3441e1a60090d9e292ce1f0803258c2dae818410db4192c89f6i0", + ["bitcoin"], + "0", + ], + queryFn: expect.any(Function), + }); + }); +}); diff --git a/libs/live-nft-react/src/hooks/useFetchOrdinalByTokenId.ts b/libs/live-nft-react/src/hooks/useFetchOrdinalByTokenId.ts new file mode 100644 index 000000000000..f871c1ef488a --- /dev/null +++ b/libs/live-nft-react/src/hooks/useFetchOrdinalByTokenId.ts @@ -0,0 +1,21 @@ +import { fetchNftsFromSimpleHashById } from "@ledgerhq/live-nft/api/simplehash"; +import { useQuery, UseQueryResult } from "@tanstack/react-query"; +import { OrdinalsChainsEnum } from "./types"; +import { SimpleHashNft } from "@ledgerhq/live-nft/api/types"; +import { NFTS_QUERY_KEY } from "../queryKeys"; + +export const useFetchOrdinalByTokenId = ( + contractAddress: string, +): UseQueryResult => { + const chain = [OrdinalsChainsEnum.INSCRIPTIONS]; + const tokenId = "0"; + return useQuery({ + queryKey: [NFTS_QUERY_KEY.FectchOrdinalsByTokenId, contractAddress, chain, tokenId], + queryFn: () => + fetchNftsFromSimpleHashById({ + chains: chain, + contract_address: contractAddress, + token_id: tokenId, + }), + }); +}; diff --git a/libs/live-nft-react/src/index.ts b/libs/live-nft-react/src/index.ts index db82f03c1f90..0fe3e2c0d1f5 100644 --- a/libs/live-nft-react/src/index.ts +++ b/libs/live-nft-react/src/index.ts @@ -5,4 +5,5 @@ export * from "./hooks/useNftFloorPrice"; export * from "./hooks/useRefreshMetadata"; export * from "./hooks/useCheckSpamScore"; export * from "./hooks/useFetchOrdinals"; +export * from "./hooks/useFetchOrdinalByTokenId"; export * from "./hooks/helpers/ordinals"; diff --git a/libs/live-nft-react/src/queryKeys.ts b/libs/live-nft-react/src/queryKeys.ts index 78730a322ce0..a83b71103635 100644 --- a/libs/live-nft-react/src/queryKeys.ts +++ b/libs/live-nft-react/src/queryKeys.ts @@ -4,4 +4,5 @@ export const NFTS_QUERY_KEY = { FloorPrice: "FloorPrice", CheckSpamScore: "CheckSpamScore", FetchOrdinals: "FetchOrdinals", + FectchOrdinalsByTokenId: "FectchOrdinalsByTokenId", }; diff --git a/libs/live-nft/src/api/simplehash.ts b/libs/live-nft/src/api/simplehash.ts index ac8d73f85dac..ddad4d81a658 100644 --- a/libs/live-nft/src/api/simplehash.ts +++ b/libs/live-nft/src/api/simplehash.ts @@ -1,4 +1,4 @@ -import network from "@ledgerhq/live-network/network"; +import network from "@ledgerhq/live-network"; import { SimpleHashRefreshResponse, SimpleHashResponse, @@ -37,7 +37,7 @@ type NftFetchOpts = { /** * wallet addresses to get NFTs from. separated by a "," */ - addresses: string; + addresses?: string; /** * cursor used to paginate the API */ @@ -54,6 +54,14 @@ type NftFetchOpts = { * spam filtering threshold, defaults to a constant % */ threshold?: number; + /** + * token id to look for + */ + token_id?: string; + /** + * contract address to look for + */ + contract_address?: string; }; const defaultOpts = { limit: PAGE_SIZE, @@ -191,3 +199,17 @@ export async function getSpamScore(opts: CheckSpamScoreOpts): Promise { + const { chains, contract_address, token_id } = { ...defaultOpts, ...opts }; + const { data } = await network({ + method: "GET", + url: `${getEnv("SIMPLE_HASH_API_BASE")}/nfts/${chains[0]}/${contract_address}/${token_id}`, + }); + + return data; +}