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

Fix #8073: Add NFT grouping by none/accounts/networks #8289

Merged
merged 6 commits into from
Oct 23, 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
19 changes: 18 additions & 1 deletion Sources/BraveWallet/Crypto/FiltersDisplaySettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@ struct FiltersDisplaySettingsView: View {
@State var isHidingUnownedNFTs: Bool
/// If we are showing the network logo on NFTs. Default is true.
@State var isShowingNFTNetworkLogo: Bool
/// If we should disable `Hide Unowned`
@State var isHidingUnownedNFTsDisabled: Bool

/// All accounts and if they are currently selected. Default is all accounts selected.
@State var accounts: [Selectable<BraveWallet.AccountInfo>]
Expand Down Expand Up @@ -152,8 +154,9 @@ struct FiltersDisplaySettingsView: View {
self._groupBy = State(initialValue: filters.groupBy)
self._sortOrder = State(initialValue: filters.sortOrder)
self._isHidingSmallBalances = State(initialValue: filters.isHidingSmallBalances)
self._isHidingUnownedNFTs = State(initialValue: filters.isHidingUnownedNFTs)
self._isHidingUnownedNFTs = State(initialValue: filters.groupBy == .accounts ? true : filters.isHidingUnownedNFTs)
self._isShowingNFTNetworkLogo = State(initialValue: filters.isShowingNFTNetworkLogo)
self._isHidingUnownedNFTsDisabled = State(initialValue: filters.groupBy == .accounts)
self._accounts = State(initialValue: filters.accounts)
self._networks = State(initialValue: filters.networks)
self.isNFTFilters = isNFTFilters
Expand All @@ -167,6 +170,9 @@ struct FiltersDisplaySettingsView: View {
ScrollView {
LazyVStack(spacing: 0) {
if isNFTFilters {
groupByRow
.padding(.vertical, rowPadding)

showNFTNetworkLogo
.padding(.vertical, rowPadding)

Expand Down Expand Up @@ -194,6 +200,16 @@ struct FiltersDisplaySettingsView: View {
}
.padding(.horizontal)
}
.onChange(of: groupBy, perform: { newValue in
if isNFTFilters {
if newValue == .accounts {
isHidingUnownedNFTs = true
isHidingUnownedNFTsDisabled = true
} else {
isHidingUnownedNFTsDisabled = false
}
}
})
.background(Color(uiColor: WalletV2Design.containerBackground))
.safeAreaInset(edge: .bottom, content: {
saveChangesContainer
Expand Down Expand Up @@ -268,6 +284,7 @@ struct FiltersDisplaySettingsView: View {
)
}
.tint(Color(.braveBlurpleTint))
.disabled(isHidingUnownedNFTsDisabled)
}

private var showNFTNetworkLogo: some View {
Expand Down
251 changes: 146 additions & 105 deletions Sources/BraveWallet/Crypto/NFT/NFTView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct NFTView: View {
@State private var isShowingAddCustomNFT: Bool = false
@State private var isNFTDiscoveryEnabled: Bool = false
@State private var nftToBeRemoved: NFTAssetViewModel?
@State private var groupToggleState: [NFTGroupViewModel.ID: Bool] = [:]

@Environment(\.buySendSwapDestination)
private var buySendSwapDestination: Binding<BuySendSwapDestination?>
Expand Down Expand Up @@ -108,53 +109,44 @@ struct NFTView: View {
AssetButton(braveSystemName: "leo.filter.settings", action: {
isPresentingFiltersDisplaySettings = true
})
.sheet(isPresented: $isPresentingFiltersDisplaySettings) {
FiltersDisplaySettingsView(
filters: nftStore.filters,
isNFTFilters: true,
networkStore: networkStore,
save: { filters in
nftStore.saveFilters(filters)
}
)
.osAvailabilityModifiers({ view in
if #available(iOS 16, *) {
view
.presentationDetents([
.fraction(0.6),
.large
])
} else {
view
}
})
}
}

private var nftHeaderView: some View {
StephenHeaps marked this conversation as resolved.
Show resolved Hide resolved
HStack {
Text(Strings.Wallet.assetsTitle)
.font(.title3.weight(.semibold))
.foregroundColor(Color(braveSystemName: .textPrimary))
Menu {
Picker("", selection: $nftStore.displayType) {
ForEach(NFTStore.NFTDisplayType.allCases) { type in
Text(type.dropdownTitle)
.foregroundColor(Color(.secondaryBraveLabel))
.tag(type)
}
}
.pickerStyle(.inline)
} label: {
HStack(spacing: 12) {
Text(nftStore.displayType.dropdownTitle)
.font(.subheadline.weight(.semibold))
Text("\(nftStore.totalDisplayedNFTCount)")
.padding(.horizontal, 8)
.padding(.vertical, 4)
.font(.caption2.weight(.semibold))
.background(
Color(braveSystemName: .primary20)
.cornerRadius(4)
)
Image(braveSystemName: "leo.carat.down")
.font(.subheadline.weight(.semibold))
}
.foregroundColor(Color(.braveBlurpleTint))
}
if nftStore.isLoadingDiscoverAssets && isNFTDiscoveryEnabled {
ProgressView()
.padding(.leading, 5)
}
Spacer()
Picker(selection: $nftStore.displayType) {
ForEach(NFTStore.NFTDisplayType.allCases) { type in
Text(type.dropdownTitle)
.foregroundColor(Color(.secondaryBraveLabel))
.tag(type)
}
} label: {
Text(nftStore.displayType.dropdownTitle)
.font(.footnote)
.foregroundColor(Color(.braveLabel))
}
filtersButton
.padding(.trailing, 10)
addCustomAssetButton
.padding(.trailing, 10)
filtersButton
}
.padding(.horizontal)
.frame(maxWidth: .infinity, alignment: .leading)
Expand Down Expand Up @@ -182,89 +174,117 @@ struct NFTView: View {
return attributedString
}

@ViewBuilder var nftGridsView: some View {
if nftStore.displayNFTs.isEmpty {
emptyView
.listRowBackground(Color(.clear))
} else {
LazyVGrid(columns: nftGrids) {
ForEach(nftStore.displayNFTs) { nft in
Button(action: {
selectedNFTViewModel = nft
}) {
VStack(alignment: .leading, spacing: 4) {
nftImage(nft)
.padding(.bottom, 8)
Text(nft.token.nftTokenTitle)
.font(.callout.weight(.medium))
.foregroundColor(Color(.braveLabel))
/// Builds the grids of NFTs without any grouping or expandable / collapse behaviour.
@ViewBuilder private func nftGridsPlainView(_ group: NFTGroupViewModel) -> some View {
LazyVGrid(columns: nftGrids) {
ForEach(group.assets) { nft in
Button(action: {
selectedNFTViewModel = nft
}) {
VStack(alignment: .leading, spacing: 4) {
nftImage(nft)
.padding(.bottom, 8)
Text(nft.token.nftTokenTitle)
.font(.callout.weight(.medium))
.foregroundColor(Color(.braveLabel))
.multilineTextAlignment(.leading)
if !nft.token.symbol.isEmpty {
Text(nft.token.symbol)
.font(.caption)
.foregroundColor(Color(.secondaryBraveLabel))
.multilineTextAlignment(.leading)
if !nft.token.symbol.isEmpty {
Text(nft.token.symbol)
.font(.caption)
.foregroundColor(Color(.secondaryBraveLabel))
.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)
}
.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: {
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)
}
}) {
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")
}
}
.contextMenu {
Button(action: {
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)
}
Button(action: {
nftToBeRemoved = nft
}) {
Label(Strings.Wallet.nftRemoveFromWallet, braveSystemImage: "leo.trash")
}) {
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: {
nftToBeRemoved = nft
}) {
Label(Strings.Wallet.nftRemoveFromWallet, braveSystemImage: "leo.trash")
}
}
}
.padding(.horizontal)
VStack(spacing: 16) {
Divider()
editUserAssetsButton
}
.padding(.top, 20)
}
}

/// Builds the expandable / collapseable section content for a given group.
@ViewBuilder private func groupedNFTSection(_ group: NFTGroupViewModel) -> some View {
if group.assets.isEmpty {
EmptyView()
} else {
WalletDisclosureGroup(
StephenHeaps marked this conversation as resolved.
Show resolved Hide resolved
isNFTGroup: true,
isExpanded: Binding(
get: { groupToggleState[group.id, default: true] },
set: { groupToggleState[group.id] = $0 }
),
content: {
nftGridsPlainView(group)
.padding(.top)
},
label: {
if case let .account(account) = group.groupType {
AddressView(address: account.address) {
PortfolioAssetGroupHeaderView(group: group)
}
} else {
PortfolioAssetGroupHeaderView(group: group)
}
}
)
}
}

var body: some View {
VStack(spacing: 16) {
LazyVStack(spacing: 16) {
nftHeaderView

nftGridsView
if nftStore.isShowingNFTEmptyState {
emptyView
} else {
ForEach(nftStore.displayNFTGroups) { group in
if group.groupType == .none {
nftGridsPlainView(group)
.padding(.horizontal)
} else {
groupedNFTSection(group)
}
}
}
}
.background(
NavigationLink(
Expand Down Expand Up @@ -375,6 +395,27 @@ struct NFTView: View {
cryptoStore.updateAssets()
}
}
.sheet(isPresented: $isPresentingFiltersDisplaySettings) {
FiltersDisplaySettingsView(
filters: nftStore.filters,
isNFTFilters: true,
networkStore: networkStore,
save: { filters in
nftStore.saveFilters(filters)
}
)
.osAvailabilityModifiers({ view in
if #available(iOS 16, *) {
view
.presentationDetents([
.fraction(0.6),
.large
])
} else {
view
}
})
}
.onAppear {
Task {
isNFTDiscoveryEnabled = await nftStore.isNFTDiscoveryEnabled()
Expand Down
Loading