damus

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

commit bffa42a13a5e799e3083bdb1834fd50657c0448e
parent 8097cfdfb8e6fe328e0a2683ec24b6d31edd7dc9
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 15 May 2023 11:57:37 -0700

Supporter Badges

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 8++++++++
Adamus/Components/Gradients/GoldSupportGradient.swift | 29+++++++++++++++++++++++++++++
Adamus/Components/SupporterBadge.swift | 73+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/ContentView.swift | 2+-
Mdamus/Models/HomeModel.swift | 4++--
Mdamus/Models/Mentions.swift | 2+-
Mdamus/Models/WalletModel.swift | 3+++
Mdamus/Nostr/Profiles.swift | 8+++++++-
Mdamus/Views/Posting/UserSearch.swift | 2+-
Mdamus/Views/Profile/EventProfileName.swift | 16++++++++++++++++
Mdamus/Views/Profile/ProfileName.swift | 16++++++++++++++++
Mdamus/Views/Profile/ProfileView.swift | 3+++
Mdamus/Views/SearchResultsView.swift | 2+-
Mdamus/Views/Wallet/WalletView.swift | 38+++++++++++++++++++++++++++++++++-----
14 files changed, 194 insertions(+), 12 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -53,6 +53,8 @@ 4C216F34286F5ACD00040376 /* DMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F33286F5ACD00040376 /* DMView.swift */; }; 4C216F362870A9A700040376 /* InputDismissKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */; }; 4C216F382871EDE300040376 /* DirectMessageModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C216F372871EDE300040376 /* DirectMessageModel.swift */; }; + 4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */; }; + 4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */; }; 4C285C8228385570008A31F1 /* CarouselView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8128385570008A31F1 /* CarouselView.swift */; }; 4C285C8428385690008A31F1 /* CreateAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C8328385690008A31F1 /* CreateAccountView.swift */; }; 4C285C86283892E7008A31F1 /* CreateAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */; }; @@ -444,6 +446,8 @@ 4C216F33286F5ACD00040376 /* DMView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DMView.swift; sourceTree = "<group>"; }; 4C216F352870A9A700040376 /* InputDismissKeyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputDismissKeyboard.swift; sourceTree = "<group>"; }; 4C216F372871EDE300040376 /* DirectMessageModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DirectMessageModel.swift; sourceTree = "<group>"; }; + 4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupporterBadge.swift; sourceTree = "<group>"; }; + 4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GoldSupportGradient.swift; sourceTree = "<group>"; }; 4C285C8128385570008A31F1 /* CarouselView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CarouselView.swift; sourceTree = "<group>"; }; 4C285C8328385690008A31F1 /* CreateAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountView.swift; sourceTree = "<group>"; }; 4C285C85283892E7008A31F1 /* CreateAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateAccountModel.swift; sourceTree = "<group>"; }; @@ -1041,6 +1045,7 @@ children = ( 4C7D09712A0AEF5E00943473 /* DamusGradient.swift */, 4C7D09732A0AEF9000943473 /* AlbyGradient.swift */, + 4C2859612A12A7F0004746F7 /* GoldSupportGradient.swift */, ); path = Gradients; sourceTree = "<group>"; @@ -1218,6 +1223,7 @@ 4CE4F0F729DB7399005914DB /* ThiccDivider.swift */, 4C1A9A2229DDDB8100516EAC /* IconLabel.swift */, 4C8D00C929DF80350036AF10 /* TruncatedText.swift */, + 4C28595F2A12A2BE004746F7 /* SupporterBadge.swift */, ); path = Components; sourceTree = "<group>"; @@ -1730,6 +1736,7 @@ 4C06670628FCB08600038D2A /* ImageCarousel.swift in Sources */, F79C7FAD29D5E9620000F946 /* EditProfilePictureControl.swift in Sources */, 4C9F18E229AA9B6C008C55EC /* CustomizeZapView.swift in Sources */, + 4C2859602A12A2BE004746F7 /* SupporterBadge.swift in Sources */, 4C75EFAF28049D350006080F /* NostrFilter.swift in Sources */, 4C3EA64C28FF59AC00C48A62 /* bech32_util.c in Sources */, 4CE1399029F0661A00AC6A0B /* RepostAction.swift in Sources */, @@ -1837,6 +1844,7 @@ 3169CAED294FCCFC00EE4006 /* Constants.swift in Sources */, 4CB9D4A72992D02B00A9A7E4 /* ProfileNameView.swift in Sources */, 4CE4F0F429D779B5005914DB /* PostBox.swift in Sources */, + 4C2859622A12A7F0004746F7 /* GoldSupportGradient.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/damus/Components/Gradients/GoldSupportGradient.swift b/damus/Components/Gradients/GoldSupportGradient.swift @@ -0,0 +1,29 @@ +// +// GoldSupportGradient.swift +// damus +// +// Created by William Casarin on 2023-05-15. +// + +import SwiftUI + +fileprivate let gold_grad_c1 = hex_col(r: 226, g: 168, b: 0) +fileprivate let gold_grad_c2 = hex_col(r: 249, g: 243, b: 100) + +fileprivate let gold_grad = [gold_grad_c2, gold_grad_c1] + +let GoldGradient: LinearGradient = + LinearGradient(colors: gold_grad, startPoint: .bottomLeading, endPoint: .topTrailing) + +struct GoldGradientView: View { + var body: some View { + GoldGradient + .edgesIgnoringSafeArea([.top,.bottom]) + } +} + +struct GoldGradientView_Previews: PreviewProvider { + static var previews: some View { + GoldGradientView() + } +} diff --git a/damus/Components/SupporterBadge.swift b/damus/Components/SupporterBadge.swift @@ -0,0 +1,73 @@ +// +// SupporterBadge.swift +// damus +// +// Created by William Casarin on 2023-05-15. +// + +import SwiftUI + +struct SupporterBadge: View { + let percent: Int + + let size: CGFloat = 17 + + var body: some View { + if percent < 100 { + Image("star.fill") + .resizable() + .frame(width:size, height:size) + .foregroundColor(support_level_color(percent)) + } else { + Image("star.fill") + .resizable() + .frame(width:size, height:size) + .foregroundStyle(GoldGradient) + } + } +} + +func support_level_color(_ percent: Int) -> Color { + if percent == 0 { + return .gray + } + + let percent_f = Double(percent) / 100.0 + let cutoff = 0.5 + let h = cutoff + (percent_f * cutoff); // Hue (note 0.2 = Green, see huge chart below) + let s = 0.9; // Saturation + let b = 0.9; // Brightness + + return Color(hue: h, saturation: s, brightness: b) +} + +struct SupporterBadge_Previews: PreviewProvider { + static func Level(_ p: Int) -> some View { + HStack(alignment: .center) { + SupporterBadge(percent: p) + .frame(width: 50) + Text("\(p)") + .frame(width: 50) + } + } + + static var previews: some View { + VStack(spacing: 0) { + VStack(spacing: 0) { + Level(1) + Level(10) + Level(20) + Level(30) + Level(40) + Level(50) + } + Level(60) + Level(70) + Level(80) + Level(90) + Level(100) + } + } +} + + diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -397,7 +397,7 @@ struct ContentView: View { return } ds.postbox.send(ev) - if let profile = ds.profiles.profiles[ev.pubkey] { + if let profile = ds.profiles.lookup_with_timestamp(id: ev.pubkey) { ds.postbox.send(profile.event) } } diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -735,7 +735,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P var old_nip05: String? = nil if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) { old_nip05 = mprof.profile.nip05 - if mprof.timestamp > ev.created_at { + if mprof.event.created_at > ev.created_at { // skip if we already have an newer profile return } @@ -752,7 +752,7 @@ func process_metadata_profile(our_pubkey: String, profiles: Profiles, profile: P print("validated nip05 for '\(nip05)'") } - DispatchQueue.main.async { + Task { @MainActor in profiles.validated[ev.pubkey] = validated profiles.nip05_pubkey[nip05] = ev.pubkey notify(.profile_updated, ProfileUpdate(pubkey: ev.pubkey, profile: profile)) diff --git a/damus/Models/Mentions.swift b/damus/Models/Mentions.swift @@ -246,7 +246,7 @@ func format_msats_abbrev(_ msats: Int64) -> String { formatter.positiveSuffix = "m" formatter.positivePrefix = "" formatter.minimumFractionDigits = 0 - formatter.maximumFractionDigits = 2 + formatter.maximumFractionDigits = 3 formatter.roundingMode = .down formatter.roundingIncrement = 0.1 formatter.multiplier = 1 diff --git a/damus/Models/WalletModel.swift b/damus/Models/WalletModel.swift @@ -16,6 +16,7 @@ enum WalletConnectState { class WalletModel: ObservableObject { var settings: UserSettingsStore private(set) var previous_state: WalletConnectState + var inital_percent: Int @Published private(set) var connect_state: WalletConnectState @@ -23,6 +24,7 @@ class WalletModel: ObservableObject { self.connect_state = state self.previous_state = .none self.settings = settings + self.inital_percent = settings.donation_percent } init(settings: UserSettingsStore) { @@ -35,6 +37,7 @@ class WalletModel: ObservableObject { self.previous_state = .none self.connect_state = .none } + self.inital_percent = settings.donation_percent } func cancel() { diff --git a/damus/Nostr/Profiles.swift b/damus/Nostr/Profiles.swift @@ -17,7 +17,7 @@ class Profiles { qos: .userInteractive, attributes: .concurrent) - var profiles: [String: TimestampedProfile] = [:] + private var profiles: [String: TimestampedProfile] = [:] var validated: [String: NIP05] = [:] var nip05_pubkey: [String: String] = [:] var zappers: [String: String] = [:] @@ -26,6 +26,12 @@ class Profiles { return validated[pk] } + func enumerated() -> EnumeratedSequence<[String: TimestampedProfile]> { + return queue.sync { + return profiles.enumerated() + } + } + func lookup_zapper(pubkey: String) -> String? { if let zapper = zappers[pubkey] { return zapper diff --git a/damus/Views/Posting/UserSearch.swift b/damus/Views/Posting/UserSearch.swift @@ -140,7 +140,7 @@ func search_users_for_autocomplete(profiles: Profiles, tags: [[String]], search } // search profile cache as well - for tup in profiles.profiles.enumerated() { + for tup in profiles.enumerated() { let pk = tup.element.key let prof = tup.element.value.profile diff --git a/damus/Views/Profile/EventProfileName.swift b/damus/Views/Profile/EventProfileName.swift @@ -15,6 +15,7 @@ struct EventProfileName: View { @State var display_name: DisplayName? @State var nip05: NIP05? + @State var donation: Int? let size: EventViewKind @@ -23,6 +24,7 @@ struct EventProfileName: View { self.pubkey = pubkey self.profile = profile self.size = size + self._donation = State(wrappedValue: profile?.damus_donation) } var friend_type: FriendType? { @@ -45,6 +47,15 @@ struct EventProfileName: View { return profile.reactions == false } + var supporter: Int? { + guard let donation, donation > 0 + else { + return nil + } + + return donation + } + var body: some View { HStack(spacing: 2) { switch current_display_name { @@ -73,6 +84,10 @@ struct EventProfileName: View { Image("zap-hashtag") .frame(width: 14, height: 14) } + + if let supporter { + SupporterBadge(percent: supporter) + } } .onReceive(handle_notify(.profile_updated)) { notif in let update = notif.object as! ProfileUpdate @@ -81,6 +96,7 @@ struct EventProfileName: View { } display_name = Profile.displayName(profile: update.profile, pubkey: pubkey) nip05 = damus_state.profiles.is_validated(pubkey) + donation = update.profile.damus_donation } } } diff --git a/damus/Views/Profile/ProfileName.swift b/damus/Views/Profile/ProfileName.swift @@ -34,6 +34,7 @@ struct ProfileName: View { @State var display_name: DisplayName? @State var nip05: NIP05? + @State var donation: Int? init(pubkey: String, profile: Profile?, damus: DamusState, show_nip5_domain: Bool = true) { self.pubkey = pubkey @@ -75,6 +76,17 @@ struct ProfileName: View { return profile.reactions == false } + var supporter: Int? { + guard let profile, + let donation = profile.damus_donation, + donation > 0 + else { + return nil + } + + return donation + } + var body: some View { HStack(spacing: 2) { Text(verbatim: "\(prefix)\(name_choice)") @@ -90,6 +102,9 @@ struct ProfileName: View { Image("zap-hashtag") .frame(width: 14, height: 14) } + if let supporter { + SupporterBadge(percent: supporter) + } } .onReceive(handle_notify(.profile_updated)) { notif in let update = notif.object as! ProfileUpdate @@ -98,6 +113,7 @@ struct ProfileName: View { } display_name = Profile.displayName(profile: update.profile, pubkey: pubkey) nip05 = damus_state.profiles.is_validated(pubkey) + donation = profile?.damus_donation } } } diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift @@ -496,6 +496,9 @@ struct ProfileView_Previews: PreviewProvider { func test_damus_state() -> DamusState { let pubkey = "3efdaebb1d8923ebd99c9e7ace3b4194ab45512e2be79c1b7d68d9243e0d2681" let damus = DamusState.empty + let settings = UserSettingsStore() + settings.donation_percent = 100 + settings.default_zap_amount = 1971 let prof = Profile(name: "damus", display_name: "damus", about: "iOS app!", picture: "https://damus.io/img/logo.png", banner: "", website: "https://damus.io", lud06: nil, lud16: "jb55@sendsats.lol", nip05: "damus.io", damus_donation: nil) let tsprof = TimestampedProfile(profile: prof, timestamp: 0, event: test_event) diff --git a/damus/Views/SearchResultsView.swift b/damus/Views/SearchResultsView.swift @@ -182,7 +182,7 @@ func make_hashtagable(_ str: String) -> String { func search_profiles(profiles: Profiles, search: String) -> [SearchedUser] { let new = search.lowercased() - return profiles.profiles.enumerated().reduce(into: []) { acc, els in + return profiles.enumerated().reduce(into: []) { acc, els in let pk = els.element.key let prof = els.element.value.profile diff --git a/damus/Views/Wallet/WalletView.swift b/damus/Views/Wallet/WalletView.swift @@ -58,7 +58,19 @@ struct WalletView: View { var tip_msats: String { let msats = Int64(percent * Double(model.settings.default_zap_amount * 1000)) let s = format_msats_abbrev(msats) - return s.split(separator: ".").first.map({ x in String(x) }) ?? s + // TODO: fix formatting and remove this hack + let parts = s.split(separator: ".") + if parts.count == 1 { + return s + } + if let end = parts[safe: 1] { + if end.allSatisfy({ c in c.isNumber }) { + return String(parts[0]) + } else { + return s + } + } + return s } var SupportDamus: some View { @@ -93,6 +105,7 @@ struct WalletView: View { Text("\(Int(binding.wrappedValue))%") .font(.title.bold()) .foregroundColor(.white) + .frame(width: 80) } HStack{ @@ -103,7 +116,7 @@ struct WalletView: View { Text("\(Image("zap.fill")) \(format_msats_abbrev(Int64(model.settings.default_zap_amount) * 1000))") .font(.title) .foregroundColor(percent == 0 ? .gray : .yellow) - .frame(width: 100) + .frame(width: 120) } Text("Zap") @@ -121,9 +134,10 @@ struct WalletView: View { Text("\(Image("zap.fill")) \(tip_msats)") .font(.title) .foregroundColor(percent == 0 ? .gray : Color.yellow) - .frame(width: 100) + .frame(width: 120) } - Text("💜") + + Text(percent == 0 ? "🩶" : "💜") .foregroundColor(.white) } Spacer() @@ -154,16 +168,30 @@ struct WalletView: View { ConnectWalletView(model: model) case .existing(let nwc): MainWalletView(nwc: nwc) + .onAppear() { + model.inital_percent = settings.donation_percent + } + .onChange(of: settings.donation_percent) { p in + guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else { + return + } + + profile.damus_donation = p + + notify(.profile_updated, ProfileUpdate(pubkey: damus_state.pubkey, profile: profile)) + } .onDisappear { guard let keypair = damus_state.keypair.to_full(), let profile = damus_state.profiles.lookup(id: damus_state.pubkey), - profile.damus_donation != settings.donation_percent + model.inital_percent != profile.damus_donation else { return } profile.damus_donation = settings.donation_percent let meta = make_metadata_event(keypair: keypair, metadata: profile) + let tsprofile = TimestampedProfile(profile: profile, timestamp: meta.created_at, event: meta) + damus_state.profiles.add(id: damus_state.pubkey, profile: tsprofile) damus_state.postbox.send(meta) } }