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

Fix #8205: Support NFT removal. Updates for NFT spam management new design #8226

Merged
merged 5 commits into from
Oct 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions Sources/BraveWallet/Crypto/NFT/NFTDetailView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,26 @@ struct NFTDetailView: View {
.frame(maxWidth: .infinity, minHeight: 300)
} else {
nftImage
.overlay(alignment: .topLeading) {
if nftDetailStore.nft.isSpam {
HStack(spacing: 4) {
Text(Strings.Wallet.nftSpam)
.padding(.vertical, 4)
.padding(.leading, 6)
.foregroundColor(Color(.braveErrorLabel))
Image(braveSystemName: "leo.warning.triangle-outline")
.padding(.vertical, 4)
.padding(.trailing, 6)
.foregroundColor(Color(.braveErrorBorder))
}
.font(.system(size: 13).weight(.semibold))
.background(
Color(uiColor: WalletV2Design.spamNFTLabelBackground)
.cornerRadius(4)
)
.padding(12)
}
}
}
VStack(alignment: .leading, spacing: 8) {
Text(nftDetailStore.nft.nftTokenTitle)
Expand Down
87 changes: 67 additions & 20 deletions Sources/BraveWallet/Crypto/NFT/NFTView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ struct NFTView: View {
@State private var isShowingNFTDiscoveryAlert: Bool = false
@State private var isShowingAddCustomNFT: Bool = false
@State private var isNFTDiscoveryEnabled: Bool = false
@State private var nftToBeRemoved: NFTAssetViewModel?

@Environment(\.buySendSwapDestination)
private var buySendSwapDestination: Binding<BuySendSwapDestination?>
Expand Down Expand Up @@ -205,17 +206,47 @@ struct NFTView: View {
.multilineTextAlignment(.leading)
}
}
.overlay(alignment: .topLeading) {
if nft.token.isSpam {
HStack(spacing: 4) {
Text(Strings.Wallet.nftSpam)
.padding(.vertical, 4)
.padding(.leading, 6)
.foregroundColor(Color(.braveErrorLabel))
Image(braveSystemName: "leo.warning.triangle-outline")
.padding(.vertical, 4)
.padding(.trailing, 6)
.foregroundColor(Color(.braveErrorBorder))
}
.font(.system(size: 13).weight(.semibold))
.background(
Color(uiColor: WalletV2Design.spamNFTLabelBackground)
.cornerRadius(4)
)
.padding(12)
}
}
}
.contextMenu {
Button(action: {
nftStore.updateNFTStatus(nft.token, visible: isHiddenNFT(nft.token), isSpam: false)
if nft.token.visible { // a collected visible NFT, mark as hidden
nftStore.updateNFTStatus(nft.token, visible: false, isSpam: false, isDeletedByUser: false)
} else { // either a hidden NFT or a junk NFT, mark as visible
nftStore.updateNFTStatus(nft.token, visible: true, isSpam: false, isDeletedByUser: false)
}
}) {
Label(isHiddenNFT(nft.token) ? Strings.Wallet.nftUnhide : Strings.recentSearchHide, braveSystemImage: isHiddenNFT(nft.token) ? "leo.eye.on" : "leo.eye.off")
if nft.token.visible { // a collected visible NFT
Label(Strings.recentSearchHide, braveSystemImage: "leo.eye.off")
} else if nft.token.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: {
nftStore.updateNFTStatus(nft.token, visible: isSpamNFT(nft.token), isSpam: !isSpamNFT(nft.token))
nftToBeRemoved = nft
}) {
Label(isSpamNFT(nft.token) ? Strings.Wallet.nftUnspam : Strings.Wallet.nftMoveToSpam, braveSystemImage: "leo.disable.outline")
Label(Strings.Wallet.nftRemoveFromWallet, braveSystemImage: "leo.trash")
}
}
}
Expand Down Expand Up @@ -301,6 +332,38 @@ struct NFTView: View {
}
)
)
.background(
WalletPromptView(
isPresented: Binding(
get: { nftToBeRemoved != nil },
set: { if !$0 { nftToBeRemoved = nil } }
),
primaryButton: .init(
title: Strings.Wallet.manageSiteConnectionsConfirmAlertRemove,
action: { _ in
guard let nft = nftToBeRemoved else { return }
nftStore.updateNFTStatus(nft.token, visible: false, isSpam: nft.token.isSpam, isDeletedByUser: true)
nftToBeRemoved = nil
}
),
secondaryButton: .init(
title: Strings.CancelString,
action: { _ in
nftToBeRemoved = nil
}
),
showCloseButton: false,
content: {
VStack(spacing: 16) {
Text(Strings.Wallet.nftRemoveFromWalletAlertTitle)
.font(.headline)
.foregroundColor(Color(.bravePrimary))
Text(Strings.Wallet.nftRemoveFromWalletAlertDescription)
.font(.footnote)
.foregroundStyle(Color(.secondaryBraveLabel))
}
})
)
.sheet(isPresented: $isShowingAddCustomNFT) {
AddCustomAssetView(
networkStore: networkStore,
Expand All @@ -321,22 +384,6 @@ struct NFTView: View {
}
}
}

