From 01701e8bdff94dd4f3f9febf5127c941c7df9b31 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 7 Aug 2026 19:58:58 +0700 Subject: [PATCH 1/3] feat(ui): group Masternodes and Voting under a Governance menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Main menu: "Voting" becomes "Governance", a submenu holding Masternodes (moved out of Tools) and Voting. Gating changes deliberately. The old Voting row was `#if DASHPAY` plus `VotingPrefs.votingEnabled`; Governance carries neither, because Masternodes was never DashPay-gated and gating the parent would make it unreachable in the dashwallet scheme. Governance is therefore always present, and its contents adapt: Masternodes always, Voting only under DASHPAY when the preference is on. With voting off, Governance holds Masternodes alone. Masternodes moves rather than being duplicated — one path to the screen. "Masternode Keychain" stays in Tools; that is key material, not governance. The hosting wrapper (UIHostingController + NavigationStack for row drill-downs) and the UIKit push for UsernameVotingViewController move verbatim, so behaviour is unchanged. Co-Authored-By: Claude Fable 5 --- DashWallet.xcodeproj/project.pbxproj | 20 +++ .../Governance/GovernanceMenuScreen.swift | 128 ++++++++++++++++++ .../Governance/GovernanceMenuViewModel.swift | 73 ++++++++++ .../UI/Menu/Main/MainMenuViewController.swift | 14 +- .../UI/Menu/Main/MainMenuViewModel.swift | 26 ++-- .../UI/Menu/Tools/ToolsMenuScreen.swift | 28 ---- .../UI/Menu/Tools/ToolsMenuViewModel.swift | 9 -- DashWallet/en.lproj/Localizable.strings | 6 + 8 files changed, 248 insertions(+), 56 deletions(-) create mode 100644 DashWallet/Sources/UI/Menu/Governance/GovernanceMenuScreen.swift create mode 100644 DashWallet/Sources/UI/Menu/Governance/GovernanceMenuViewModel.swift diff --git a/DashWallet.xcodeproj/project.pbxproj b/DashWallet.xcodeproj/project.pbxproj index 8257997fb..7f5ea2d1f 100644 --- a/DashWallet.xcodeproj/project.pbxproj +++ b/DashWallet.xcodeproj/project.pbxproj @@ -772,6 +772,10 @@ 756557B62CE84FFA0060348D /* FeeInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 756557B42CE84FF70060348D /* FeeInfo.swift */; }; 7566F4832BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7566F4822BB69498005238D2 /* ToolsMenuScreen.swift */; }; 7566F4842BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7566F4822BB69498005238D2 /* ToolsMenuScreen.swift */; }; + 5AGOV0002F9A0000000000S2 /* GovernanceMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AGOV0002F9A0000000000S1 /* GovernanceMenuScreen.swift */; }; + 5AGOV0002F9A0000000000S3 /* GovernanceMenuScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AGOV0002F9A0000000000S1 /* GovernanceMenuScreen.swift */; }; + 5AGOV0002F9A0000000000V2 /* GovernanceMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AGOV0002F9A0000000000V1 /* GovernanceMenuViewModel.swift */; }; + 5AGOV0002F9A0000000000V3 /* GovernanceMenuViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AGOV0002F9A0000000000V1 /* GovernanceMenuViewModel.swift */; }; 7566F48A2BB6CAF2005238D2 /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7566F4892BB6CAF2005238D2 /* MenuItem.swift */; }; 7566F48B2BB6CAF2005238D2 /* MenuItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7566F4892BB6CAF2005238D2 /* MenuItem.swift */; }; 7569D6832DE6DA6800768BFF /* ExploreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7569D6822DE6DA6800768BFF /* ExploreViewController.swift */; }; @@ -2897,6 +2901,8 @@ 755E6DFD2A99E7A000A42870 /* DWInvitationSetupState.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DWInvitationSetupState.h; sourceTree = ""; }; 756557B42CE84FF70060348D /* FeeInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeeInfo.swift; sourceTree = ""; }; 7566F4822BB69498005238D2 /* ToolsMenuScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ToolsMenuScreen.swift; sourceTree = ""; }; + 5AGOV0002F9A0000000000S1 /* GovernanceMenuScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovernanceMenuScreen.swift; sourceTree = ""; }; + 5AGOV0002F9A0000000000V1 /* GovernanceMenuViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GovernanceMenuViewModel.swift; sourceTree = ""; }; 7566F4892BB6CAF2005238D2 /* MenuItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuItem.swift; sourceTree = ""; }; 7569D6822DE6DA6800768BFF /* ExploreViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreViewController.swift; sourceTree = ""; }; 7569D6852DE6DB1B00768BFF /* ExploreHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreHeaderView.swift; sourceTree = ""; }; @@ -4722,6 +4728,7 @@ 2A7A7BD32348CB4F00451078 /* Settings */, 51AA00102F970010005A0010 /* SyncInfo */, 2A7A7BDA2348DBE700451078 /* Tools */, + 5AGOV0002F9A0000000000G0 /* Governance */, 755049AB2C846576008FA7EB /* MenuItemModel.swift */, ); path = Menu; @@ -4760,6 +4767,15 @@ path = Settings; sourceTree = ""; }; + 5AGOV0002F9A0000000000G0 /* Governance */ = { + isa = PBXGroup; + children = ( + 5AGOV0002F9A0000000000S1 /* GovernanceMenuScreen.swift */, + 5AGOV0002F9A0000000000V1 /* GovernanceMenuViewModel.swift */, + ); + path = Governance; + sourceTree = ""; + }; 2A7A7BDA2348DBE700451078 /* Tools */ = { isa = PBXGroup; children = ( @@ -10039,6 +10055,8 @@ 47CF46A1296540EF0067B6EE /* AccountService.swift in Sources */, 47C6E6E7291A90B3003FEDF2 /* ListHandlerView.swift in Sources */, 7566F4832BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */, + 5AGOV0002F9A0000000000S2 /* GovernanceMenuScreen.swift in Sources */, + 5AGOV0002F9A0000000000V2 /* GovernanceMenuViewModel.swift in Sources */, RC00C0012FA0000000000001 /* CSVExportSheet.swift in Sources */, 754D020E2D1558EB005CA466 /* GroupedTransactions.swift in Sources */, 11BD738128E7356100A34022 /* CrowdNode.swift in Sources */, @@ -10664,6 +10682,8 @@ C9D2C7AB2A320AA000D15901 /* DWBaseFormTableViewCell.m in Sources */, C9D2C7AC2A320AA000D15901 /* AccountCell.swift in Sources */, 7566F4842BB6949E005238D2 /* ToolsMenuScreen.swift in Sources */, + 5AGOV0002F9A0000000000S3 /* GovernanceMenuScreen.swift in Sources */, + 5AGOV0002F9A0000000000V3 /* GovernanceMenuViewModel.swift in Sources */, RC00C0022FA0000000000001 /* CSVExportSheet.swift in Sources */, C943B33A2A408CED00AF23C5 /* DWUploadAvatarModel.m in Sources */, C943B3392A408CED00AF23C5 /* DWUploadAvatarViewController.m in Sources */, diff --git a/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuScreen.swift b/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuScreen.swift new file mode 100644 index 000000000..7725a4c3c --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuScreen.swift @@ -0,0 +1,128 @@ +// +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import DashUIKit +import SwiftUI +import UIKit + +/// Governance submenu: the wallet's masternode-facing surfaces, reached from +/// the main menu's "Governance" row. Groups Masternodes (moved here from +/// Tools) with username Voting (moved here from the main menu). +struct GovernanceMenuScreen: View { + private let vc: UINavigationController + + @StateObject private var viewModel = GovernanceMenuViewModel() + + init(vc: UINavigationController) { + self.vc = vc + } + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + NavBarBack { + vc.popViewController(animated: true) + } + + TopIntro(title: NSLocalizedString("Governance", comment: "Governance")) + .padding(.leading, 20) + .padding(.trailing, 60) + .padding(.top, 10) + .padding(.bottom, 20) + + VStack(spacing: 2) { + ForEach(viewModel.items) { item in + MenuItem( + title: item.title, + subtitle: item.subtitle, + details: item.details, + icon: item.icon, + showInfo: item.showInfo, + showChevron: false, + isToggled: item.isToggled, + action: item.action + ) + .frame(minHeight: 56) + } + } + .padding(6) + .background(Color.dash.secondaryBackground) + .clipShape(RoundedRectangle(cornerRadius: 20, style: .continuous)) + .shadow(color: Color.dash.shadow, radius: 20, x: 0, y: 5) + .padding(.horizontal, 20) + + Spacer() + } + .background(Color.dash.primaryBackground) + .navigationBarHidden(true) + .onReceive(viewModel.$navigationDestination) { destination in + handleNavigation(destination) + } + } + + private func handleNavigation(_ destination: GovernanceMenuNavigationDestination?) { + switch destination { + case .masternodes: + showMasternodes() + #if DASHPAY + case .voting: + showVoting() + #endif + case .none: + break + } + + if destination != nil { + viewModel.resetNavigation() + } + } + + /// Same hosting wrapper Tools used when this screen lived there: the + /// SwiftUI list needs its own `NavigationStack` for row drill-downs, so + /// the back button is wired explicitly to pop the UIKit stack. + private func showMasternodes() { + let navController = vc + let popRoot: () -> Void = { [weak navController] in + _ = navController?.popViewController(animated: true) + } + + let hosting = UIHostingController( + rootView: AnyView( + NavigationStack { + MasternodesScreen() + .navigationTitle(NSLocalizedString("Masternodes", comment: "Masternodes")) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarLeading) { + Button(action: popRoot) { + Image(systemName: "chevron.left") + } + } + } + } + ) + ) + hosting.hidesBottomBarWhenPushed = true + vc.pushViewController(hosting, animated: true) + } + + #if DASHPAY + private func showVoting() { + let controller = UsernameVotingViewController.controller() + controller.hidesBottomBarWhenPushed = true + vc.pushViewController(controller, animated: true) + } + #endif +} diff --git a/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuViewModel.swift b/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuViewModel.swift new file mode 100644 index 000000000..1e492d9fc --- /dev/null +++ b/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuViewModel.swift @@ -0,0 +1,73 @@ +// +// Copyright © 2026 Dash Core Group. All rights reserved. +// +// Licensed under the MIT License (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://opensource.org/licenses/MIT +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +enum GovernanceMenuNavigationDestination { + case masternodes + #if DASHPAY + case voting + #endif +} + +@MainActor +final class GovernanceMenuViewModel: ObservableObject { + @Published var items: [MenuItemModel] = [] + @Published var navigationDestination: GovernanceMenuNavigationDestination? + + init() { + setupMenuItems() + } + + private func setupMenuItems() { + var allItems: [MenuItemModel] = [] + + // Masternodes — not DashPay-gated: masternode ownership is a Core + // concern and this screen is reachable in every build configuration + // (it previously lived under Tools with no gate). + allItems.append(MenuItemModel( + title: NSLocalizedString("Masternodes", comment: "Masternodes"), + subtitle: NSLocalizedString( + "Masternodes and evonodes using this wallet's keys", comment: "Masternodes"), + icon: .system("server.rack"), + action: { [weak self] in + self?.navigationDestination = .masternodes + } + )) + + #if DASHPAY + // Username voting, behind the same preference that gated it on the + // main menu (Settings → "Username voting"). + if VotingPrefs.shared.votingEnabled { + allItems.append(MenuItemModel( + title: NSLocalizedString("Voting", comment: ""), + subtitle: NSLocalizedString( + "Vote on contested usernames", comment: "Voting"), + icon: .custom("menu_voting", maxHeight: 30), + action: { [weak self] in + self?.navigationDestination = .voting + } + )) + } + #endif + + items = allItems + } + + func resetNavigation() { + navigationDestination = nil + } +} diff --git a/DashWallet/Sources/UI/Menu/Main/MainMenuViewController.swift b/DashWallet/Sources/UI/Menu/Main/MainMenuViewController.swift index 602f40b53..940496d07 100644 --- a/DashWallet/Sources/UI/Menu/Main/MainMenuViewController.swift +++ b/DashWallet/Sources/UI/Menu/Main/MainMenuViewController.swift @@ -152,6 +152,7 @@ struct MainMenuScreen: View { @ObservedObject private var viewModel: MainMenuViewModel @State private var openSettings: Bool = false @State private var showTools: Bool = false + @State private var showGovernance: Bool = false @State private var showSyncInfo: Bool = false @State private var showWallets: Bool = false @State private var showIdentities: Bool = false @@ -337,6 +338,13 @@ struct MainMenuScreen: View { ) { EmptyView() } + + NavigationLink( + destination: GovernanceMenuScreen(vc: vc), + isActive: $showGovernance + ) { + EmptyView() + } NavigationLink( destination: SyncInfoMenuScreen(vc: vc), @@ -451,10 +459,8 @@ struct MainMenuScreen: View { showTools = true case .support: onContactSupport() - #if DASHPAY - case .voting: - showVoting() - #endif + case .governance: + showGovernance = true case .none: return } diff --git a/DashWallet/Sources/UI/Menu/Main/MainMenuViewModel.swift b/DashWallet/Sources/UI/Menu/Main/MainMenuViewModel.swift index 71c9efa8b..6cf35d5c8 100644 --- a/DashWallet/Sources/UI/Menu/Main/MainMenuViewModel.swift +++ b/DashWallet/Sources/UI/Menu/Main/MainMenuViewModel.swift @@ -28,9 +28,7 @@ enum MainMenuNavigationDestination { case settings case tools case support - #if DASHPAY - case voting - #endif + case governance } protocol MainMenuViewModelDelegate: AnyObject { @@ -166,18 +164,16 @@ class MainMenuViewModel: ObservableObject { } )) - #if DASHPAY - // Voting - if VotingPrefs.shared.votingEnabled { - allItems.append(MenuItemModel( - title: NSLocalizedString("Voting", comment: ""), - icon: .custom("menu_voting", maxHeight: 30), - action: { [weak self] in - self?.navigationDestination = .voting - } - )) - } - #endif + // Governance — Masternodes plus (on DashPay builds, when enabled) + // username Voting. Not DashPay-gated: Masternodes is a Core-side + // surface that every build configuration can reach. + allItems.append(MenuItemModel( + title: NSLocalizedString("Governance", comment: "Governance"), + icon: .custom("menu_voting", maxHeight: 30), + action: { [weak self] in + self?.navigationDestination = .governance + } + )) self.items = allItems } diff --git a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuScreen.swift b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuScreen.swift index 94ccfebe7..dab83a9e7 100644 --- a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuScreen.swift +++ b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuScreen.swift @@ -197,8 +197,6 @@ struct ToolsMenuScreen: View { showExtendedPublicKeys() case .masternodeKeys: showMasternodeKeys() - case .masternodes: - showMasternodes() case .csvExport: showCSVExportSheet = true case .zenLedger: @@ -271,32 +269,6 @@ struct ToolsMenuScreen: View { vc.pushViewController(controller, animated: true) } - private func showMasternodes() { - let navController = vc - let popRoot: () -> Void = { [weak navController] in - _ = navController?.popViewController(animated: true) - } - - let hosting = UIHostingController( - rootView: AnyView( - NavigationStack { - MasternodesScreen() - .navigationTitle(NSLocalizedString("Masternodes", comment: "")) - .navigationBarTitleDisplayMode(.inline) - .toolbar { - ToolbarItem(placement: .navigationBarLeading) { - Button(action: popRoot) { - Image(systemName: "chevron.left") - } - } - } - } - ) - ) - hosting.hidesBottomBarWhenPushed = true - vc.pushViewController(hosting, animated: true) - } - private func handleCSVExport() { Task { do { diff --git a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewModel.swift b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewModel.swift index f6e04cbd3..cc8451dbd 100644 --- a/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewModel.swift +++ b/DashWallet/Sources/UI/Menu/Tools/ToolsMenuViewModel.swift @@ -22,7 +22,6 @@ import SwiftDashSDK enum ToolsMenuNavigationDestination { case extendedPublicKeys case masternodeKeys - case masternodes case csvExport case zenLedger case storageExplorer @@ -101,14 +100,6 @@ class ToolsMenuViewModel: ObservableObject { self?.navigationDestination = .masternodeKeys } ), - MenuItemModel( - title: NSLocalizedString("Masternodes", comment: ""), - subtitle: NSLocalizedString("Masternodes and evonodes using this wallet's keys", comment: "Masternodes"), - icon: .system("server.rack"), - action: { [weak self] in - self?.navigationDestination = .masternodes - } - ), MenuItemModel( title: NSLocalizedString("CSV Export", comment: ""), icon: .custom("image.csv.export", maxHeight: 30), diff --git a/DashWallet/en.lproj/Localizable.strings b/DashWallet/en.lproj/Localizable.strings index f74bc2ab4..7823710ae 100644 --- a/DashWallet/en.lproj/Localizable.strings +++ b/DashWallet/en.lproj/Localizable.strings @@ -1447,6 +1447,9 @@ /* CrowdNode */ "Get Rewards Instantly" = "Get Rewards Instantly"; +/* Governance */ +"Governance" = "Governance"; + /* No comment provided by engineer. */ "Get Started" = "Get Started"; @@ -3930,6 +3933,9 @@ /* Balance info sheet */ "Uses one of the most audited and tested zero-knowledge libraries (Orchard)" = "Uses one of the most audited and tested zero-knowledge libraries (Orchard)"; +/* Voting */ +"Vote on contested usernames" = "Vote on contested usernames"; + /* Raw transaction inspector */ "Version" = "Version"; From 2d64eb57c460e558368a09043cf04d412a6199cd Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 7 Aug 2026 19:58:58 +0700 Subject: [PATCH 2/3] feat(ui): split retired masternodes into a collapsed section Registrations still on the network stay expanded at the top; retired ones move to a DisclosureGroup at the bottom, collapsed by default with a count on its label. Only `retired` (collateral spent / revoked / expired) moves down. `inactive` (PoSe-banned) and `unknown` (masternode list not yet synced) stay in the top section: a banned node is a problem worth surfacing, and unknown is a transient sync state, so burying either would hide something actionable. Both sections render only when non-empty, so a wallet with no retired nodes sees no stray disclosure row, and the existing empty state is untouched. Row construction is shared between the two sections rather than duplicated. Co-Authored-By: Claude Fable 5 --- .../UI/Menu/Tools/MasternodesScreen.swift | 62 ++++++++++++++++--- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/DashWallet/Sources/UI/Menu/Tools/MasternodesScreen.swift b/DashWallet/Sources/UI/Menu/Tools/MasternodesScreen.swift index 0040cb7cd..87afa5c71 100644 --- a/DashWallet/Sources/UI/Menu/Tools/MasternodesScreen.swift +++ b/DashWallet/Sources/UI/Menu/Tools/MasternodesScreen.swift @@ -33,6 +33,21 @@ final class MasternodesViewModel: ObservableObject { @Published private(set) var masternodes: [PlatformMasternode] = [] @Published private(set) var loaded = false + /// Registrations still on the network — active, PoSe-banned, or (before + /// the list arrives) indeterminate. These stay expanded at the top: an + /// inactive node is a problem the user probably wants to see, not + /// history. + var currentMasternodes: [PlatformMasternode] { + masternodes.filter { MasternodeStatus(rawValue: $0.status) != .retired } + } + + /// Registrations no longer in the masternode list — collateral spent, + /// revoked, or expired. History: shown in a collapsed section at the + /// bottom. + var retiredMasternodes: [PlatformMasternode] { + masternodes.filter { MasternodeStatus(rawValue: $0.status) == .retired } + } + /// In-wallet key index by base58 address for the two address-carrying /// families. Operator/platform ownership comes pre-resolved on the /// aggregation row; owner/voting is joined here against the derived @@ -166,6 +181,9 @@ private extension PlatformMasternode { struct MasternodesScreen: View { @StateObject private var viewModel = MasternodesViewModel() + /// Retired registrations are history — collapsed until asked for. + @State private var isRetiredExpanded = false + var body: some View { List { if viewModel.loaded && viewModel.masternodes.isEmpty { @@ -187,15 +205,32 @@ struct MasternodesScreen: View { .padding(.vertical, 20) } } else { - Section { - ForEach(viewModel.masternodes, id: \.proTxHash) { masternode in - NavigationLink { - MasternodeDetailScreen( - masternode: masternode, - ownerOwnership: viewModel.ownerOwnership(for: masternode), - votingOwnership: viewModel.votingOwnership(for: masternode)) + if !viewModel.currentMasternodes.isEmpty { + Section { + ForEach(viewModel.currentMasternodes, id: \.proTxHash) { masternode in + row(for: masternode) + } + } + } + + if !viewModel.retiredMasternodes.isEmpty { + Section { + DisclosureGroup(isExpanded: $isRetiredExpanded) { + ForEach(viewModel.retiredMasternodes, id: \.proTxHash) { masternode in + row(for: masternode) + } } label: { - MasternodeListRow(masternode: masternode) + HStack { + Text(NSLocalizedString("Retired", comment: "Masternodes")) + .font(.subheadline) + .fontWeight(.medium) + + Spacer() + + Text("\(viewModel.retiredMasternodes.count)") + .font(.caption) + .foregroundColor(Color.dash.secondaryText) + } } } } @@ -205,6 +240,17 @@ struct MasternodesScreen: View { viewModel.load() } } + + private func row(for masternode: PlatformMasternode) -> some View { + NavigationLink { + MasternodeDetailScreen( + masternode: masternode, + ownerOwnership: viewModel.ownerOwnership(for: masternode), + votingOwnership: viewModel.votingOwnership(for: masternode)) + } label: { + MasternodeListRow(masternode: masternode) + } + } } // MARK: - MasternodeListRow From bc7a3252b365650d6c51ad777e0f4ec9b3666875 Mon Sep 17 00:00:00 2001 From: Quantum Explorer Date: Fri, 7 Aug 2026 20:16:13 +0700 Subject: [PATCH 3/3] style(ui): import Combine explicitly in GovernanceMenuViewModel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ObservableObject` and `@Published` are Combine symbols; the file imported only Foundation and relied on transitively picking them up. It compiled either way, but every sibling menu view model (MainMenuViewModel, ToolsMenuViewModel, SettingsMenuViewModel) imports Combine explicitly — this was the outlier. Co-Authored-By: Claude Fable 5 --- .../Sources/UI/Menu/Governance/GovernanceMenuViewModel.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuViewModel.swift b/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuViewModel.swift index 1e492d9fc..27392eca5 100644 --- a/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuViewModel.swift +++ b/DashWallet/Sources/UI/Menu/Governance/GovernanceMenuViewModel.swift @@ -14,6 +14,7 @@ // limitations under the License. // +import Combine import Foundation enum GovernanceMenuNavigationDestination {