damus

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

commit ac82f1bc09b1d421f3290fb3cf5c756ccbab7f03
parent 209f3e87594e873d61f5f0a1f268105f5508052c
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 20 Apr 2023 13:40:37 -0700

Add OnlyZaps Mode

Changelog-Added: Add OnlyZaps mode: disable reactions, only zaps!

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 6+-----
Mdamus/ContentView.swift | 19++++++++++++++++++-
Mdamus/Models/HomeModel.swift | 8++++----
Mdamus/Models/ThreadModel.swift | 2+-
Mdamus/Models/UserSettingsStore.swift | 6+++---
Mdamus/Nostr/Nostr.swift | 5+++++
Mdamus/Nostr/NostrEvent.swift | 2+-
Ddamus/Nostr/NostrMetadata.swift | 25-------------------------
Mdamus/Util/Notifications.swift | 2+-
Mdamus/Views/ActionBar/EventActionBar.swift | 14+++++++++++++-
Mdamus/Views/ActionBar/EventDetailBar.swift | 2+-
Ddamus/Views/EditMetadataView.swift | 246-------------------------------------------------------------------------------
Adamus/Views/Profile/EditMetadataView.swift | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mdamus/Views/Profile/ProfileView.swift | 25+++++++++++++++++++------
Mdamus/Views/SaveKeysView.swift | 4++++
Mdamus/Views/Settings/AppearanceSettingsView.swift | 8--------
Mdamus/Views/Settings/ZapSettingsView.swift | 12++++++++++++
17 files changed, 335 insertions(+), 303 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -143,7 +143,6 @@ 4C8D00CF29E38B950036AF10 /* nostr_bech32.c in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00CE29E38B950036AF10 /* nostr_bech32.c */; }; 4C8D00D429E3C5D40036AF10 /* NIP19Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */; }; 4C8EC52529D1FA6C0085D9A8 /* DamusColors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */; }; - 4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */; }; 4C90BD18283A9EE5008EE7EF /* LoginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD17283A9EE5008EE7EF /* LoginView.swift */; }; 4C90BD1A283AA67F008EE7EF /* Bech32.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD19283AA67F008EE7EF /* Bech32.swift */; }; 4C90BD1C283AC38E008EE7EF /* Bech32Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */; }; @@ -550,7 +549,6 @@ 4C8D00D229E3C19F0036AF10 /* str_block.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = str_block.h; sourceTree = "<group>"; }; 4C8D00D329E3C5D40036AF10 /* NIP19Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NIP19Tests.swift; sourceTree = "<group>"; }; 4C8EC52429D1FA6C0085D9A8 /* DamusColors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DamusColors.swift; sourceTree = "<group>"; }; - 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NostrMetadata.swift; sourceTree = "<group>"; }; 4C90BD17283A9EE5008EE7EF /* LoginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginView.swift; sourceTree = "<group>"; }; 4C90BD19283AA67F008EE7EF /* Bech32.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32.swift; sourceTree = "<group>"; }; 4C90BD1B283AC38E008EE7EF /* Bech32Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bech32Tests.swift; sourceTree = "<group>"; }; @@ -906,7 +904,6 @@ 4C64987B286D03E000EAE2B3 /* DirectMessagesView.swift */, 4C216F31286E388800040376 /* DMChatView.swift */, 4C216F33286F5ACD00040376 /* DMView.swift */, - E990020E2955F837003BBC5A /* EditMetadataView.swift */, 3169CAE4294E699400EE4006 /* Empty Views */, 4C75EFB82804A2740006080F /* EventView.swift */, 4CEE2AF0280B216B00AB5EEF /* EventDetailView.swift */, @@ -961,7 +958,6 @@ 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */, 4C3BEFD32819DE8F00B3DE84 /* NostrKind.swift */, 4C363A8F28247A1D006E126D /* NostrLink.swift */, - 4C90BD152839DB54008EE7EF /* NostrMetadata.swift */, ); path = Nostr; sourceTree = "<group>"; @@ -1051,6 +1047,7 @@ 4CEE2AF6280B2DEA00AB5EEF /* ProfileName.swift */, 4C285C892838B985008A31F1 /* ProfilePictureSelector.swift */, F79C7FAC29D5E9620000F946 /* EditProfilePictureControl.swift */, + E990020E2955F837003BBC5A /* EditMetadataView.swift */, 4CEE2AF2280B25C500AB5EEF /* ProfilePicView.swift */, 4C8682862814DE470026224F /* ProfileView.swift */, 4CB9D4A62992D02B00A9A7E4 /* ProfileNameView.swift */, @@ -1593,7 +1590,6 @@ 4C1A9A2329DDDB8100516EAC /* IconLabel.swift in Sources */, 4C3EA64928FF597700C48A62 /* bech32.c in Sources */, 4CE879522996B68900F758CC /* RelayType.swift in Sources */, - 4C90BD162839DB54008EE7EF /* NostrMetadata.swift in Sources */, 4CE8795B2996C47A00F758CC /* ZapsModel.swift in Sources */, 4C3A1D3729637E0500558C0F /* PreviewCache.swift in Sources */, 4C3EA67528FF7A5A00C48A62 /* take.c in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -498,8 +498,25 @@ struct ContentView: View { open_event(ev: target) } } - .onReceive(handle_notify(.hide_reactions)) { notif in + .onReceive(handle_notify(.onlyzaps_mode)) { notif in + let hide = notif.object as! Bool home.filter_events() + + guard let damus_state else { + return + } + + guard let profile = damus_state.profiles.lookup(id: damus_state.pubkey) else { + return + } + + profile.reactions = !hide + + guard let profile_ev = make_metadata_event(keypair: damus_state.keypair, metadata: profile) else { + return + } + + damus_state.postbox.send(profile_ev) } .alert(NSLocalizedString("Deleted Account", comment: "Alert message to indicate this is a deleted account"), isPresented: $is_deleted_account) { Button(NSLocalizedString("Logout", comment: "Button to close the alert that informs that the current account has been deleted.")) { diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -202,7 +202,7 @@ class HomeModel: ObservableObject { } notifications.filter { ev in - if damus_state.settings.hide_reactions && ev.known_kind == NostrKind.like { + if damus_state.settings.onlyzaps_mode && ev.known_kind == NostrKind.like { return false } @@ -261,7 +261,7 @@ class HomeModel: ObservableObject { return } - if damus_state.settings.hide_reactions { + if damus_state.settings.onlyzaps_mode { return } @@ -389,7 +389,7 @@ class HomeModel: ObservableObject { NostrKind.text.rawValue, NostrKind.boost.rawValue ] - if !damus_state.settings.hide_reactions { + if !damus_state.settings.onlyzaps_mode { home_filter_kinds.append(NostrKind.like.rawValue) } var home_filter = NostrFilter.filter_kinds(home_filter_kinds) @@ -402,7 +402,7 @@ class HomeModel: ObservableObject { NostrKind.boost.rawValue, NostrKind.zap.rawValue, ] - if !damus_state.settings.hide_reactions { + if !damus_state.settings.onlyzaps_mode { notifications_filter_kinds.append(NostrKind.like.rawValue) } var notifications_filter = NostrFilter.filter_kinds(notifications_filter_kinds) diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -89,7 +89,7 @@ class ThreadModel: ObservableObject { meta_events.referenced_ids = [event.id] var kinds = [NostrKind.zap.rawValue, NostrKind.text.rawValue, NostrKind.boost.rawValue] - if !damus_state.settings.hide_reactions { + if !damus_state.settings.onlyzaps_mode { kinds.append(NostrKind.like.rawValue) } meta_events.kinds = kinds diff --git a/damus/Models/UserSettingsStore.swift b/damus/Models/UserSettingsStore.swift @@ -202,9 +202,9 @@ class UserSettingsStore: ObservableObject { } } - @Published var hide_reactions: Bool { + @Published var onlyzaps_mode: Bool { didSet { - UserDefaults.standard.set(hide_reactions, forKey: "hide_reactions") + UserDefaults.standard.set(onlyzaps_mode, forKey: "onlyzaps_mode") } } @@ -302,7 +302,7 @@ class UserSettingsStore: ObservableObject { disable_animation = should_disable_image_animation() auto_translate = UserDefaults.standard.object(forKey: "auto_translate") as? Bool ?? true show_only_preferred_languages = UserDefaults.standard.object(forKey: "show_only_preferred_languages") as? Bool ?? false - hide_reactions = UserDefaults.standard.object(forKey: "hide_reactions") as? Bool ?? false + onlyzaps_mode = UserDefaults.standard.object(forKey: "hide_reactions") as? Bool ?? false // Note from @tyiu: // Default translation service is disabled by default for now until we gain some confidence that it is working well in production. diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift @@ -52,6 +52,11 @@ class Profile: Codable { set_val(key, val) } + var reactions: Bool? { + get { return get_val("reactions"); } + set(s) { set_val("reactions", s) } + } + var deleted: Bool? { get { return get_val("deleted"); } set(s) { set_val("deleted", s) } diff --git a/damus/Nostr/NostrEvent.swift b/damus/Nostr/NostrEvent.swift @@ -560,7 +560,7 @@ func make_first_contact_event(keypair: Keypair) -> NostrEvent? { return ev } -func make_metadata_event(keypair: Keypair, metadata: NostrMetadata) -> NostrEvent? { +func make_metadata_event(keypair: Keypair, metadata: Profile) -> NostrEvent? { guard let privkey = keypair.privkey else { return nil } diff --git a/damus/Nostr/NostrMetadata.swift b/damus/Nostr/NostrMetadata.swift @@ -1,25 +0,0 @@ -// -// NostrMetadata.swift -// damus -// -// Created by William Casarin on 2022-05-21. -// - -import Foundation - - -struct NostrMetadata: Codable { - let display_name: String? - let name: String? - let about: String? - let website: String? - let nip05: String? - let picture: String? - let banner: String? - let lud06: String? - let lud16: String? -} - -func create_account_to_metadata(_ model: CreateAccountModel) -> NostrMetadata { - return NostrMetadata(display_name: model.real_name, name: model.nick_name, about: model.about, website: nil, nip05: nil, picture: model.profile_image, banner: nil, lud06: nil, lud16: nil) -} diff --git a/damus/Util/Notifications.swift b/damus/Util/Notifications.swift @@ -113,7 +113,7 @@ extension Notification.Name { static var local_notification: Notification.Name { return Notification.Name("local_notification") } - static var hide_reactions: Notification.Name { + static var onlyzaps_mode: Notification.Name { return Notification.Name("hide_reactions") } } diff --git a/damus/Views/ActionBar/EventActionBar.swift b/damus/Views/ActionBar/EventActionBar.swift @@ -46,6 +46,18 @@ struct EventActionBar: View { test_lnurl ?? damus_state.profiles.lookup(id: event.pubkey)?.lnurl } + var show_like: Bool { + if settings.onlyzaps_mode { + return false + } + + guard let profile = damus_state.profiles.lookup(id: event.pubkey) else { + return true + } + + return profile.reactions ?? true + } + var body: some View { HStack { if damus_state.keypair.privkey != nil { @@ -75,7 +87,7 @@ struct EventActionBar: View { .foregroundColor(bar.boosted ? Color.green : Color.gray) } - if !settings.hide_reactions { + if show_like { Spacer() HStack(spacing: 4) { diff --git a/damus/Views/ActionBar/EventDetailBar.swift b/damus/Views/ActionBar/EventDetailBar.swift @@ -32,7 +32,7 @@ struct EventDetailBar: View { .buttonStyle(PlainButtonStyle()) } - if bar.likes > 0 && !state.settings.hide_reactions { + if bar.likes > 0 && !state.settings.onlyzaps_mode { NavigationLink(destination: ReactionsView(damus_state: state, model: ReactionsModel(state: state, target: target))) { let noun = Text(verbatim: "\(reactionsCountString(bar.likes))").foregroundColor(.gray) Text("\(Text("\(bar.likes)").font(.body.bold())) \(noun)", comment: "Sentence composed of 2 variables to describe how many reactions there are on a post. In source English, the first variable is the number of reactions, and the second variable is 'Reaction' or 'Reactions'.") diff --git a/damus/Views/EditMetadataView.swift b/damus/Views/EditMetadataView.swift @@ -1,246 +0,0 @@ -// -// EditMetadataView.swift -// damus -// -// Created by Thomas Tastet on 23/12/2022. -// - -import SwiftUI -import Combine - -let PPM_SIZE: CGFloat = 80.0 -let BANNER_HEIGHT: CGFloat = 150.0; - -func isHttpsUrl(_ string: String) -> Bool { - let urlRegEx = "^https://.*$" - let urlTest = NSPredicate(format:"SELF MATCHES %@", urlRegEx) - return urlTest.evaluate(with: string) -} - -func isImage(_ urlString: String) -> Bool { - let imageTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif", "image/tiff", "image/bmp", "image/webp"] - - guard let url = URL(string: urlString) else { - return false - } - - var result = false - let semaphore = DispatchSemaphore(value: 0) - - let task = URLSession.shared.dataTask(with: url) { data, response, error in - if let error = error { - print(error) - semaphore.signal() - return - } - - guard let httpResponse = response as? HTTPURLResponse, - let contentType = httpResponse.allHeaderFields["Content-Type"] as? String else { - semaphore.signal() - return - } - - if imageTypes.contains(contentType.lowercased()) { - result = true - } - - semaphore.signal() - } - - task.resume() - semaphore.wait() - - return result -} - -struct EditMetadataView: View { - let damus_state: DamusState - @State var display_name: String - @State var about: String - @State var picture: String - @State var banner: String - @State var nip05: String - @State var name: String - @State var ln: String - @State var website: String - - @Environment(\.dismiss) var dismiss - @Environment(\.colorScheme) var colorScheme - - @State var confirm_ln_address: Bool = false - @StateObject var profileUploadViewModel = ProfileUploadingViewModel() - - init (damus_state: DamusState) { - self.damus_state = damus_state - let data = damus_state.profiles.lookup(id: damus_state.pubkey) - - _name = State(initialValue: data?.name ?? "") - _display_name = State(initialValue: data?.display_name ?? "") - _about = State(initialValue: data?.about ?? "") - _website = State(initialValue: data?.website ?? "") - _picture = State(initialValue: data?.picture ?? "") - _banner = State(initialValue: data?.banner ?? "") - _nip05 = State(initialValue: data?.nip05 ?? "") - _ln = State(initialValue: data?.lud16 ?? data?.lud06 ?? "") - } - - func imageBorderColor() -> Color { - colorScheme == .light ? DamusColors.white : DamusColors.black - } - - func save() { - let metadata = NostrMetadata( - display_name: display_name, - name: name, - about: about, - website: website, - nip05: nip05.isEmpty ? nil : nip05, - picture: picture.isEmpty ? nil : picture, - banner: banner.isEmpty ? nil : banner, - lud06: ln.contains("@") ? nil : ln, - lud16: ln.contains("@") ? ln : nil - ); - - let m_metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: metadata) - - if let metadata_ev = m_metadata_ev { - damus_state.postbox.send(metadata_ev) - } - } - - func is_ln_valid(ln: String) -> Bool { - return ln.contains("@") || ln.lowercased().starts(with: "lnurl") - } - - var nip05_parts: NIP05? { - return NIP05.parse(nip05) - } - - var TopSection: some View { - ZStack(alignment: .top) { - GeometryReader { geo in - BannerImageView(pubkey: damus_state.pubkey, profiles: damus_state.profiles) - .aspectRatio(contentMode: .fill) - .frame(width: geo.size.width, height: BANNER_HEIGHT) - .clipped() - }.frame(height: BANNER_HEIGHT) - VStack(alignment: .leading) { - let pfp_size: CGFloat = 90.0 - - HStack(alignment: .center) { - ProfilePictureSelector(pubkey: damus_state.pubkey, damus_state: damus_state, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:)) - .offset(y: -(pfp_size/2.0)) // Increase if set a frame - - Spacer() - }.padding(.bottom,-(pfp_size/2.0)) - } - .padding(.horizontal,18) - .padding(.top,BANNER_HEIGHT) - } - } - - var body: some View { - VStack(alignment: .leading) { - TopSection - Form { - Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) { - TextField("Satoshi Nakamoto", text: $display_name) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - } - - Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) { - TextField("satoshi", text: $name) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - - } - - Section (NSLocalizedString("Profile Picture", comment: "Label for Profile Picture section of user profile form.")) { - TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $picture) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - } - - Section (NSLocalizedString("Banner Image", comment: "Label for Banner Image section of user profile form.")) { - TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $banner) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - } - - Section(NSLocalizedString("Website", comment: "Label for Website section of user profile form.")) { - TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - } - - Section(NSLocalizedString("About Me", comment: "Label for About Me section of user profile form.")) { - let placeholder = NSLocalizedString("Absolute Boss", comment: "Placeholder text for About Me description.") - ZStack(alignment: .topLeading) { - TextEditor(text: $about) - .textInputAutocapitalization(.sentences) - .frame(minHeight: 20, alignment: .leading) - .multilineTextAlignment(.leading) - Text(about.isEmpty ? placeholder : about) - .padding(.leading, 4) - .opacity(about.isEmpty ? 1 : 0) - .foregroundColor(Color(uiColor: .placeholderText)) - } - } - - Section(NSLocalizedString("Bitcoin Lightning Tips", comment: "Label for Bitcoin Lightning Tips section of user profile form.")) { - TextField(NSLocalizedString("Lightning Address or LNURL", comment: "Placeholder text for entry of Lightning Address or LNURL."), text: $ln) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - } - - Section(content: { - TextField(NSLocalizedString("jb55@jb55.com", comment: "Placeholder example text for identifier used for NIP-05 verification."), text: $nip05) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - .onReceive(Just(nip05)) { newValue in - self.nip05 = newValue.trimmingCharacters(in: .whitespaces) - } - }, header: { - Text("NIP-05 Verification", comment: "Label for NIP-05 Verification section of user profile form.") - }, footer: { - if let parts = nip05_parts { - Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.") - } else if !nip05.isEmpty { - Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.") - } else { - Text("") // without this, the keyboard dismisses unnecessarily when the footer changes state - } - }) - - Button(NSLocalizedString("Save", comment: "Button for saving profile.")) { - if !ln.isEmpty && !is_ln_valid(ln: ln) { - confirm_ln_address = true - } else { - save() - dismiss() - } - } - .disabled(profileUploadViewModel.isLoading) - .alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) { - Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) { - } - } message: { - Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.") - } - } - } - .ignoresSafeArea(edges: .top) - .background(Color(.systemGroupedBackground)) - } - - func uploadedProfilePicture(image_url: URL?) { - picture = image_url?.absoluteString ?? "" - } -} - -struct EditMetadataView_Previews: PreviewProvider { - static var previews: some View { - EditMetadataView(damus_state: test_damus_state()) - } -} diff --git a/damus/Views/Profile/EditMetadataView.swift b/damus/Views/Profile/EditMetadataView.swift @@ -0,0 +1,252 @@ +// +// EditMetadataView.swift +// damus +// +// Created by Thomas Tastet on 23/12/2022. +// + +import SwiftUI +import Combine + +let PPM_SIZE: CGFloat = 80.0 +let BANNER_HEIGHT: CGFloat = 150.0; + +func isHttpsUrl(_ string: String) -> Bool { + let urlRegEx = "^https://.*$" + let urlTest = NSPredicate(format:"SELF MATCHES %@", urlRegEx) + return urlTest.evaluate(with: string) +} + +func isImage(_ urlString: String) -> Bool { + let imageTypes = ["image/jpg", "image/jpeg", "image/png", "image/gif", "image/tiff", "image/bmp", "image/webp"] + + guard let url = URL(string: urlString) else { + return false + } + + var result = false + let semaphore = DispatchSemaphore(value: 0) + + let task = URLSession.shared.dataTask(with: url) { data, response, error in + if let error = error { + print(error) + semaphore.signal() + return + } + + guard let httpResponse = response as? HTTPURLResponse, + let contentType = httpResponse.allHeaderFields["Content-Type"] as? String else { + semaphore.signal() + return + } + + if imageTypes.contains(contentType.lowercased()) { + result = true + } + + semaphore.signal() + } + + task.resume() + semaphore.wait() + + return result +} + +struct EditMetadataView: View { + let damus_state: DamusState + @State var display_name: String + @State var about: String + @State var picture: String + @State var banner: String + @State var nip05: String + @State var name: String + @State var ln: String + @State var website: String + let profile: Profile? + + @Environment(\.dismiss) var dismiss + @Environment(\.colorScheme) var colorScheme + + @State var confirm_ln_address: Bool = false + @StateObject var profileUploadViewModel = ProfileUploadingViewModel() + + init (damus_state: DamusState) { + self.damus_state = damus_state + let data = damus_state.profiles.lookup(id: damus_state.pubkey) + self.profile = data + + _name = State(initialValue: data?.name ?? "") + _display_name = State(initialValue: data?.display_name ?? "") + _about = State(initialValue: data?.about ?? "") + _website = State(initialValue: data?.website ?? "") + _picture = State(initialValue: data?.picture ?? "") + _banner = State(initialValue: data?.banner ?? "") + _nip05 = State(initialValue: data?.nip05 ?? "") + _ln = State(initialValue: data?.lud16 ?? data?.lud06 ?? "") + } + + func imageBorderColor() -> Color { + colorScheme == .light ? DamusColors.white : DamusColors.black + } + + func to_profile() -> Profile { + let profile = self.profile ?? Profile() + + profile.name = name + profile.display_name = display_name + profile.about = about + profile.website = website + profile.nip05 = nip05.isEmpty ? nil : nip05 + profile.picture = picture.isEmpty ? nil : picture + profile.banner = banner.isEmpty ? nil : banner + profile.lud06 = ln.contains("@") ? nil : ln + profile.lud16 = ln.contains("@") ? ln : nil + + return profile + } + + func save() { + let profile = to_profile() + guard let metadata_ev = make_metadata_event(keypair: damus_state.keypair, metadata: profile) else { + return + } + damus_state.postbox.send(metadata_ev) + } + + func is_ln_valid(ln: String) -> Bool { + return ln.contains("@") || ln.lowercased().starts(with: "lnurl") + } + + var nip05_parts: NIP05? { + return NIP05.parse(nip05) + } + + var TopSection: some View { + ZStack(alignment: .top) { + GeometryReader { geo in + BannerImageView(pubkey: damus_state.pubkey, profiles: damus_state.profiles) + .aspectRatio(contentMode: .fill) + .frame(width: geo.size.width, height: BANNER_HEIGHT) + .clipped() + }.frame(height: BANNER_HEIGHT) + VStack(alignment: .leading) { + let pfp_size: CGFloat = 90.0 + + HStack(alignment: .center) { + ProfilePictureSelector(pubkey: damus_state.pubkey, damus_state: damus_state, viewModel: profileUploadViewModel, callback: uploadedProfilePicture(image_url:)) + .offset(y: -(pfp_size/2.0)) // Increase if set a frame + + Spacer() + }.padding(.bottom,-(pfp_size/2.0)) + } + .padding(.horizontal,18) + .padding(.top,BANNER_HEIGHT) + } + } + + var body: some View { + VStack(alignment: .leading) { + TopSection + Form { + Section(NSLocalizedString("Your Name", comment: "Label for Your Name section of user profile form.")) { + TextField("Satoshi Nakamoto", text: $display_name) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Section(NSLocalizedString("Username", comment: "Label for Username section of user profile form.")) { + TextField("satoshi", text: $name) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + + } + + Section (NSLocalizedString("Profile Picture", comment: "Label for Profile Picture section of user profile form.")) { + TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $picture) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Section (NSLocalizedString("Banner Image", comment: "Label for Banner Image section of user profile form.")) { + TextField(NSLocalizedString("https://example.com/pic.jpg", comment: "Placeholder example text for profile picture URL."), text: $banner) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Section(NSLocalizedString("Website", comment: "Label for Website section of user profile form.")) { + TextField(NSLocalizedString("https://jb55.com", comment: "Placeholder example text for website URL for user profile."), text: $website) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Section(NSLocalizedString("About Me", comment: "Label for About Me section of user profile form.")) { + let placeholder = NSLocalizedString("Absolute Boss", comment: "Placeholder text for About Me description.") + ZStack(alignment: .topLeading) { + TextEditor(text: $about) + .textInputAutocapitalization(.sentences) + .frame(minHeight: 20, alignment: .leading) + .multilineTextAlignment(.leading) + Text(about.isEmpty ? placeholder : about) + .padding(.leading, 4) + .opacity(about.isEmpty ? 1 : 0) + .foregroundColor(Color(uiColor: .placeholderText)) + } + } + + Section(NSLocalizedString("Bitcoin Lightning Tips", comment: "Label for Bitcoin Lightning Tips section of user profile form.")) { + TextField(NSLocalizedString("Lightning Address or LNURL", comment: "Placeholder text for entry of Lightning Address or LNURL."), text: $ln) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + } + + Section(content: { + TextField(NSLocalizedString("jb55@jb55.com", comment: "Placeholder example text for identifier used for NIP-05 verification."), text: $nip05) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + .onReceive(Just(nip05)) { newValue in + self.nip05 = newValue.trimmingCharacters(in: .whitespaces) + } + }, header: { + Text("NIP-05 Verification", comment: "Label for NIP-05 Verification section of user profile form.") + }, footer: { + if let parts = nip05_parts { + Text("'\(parts.username)' at '\(parts.host)' will be used for verification", comment: "Description of how the nip05 identifier would be used for verification.") + } else if !nip05.isEmpty { + Text("'\(nip05)' is an invalid NIP-05 identifier. It should look like an email.", comment: "Description of why the nip05 identifier is invalid.") + } else { + Text("") // without this, the keyboard dismisses unnecessarily when the footer changes state + } + }) + + Button(NSLocalizedString("Save", comment: "Button for saving profile.")) { + if !ln.isEmpty && !is_ln_valid(ln: ln) { + confirm_ln_address = true + } else { + save() + dismiss() + } + } + .disabled(profileUploadViewModel.isLoading) + .alert(NSLocalizedString("Invalid Tip Address", comment: "Title of alerting as invalid tip address."), isPresented: $confirm_ln_address) { + Button(NSLocalizedString("Ok", comment: "Button to dismiss the alert.")) { + } + } message: { + Text("The address should either begin with LNURL or should look like an email address.", comment: "Giving the description of the alert message.") + } + } + } + .ignoresSafeArea(edges: .top) + .background(Color(.systemGroupedBackground)) + } + + func uploadedProfilePicture(image_url: URL?) { + picture = image_url?.absoluteString ?? "" + } +} + +struct EditMetadataView_Previews: PreviewProvider { + static var previews: some View { + EditMetadataView(damus_state: test_damus_state()) + } +} diff --git a/damus/Views/Profile/ProfileView.swift b/damus/Views/Profile/ProfileView.swift @@ -245,20 +245,33 @@ struct ProfileView: View { } func lnButton(lnurl: String, profile: Profile) -> some View { - Button(action: { + let button_img = profile.reactions == false ? "bolt.brakesignal" : "bolt.circle" + return Button(action: { if damus_state.settings.show_wallet_selector { showing_select_wallet = true } else { open_with_wallet(wallet: damus_state.settings.default_wallet.model, invoice: lnurl) } }) { - Image(systemName: "bolt.circle") + Image(systemName: button_img) .profile_button_style(scheme: colorScheme) .contextMenu { - Button { - UIPasteboard.general.string = profile.lnurl ?? "" - } label: { - Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), systemImage: "doc.on.doc") + if profile.reactions == false { + Text("OnlyZaps Enabled") + } + + if let addr = profile.lud16 { + Button { + UIPasteboard.general.string = addr + } label: { + Label(addr, systemImage: "doc.on.doc") + } + } else if let lnurl = profile.lud06 { + Button { + UIPasteboard.general.string = profile.lnurl ?? "" + } label: { + Label(NSLocalizedString("Copy LNURL", comment: "Context menu option for copying a user's Lightning URL."), systemImage: "doc.on.doc") + } } } diff --git a/damus/Views/SaveKeysView.swift b/damus/Views/SaveKeysView.swift @@ -217,3 +217,7 @@ struct SaveKeysView_Previews: PreviewProvider { SaveKeysView(account: model) } } + +func create_account_to_metadata(_ model: CreateAccountModel) -> Profile { + return Profile(name: model.nick_name, display_name: model.real_name, about: model.about, picture: model.profile_image, banner: nil, website: nil, lud06: nil, lud16: nil, nip05: nil) +} diff --git a/damus/Views/Settings/AppearanceSettingsView.swift b/damus/Views/Settings/AppearanceSettingsView.swift @@ -14,14 +14,6 @@ struct AppearanceSettingsView: View { var body: some View { Form { - Section(header: Text(NSLocalizedString("Reactions", comment: "Section header for reaction settings"))) { - Toggle(NSLocalizedString("Hide Reactions", comment: "Setting to hide reactions."), isOn: $settings.hide_reactions) - .toggleStyle(.switch) - .onChange(of: settings.hide_reactions) { newVal in - notify(.hide_reactions, newVal) - } - } - Section(header: Text(NSLocalizedString("Text Truncation", comment: "Section header for damus text truncation user configuration"))) { Toggle(NSLocalizedString("Truncate timeline text", comment: "Setting to truncate text in timeline"), isOn: $settings.truncate_timeline_text) .toggleStyle(.switch) diff --git a/damus/Views/Settings/ZapSettingsView.swift b/damus/Views/Settings/ZapSettingsView.swift @@ -24,6 +24,18 @@ struct ZapSettingsView: View { var body: some View { Form { + Section( + header: Text(NSLocalizedString("OnlyZaps", comment: "Section header for enabling OnlyZaps mode (hide reactions)")), + footer: Text(NSLocalizedString("Hide all 🤙's. Others will not be able to send you 🤙's", comment: "Section footer describing onlyzaps mode")) + + ) { + Toggle(NSLocalizedString("Enable OnlyZaps mode", comment: "Setting toggle to hide reactions."), isOn: $settings.onlyzaps_mode) + .toggleStyle(.switch) + .onChange(of: settings.onlyzaps_mode) { newVal in + notify(.onlyzaps_mode, newVal) + } + } + Section(NSLocalizedString("Wallet", comment: "Title for section in zap settings that controls the Lightning wallet selection.")) { Toggle(NSLocalizedString("Show wallet selector", comment: "Toggle to show or hide selection of wallet."), isOn: $settings.show_wallet_selector).toggleStyle(.switch)