Add support for account deletion
As per apple guidelines Changelog-Added: Added support for account deletion
This commit is contained in:
@@ -116,6 +116,7 @@
|
|||||||
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
|
4C987B57283FD07F0042CE38 /* FollowersModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C987B56283FD07F0042CE38 /* FollowersModel.swift */; };
|
||||||
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
|
4C99737B28C92A9200E53835 /* ChatroomMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4C99737A28C92A9200E53835 /* ChatroomMetadata.swift */; };
|
||||||
4CA2EFA0280E37AC0044ACD8 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA2EF9F280E37AC0044ACD8 /* TimelineView.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 */; };
|
4CACA9D5280C31E100D9BBE8 /* ReplyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9D4280C31E100D9BBE8 /* ReplyView.swift */; };
|
||||||
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
|
4CACA9DC280C38C000D9BBE8 /* Profiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CACA9DB280C38C000D9BBE8 /* Profiles.swift */; };
|
||||||
4CB55EF3295E5D59007FD187 /* RecommendedRelayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB55EF2295E5D59007FD187 /* RecommendedRelayView.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
4CB55EF2295E5D59007FD187 /* RecommendedRelayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecommendedRelayView.swift; sourceTree = "<group>"; };
|
||||||
@@ -691,6 +693,7 @@
|
|||||||
4CF0ABD72981980C00D66079 /* Lists.swift */,
|
4CF0ABD72981980C00D66079 /* Lists.swift */,
|
||||||
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
|
4CF0ABEF29857E9200D66079 /* Bech32Object.swift */,
|
||||||
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
|
7C60CAEE298471A1009C80D6 /* CoreSVG.swift */,
|
||||||
|
4CAAD8AC298851D000060CEA /* AccountDeletion.swift */,
|
||||||
);
|
);
|
||||||
path = Util;
|
path = Util;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -1109,6 +1112,7 @@
|
|||||||
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
|
9609F058296E220800069BF3 /* BannerImageView.swift in Sources */,
|
||||||
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
4C363A94282704FA006E126D /* Post.swift in Sources */,
|
||||||
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
4C216F32286E388800040376 /* DMChatView.swift in Sources */,
|
||||||
|
4CAAD8AD298851D000060CEA /* AccountDeletion.swift in Sources */,
|
||||||
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
4C3EA67928FF7ABF00C48A62 /* list.c in Sources */,
|
||||||
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
4C64987E286D082C00EAE2B3 /* DirectMessagesModel.swift in Sources */,
|
||||||
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
4C363A8828236948006E126D /* BlocksView.swift in Sources */,
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ struct ContentView: View {
|
|||||||
@State var damus_state: DamusState? = nil
|
@State var damus_state: DamusState? = nil
|
||||||
@State var selected_timeline: Timeline? = .home
|
@State var selected_timeline: Timeline? = .home
|
||||||
@State var is_thread_open: Bool = false
|
@State var is_thread_open: Bool = false
|
||||||
|
@State var is_deleted_account: Bool = false
|
||||||
@State var is_profile_open: Bool = false
|
@State var is_profile_open: Bool = false
|
||||||
@State var event: NostrEvent? = nil
|
@State var event: NostrEvent? = nil
|
||||||
@State var active_profile: String? = nil
|
@State var active_profile: String? = nil
|
||||||
@@ -348,6 +349,9 @@ struct ContentView: View {
|
|||||||
}
|
}
|
||||||
.onReceive(handle_notify(.like)) { like in
|
.onReceive(handle_notify(.like)) { like in
|
||||||
}
|
}
|
||||||
|
.onReceive(handle_notify(.deleted_account)) { notif in
|
||||||
|
self.is_deleted_account = true
|
||||||
|
}
|
||||||
.onReceive(handle_notify(.report)) { notif in
|
.onReceive(handle_notify(.report)) { notif in
|
||||||
let target = notif.object as! ReportTarget
|
let target = notif.object as! ReportTarget
|
||||||
self.active_sheet = .report(target)
|
self.active_sheet = .report(target)
|
||||||
@@ -434,7 +438,13 @@ struct ContentView: View {
|
|||||||
.onReceive(handle_notify(.new_mutes)) { notif in
|
.onReceive(handle_notify(.new_mutes)) { notif in
|
||||||
home.filter_muted()
|
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.")) {
|
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
|
user_blocked_confirm = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ class FollowersModel: ObservableObject {
|
|||||||
if ev.known_kind == .contacts {
|
if ev.known_kind == .contacts {
|
||||||
handle_contact_event(ev)
|
handle_contact_event(ev)
|
||||||
} else if ev.known_kind == .metadata {
|
} 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):
|
case .notice(let msg):
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class FollowingModel {
|
|||||||
switch nev {
|
switch nev {
|
||||||
case .event(_, let ev):
|
case .event(_, let ev):
|
||||||
if ev.kind == 0 {
|
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):
|
case .notice(let msg):
|
||||||
print("followingmodel notice: \(msg)")
|
print("followingmodel notice: \(msg)")
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ class HomeModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func handle_metadata_event(_ ev: NostrEvent) {
|
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? {
|
func get_last_event_of_kind(relay_id: String, kind: Int) -> NostrEvent? {
|
||||||
@@ -530,11 +530,18 @@ func print_filters(relay_id: String?, filters groups: [[NostrFilter]]) {
|
|||||||
print("-----")
|
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 {
|
guard let profile: Profile = decode_data(Data(ev.content.utf8)) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if our_pubkey == ev.pubkey && (profile.deleted ?? false) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
notify(.deleted_account, ())
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
var old_nip05: String? = nil
|
var old_nip05: String? = nil
|
||||||
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
if let mprof = profiles.lookup_with_timestamp(id: ev.pubkey) {
|
||||||
old_nip05 = mprof.profile.nip05
|
old_nip05 = mprof.profile.nip05
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ class ProfileModel: ObservableObject, Equatable {
|
|||||||
} else if ev.known_kind == .contacts {
|
} else if ev.known_kind == .contacts {
|
||||||
handle_profile_contact_event(ev)
|
handle_profile_contact_event(ev)
|
||||||
} else if ev.known_kind == .metadata {
|
} 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)
|
seen_event.insert(ev.id)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func load_profiles(profiles_subid: String, relay_id: String, events: [NostrEvent
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ev.known_kind == .metadata {
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ class ThreadModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ev.known_kind == .metadata {
|
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 {
|
} else if ev.is_textlike {
|
||||||
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
|
self.add_event(ev, privkey: self.damus_state.keypair.privkey)
|
||||||
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {
|
} else if ev.known_kind == .channel_meta || ev.known_kind == .channel_create {
|
||||||
|
|||||||
@@ -24,18 +24,22 @@ struct Profile: Codable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func str(_ str: String) -> String? {
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let s = val.value as? String else {
|
guard let s = val.value as? T else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
private mutating func set_str(_ key: String, _ val: String?) {
|
private mutating func set_val<T>(_ key: String, _ val: T?) {
|
||||||
if val == nil {
|
if val == nil {
|
||||||
self.value.removeValue(forKey: key)
|
self.value.removeValue(forKey: key)
|
||||||
return
|
return
|
||||||
@@ -44,6 +48,15 @@ struct Profile: Codable {
|
|||||||
self.value[key] = AnyCodable.init(val)
|
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? {
|
var display_name: String? {
|
||||||
get { return str("display_name"); }
|
get { return str("display_name"); }
|
||||||
set(s) { set_str("display_name", s) }
|
set(s) { set_str("display_name", s) }
|
||||||
@@ -109,6 +122,10 @@ struct Profile: Codable {
|
|||||||
return make_ln_url(self.lnurl)
|
return make_ln_url(self.lnurl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
self.value = [:]
|
||||||
|
}
|
||||||
|
|
||||||
init(from decoder: Decoder) throws {
|
init(from decoder: Decoder) throws {
|
||||||
let container = try decoder.singleValueContainer()
|
let container = try decoder.singleValueContainer()
|
||||||
self.value = try container.decode([String: AnyCodable].self)
|
self.value = try container.decode([String: AnyCodable].self)
|
||||||
|
|||||||
22
damus/Util/AccountDeletion.swift
Normal file
22
damus/Util/AccountDeletion.swift
Normal file
@@ -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
|
||||||
|
}
|
||||||
@@ -95,6 +95,9 @@ extension Notification.Name {
|
|||||||
static var new_unmutes: Notification.Name {
|
static var new_unmutes: Notification.Name {
|
||||||
return Notification.Name("new_unmutes")
|
return Notification.Name("new_unmutes")
|
||||||
}
|
}
|
||||||
|
static var deleted_account: Notification.Name {
|
||||||
|
return Notification.Name("deleted_account")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
func handle_notify(_ name: Notification.Name) -> NotificationCenter.Publisher {
|
||||||
|
|||||||
@@ -13,12 +13,14 @@ struct ConfigView: View {
|
|||||||
@Environment(\.dismiss) var dismiss
|
@Environment(\.dismiss) var dismiss
|
||||||
@State var show_add_relay: Bool = false
|
@State var show_add_relay: Bool = false
|
||||||
@State var confirm_logout: Bool = false
|
@State var confirm_logout: Bool = false
|
||||||
|
@State var confirm_delete_account: Bool = false
|
||||||
@State var new_relay: String = ""
|
@State var new_relay: String = ""
|
||||||
@State var show_privkey: Bool = false
|
@State var show_privkey: Bool = false
|
||||||
@State var privkey: String
|
@State var privkey: String
|
||||||
@State var privkey_copied: Bool = false
|
@State var privkey_copied: Bool = false
|
||||||
@State var pubkey_copied: Bool = false
|
@State var pubkey_copied: Bool = false
|
||||||
@State var relays: [RelayDescriptor]
|
@State var relays: [RelayDescriptor]
|
||||||
|
@State var delete_text: String = ""
|
||||||
@EnvironmentObject var user_settings: UserSettingsStore
|
@EnvironmentObject var user_settings: UserSettingsStore
|
||||||
|
|
||||||
let generator = UIImpactFeedbackGenerator(style: .light)
|
let generator = UIImpactFeedbackGenerator(style: .light)
|
||||||
@@ -133,11 +135,36 @@ struct ConfigView: View {
|
|||||||
Button(NSLocalizedString("Logout", comment: "Button to logout the user.")) {
|
Button(NSLocalizedString("Logout", comment: "Button to logout the user.")) {
|
||||||
confirm_logout = true
|
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."))
|
.navigationTitle(NSLocalizedString("Settings", comment: "Navigation title for Settings view."))
|
||||||
.navigationBarTitleDisplayMode(.large)
|
.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) {
|
.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) {
|
Button(NSLocalizedString("Cancel", comment: "Cancel out of logging out the user."), role: .cancel) {
|
||||||
confirm_logout = false
|
confirm_logout = false
|
||||||
|
|||||||
Reference in New Issue
Block a user