Skip to content
This repository has been archived by the owner on Jun 16, 2022. It is now read-only.

[LIVE-1174] - Feature: Upgrade NFT architecture #4870

Merged
merged 13 commits into from
Apr 5, 2022
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@
"@ledgerhq/hw-transport": "6.24.1",
"@ledgerhq/hw-transport-http": "6.26.0",
"@ledgerhq/hw-transport-node-hid-singleton": "6.26.0",
"@ledgerhq/live-common": "https://github.com/LedgerHQ/ledger-live-common.git\\#sunset-libcore",
"@ledgerhq/ledger-core": "6.14.5",
"@ledgerhq/live-common": "https://github.com/LedgerHQ/ledger-live-common.git#921fef7bfbd31642322c8cbfb0b746dae5d6774e",
"@ledgerhq/logs": "6.10.0",
"@ledgerhq/react-ui": "^0.7.4",
"@open-wc/webpack-import-meta-loader": "^0.4.7",
Expand Down
74 changes: 74 additions & 0 deletions src/helpers/nftLinksFactory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import IconOpensea from "~/renderer/icons/Opensea";
import IconRarible from "~/renderer/icons/Rarible";
import IconGlobe from "~/renderer/icons/Globe";
import { openURL } from "~/renderer/linking";

const linksPerCurrency = {
ethereum: (t, links) => [
links?.opensea && {
key: "opensea",
id: "opensea",
label: t("NFT.viewer.actions.open", { viewer: "Opensea.io" }),
Icon: IconOpensea,
type: "external",
callback: () => openURL(links.opensea),
},
links?.rarible && {
key: "rarible",
id: "rarible",
label: t("NFT.viewer.actions.open", { viewer: "Rarible" }),
Icon: IconRarible,
type: "external",
callback: () => openURL(links.rarible),
},
{
key: "sep2",
id: "sep2",
type: "separator",
label: "",
},
links?.etherscan && {
key: "etherscan",
id: "etherscan",
label: t("NFT.viewer.actions.open", { viewer: "Explorer" }),
Icon: IconGlobe,
type: "external",
callback: () => openURL(links.etherscan),
},
],
polygon: (t, links) => [
links?.opensea && {
key: "opensea",
id: "opensea",
label: t("NFT.viewer.actions.open", { viewer: "Opensea.io" }),
Icon: IconOpensea,
type: "external",
callback: () => openURL(links.opensea),
},
links?.rarible && {
key: "rarible",
id: "rarible",
label: t("NFT.viewer.actions.open", { viewer: "Rarible" }),
Icon: IconRarible,
type: "external",
callback: () => openURL(links.rarible),
},
{
key: "sep2",
id: "sep2",
type: "separator",
label: "",
},
links?.polygonscan && {
key: "polygonscan",
lambertkevin marked this conversation as resolved.
Show resolved Hide resolved
id: "polygonscan",
label: t("NFT.viewer.actions.open", { viewer: "Explorer" }),
Icon: IconGlobe,
type: "external",
callback: () => openURL(links.polygonscan),
},
],
};

export default (currencyId, t, links) =>
linksPerCurrency?.[currencyId]?.(t, links).filter(x => x) || [];
1 change: 1 addition & 0 deletions src/renderer/bridge/proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const getCurrencyBridge = (currency: CryptoCurrency): CurrencyBridge => {
hydrate: value => bridgeImpl.getCurrencyBridge(currency).hydrate(value, currency),

scanAccounts,
nftMetadataResolver: bridgeImpl.getCurrencyBridge(currency).nftMetadataResolver,
};

