From f45a39bfcf83b84ae7a641b99740c4eaf91c2934 Mon Sep 17 00:00:00 2001 From: Khushboo Mehta Date: Tue, 26 Mar 2024 16:00:16 +0100 Subject: [PATCH] feat(@desktop/wallet): Link out from collectible details view to opensea fixes #13918 --- src/app/global/utils.nim | 14 +++- src/app_service/service/eth/utils.nim | 2 +- src/app_service/service/stickers/service.nim | 2 - .../service/wallet_account/service.nim | 2 +- .../controls/CollectibleDetailsHeader.qml | 14 ++-- ui/app/AppLayouts/Wallet/stores/RootStore.qml | 63 +++++++++++++++- .../collectibles/CollectibleDetailView.qml | 72 +++++++++---------- ui/imports/utils/Constants.qml | 17 +++++ ui/imports/utils/Utils.qml | 4 ++ 9 files changed, 141 insertions(+), 49 deletions(-) diff --git a/src/app/global/utils.nim b/src/app/global/utils.nim index 13db10b9b5..d80400358a 100644 --- a/src/app/global/utils.nim +++ b/src/app/global/utils.nim @@ -1,4 +1,4 @@ -import NimQml, strutils, uri, stew/shims/strformat, strutils, stint, re +import NimQml, strutils, uri, stew/shims/strformat, strutils, stint, re, httpclient import stew/byteutils import ./utils/qrcodegen import ./utils/time_utils @@ -9,6 +9,8 @@ import ../../app_service/service/visual_identity/service as procs_from_visual_id include ../../app_service/service/accounts/utils +const URL_STATUS_OK* = "200 OK" + QtObject: type Utils* = ref object of QObject @@ -181,4 +183,12 @@ QtObject: return value proc addTimestampToURL*(self: Utils, url: string): string {.slot.} = - return time_utils.addTimestampToURL(url) \ No newline at end of file + return time_utils.addTimestampToURL(url) + + proc isValidURL*(self: Utils, url: string): bool {.slot.} = + var client = newHttpClient() + defer: client.close() + try: + return client.head(url).status == URL_STATUS_OK + except: + return false diff --git a/src/app_service/service/eth/utils.nim b/src/app_service/service/eth/utils.nim index cc8d23d4f3..6edd087c89 100644 --- a/src/app_service/service/eth/utils.nim +++ b/src/app_service/service/eth/utils.nim @@ -1,5 +1,5 @@ import - json, tables, sequtils, httpclient, net + json, tables, sequtils, net import json, strutils, stew/shims/strformat, tables, chronicles, unicode, times import json_serialization, chronicles, libp2p/[multihash, multicodec, cid], stint, nimcrypto diff --git a/src/app_service/service/stickers/service.nim b/src/app_service/service/stickers/service.nim index 5a101a87f2..04e2498424 100644 --- a/src/app_service/service/stickers/service.nim +++ b/src/app_service/service/stickers/service.nim @@ -1,7 +1,5 @@ import NimQml, Tables, json, sequtils, chronicles, strutils, sets, stint -import httpclient - import app/core/[main] import app/core/tasks/[qt, threadpool] diff --git a/src/app_service/service/wallet_account/service.nim b/src/app_service/service/wallet_account/service.nim index 0cab07d58d..657554f893 100644 --- a/src/app_service/service/wallet_account/service.nim +++ b/src/app_service/service/wallet_account/service.nim @@ -1,4 +1,4 @@ -import NimQml, Tables, json, sequtils, sugar, chronicles, stew/shims/strformat, stint, httpclient +import NimQml, Tables, json, sequtils, sugar, chronicles, stew/shims/strformat, stint import net, strutils, os, times, algorithm, options import web3/ethtypes diff --git a/ui/app/AppLayouts/Wallet/controls/CollectibleDetailsHeader.qml b/ui/app/AppLayouts/Wallet/controls/CollectibleDetailsHeader.qml index 5a690b9bb6..acd95f744c 100644 --- a/ui/app/AppLayouts/Wallet/controls/CollectibleDetailsHeader.qml +++ b/ui/app/AppLayouts/Wallet/controls/CollectibleDetailsHeader.qml @@ -24,6 +24,10 @@ ColumnLayout { property string networkIconURL property string networkExplorerName + property bool collectibleLinkEnabled + property bool collectionLinkEnabled + property bool explorerLinkEnabled + signal collectionTagClicked() signal openCollectibleExternally() signal openCollectibleOnExplorer() @@ -58,15 +62,15 @@ ColumnLayout { size: StatusBaseButton.Size.Small text: root.networkExplorerName icon.name: "external" - asset.emoji: d.effectiveEmoji onClicked: root.openCollectibleOnExplorer() + visible: root.explorerLinkEnabled } StatusButton { size: StatusBaseButton.Size.Small text: "OpenSea" icon.name: "external" - asset.emoji: d.effectiveEmoji onClicked: root.openCollectibleExternally() + visible: root.collectibleLinkEnabled } } } @@ -78,11 +82,13 @@ ColumnLayout { id: collectionTag asset.name: !!root.communityImage ? root.communityImage: !sensor.containsMouse ? root.isCollection ? "tiny/folder" : "tiny/profile" : "tiny/external" asset.isImage: !!root.communityImage + enabled: root.collectionLinkEnabled MouseArea { id: sensor anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor + hoverEnabled: root.collectionLinkEnabled + cursorShape: root.collectionLinkEnabled ? Qt.PointingHandCursor: undefined + enabled: root.collectionLinkEnabled onClicked: { root.collectionTagClicked() } diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml index 25a414bfe2..1482f7fb85 100644 --- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml +++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml @@ -493,7 +493,7 @@ QtObject { return root.mainModuleInst.addressWasShown(address) } - function getExplorerUrl(networkShortName, contractAddress, tokenId) { + function getExplorerDomain(networkShortName) { let link = Constants.networkExplorerLinks.etherscan if (networkShortName === Constants.networkShortChainNames.mainnet) { if (root.areTestNetworksEnabled) { @@ -503,7 +503,6 @@ QtObject { link = Constants.networkExplorerLinks.goerliEtherscan } } - return "%1/nft/%2/%3".arg(link).arg(contractAddress).arg(tokenId) } if (networkShortName === Constants.networkShortChainNames.arbitrum) { link = Constants.networkExplorerLinks.arbiscan @@ -514,7 +513,6 @@ QtObject { link = Constants.networkExplorerLinks.goerliArbiscan } } - return "%1/token/%2?a=%3".arg(link).arg(contractAddress).arg(tokenId) } else if (networkShortName === Constants.networkShortChainNames.optimism) { link = Constants.networkExplorerLinks.optimism if (root.areTestNetworksEnabled) { @@ -524,6 +522,16 @@ QtObject { link = Constants.networkExplorerLinks.goerliOptimism } } + } + return link + } + + function getExplorerUrl(networkShortName, contractAddress, tokenId) { + let link = getExplorerDomain(networkShortName) + if (networkShortName === Constants.networkShortChainNames.mainnet) { + return "%1/nft/%2/%3".arg(link).arg(contractAddress).arg(tokenId) + } + else { return "%1/token/%2?a=%3".arg(link).arg(contractAddress).arg(tokenId) } } @@ -537,4 +545,53 @@ QtObject { } return qsTr("Etherscan Explorer") } + + function getOpenSeaNetworkName(networkShortName) { + let networkName = Constants.openseaExplorerLinks.ethereum + if (networkShortName === Constants.networkShortChainNames.mainnet) { + if (root.areTestNetworksEnabled) { + if (!root.isGoerliEnabled) { + networkName = Constants.openseaExplorerLinks.sepoliaEthereum + } else { + networkName = Constants.openseaExplorerLinks.goerliEthereum + } + } + } + if (networkShortName === Constants.networkShortChainNames.arbitrum) { + networkName = Constants.openseaExplorerLinks.arbitrum + if (root.areTestNetworksEnabled) { + if (!root.isGoerliEnabled) { + networkName = Constants.openseaExplorerLinks.sepoliaArbitrum + } else { + networkName = Constants.openseaExplorerLinks.goerliArbitrum + } + } + } else if (networkShortName === Constants.networkShortChainNames.optimism) { + networkName = Constants.openseaExplorerLinks.optimism + if (root.areTestNetworksEnabled) { + if (!root.isGoerliEnabled) { + networkName = Constants.openseaExplorerLinks.sepoliaOptimism + } else { + networkName = Constants.openseaExplorerLinks.goerliOptimism + } + } + } + return networkName + } + + function getOpenseaDomainName() { + return root.areTestNetworksEnabled ? Constants.openseaExplorerLinks.testnetLink : Constants.openseaExplorerLinks.mainnetLink + } + + function getOpenSeaCollectionUrl(networkShortName, contractAddress) { + let networkName = getOpenSeaNetworkName(networkShortName) + let baseLink = root.areTestNetworksEnabled ? Constants.openseaExplorerLinks.testnetLink : Constants.openseaExplorerLinks.mainnetLink + return "%1/assets/%2/%3".arg(baseLink).arg(networkName).arg(contractAddress) + } + + function getOpenSeaCollectibleUrl(networkShortName, contractAddress, tokenId) { + let networkName = getOpenSeaNetworkName(networkShortName) + let baseLink = root.areTestNetworksEnabled ? Constants.openseaExplorerLinks.testnetLink : Constants.openseaExplorerLinks.mainnetLink + return "%1/assets/%2/%3/%4".arg(baseLink).arg(networkName).arg(contractAddress).arg(tokenId) + } } diff --git a/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleDetailView.qml b/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleDetailView.qml index 640d79f7d2..591ec4c83d 100644 --- a/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleDetailView.qml +++ b/ui/app/AppLayouts/Wallet/views/collectibles/CollectibleDetailView.qml @@ -36,6 +36,13 @@ Item { readonly property var communityDetails: isCommunityCollectible ? root.communitiesStore.getCommunityDetailsAsJson(collectible.communityId) : null + QtObject { + id: d + readonly property string collectibleLink: root.walletRootStore.getOpenSeaCollectibleUrl(collectible.networkShortName, collectible.contractAddress, collectible.tokenId) + readonly property string collectionLink: root.walletRootStore.getOpenSeaCollectionUrl(collectible.networkShortName, collectible.contractAddress) + readonly property string blockExplorerLink: root.walletRootStore.getExplorerUrl(collectible.networkShortName, collectible.contractAddress, collectible.tokenId) + } + CollectibleDetailsHeader { id: collectibleHeader anchors.top: parent.top @@ -50,22 +57,22 @@ Item { networkColor: collectible.networkColor networkIconURL: collectible.networkIconUrl networkExplorerName: root.walletRootStore.getExplorerNameForNetwork(collectible.networkShortName) + collectibleLinkEnabled: Utils.getUrlStatus(d.collectibleLink) + collectionLinkEnabled: (!!communityDetails && communityDetails.name) || Utils.getUrlStatus(d.collectionLink) + explorerLinkEnabled: Utils.getUrlStatus(d.blockExplorerLink) onCollectionTagClicked: { if (root.isCommunityCollectible) { Global.switchToCommunity(collectible.communityId) } - /* TODO for non community token link out to collection on opensea - https://github.com/status-im/status-desktop/issues/13918 */ - + else { + Global.openLinkWithConfirmation(d.collectionLink, root.walletRootStore.getOpenseaDomainName()) + } } - onOpenCollectibleExternally: { - /* TODO add link out to opensea - https://github.com/status-im/status-desktop/issues/13918 */ - } - onOpenCollectibleOnExplorer: Global.openLink(root.walletRootStore.getExplorerUrl(collectible.networkShortName, collectible.contractAddress, collectible.tokenId)) + onOpenCollectibleExternally: Global.openLinkWithConfirmation(d.collectibleLink, root.walletRootStore.getOpenseaDomainName()) + onOpenCollectibleOnExplorer: Global.openLinkWithConfirmation(d.blockExplorerLink, root.walletRootStore.getExplorerDomain(networkShortName)) } - ColumnLayout { + Column { id: collectibleBody anchors.top: collectibleHeader.bottom anchors.topMargin: 25 @@ -82,8 +89,8 @@ Item { readonly property real visibleImageHeight: (collectibleimage.visible ? collectibleimage.height : privilegedCollectibleImage.height) readonly property real visibleImageWidth: (collectibleimage.visible ? collectibleimage.width : privilegedCollectibleImage.width) - Layout.preferredHeight: collectibleImageDetails.visibleImageHeight - Layout.preferredWidth: parent.width + height: collectibleImageDetails.visibleImageHeight + width: parent.width spacing: 24 // Special artwork representation for community `Owner and Master Token` token types: @@ -132,36 +139,28 @@ Item { wrapMode: Text.WordWrap } - StatusScrollView { - id: descriptionScrollView + StatusBaseText { + id: descriptionText width: parent.width height: collectibleImageDetails.height - collectibleName.height - parent.spacing - contentWidth: availableWidth - - padding: 0 - - StatusBaseText { - id: descriptionText - width: descriptionScrollView.availableWidth - - text: collectible.description - textFormat: Text.MarkdownText - color: Theme.palette.directColor4 - font.pixelSize: 15 - lineHeight: 22 - lineHeightMode: Text.FixedHeight - elide: Text.ElideRight - wrapMode: Text.Wrap - } + clip: true + text: collectible.description + textFormat: Text.MarkdownText + color: Theme.palette.directColor4 + font.pixelSize: 15 + lineHeight: 22 + lineHeightMode: Text.FixedHeight + elide: Text.ElideRight + wrapMode: Text.Wrap } } } StatusTabBar { id: collectiblesDetailsTab - Layout.fillWidth: true - Layout.topMargin: Style.current.xlPadding + width: parent.width + topPadding: Style.current.xlPadding visible: collectible.traits.count > 0 StatusTabButton { @@ -178,14 +177,15 @@ Item { StatusScrollView { id: scrollView - Layout.fillWidth: true - Layout.fillHeight: true + width: parent.width + height: parent.height contentWidth: availableWidth Loader { id: tabLoader - Layout.fillWidth: true - Layout.fillHeight: true + width: parent.width + height: parent.height + sourceComponent: { switch (collectiblesDetailsTab.currentIndex) { case 0: return traitsView diff --git a/ui/imports/utils/Constants.qml b/ui/imports/utils/Constants.qml index 2e038fc3a0..1257d2619b 100644 --- a/ui/imports/utils/Constants.qml +++ b/ui/imports/utils/Constants.qml @@ -916,6 +916,23 @@ QtObject { readonly property string txPath: "tx" } + readonly property QtObject openseaExplorerLinks: QtObject { + readonly property string mainnetLink: "https://opensea.io" + readonly property string testnetLink: "https://testnets.opensea.io" + + readonly property string ethereum: "ethereum" + readonly property string arbitrum: "arbitrum" + readonly property string optimism: "optimism" + + readonly property string goerliEthereum: "goerli" + readonly property string goerliArbitrum: "arbitrum-goerli" + readonly property string goerliOptimism: "optimism-goerli" + + readonly property string sepoliaEthereum: "sepolia" + readonly property string sepoliaArbitrum: "arbitrum-sepolia" + readonly property string sepoliaOptimism: "optimism-sepolia" + } + readonly property string api_request: "api-request" readonly property string web3SendAsyncReadOnly: "web3-send-async-read-only" readonly property string web3DisconnectAccount: "web3-disconnect-account" diff --git a/ui/imports/utils/Utils.qml b/ui/imports/utils/Utils.qml index d5a907e106..60d4740205 100644 --- a/ui/imports/utils/Utils.qml +++ b/ui/imports/utils/Utils.qml @@ -991,4 +991,8 @@ QtObject { return communitiesModuleInst.isDisplayNameDupeOfCommunityMember(displayName) } + + function getUrlStatus(url) { + return globalUtilsInst.isValidURL(url) + } }