damus

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

commit e1578c0337c22e38cb70859af8f337c42005286e
parent 9fa11118d3838e3a279442cf05e0e38693d3e3ab
Author: William Casarin <jb55@jb55.com>
Date:   Mon, 30 Jan 2023 13:26:04 -0800

Add support for account deletion

As per apple guidelines

Changelog-Added: Added support for account deletion

Diffstat:
Mdamus.xcodeproj/project.pbxproj | 4++++
Mdamus/ContentView.swift | 12+++++++++++-
Mdamus/Models/FollowersModel.swift | 2+-
Mdamus/Models/FollowingModel.swift | 2+-
Mdamus/Models/HomeModel.swift | 11+++++++++--
Mdamus/Models/ProfileModel.swift | 2+-
Mdamus/Models/SearchHomeModel.swift | 2+-
Mdamus/Models/ThreadModel.swift | 2+-
Mdamus/Nostr/Nostr.swift | 23++++++++++++++++++++---
Adamus/Util/AccountDeletion.swift | 22++++++++++++++++++++++
Mdamus/Util/Notifications.swift | 3+++
Mdamus/Views/ConfigView.swift | 29++++++++++++++++++++++++++++-
12 files changed, 102 insertions(+), 12 deletions(-)

diff --git a/damus.xcodeproj/project.pbxproj b/damus.xcodeproj/project.pbxproj @@ -116,6 +116,7 @@ 4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; }; 4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; }; 4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */; }; + 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */; }; 4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; }; 4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; }; 4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */; }; @@ -369,6 +370,7 @@ 4C987B56283FD07F0042CE38 /* FollowersModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FollowersModel.swift; sourceTree = "<group>"; }; 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatroomMetadata.swift; sourceTree = "<group>"; }; 4CA2EF9F280E37AC0044ACD8 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = "<group>"; }; + 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDeletion.swift; sourceTree = "<group>"; }; 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplyView.swift; sourceTree = "<group>"; }; 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profiles.swift; sourceTree = "<group>"; }; 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = "<group>"; }; @@ -691,6 +693,7 @@ 4CF0ABD72981980C00D66079 /* Lists.swift */, 4CF0ABEF29857E9200D66079 /* Bech32Object.swift */, 7C60CAEE298471A1009C80D6 /* CoreSVG.swift */, + 4CAAD8AC298851D000060CEA /* AccountDeletion.swift */, ); path = Util; sourceTree = "<group>"; @@ -1109,6 +1112,7 @@ 9609F058296E220800069BF3 /* BannerImageView.swift in Sources */, 4C363A94282704FA006E126D /* Post.swift in Sources */, 4C216F32286E388800040376 /* DMChatView.swift in Sources */, + 4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */, 4C3EA67928FF7ABF00C48A62 /* list.c in Sources */, 4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */, 4C363A8828236948006E126D /* BlocksView.swift in Sources */, diff --git a/damus/ContentView.swift b/damus/ContentView.swift @@ -73,6 +73,7 @@ struct ContentView: View { @State var damus_state: DamusState? = nil @State var selected_timeline: Timeline? = .home @State var is_thread_open: Bool = false + @State var is_deleted_account: Bool = false @State var is_profile_open: Bool = false @State var event: NostrEvent? = nil @State var active_profile: String? = nil @@ -348,6 +349,9 @@ struct ContentView: View { } .onReceive(handle_notify(.like)) { like in } + .onReceive(handle_notify(.deleted_account)) { notif in + self.is_deleted_account = true + } .onReceive(handle_notify(.report)) { notif in let target = notif.object as! ReportTarget self.active_sheet = .report(target) @@ -434,7 +438,13 @@ struct ContentView: View { .onReceive(handle_notify(.new_mutes)) { notif in home.filter_muted() } - .alert(NSLocalizedString("User blocked", comment: "Alert message to indicate "), isPresented: $user_blocked_confirm, actions: { + .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.")) { + is_deleted_account = false + notify(.logout, ()) + } + } + .alert(NSLocalizedString("User blocked", comment: "Alert message to indicate the user has been blocked"), isPresented: $user_blocked_confirm, actions: { Button(NSLocalizedString("Thanks!", comment: "Button to close out of alert that informs that the action to block a user was successful.")) { user_blocked_confirm = false } diff --git a/damus/Models/FollowersModel.swift b/damus/Models/FollowersModel.swift @@ -86,7 +86,7 @@ class FollowersModel: ObservableObject { if ev.known_kind == .contacts { handle_contact_event(ev) } else if ev.known_kind == .metadata { - process_metadata_event(profiles: damus_state.profiles, ev: ev) + process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } case .notice(let msg): diff --git a/damus/Models/FollowingModel.swift b/damus/Models/FollowingModel.swift @@ -60,7 +60,7 @@ class FollowingModel { switch nev { case .event(_, let ev): if ev.kind == 0 { - process_metadata_event(profiles: damus_state.profiles, ev: ev) + process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } case .notice(let msg): print("followingmodel notice: \(msg)") diff --git a/damus/Models/HomeModel.swift b/damus/Models/HomeModel.swift @@ -372,7 +372,7 @@ class HomeModel: ObservableObject { } func handle_metadata_event(_ ev: NostrEvent) { - process_metadata_event(profiles: damus_state.profiles, ev: ev) + process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? { @@ -530,10 +530,17 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) { print("-----") } -func process_metadata_event(profiles: Profiles, ev: NostrEvent) { +func process_metadata_event(our_pubkey: String, profiles: Profiles, ev: NostrEvent) { guard let profile: Profile = decode_data(Data(ev.content.utf8)) else { return } + + if our_pubkey == ev.pubkey && (profile.deleted ?? false) { + DispatchQueue.main.async { + notify(.deleted_account, ()) + } + return + } var old_nip05: String? = nil if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) { diff --git a/damus/Models/ProfileModel.swift b/damus/Models/ProfileModel.swift @@ -97,7 +97,7 @@ class ProfileModel: ObservableObject, Equatable { } else if ev.known_kind == .contacts { handle_profile_contact_event(ev) } else if ev.known_kind == .metadata { - process_metadata_event(profiles: damus.profiles, ev: ev) + process_metadata_event(our_pubkey: damus.pubkey, profiles: damus.profiles, ev: ev) } seen_event.insert(ev.id) } diff --git a/damus/Models/SearchHomeModel.swift b/damus/Models/SearchHomeModel.swift @@ -129,7 +129,7 @@ func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent } if ev.known_kind == .metadata { - process_metadata_event(profiles: damus_state.profiles, ev: ev) + process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } } diff --git a/damus/Models/ThreadModel.swift b/damus/Models/ThreadModel.swift @@ -190,7 +190,7 @@ class ThreadModel: ObservableObject { } if ev.known_kind == .metadata { - process_metadata_event(profiles: damus_state.profiles, ev: ev) + process_metadata_event(our_pubkey: damus_state.pubkey, profiles: damus_state.profiles, ev: ev) } else if ev.is_textlike { self.add_event(ev, privkey: self.damus_state.keypair.privkey) } else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create { diff --git a/damus/Nostr/Nostr.swift b/damus/Nostr/Nostr.swift @@ -24,18 +24,22 @@ struct Profile: Codable { } private func str(_ str: String) -> String? { - guard let val = self.value[str] else{ + return get_val(str) + } + + private func get_val<T>(_ v: String) -> T? { + guard let val = self.value[v] else{ return nil } - guard let s = val.value as? String else { + guard let s = val.value as? T else { return nil } return s } - private mutating func set_str(_ key: String, _ val: String?) { + private mutating func set_val<T>(_ key: String, _ val: T?) { if val == nil { self.value.removeValue(forKey: key) return @@ -44,6 +48,15 @@ struct Profile: Codable { self.value[key] = AnyCodable.init(val) } + private mutating func set_str(_ key: String, _ val: String?) { + set_val(key, val) + } + + var deleted: Bool? { + get { return get_val("deleted"); } + set(s) { set_val("deleted", s) } + } + var display_name: String? { get { return str("display_name"); } set(s) { set_str("display_name", s) } @@ -109,6 +122,10 @@ struct Profile: Codable { return make_ln_url(self.lnurl) } + init() { + self.value = [:] + } + init(from decoder: Decoder) throws { let container = try decoder.singleValueContainer() self.value = try container.decode([String: AnyCodable].self) diff --git a/damus/Util/AccountDeletion.swift b/damus/Util/AccountDeletion.swift @@ -0,0 +1,22 @@ +// +// AccountDeletion.swift +// damus +// +// Created by William Casarin on 2023-01-30. +// + +import Foundation + + +func created_deleted_account_profile(keypair: FullKeypair) -> NostrEvent { + var profile = Profile() + profile.deleted = true + profile.about = "account deleted" + profile.name = "nobody" + + let content = encode_json(profile)! + let ev = NostrEvent(content: content, pubkey: keypair.pubkey, kind: 0) + ev.id = calculate_event_id(ev: ev) + ev.sig = sign_event(privkey: keypair.privkey, ev: ev) + return ev +} diff --git a/damus/Util/Notifications.swift b/damus/Util/Notifications.swift @@ -95,6 +95,9 @@ extension Notification.Name { static var new_unmutes: Notification.Name { return Notification.Name("new_unmutes") } + static var deleted_account: Notification.Name { + return Notification.Name("deleted_account") + } } func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher { diff --git a/damus/Views/ConfigView.swift b/damus/Views/ConfigView.swift @@ -13,12 +13,14 @@ struct ConfigView: View { @Environment(\.dismiss) var dismiss @State var show_add_relay: Bool = false @State var confirm_logout: Bool = false + @State var confirm_delete_account: Bool = false @State var new_relay: String = "" @State var show_privkey: Bool = false @State var privkey: String @State var privkey_copied: Bool = false @State var pubkey_copied: Bool = false @State var relays: [RelayDescriptor] + @State var delete_text: String = "" @EnvironmentObject var user_settings: UserSettingsStore let generator = UIImpactFeedbackGenerator(style: .light) @@ -128,16 +130,41 @@ struct ConfigView: View { KingfisherManager.shared.cache.cleanExpiredDiskCache() } } - + Section(NSLocalizedString("Reset", comment: "Section title for resetting the user")) { Button(NSLocalizedString("Logout", comment: "Button to logout the user.")) { confirm_logout = true } + + if state.is_privkey_user { + Button(NSLocalizedString("Delete Account", comment: "Button to delete the user's account."), role: .destructive) { + confirm_delete_account = true + } + } } } } .navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view.")) .navigationBarTitleDisplayMode(.large) + .alert(NSLocalizedString("Delete Account", comment: "Alert for deleting the users account."), isPresented: $confirm_delete_account) { + TextField("Type DELETE to delete", text: $delete_text) + Button(NSLocalizedString("Cancel", comment: "Cancel deleting the user."), role: .cancel) { + confirm_delete_account = false + } + Button(NSLocalizedString("Delete", comment: "Button for deleting the users account."), role: .destructive) { + guard let full_kp = state.keypair.to_full() else { + return + } + + guard delete_text == "DELETE" else { + return + } + + let ev = created_deleted_account_profile(keypair: full_kp) + state.pool.send(.event(ev)) + notify(.logout, ()) + } + } .alert(NSLocalizedString("Logout", comment: "Alert for logging out the user."), isPresented: $confirm_logout) { Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) { confirm_logout = false