if (getPreloadStrategy) {
Expand Down
36 changes: 21 additions & 15 deletions src/renderer/components/Breadcrumb/NFTCrumb.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// @flow
import React, { useCallback, useMemo } from "react";
import React, { useCallback, useMemo, memo } from "react";
import { useHistory, useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { nftsByCollections } from "@ledgerhq/live-common/lib/nft";
Expand All @@ -13,6 +13,7 @@ import IconAngleUp from "~/renderer/icons/AngleUp";
import { Separator, Item, TextLink, AngleDown, Check } from "./common";
import { setTrackingSource } from "~/renderer/analytics/TrackPage";
import CollectionName from "~/renderer/screens/nft/CollectionName";
import type { ProtoNFT } from "@ledgerhq/live-common/lib/nft";

const LabelWithMeta = ({
item,
Expand All @@ -21,12 +22,12 @@ const LabelWithMeta = ({
isActive: boolean,
item: {
label: string,
collection: { nfts: any[], contract: string, standard: string },
content: ProtoNFT,
},
}) => (
<Item isActive={isActive}>
<Text ff={`Inter|${isActive ? "SemiBold" : "Regular"}`} fontSize={4}>
<CollectionName collection={item.collection} fallback={item.collection.contract} />
<CollectionName nft={item.content} fallback={item.content.contract} />
</Text>
{isActive && (
<Check>
Expand All @@ -36,23 +37,26 @@ const LabelWithMeta = ({
</Item>
);

export default function NFTCrumb() {
const NFTCrumb = () => {
const history = useHistory();
const { id, collectionAddress } = useParams();
const account = useSelector(state => accountSelector(state, { accountId: id }));
const collections = nftsByCollections(account.nfts);
const collections = useMemo(() => nftsByCollections(account.nfts), [account.nfts]);

const items = useMemo(
() =>
collections.map(collection => ({
key: collection.contract,
label: collection.contract,
collection,
Object.entries(collections).map(([contract, nfts]: any) => ({
key: contract,
label: contract,
content: nfts[0],
})),
[collections],
);
const activeItem =
items.find((item: any) => item.collection.contract === collectionAddress) || items[0];

const activeItem = useMemo(
() => items.find((item: any) => item.nft?.contract === collectionAddress) || items[0],
[collectionAddress, items],
);

const onCollectionSelected = useCallback(
item => {
Expand Down Expand Up @@ -91,12 +95,12 @@ export default function NFTCrumb() {
renderItem={LabelWithMeta}
onChange={onCollectionSelected}
>
{({ isOpen, value }) => (
{({ isOpen }) => (
<TextLink>
<Button>
<CollectionName
collection={activeItem.collection}
fallback={activeItem.collection.contract}
nft={activeItem.content}
fallback={activeItem?.content?.contract}
/>
</Button>
<AngleDown>
Expand All @@ -109,4 +113,6 @@ export default function NFTCrumb() {
) : null}
</>
);
}
};

export default memo<{}>(NFTCrumb);
57 changes: 14 additions & 43 deletions src/renderer/components/ContextMenu/NFTContextMenu.js
Original file line number Diff line number Diff line change
@@ -1,62 +1,33 @@
// @flow
import React from "react";
import React, { useMemo, memo } from "react";
import { useTranslation } from "react-i18next";
import { useNftMetadata } from "@ledgerhq/live-common/lib/nft/NftMetadataProvider";
import IconOpensea from "~/renderer/icons/Opensea";
import IconRarible from "~/renderer/icons/Rarible";
import IconGlobe from "~/renderer/icons/Globe";
import { openURL } from "~/renderer/linking";
import ContextMenuItem from "./ContextMenuItem";
import nftLinksFactory from "~/helpers/nftLinksFactory";

type Props = {
contract: string,
tokenId: string,
currencyId: string,
leftClick?: boolean,
children: any,
};

export default function NFTContextMenu({ leftClick, children, contract, tokenId }: Props) {
const NFTContextMenu = ({ leftClick, children, contract, tokenId, currencyId }: Props) => {
const { t } = useTranslation();
const { metadata } = useNftMetadata(contract, tokenId);

const defaultLinks = {
openSea: `https://opensea.io/assets/${contract}/${tokenId}`,
rarible: `https://rarible.com/token/${contract}:${tokenId}`,
etherscan: `https://etherscan.io/token/${contract}?a=${tokenId}`,
};

const menuItems = [
{
key: "opensea",
label: t("NFT.viewer.actions.open", { viewer: "Opensea.io" }),
Icon: IconOpensea,
type: "external",
callback: () => openURL(metadata?.links?.opensea || defaultLinks.openSea),
},
{
key: "rarible",
label: t("NFT.viewer.actions.open", { viewer: "Rarible" }),
Icon: IconRarible,
type: "external",
callback: () => openURL(metadata?.links?.rarible || defaultLinks.rarible),
},
{
key: "sep2",
type: "separator",
label: "",
},
{
key: "etherscan",
label: t("NFT.viewer.actions.open", { viewer: "Explorer" }),
Icon: IconGlobe,
type: "external",
callback: () => openURL(metadata?.links?.etherscan || defaultLinks.etherscan),
},
];
const { status, metadata } = useNftMetadata(contract, tokenId, currencyId);
const links = useMemo(() => nftLinksFactory(currencyId, t, metadata?.links), [
currencyId,
metadata?.links,
t,
]);
const menuItems = useMemo(() => (status === "loaded" ? links : []), [links, status]);

return (
<ContextMenuItem leftClick={leftClick} items={menuItems}>
{children}
</ContextMenuItem>
);
}
};

export default memo<Props>(NFTContextMenu);
1 change: 1 addition & 0 deletions src/renderer/components/DropDownSelector.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ export type DropDownItemType = {
key: string,
label: any,
disabled?: boolean,
content?: any,
};

const OptionContainer = styled.div`
Expand Down
72 changes: 23 additions & 49 deletions src/renderer/drawers/NFTViewerDrawer/ExternalViewerButton.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// @flow

import React from "react";
import React, { useMemo, memo } from "react";
import { useTranslation } from "react-i18next";
import styled from "styled-components";

Expand All @@ -9,16 +9,12 @@ import Button from "~/renderer/components/Button";
import DropDownSelector, { DropDownItem } from "~/renderer/components/DropDownSelector";
import IconDots from "~/renderer/icons/Dots";
import IconExternal from "~/renderer/icons/ExternalLink";
import IconOpensea from "~/renderer/icons/Opensea";
import IconRarible from "~/renderer/icons/Rarible";
import IconGlobe from "~/renderer/icons/Globe";
import { openURL } from "~/renderer/linking";
import nftLinksFactory from "~/helpers/nftLinksFactory";

import type { NFTMetadataResponse } from "@ledgerhq/live-common/lib/types";
import type { DropDownItemType } from "~/renderer/components/DropDownSelector";
import type { ThemedComponent } from "~/renderer/styles/StyleProvider";

import type { NFTMetadataResponse } from "@ledgerhq/live-common/lib/types";

const Separator: ThemedComponent<{}> = styled.div`
background-color: ${p => p.theme.colors.palette.divider};
height: 1px;
Expand All @@ -37,68 +33,44 @@ const Item: ThemedComponent<{
display: flex;
`;

type ItemType = DropDownItemType & {
icon?: React$Element<*>,
onClick?: Function,
type?: "separator",
};

type ExternalViewerButtonProps = {
links: $PropertyType<$PropertyType<NFTMetadataResponse, "result">, "links">,
contract: string,
tokenId: string,
currencyId: string,
};

export const ExternalViewerButton = ({ links, contract, tokenId }: ExternalViewerButtonProps) => {
const ExternalViewerButton = ({
links,
contract,
tokenId,
currencyId,
}: ExternalViewerButtonProps) => {
const { t } = useTranslation();

const defaultLinks = {
openSea: `https://opensea.io/assets/${contract}/${tokenId}`,
rarible: `https://rarible.com/token/${contract}:${tokenId}`,
etherscan: `https://etherscan.io/token/${contract}?a=${tokenId}`,
};

const items: DropDownItemType[] = [
{
key: "opensea",
label: t("NFT.viewer.actions.open", { viewer: "Opensea.io" }),
icon: <IconOpensea size={16} />,
onClick: () => openURL(links?.opensea || defaultLinks.openSea),
},
{
key: "rarible",
label: t("NFT.viewer.actions.open", { viewer: "Rarible" }),
icon: <IconRarible size={16} />,
onClick: () => openURL(links?.rarible || defaultLinks.rarible),
},
{
key: "sep2",
type: "separator",
label: "",
},
{
key: "etherscan",
label: t("NFT.viewer.actions.open", { viewer: "Explorer" }),
icon: <IconGlobe size={16} />,
onClick: () => openURL(links?.etherscan || defaultLinks.etherscan),
},
];
const items: DropDownItemType[] = useMemo(() => nftLinksFactory(currencyId, t, links), [
currencyId,
links,
t,
]);

const renderItem = ({ item }: { item: ItemType }) => {
const renderItem = ({ item }) => {
if (item.type === "separator") {
return <Separator />;
}

const Icon = item.Icon ? React.createElement(item.Icon, { size: 16 }) : <></>;

return (
<Item
id={`external-popout-${item.key}`}
id={`external-popout-${item.id}`}
horizontal
flow={2}
onClick={item.onClick}
disableHover={item.key === "hideEmpty"}
disableHover={item.id === "hideEmpty"}
>
<Box horizontal>
{item.icon ? <Box mr={2}>{item.icon}</Box> : null}
{item.Icon ? <Box mr={2}>{Icon}</Box> : null}
{item.label}
</Box>
<Box ml={4}>
Expand All @@ -125,3 +97,5 @@ export const ExternalViewerButton = ({ links, contract, tokenId }: ExternalViewe
</DropDownSelector>
);
};

export default memo<ExternalViewerButtonProps>(ExternalViewerButton);
Loading