diff --git a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt
index f20cde508..55a6dee8a 100644
--- a/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt
+++ b/apps/android/app/src/main/java/chat/simplex/app/views/usersettings/DeveloperView.kt
@@ -39,7 +39,7 @@ fun DeveloperView(
SettingsPreferenceItem(Icons.Outlined.Code, stringResource(R.string.show_developer_options), developerTools, devTools)
}
SectionTextFooter(
- generalGetString(if (devTools.value) R.string.show_dev_options else R.string.hide_dev_options) +
+ generalGetString(if (devTools.value) R.string.show_dev_options else R.string.hide_dev_options) + " " +
generalGetString(R.string.developer_options)
)
SectionSpacer()
diff --git a/apps/android/app/src/main/res/values/strings.xml b/apps/android/app/src/main/res/values/strings.xml
index aa6d9f453..8137e48ab 100644
--- a/apps/android/app/src/main/res/values/strings.xml
+++ b/apps/android/app/src/main/res/values/strings.xml
@@ -511,8 +511,8 @@
Core version: v%s
Core built at: %s
simplexmq: v%s (%2s)
- Show:\
- Hide:\
+ Show:
+ Hide:
Show developer options
Database IDs and Transport isolation option.
diff --git a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
index 9f3a6684d..ce0fd5cae 100644
--- a/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
+++ b/apps/ios/Shared/Views/UserSettings/DeveloperView.swift
@@ -39,7 +39,7 @@ struct DeveloperView: View {
Toggle("Show developer options", isOn: $developerTools)
}
} footer: {
- (developerTools ? Text("Show: ") : Text("Hide: ")) + Text("Database IDs and Transport isolation option.")
+ (developerTools ? Text("Show:") : Text("Hide:")) + Text(" ") + Text("Database IDs and Transport isolation option.")
}
Section {
diff --git a/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift b/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift
index d01fee92f..509874619 100644
--- a/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift
+++ b/apps/ios/Shared/Views/UserSettings/HiddenProfileView.swift
@@ -33,7 +33,7 @@ struct HiddenProfileView: View {
}
Section {
- PassphraseField(key: $hidePassword, placeholder: "Password to show", valid: true, showStrength: true)
+ PassphraseField(key: $hidePassword, placeholder: "Password to show", valid: passwordValid, showStrength: true)
PassphraseField(key: $confirmHidePassword, placeholder: "Confirm password", valid: confirmValid)
settingsRow("lock") {
@@ -72,9 +72,11 @@ struct HiddenProfileView: View {
}
}
+ var passwordValid: Bool { hidePassword == hidePassword.trimmingCharacters(in: .whitespaces) }
+
var confirmValid: Bool { confirmHidePassword == "" || hidePassword == confirmHidePassword }
- var saveDisabled: Bool { hidePassword == "" || confirmHidePassword == "" || !confirmValid }
+ var saveDisabled: Bool { hidePassword == "" || !passwordValid || confirmHidePassword == "" || !confirmValid }
}
struct ProfilePrivacyView_Previews: PreviewProvider {
diff --git a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
index 01de1a8b3..ef41b9919 100644
--- a/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
+++ b/apps/ios/Shared/Views/UserSettings/UserProfilesView.swift
@@ -13,15 +13,17 @@ struct UserProfilesView: View {
@AppStorage(DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE) private var showHiddenProfilesNotice = true
@AppStorage(DEFAULT_SHOW_MUTE_PROFILE_ALERT) private var showMuteProfileAlert = true
@State private var showDeleteConfirmation = false
- @State private var userToDelete: UserInfo?
+ @State private var userToDelete: User?
@State private var alert: UserProfilesAlert?
@State private var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
@State private var searchTextOrPassword = ""
@State private var selectedUser: User?
@State private var profileHidden = false
+ @State private var profileAction: UserProfileAction?
+ @State private var actionPassword = ""
private enum UserProfilesAlert: Identifiable {
- case deleteUser(userInfo: UserInfo, delSMPQueues: Bool)
+ case deleteUser(user: User, delSMPQueues: Bool)
case cantDeleteLastUser
case hiddenProfilesNotice
case muteProfileAlert
@@ -30,7 +32,7 @@ struct UserProfilesView: View {
var id: String {
switch self {
- case let .deleteUser(userInfo, delSMPQueues): return "deleteUser \(userInfo.user.userId) \(delSMPQueues)"
+ case let .deleteUser(user, delSMPQueues): return "deleteUser \(user.userId) \(delSMPQueues)"
case .cantDeleteLastUser: return "cantDeleteLastUser"
case .hiddenProfilesNotice: return "hiddenProfilesNotice"
case .muteProfileAlert: return "muteProfileAlert"
@@ -40,6 +42,16 @@ struct UserProfilesView: View {
}
}
+ private enum UserProfileAction: Identifiable {
+ case deleteUser(user: User, delSMPQueues: Bool)
+
+ var id: String {
+ switch self {
+ case let .deleteUser(user, delSMPQueues): return "deleteUser \(user.userId) \(delSMPQueues)"
+ }
+ }
+ }
+
var body: some View {
if authorized {
userProfilesView()
@@ -69,7 +81,7 @@ struct UserProfilesView: View {
if let i = indexSet.first {
if m.users.count > 1 && (m.users[i].user.hidden || visibleUsersCount > 1) {
showDeleteConfirmation = true
- userToDelete = users[i]
+ userToDelete = users[i].user
} else {
alert = .cantDeleteLastUser
}
@@ -114,14 +126,17 @@ struct UserProfilesView: View {
withAnimation { profileHidden = false }
}
}
+ .sheet(item: $profileAction) { action in
+ profileActionView(action)
+ }
.alert(item: $alert) { alert in
switch alert {
- case let .deleteUser(userInfo, delSMPQueues):
+ case let .deleteUser(user, delSMPQueues):
return Alert(
title: Text("Delete user profile?"),
message: Text("All chats and messages will be deleted - this cannot be undone!"),
primaryButton: .destructive(Text("Delete")) {
- Task { await removeUser(userInfo, delSMPQueues) }
+ Task { await removeUser(user, delSMPQueues, viewPwd: userViewPassword(user)) }
},
secondaryButton: .cancel()
)
@@ -179,36 +194,81 @@ struct UserProfilesView: View {
m.users.filter({ u in !u.user.hidden }).count
}
- private func userViewPassword(_ user: User) -> String? {
- user.activeUser || !user.hidden ? nil : searchTextOrPassword
+ private func correctPassword(_ user: User, _ pwd: String) -> Bool {
+ if let ph = user.viewPwdHash {
+ return pwd != "" && chatPasswordHash(pwd, ph.salt) == ph.hash
+ }
+ return false
}
- private func deleteModeButton(_ title: LocalizedStringKey, _ delSMPQueues: Bool) -> some View {
- Button(title, role: .destructive) {
- if let userInfo = userToDelete {
- alert = .deleteUser(userInfo: userInfo, delSMPQueues: delSMPQueues)
+ private func userViewPassword(_ user: User) -> String? {
+ !user.hidden ? nil : searchTextOrPassword
+ }
+
+ @ViewBuilder private func profileActionView(_ action: UserProfileAction) -> some View {
+ let passwordValid = actionPassword == actionPassword.trimmingCharacters(in: .whitespaces)
+ switch action {
+ case let .deleteUser(user, delSMPQueues):
+ let actionEnabled = actionPassword != "" && passwordValid && correctPassword(user, actionPassword)
+ List {
+ Text("Delete user")
+ .font(.title)
+ .bold()
+ .listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
+ .listRowBackground(Color.clear)
+
+ Section() {
+ ProfilePreview(profileOf: user).padding(.leading, -8)
+ }
+
+ Section {
+ PassphraseField(key: $actionPassword, placeholder: "Profile password", valid: passwordValid)
+ settingsRow("trash") {
+ Button("Delete user", role: .destructive) {
+ profileAction = nil
+ Task { await removeUser(user, delSMPQueues, viewPwd: actionPassword) }
+ }
+ .disabled(!actionEnabled)
+ }
+ } footer: {
+ if actionEnabled {
+ Text("All chats and messages will be deleted - this cannot be undone!")
+ .font(.callout)
+ }
+ }
}
}
}
- private func removeUser(_ userInfo: UserInfo, _ delSMPQueues: Bool) async {
+ private func deleteModeButton(_ title: LocalizedStringKey, _ delSMPQueues: Bool) -> some View {
+ Button(title, role: .destructive) {
+ if let user = userToDelete {
+ if user.hidden && user.activeUser && !correctPassword(user, searchTextOrPassword) {
+ profileAction = .deleteUser(user: user, delSMPQueues: delSMPQueues)
+ } else {
+ alert = .deleteUser(user: user, delSMPQueues: delSMPQueues)
+ }
+ }
+ }
+ }
+
+ private func removeUser(_ user: User, _ delSMPQueues: Bool, viewPwd: String?) async {
do {
- let u = userInfo.user
- if u.activeUser {
+ if user.activeUser {
if let newActive = m.users.first(where: { u in !u.user.activeUser && !u.user.hidden }) {
try await changeActiveUserAsync_(newActive.user.userId, viewPwd: nil)
- try await deleteUser(u)
+ try await deleteUser()
}
} else {
- try await deleteUser(u)
+ try await deleteUser()
}
} catch let error {
let a = getErrorAlert(error, "Error deleting user profile")
alert = .error(title: a.title, error: a.message)
}
- func deleteUser(_ user: User) async throws {
- try await apiDeleteUser(user.userId, delSMPQueues, viewPwd: userViewPassword(user))
+ func deleteUser() async throws {
+ try await apiDeleteUser(user.userId, delSMPQueues, viewPwd: viewPwd)
await MainActor.run { withAnimation { m.removeUser(user) } }
}
}