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) } } } }