damus

nostr ios client
git clone git://jb55.com/damus
Log | Files | Refs | README | LICENSE

commit b9198d6bd7a2bd30ef7166f00990c0a76168a698
parent 14bf187a6e8a66628f6189fff92d73daaf426cdb
Author: Terry Yiu <git@tyiu.xyz>
Date:   Sun,  1 Jun 2025 17:22:51 -0400

Add privacy-based redaction to wallet view

Changelog-Changed: Added privacy-based redaction to wallet view
Signed-off-by: Terry Yiu <git@tyiu.xyz>

Diffstat:
Mdamus/Models/UserSettingsStore.swift | 5++++-
Mdamus/Views/Onboarding/SuggestedUserView.swift | 1-
Mdamus/Views/Profile/ProfilePicView.swift | 21++++++++++++++++-----
Mdamus/Views/Settings/ZapSettingsView.swift | 2++
Mdamus/Views/Wallet/BalanceView.swift | 47+++++++++++++++++++++++++++++++++--------------
Mdamus/Views/Wallet/NWCSettings.swift | 5++++-
Mdamus/Views/Wallet/TransactionsView.swift | 90++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mdamus/Views/Wallet/WalletView.swift | 10++++++----
8 files changed, 120 insertions(+), 61 deletions(-)

diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift @@ -115,7 +115,10 @@ class UserSettingsStore: ObservableObject { @Setting(key: "dismiss_wallet_high_balance_warning", default_value: false) var dismiss_wallet_high_balance_warning: Bool - + + @Setting(key: "hide_wallet_balance", default_value: false) + var hide_wallet_balance: Bool + @Setting(key: "left_handed", default_value: false) var left_handed: Bool diff --git a/damus/Views/Onboarding/SuggestedUserView.swift b/damus/Views/Onboarding/SuggestedUserView.swift @@ -35,7 +35,6 @@ struct SuggestedUserView: View { let target = FollowTarget.pubkey(user.pubkey) InnerProfilePicView(url: user.pfp, fallbackUrl: nil, - pubkey: target.pubkey, size: 50, highlight: .none, disable_animation: false) diff --git a/damus/Views/Profile/ProfilePicView.swift b/damus/Views/Profile/ProfilePicView.swift @@ -31,7 +31,6 @@ func pfp_line_width(_ h: Highlight) -> CGFloat { struct InnerProfilePicView: View { let url: URL? let fallbackUrl: URL? - let pubkey: Pubkey let size: CGFloat let highlight: Highlight let disable_animation: Bool @@ -65,16 +64,19 @@ struct InnerProfilePicView: View { struct ProfilePicView: View { + @Environment(\.redactionReasons) var redactionReasons + let pubkey: Pubkey let size: CGFloat let highlight: Highlight let profiles: Profiles let disable_animation: Bool let zappability_indicator: Bool - + let privacy_sensitive: Bool + @State var picture: String? - init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil) { + init(pubkey: Pubkey, size: CGFloat, highlight: Highlight, profiles: Profiles, disable_animation: Bool, picture: String? = nil, show_zappability: Bool? = nil, privacy_sensitive: Bool = false) { self.pubkey = pubkey self.profiles = profiles self.size = size @@ -82,15 +84,24 @@ struct ProfilePicView: View { self._picture = State(initialValue: picture) self.disable_animation = disable_animation self.zappability_indicator = show_zappability ?? false + self.privacy_sensitive = privacy_sensitive } - + + var privacy_sensitive_pubkey: Pubkey { + if privacy_sensitive && redactionReasons.contains(.privacy) { + ANON_PUBKEY + } else { + pubkey + } + } + func get_lnurl() -> String? { return profiles.lookup_with_timestamp(pubkey)?.unsafeUnownedValue?.lnurl } var body: some View { ZStack (alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { - InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(pubkey)), pubkey: pubkey, size: size, highlight: highlight, disable_animation: disable_animation) + InnerProfilePicView(url: get_profile_url(picture: picture, pubkey: privacy_sensitive_pubkey, profiles: profiles), fallbackUrl: URL(string: robohash(privacy_sensitive_pubkey)), size: size, highlight: highlight, disable_animation: disable_animation) .onReceive(handle_notify(.profile_updated)) { updated in guard updated.pubkey == self.pubkey else { return diff --git a/damus/Views/Settings/ZapSettingsView.swift b/damus/Views/Settings/ZapSettingsView.swift @@ -67,6 +67,8 @@ struct ZapSettingsView: View { Section(NSLocalizedString("NWC wallet", comment: "Title for section in zap settings that controls general NWC wallet settings.")) { Toggle(NSLocalizedString("Disable high balance warning", comment: "Setting to disable high balance warnings on the user's wallet"), isOn: $settings.dismiss_wallet_high_balance_warning) .toggleStyle(.switch) + Toggle(NSLocalizedString("Hide balance", comment: "Setting to hide wallet balance."), isOn: $settings.hide_wallet_balance) + .toggleStyle(.switch) } } .navigationTitle(NSLocalizedString("Zaps", comment: "Navigation title for zap settings.")) diff --git a/damus/Views/Wallet/BalanceView.swift b/damus/Views/Wallet/BalanceView.swift @@ -9,7 +9,9 @@ import SwiftUI struct BalanceView: View { var balance: Int64? - + + @Binding var hide_balance: Bool + var body: some View { VStack(spacing: 5) { Text("Current balance", comment: "Label for displaying current wallet balance") @@ -28,29 +30,46 @@ struct BalanceView: View { } func numericalBalanceView(text: String) -> some View { - HStack { - Text(verbatim: text) - .lineLimit(1) - .minimumScaleFactor(0.70) - .font(.veryVeryLargeTitle) - .fontWeight(.heavy) - .foregroundStyle(PinkGradient) - - HStack(alignment: .top) { - Text("SATS", comment: "Abbreviation for Satoshis, smallest bitcoin unit") - .font(.caption) + Group { + if hide_balance { + Text(verbatim: "*****") + .lineLimit(1) + .minimumScaleFactor(0.70) + .font(.veryVeryLargeTitle) .fontWeight(.heavy) .foregroundStyle(PinkGradient) + + } else { + HStack { + Text(verbatim: text) + .lineLimit(1) + .minimumScaleFactor(0.70) + .font(.veryVeryLargeTitle) + .fontWeight(.heavy) + .foregroundStyle(PinkGradient) + + HStack(alignment: .top) { + Text("SATS", comment: "Abbreviation for Satoshis, smallest bitcoin unit") + .font(.caption) + .fontWeight(.heavy) + .foregroundStyle(PinkGradient) + } + } } } + .privacySensitive() .padding(.bottom) + .onTapGesture { + hide_balance.toggle() + } } } struct BalanceView_Previews: PreviewProvider { + @State private static var hide_balance: Bool = false static var previews: some View { - BalanceView(balance: 100000000) - BalanceView(balance: nil) + BalanceView(balance: 100000000, hide_balance: $hide_balance) + BalanceView(balance: nil, hide_balance: $hide_balance) } } diff --git a/damus/Views/Wallet/NWCSettings.swift b/damus/Views/Wallet/NWCSettings.swift @@ -138,7 +138,10 @@ struct NWCSettings: View { Toggle(NSLocalizedString("Disable high balance warning", comment: "Setting to disable high balance warnings on the user's wallet"), isOn: $settings.dismiss_wallet_high_balance_warning) .toggleStyle(.switch) - + + Toggle(NSLocalizedString("Hide balance", comment: "Setting to hide wallet balance."), isOn: $settings.hide_wallet_balance) + .toggleStyle(.switch) + Button(action: { self.model.disconnect() dismiss() diff --git a/damus/Views/Wallet/TransactionsView.swift b/damus/Views/Wallet/TransactionsView.swift @@ -8,14 +8,18 @@ import SwiftUI struct TransactionView: View { - + @Environment(\.redactionReasons) var redactionReasons + let damus_state: DamusState var transaction: WalletConnect.Transaction - + + @Binding var hide_balance: Bool + var body: some View { + let redactedForPrivacy = redactionReasons.contains(.privacy) let isIncomingTransaction = transaction.type == "incoming" let txType = isIncomingTransaction ? "arrow-bottom-left" : "arrow-top-right" - let txColor = isIncomingTransaction ? DamusColors.success : Color.gray + let txColor = (isIncomingTransaction && !hide_balance && !redactedForPrivacy) ? DamusColors.success : Color.gray let txOp = isIncomingTransaction ? "+" : "-" let created_at = Date.init(timeIntervalSince1970: TimeInterval(transaction.created_at)) let formatter = RelativeDateTimeFormatter() @@ -26,21 +30,23 @@ struct TransactionView: View { VStack(alignment: .leading) { HStack(alignment: .center) { ZStack { - ProfilePicView(pubkey: pubkey, size: 45, highlight: .custom(.damusAdaptableBlack, 0.1), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation) + ProfilePicView(pubkey: pubkey, size: 45, highlight: .custom(.damusAdaptableBlack, 0.1), profiles: damus_state.profiles, disable_animation: damus_state.settings.disable_animation, privacy_sensitive: true) .onTapGesture { damus_state.nav.push(route: Route.ProfileByKey(pubkey: pubkey)) } - - Image(txType) - .resizable() - .frame(width: 18, height: 18) - .foregroundColor(.white) - .padding(2) - .background(txColor) - .clipShape(Circle()) - .overlay(Circle().stroke(Color.damusAdaptableWhite, lineWidth: 1.0)) - .padding(.top, 25) - .padding(.leading, 35) + + if !hide_balance && !redactedForPrivacy { + Image(txType) + .resizable() + .frame(width: 18, height: 18) + .foregroundColor(.white) + .padding(2) + .background(txColor) + .clipShape(Circle()) + .overlay(Circle().stroke(Color.damusAdaptableWhite, lineWidth: 1.0)) + .padding(.top, 25) + .padding(.leading, 35) + } } VStack(alignment: .leading, spacing: 10) { @@ -58,10 +64,17 @@ struct TransactionView: View { Spacer() - Text(verbatim: "\(txOp) \(format_msats(transaction.amount))") - .font(.headline) - .foregroundColor(txColor) - .bold() + if hide_balance { + Text(verbatim: "*****") + .font(.headline) + .foregroundColor(txColor) + .bold() + } else { + Text(verbatim: "\(txOp) \(format_msats(transaction.amount))") + .font(.headline) + .foregroundColor(txColor) + .bold() + } } .frame(maxWidth: .infinity, minHeight: 75, alignment: .center) .padding(.horizontal, 10) @@ -107,27 +120,32 @@ struct TransactionsView: View { transactions?.sorted(by: { $0.created_at > $1.created_at }) } + @Binding var hide_balance: Bool + var body: some View { VStack(alignment: .leading, spacing: 10) { Text("Latest transactions", comment: "Heading for latest wallet transactions list") .foregroundStyle(DamusColors.neutral6) - - if let sortedTransactions { - if sortedTransactions.isEmpty { - emptyTransactions - } else { - ForEach(sortedTransactions, id: \.self) { transaction in - TransactionView(damus_state: damus_state, transaction: transaction) + + Group { + if let sortedTransactions { + if sortedTransactions.isEmpty { + emptyTransactions + } else { + ForEach(sortedTransactions, id: \.self) { transaction in + TransactionView(damus_state: damus_state, transaction: transaction, hide_balance: $hide_balance) + } } } + else { + // Make sure we do not show "No transactions yet" to the user when still loading (or when failed to load) + // This is important because if we show that when things are not loaded properly, we risk scaring the user into thinking that they have lost funds. + emptyTransactions + .redacted(reason: .placeholder) + .shimmer(true) + } } - else { - // Make sure we do not show "No transactions yet" to the user when still loading (or when failed to load) - // This is important because if we show that when things are not loaded properly, we risk scaring the user into thinking that they have lost funds. - emptyTransactions - .redacted(reason: .placeholder) - .shimmer(true) - } + .privacySensitive() } } @@ -154,8 +172,10 @@ struct TransactionsView_Previews: PreviewProvider { static let transaction3: WalletConnect.Transaction = WalletConnect.Transaction(type: "outgoing", invoice: "", description: "", description_hash: "", preimage: "", payment_hash: "123456789042", amount: 303000, fees_paid: 0, created_at: 1737590101, expires_at: 0, settled_at: 0, metadata: nil) static let transaction4: WalletConnect.Transaction = WalletConnect.Transaction(type: "incoming", invoice: "", description: "", description_hash: "", preimage: "", payment_hash: "1234567890662", amount: 720000, fees_paid: 0, created_at: 1737090300, expires_at: 0, settled_at: 0, metadata: nil) static var test_transactions: [WalletConnect.Transaction] = [transaction1, transaction2, transaction3, transaction4] - + + @State private static var hide_balance: Bool = false + static var previews: some View { - TransactionsView(damus_state: tds, transactions: test_transactions) + TransactionsView(damus_state: tds, transactions: test_transactions, hide_balance: $hide_balance) } } diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift @@ -14,7 +14,8 @@ struct WalletView: View { @State var show_settings: Bool = false @ObservedObject var model: WalletModel @ObservedObject var settings: UserSettingsStore - + @State private var showBalance: Bool = false + init(damus_state: DamusState, model: WalletModel? = nil) { self.damus_state = damus_state self._model = ObservedObject(wrappedValue: model ?? damus_state.wallet) @@ -47,6 +48,7 @@ struct WalletView: View { .bold() .foregroundStyle(.damusWarningTertiary) } + .privacySensitive() .padding() .overlay( RoundedRectangle(cornerRadius: 20) @@ -56,9 +58,9 @@ struct WalletView: View { VStack(spacing: 5) { - BalanceView(balance: model.balance) - - TransactionsView(damus_state: damus_state, transactions: model.transactions) + BalanceView(balance: model.balance, hide_balance: $settings.hide_wallet_balance) + + TransactionsView(damus_state: damus_state, transactions: model.transactions, hide_balance: $settings.hide_wallet_balance) } } .navigationTitle(NSLocalizedString("Wallet", comment: "Navigation title for Wallet view"))