Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Commit

Permalink
Fix 8064: Some v2 updates in NFT details (#8408)
Browse files Browse the repository at this point in the history
  • Loading branch information
nuo-xu authored Nov 17, 2023
1 parent d3e5b01 commit 709ce66
Show file tree
Hide file tree
Showing 13 changed files with 453 additions and 166 deletions.
327 changes: 228 additions & 99 deletions Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,37 @@ import BraveUI
import SDWebImageSwiftUI

struct NFTDetailView: View {
@ObservedObject var keyringStore: KeyringStore
@ObservedObject var nftDetailStore: NFTDetailStore
@Binding var buySendSwapDestination: BuySendSwapDestination?
var onNFTMetadataRefreshed: ((NFTMetadata) -> Void)?
var onNFTStatusUpdated: (() -> Void)?

@Environment(\.openURL) private var openWalletURL
@Environment(\.presentationMode) @Binding private var presentationMode
@Environment(\.horizontalSizeClass) private var horizontalSizeClass

@State private var isPresentingRemoveAlert: Bool = false

@ViewBuilder private var noImageView: some View {
Text(Strings.Wallet.nftDetailImageNotAvailable)
.foregroundColor(Color(.secondaryBraveLabel))
.frame(maxWidth: .infinity, minHeight: 300)
}

@ViewBuilder private var nftLogo: some View {
if let image = nftDetailStore.networkInfo.nativeTokenLogoImage, !nftDetailStore.isLoading {
Image(uiImage: image)
.resizable()
.frame(width: 32, height: 32)
.overlay {
Circle()
.stroke(lineWidth: 2)
.foregroundColor(Color(braveSystemName: .containerBackground))
}
}
}

@ViewBuilder private var nftImage: some View {
NFTImageView(urlString: nftDetailStore.nftMetadata?.imageURLString ?? "", isLoading: nftDetailStore.isLoading) {
noImageView
Expand All @@ -36,9 +55,9 @@ struct NFTDetailView: View {
}

var body: some View {
ScrollView(.vertical) {
VStack(alignment: .leading, spacing: 24) {
VStack(spacing: 8) {
Form {
Section {
VStack(alignment: .leading, spacing: 16) {
nftImage
.overlay(alignment: .topLeading) {
if nftDetailStore.nft.isSpam {
Expand All @@ -60,129 +79,239 @@ struct NFTDetailView: View {
.padding(12)
}
}
.overlay(alignment: .bottomTrailing) {
ZStack {
if let owner = nftDetailStore.owner {
Blockie(address: owner.address, shape: .rectangle)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(lineWidth: 2)
.foregroundColor(Color(braveSystemName: .containerBackground))
)
.frame(width: 32, height: 32)
.zIndex(1)
.offset(x: -28)
}
nftLogo
}
.offset(y: 16)
}
VStack(alignment: .leading, spacing: 8) {
Text(nftDetailStore.nft.nftTokenTitle)
Text(nftDetailStore.nft.nftDetailTitle)
.font(.title3.weight(.semibold))
.foregroundColor(Color(.braveLabel))
Text(nftDetailStore.nft.name)
.foregroundColor(Color(.secondaryBraveLabel))
Button(action: {
buySendSwapDestination = BuySendSwapDestination(
kind: .send,
initialToken: nftDetailStore.nft
)
}) {
Text(Strings.Wallet.nftDetailSendNFTButtonTitle)
.frame(maxWidth: .infinity)
}
.buttonStyle(BraveFilledButtonStyle(size: .large))
}
}
if let nftMetadata = nftDetailStore.nftMetadata, let description = nftMetadata.description, !description.isEmpty {
VStack(alignment: .leading, spacing: 8) {
Text(Strings.Wallet.nftDetailDescription)
.font(.headline.weight(.semibold))
.foregroundColor(Color(.braveLabel))
Text(description)
.foregroundColor(Color(.braveLabel))
.transaction { transaction in
transaction.animation = nil
transaction.disablesAnimations = true
}
}
VStack(spacing: 16) {
Group {
HStack {
Text(Strings.Wallet.nftDetailBlockchain)
.font(.headline.weight(.semibold))
Spacer()
Text(nftDetailStore.networkInfo.chainName)
.listRowInsets(.zero)
.listRowBackground(Color.clear)
}
Section {
List {
if let owner = nftDetailStore.owner {
NFTDetailRow(title: Strings.Wallet.nftDetailOwnedBy) {
AddressView(address: owner.address) {
HStack {
Text(owner.name)
.foregroundColor(Color(.braveBlurpleTint))
Text(owner.address.truncatedAddress)
.foregroundColor(Color(.braveLabel))
}
.font(.subheadline)
}
}
HStack {
Text(Strings.Wallet.nftDetailTokenStandard)
.font(.headline.weight(.semibold))
Spacer()
Text(nftDetailStore.nft.isErc721 ? Strings.Wallet.nftDetailERC721 : Strings.Wallet.nftDetailSPL)
}
if nftDetailStore.nft.isErc721, let tokenId = Int(nftDetailStore.nft.tokenId.removingHexPrefix, radix: 16) {
NFTDetailRow(title: Strings.Wallet.nftDetailTokenID) {
Text("\(tokenId)")
.font(.subheadline)
.foregroundColor(Color(.braveLabel))
}
if nftDetailStore.nft.isErc721 {
HStack {
Text(Strings.Wallet.nftDetailTokenID)
.font(.headline.weight(.semibold))
Spacer()
Button(action: {
if let explorerURL = nftDetailStore.networkInfo.blockExplorerUrls.first {
let baseURL = "\(explorerURL)/token/\(nftDetailStore.nft.contractAddress)"
var nftURL = URL(string: baseURL)
if let tokenId = Int(nftDetailStore.nft.tokenId.removingHexPrefix, radix: 16) {
nftURL = URL(string: "\(baseURL)?a=\(tokenId)")
}

if let url = nftURL {
openWalletURL(url)
}
}
}) {
if let tokenId = Int(nftDetailStore.nft.tokenId.removingHexPrefix, radix: 16) {
HStack {
Text(verbatim: "#\(tokenId)")
Image(systemName: "arrow.up.forward.square")
}
.foregroundColor(Color(.braveBlurple))
} else {
HStack {
Text("\(nftDetailStore.nft.name) #\(nftDetailStore.nft.tokenId)")
Image(systemName: "arrow.up.forward.square")
}
.foregroundColor(Color(.braveBlurple))
}
}
}
NFTDetailRow(title: nftDetailStore.nft.isErc721 ? Strings.Wallet.contractAddressAccessibilityLabel : Strings.Wallet.tokenMintAddress) {
Button {
if let url = nftDetailStore.networkInfo.nftBlockExplorerURL(nftDetailStore.nft) {
openWalletURL(url)
}
} else {
} label: {
HStack {
Text(Strings.Wallet.tokenMintAddress)
.font(.headline.weight(.semibold))
Spacer()
Button(action: {
if WalletConstants.supportedTestNetworkChainIds.contains(nftDetailStore.networkInfo.chainId) {
if let components = nftDetailStore.networkInfo.blockExplorerUrls.first?.separatedBy("/?cluster="), let baseURL = components.first {
let cluster = components.last ?? ""
if let nftURL = URL(string: "\(baseURL)/address/\(nftDetailStore.nft.contractAddress)/?cluster=\(cluster)") {
openWalletURL(nftURL)
}
}
} else {
if let explorerURL = nftDetailStore.networkInfo.blockExplorerUrls.first, let nftURL = URL(string: "\(explorerURL)/address/\(nftDetailStore.nft.contractAddress)") {
openWalletURL(nftURL)
}
}
}) {
HStack {
Text("\(nftDetailStore.nft.contractAddress.truncatedAddress)")
Image(systemName: "arrow.up.forward.square")
}
.foregroundColor(Color(.braveBlurple))
}
Text(nftDetailStore.nft.contractAddress.truncatedAddress)
Image(systemName: "arrow.up.forward.square")
}
.font(.subheadline)
.foregroundColor(Color(.braveBlurpleTint))
}
}
.foregroundColor(Color(.braveLabel))
NFTDetailRow(title: Strings.Wallet.nftDetailBlockchain) {
Text(nftDetailStore.networkInfo.chainName)
.font(.subheadline)
.foregroundColor(Color(.braveLabel))
}
NFTDetailRow(title: Strings.Wallet.nftDetailTokenStandard) {
Text(nftDetailStore.nft.isErc721 ? Strings.Wallet.nftDetailERC721 : Strings.Wallet.nftDetailSPL)
.font(.subheadline)
.foregroundColor(Color(.braveLabel))
}
}
if isSVGImage {
Text(Strings.Wallet.nftDetailSVGImageDisclaimer)
.multilineTextAlignment(.center)
.font(.footnote)
.foregroundColor(Color(.secondaryBraveLabel))
.frame(maxWidth: .infinity)
.listRowBackground(Color(.secondaryBraveGroupedBackground))
} header: {
Text(Strings.Wallet.nftDetailOverview)
}
if let nftMetadata = nftDetailStore.nftMetadata, let description = nftMetadata.description, !description.isEmpty {
Section {
Text(description)
.font(.subheadline)
.foregroundColor(Color(.braveLabel))
.listRowBackground(Color(.secondaryBraveGroupedBackground))
.osAvailabilityModifiers({
if horizontalSizeClass == .regular {
$0.listRowInsets(EdgeInsets(top: 16, leading: 16, bottom: 16, trailing: 16))
} else {
$0
}
})
} header: {
Text(Strings.Wallet.nftDetailDescription)
}
}
if let attributes = nftDetailStore.nftMetadata?.attributes {
Section {
List {
ForEach(attributes) { attribute in
NFTDetailRow(title: attribute.type) {
Text(attribute.value)
.font(.subheadline)
.foregroundColor(Color(.braveLabel))
}
}
}
.listRowBackground(Color(.secondaryBraveGroupedBackground))
} header: {
Text(Strings.Wallet.nftDetailProperties)
}
}
.padding()
}
.listBackgroundColor(Color(.braveGroupedBackground))
.onChange(of: nftDetailStore.nftMetadata, perform: { newValue in
if let newMetadata = newValue {
onNFTMetadataRefreshed?(newMetadata)
}
})
.onChange(of: keyringStore.isWalletLocked, perform: { isLocked in
guard isLocked else { return }
if isPresentingRemoveAlert {
isPresentingRemoveAlert = false
}
})
.onAppear {
nftDetailStore.update()
}
.background(Color(UIColor.braveGroupedBackground).ignoresSafeArea())
.navigationBarTitle(Strings.Wallet.nftDetailTitle)
.navigationBarTitle(nftDetailStore.nft.nftDetailTitle)
.toolbar {
ToolbarItemGroup(placement: .navigationBarTrailing) {
Menu {
if nftDetailStore.nft.visible {
Button(action: {
buySendSwapDestination = BuySendSwapDestination(
kind: .send,
initialToken: nftDetailStore.nft
)
}) {
Label(Strings.Wallet.nftDetailSendNFTButtonTitle, braveSystemImage: "leo.send")
}
.buttonStyle(BraveFilledButtonStyle(size: .large))
}
Button(action: {
if nftDetailStore.nft.visible { // a collected visible NFT, mark as hidden
nftDetailStore.updateNFTStatus(visible: false, isSpam: false, isDeletedByUser: false, completion: {
onNFTStatusUpdated?()
})
} else { // either a hidden NFT or a junk NFT, mark as visible
nftDetailStore.updateNFTStatus(visible: true, isSpam: false, isDeletedByUser: false, completion: {
onNFTStatusUpdated?()
})
}
}) {
if nftDetailStore.nft.visible { // a collected visible NFT
Label(Strings.recentSearchHide, braveSystemImage: "leo.eye.off")
} else if nftDetailStore.nft.isSpam { // a spam NFT
Label(Strings.Wallet.nftUnspam, braveSystemImage: "leo.disable.outline")
} else { // a hidden but not spam NFT
Label(Strings.Wallet.nftUnhide, braveSystemImage: "leo.eye.on")
}
}
Button(action: {
isPresentingRemoveAlert = true
}) {
Label(Strings.Wallet.nftRemoveFromWallet, braveSystemImage: "leo.trash")
}
} label: {
Label(Strings.Wallet.otherWalletActionsAccessibilityTitle, braveSystemImage: "leo.more.horizontal")
.labelStyle(.iconOnly)
.foregroundColor(Color(.braveBlurpleTint))
}
}
}
.background(
WalletPromptView(
isPresented: $isPresentingRemoveAlert,
primaryButton: .init(
title: Strings.Wallet.manageSiteConnectionsConfirmAlertRemove,
action: { _ in
nftDetailStore.updateNFTStatus(visible: false, isSpam: nftDetailStore.nft.isSpam, isDeletedByUser: true, completion: {
onNFTStatusUpdated?()
presentationMode.dismiss()
})
isPresentingRemoveAlert = false
}
),
secondaryButton: .init(
title: Strings.CancelString,
action: { _ in
isPresentingRemoveAlert = false
}
),
showCloseButton: false,
content: {
VStack(spacing: 16) {
Text(Strings.Wallet.nftRemoveFromWalletAlertTitle)
.font(.headline)
.foregroundColor(Color(.bravePrimary))
Text(Strings.Wallet.nftRemoveFromWalletAlertDescription)
.font(.footnote)
.foregroundStyle(Color(.secondaryBraveLabel))
}
.padding(.bottom, 28)
})
)
}
}

struct NFTDetailRow<ValueContent: View>: View {
var title: String
var valueContent: () -> ValueContent

init(
title: String,
@ViewBuilder valueContent: @escaping () -> ValueContent
) {
self.title = title
self.valueContent = valueContent
}
var body: some View {
HStack {
Text(title)
.font(.subheadline)
.foregroundColor(Color(.secondaryLabel))
Spacer()
valueContent()
.multilineTextAlignment(.trailing)
}
}
}
Loading

0 comments on commit 709ce66

Please sign in to comment.