private func isSpamNFT(_ nft: BraveWallet.BlockchainToken) -> Bool {
if nftStore.displayType == .spam {
return true
} else {
return nft.isSpam
}
}

private func isHiddenNFT(_ nft: BraveWallet.BlockchainToken) -> Bool {
if nftStore.displayType == .spam {
return false
} else {
return !nft.visible
}
}
}

#if DEBUG
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,7 @@ class AccountActivityStore: ObservableObject, WalletObserverStore {
let tokens: [BraveWallet.BlockchainToken]
let sortOrder: Int
}
let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: networksForAccount, includingSpam: true)
let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: networksForAccount, includingUserDeleted: true)
let allTokens = await blockchainRegistry.allTokens(in: networksForAccountCoin).flatMap(\.tokens)
var updatedUserAssets: [AssetViewModel] = []
var updatedUserNFTs: [NFTAssetViewModel] = []
Expand Down
2 changes: 1 addition & 1 deletion Sources/BraveWallet/Crypto/Stores/AssetDetailStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ class AssetDetailStore: ObservableObject, WalletObserverStore {
) async -> [TransactionSummary] {
guard case let .blockchainToken(token) = assetDetailType
else { return [] }
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens }
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens }
let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: network.coin)
let allTransactions = await withTaskGroup(of: [BraveWallet.TransactionInfo].self) { @MainActor group -> [BraveWallet.TransactionInfo] in
for account in keyring.accountInfos {
Expand Down
37 changes: 22 additions & 15 deletions Sources/BraveWallet/Crypto/Stores/NFTStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ public class NFTStore: ObservableObject, WalletObserverStore {
case .visible:
return userNFTs.filter(\.token.visible)
case .hidden:
return userNFTs.filter { !$0.token.visible && !$0.token.isSpam }
case .spam:
return userNFTs.filter(\.token.isSpam)
return userNFTs.filter { !$0.token.visible }
}
}
/// All User Accounts
Expand Down Expand Up @@ -75,7 +73,6 @@ public class NFTStore: ObservableObject, WalletObserverStore {
enum NFTDisplayType: Int, CaseIterable, Identifiable {
case visible
case hidden
case spam

var id: Int {
rawValue
Expand All @@ -84,11 +81,9 @@ public class NFTStore: ObservableObject, WalletObserverStore {
var dropdownTitle: String {
switch self {
case .visible:
return Strings.Wallet.nftsTitle
return Strings.Wallet.nftCollected
case .hidden:
return Strings.Wallet.nftHidden
case .spam:
return Strings.Wallet.nftSpam
}
}

Expand All @@ -98,16 +93,14 @@ public class NFTStore: ObservableObject, WalletObserverStore {
return Strings.Wallet.nftPageEmptyTitle
case .hidden:
return Strings.Wallet.nftInvisiblePageEmptyTitle
case .spam:
return Strings.Wallet.nftSpamPageEmptyTitle
}
}

var emptyDescription: String? {
switch self {
case .visible:
return Strings.Wallet.nftPageEmptyDescription
case .hidden, .spam:
case .hidden:
return nil
}
}
Expand Down Expand Up @@ -344,17 +337,21 @@ public class NFTStore: ObservableObject, WalletObserverStore {
selectedAccounts: [BraveWallet.AccountInfo],
simpleHashSpamNFTs: [NetworkAssets]
) -> [NetworkAssets] {
// all user marked deleted NFTs
let allUserMarkedDeletedNFTs = assetManager.getAllUserDeletedNFTs()
// all spam NFTs marked by user
let allUserMarkedSpamNFTs = assetManager.getAllUserNFTs(networks: selectedNetworks, isSpam: true)
// filter out any spam NFTs from `simpleHashSpamNFTs` that are marked
// not-spam by user
// not-spam or deleted by user
var updatedSimpleHashSpamNFTs: [NetworkAssets] = []
for simpleHashSpamNFTsOnNetwork in simpleHashSpamNFTs {
let userMarkedNotSpamTokensOnNetwork = assetManager.getAllUserNFTs(networks: [simpleHashSpamNFTsOnNetwork.network], isSpam: false).flatMap(\.tokens)
let filteredSimpleHashSpamTokens = simpleHashSpamNFTsOnNetwork.tokens.filter { simpleHashSpamToken in
!userMarkedNotSpamTokensOnNetwork.contains { token in
return !userMarkedNotSpamTokensOnNetwork.contains { token in
token.id == simpleHashSpamToken.id
}
} && !allUserMarkedDeletedNFTs.contains(where: { deletedNFT in
deletedNFT.contractAddress == simpleHashSpamToken.contractAddress && deletedNFT.chainId == simpleHashSpamToken.chainId && deletedNFT.tokenId == simpleHashSpamToken.tokenId
})
}
updatedSimpleHashSpamNFTs.append(NetworkAssets(network: simpleHashSpamNFTsOnNetwork.network, tokens: filteredSimpleHashSpamTokens, sortOrder: simpleHashSpamNFTsOnNetwork.sortOrder))
}
Expand Down Expand Up @@ -387,8 +384,18 @@ public class NFTStore: ObservableObject, WalletObserverStore {
walletService.setNftDiscoveryEnabled(true)
}

func updateNFTStatus(_ token: BraveWallet.BlockchainToken, visible: Bool, isSpam: Bool) {
assetManager.updateUserAsset(for: token, visible: visible, isSpam: isSpam) { [weak self] in
func updateNFTStatus(
_ token: BraveWallet.BlockchainToken,
visible: Bool,
isSpam: Bool,
isDeletedByUser: Bool
) {
assetManager.updateUserAsset(
for: token,
visible: visible,
isSpam: isSpam,
isDeletedByUser: isDeletedByUser
) { [weak self] in
guard let self else { return }
let selectedAccounts = self.filters.accounts.filter(\.isSelected).map(\.model)
let selectedNetworks = self.filters.networks.filter(\.isSelected).map(\.model)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
let transactionNetworks: [BraveWallet.NetworkInfo] = Set(allTxs.map(\.chainId))
.compactMap { chainId in allNetworks.first(where: { $0.chainId == chainId }) }
for network in transactionNetworks {
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens }
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens }
await fetchAssetRatios(for: userAssets)
}
await fetchUnknownTokens(for: unapprovedTxs)
Expand Down Expand Up @@ -307,7 +307,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
return
}
let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: coin) + tokenInfoCache.map(\.value)
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens }
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens }
let solEstimatedTxFee: UInt64? = solEstimatedTxFeeCache[transaction.id]

if transaction.isEIP1559Transaction {
Expand Down Expand Up @@ -431,7 +431,7 @@ public class TransactionConfirmationStore: ObservableObject, WalletObserverStore
guard let network = allNetworks.first(where: { $0.chainId == BraveWallet.MainnetChainId }) else {
return
}
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens }
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens }
let allTokens = await blockchainRegistry.allTokens(network.chainId, coin: network.coin)
let unknownTokenContractAddresses = mainnetTransactions.flatMap(\.tokenContractAddresses)
.filter { contractAddress in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ class TransactionDetailsStore: ObservableObject, WalletObserverStore {
let keringId = BraveWallet.KeyringId.keyringId(for: coin, on: transaction.chainId)
let keyring = await keyringService.keyringInfo(keringId)
var allTokens: [BraveWallet.BlockchainToken] = await blockchainRegistry.allTokens(network.chainId, coin: network.coin) + tokenInfoCache.map(\.value)
let userAssets: [BraveWallet.BlockchainToken] = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingSpam: true).flatMap { $0.tokens }
let userAssets: [BraveWallet.BlockchainToken] = assetManager.getAllUserAssetsInNetworkAssets(networks: [network], includingUserDeleted: true).flatMap { $0.tokens }
let unknownTokenContractAddresses = transaction.tokenContractAddresses
.filter { contractAddress in
!userAssets.contains(where: { $0.contractAddress(in: network).caseInsensitiveCompare(contractAddress) == .orderedSame })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ class TransactionsActivityStore: ObservableObject, WalletObserverStore {
let allTransactions = await txService.allTransactions(
networksForCoin: networksForCoin, for: allKeyrings
).filter { $0.txStatus != .rejected }
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworksAllCoins, includingSpam: true).flatMap(\.tokens)
let userAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworksAllCoins, includingUserDeleted: true).flatMap(\.tokens)
let allTokens = await blockchainRegistry.allTokens(
in: allNetworksAllCoins
).flatMap(\.tokens)
Expand Down
10 changes: 5 additions & 5 deletions Sources/BraveWallet/Crypto/Stores/UserAssetsStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ public class AssetStore: ObservableObject, Equatable, WalletObserverStore {
@Published var token: BraveWallet.BlockchainToken
@Published var isVisible: Bool {
didSet {
assetManager.updateUserAsset(for: token, visible: isVisible, isSpam: false, completion: nil)
assetManager.updateUserAsset(for: token, visible: isVisible, isSpam: false, isDeletedByUser: false, completion: nil)
}
}
var network: BraveWallet.NetworkInfo
Expand Down Expand Up @@ -137,7 +137,7 @@ public class UserAssetsStore: ObservableObject, WalletObserverStore {
}
}
let networks: [BraveWallet.NetworkInfo] = self.networkFilters.filter(\.isSelected).map(\.model)
let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: networks, includingSpam: true)
let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: networks, includingUserDeleted: false)
var allTokens = await self.blockchainRegistry.allTokens(in: networks)
// Filter `allTokens` to remove any tokens existing in `allUserAssets`. This is possible for ERC721 tokens in the registry without a `tokenId`, which requires the user to add as a custom token
let allUserTokens = allUserAssets.flatMap(\.tokens)
Expand Down Expand Up @@ -183,7 +183,7 @@ public class UserAssetsStore: ObservableObject, WalletObserverStore {
_ asset: BraveWallet.BlockchainToken,
completion: @escaping (_ success: Bool) -> Void
) {
if assetManager.getUserAsset(asset) != nil {
if let existedAsset = assetManager.getUserAsset(asset), !existedAsset.isDeletedByUser {
completion(false)
} else {
assetManager.addUserAsset(asset) { [weak self] in
Expand Down Expand Up @@ -233,7 +233,7 @@ public class UserAssetsStore: ObservableObject, WalletObserverStore {

@MainActor func allAssets() async -> [AssetViewModel] {
let allNetworks = await rpcService.allNetworksForSupportedCoins()
let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworks, includingSpam: true)
let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworks, includingUserDeleted: false)
// Filter `allTokens` to remove any tokens existing in `allUserAssets`. This is possible for ERC721 tokens in the registry without a `tokenId`, which requires the user to add as a custom token
let allUserTokens = allUserAssets.flatMap(\.tokens)
let allBlockchainTokens = await blockchainRegistry.allTokens(in: allNetworks)
Expand Down Expand Up @@ -262,7 +262,7 @@ public class UserAssetsStore: ObservableObject, WalletObserverStore {

@MainActor func allNFTMetadata() async -> [String: NFTMetadata] {
let allNetworks = await rpcService.allNetworksForSupportedCoins()
let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworks, includingSpam: true)
let allUserAssets = assetManager.getAllUserAssetsInNetworkAssets(networks: allNetworks, includingUserDeleted: true)
// Filter `allTokens` to remove any tokens existing in `allUserAssets`. This is possible for ERC721 tokens in the registry without a `tokenId`, which requires the user to add as a custom token
let allUserTokens = allUserAssets.flatMap(\.tokens)

Expand Down
7 changes: 7 additions & 0 deletions Sources/BraveWallet/Extensions/WalletColors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -250,4 +250,11 @@ enum WalletV2Design {
static let passwordMediumYellow = UIColor(rgb: 0xbd9600)

static let passwordStrongGreen = UIColor(rgb: 0x31803e)

static let spamNFTLabelBackground = UIColor(
red: 1,
green: 209 / 255,
blue: 207 / 255,
alpha: 1
)
}
Loading