diff --git a/apps/ios/Shared/ContentView.swift b/apps/ios/Shared/ContentView.swift index 910922d704..8edc66df43 100644 --- a/apps/ios/Shared/ContentView.swift +++ b/apps/ios/Shared/ContentView.swift @@ -192,7 +192,7 @@ struct ContentView: View { private func justAuthenticate() { userAuthorized = false let laMode = privacyLocalAuthModeDefault.get() - authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason")) { laResult in + authenticate(reason: NSLocalizedString("Unlock app", comment: "authentication reason"), selfDestruct: true) { laResult in logger.debug("authenticate callback: \(String(describing: laResult))") switch (laResult) { case .success: diff --git a/apps/ios/Shared/Model/NtfManager.swift b/apps/ios/Shared/Model/NtfManager.swift index 4a4511eae6..ad41703c91 100644 --- a/apps/ios/Shared/Model/NtfManager.swift +++ b/apps/ios/Shared/Model/NtfManager.swift @@ -42,7 +42,7 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { changeActiveUser(userId, viewPwd: nil) } if content.categoryIdentifier == ntfCategoryContactRequest && action == ntfActionAcceptContact, - let chatId = content.userInfo["chatId"] as? String { + let chatId = content.userInfo["chatId"] as? String { if case let .contactRequest(contactRequest) = chatModel.getChat(chatId)?.chatInfo { Task { await acceptContactRequest(contactRequest) } } else { @@ -107,8 +107,8 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { // in another chat return recent ? [.banner, .list] : [.sound, .banner, .list] } - // this notification is deliverd from the notifications server - // when the app is in foreground it does not need to be shown + // this notification is deliverd from the notifications server + // when the app is in foreground it does not need to be shown case ntfCategoryCheckMessage: return [] case ntfCategoryCallInvitation: return [] default: return [.sound, .banner, .list] @@ -247,8 +247,12 @@ class NtfManager: NSObject, UNUserNotificationCenterDelegate, ObservableObject { } } - func removeNotifications(_ ids : [String]){ - UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: ids) - UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: ids) + func removeAllNotifications() async { + let nc = UNUserNotificationCenter.current() + let settings = await nc.notificationSettings() + if settings.authorizationStatus == .authorized { + nc.removeAllPendingNotificationRequests() + nc.removeAllDeliveredNotifications() + } } } diff --git a/apps/ios/Shared/Model/SimpleXAPI.swift b/apps/ios/Shared/Model/SimpleXAPI.swift index 55109fd166..f616a9929c 100644 --- a/apps/ios/Shared/Model/SimpleXAPI.swift +++ b/apps/ios/Shared/Model/SimpleXAPI.swift @@ -125,7 +125,7 @@ func apiGetActiveUser() throws -> User? { } } -func apiCreateActiveUser(_ p: Profile) throws -> User { +func apiCreateActiveUser(_ p: Profile?) throws -> User { let r = chatSendCmdSync(.createActiveUser(profile: p)) if case let .activeUser(user) = r { return user } throw r diff --git a/apps/ios/Shared/Views/Database/DatabaseView.swift b/apps/ios/Shared/Views/Database/DatabaseView.swift index cf926e61a7..15efd5955d 100644 --- a/apps/ios/Shared/Views/Database/DatabaseView.swift +++ b/apps/ios/Shared/Views/Database/DatabaseView.swift @@ -317,10 +317,7 @@ struct DatabaseView: View { private func stopChat() { Task { do { - try await apiStopChat() - ChatReceiver.shared.stop() - await MainActor.run { m.chatRunning = false } - appStateGroupDefault.set(.stopped) + try await stopChatAsync() } catch let error { await MainActor.run { runChat = true @@ -374,9 +371,7 @@ struct DatabaseView: View { progressIndicator = true Task { do { - try await apiDeleteStorage() - _ = kcDatabasePassword.remove() - storeDBPassphraseGroupDefault.set(true) + try await deleteChatAsync() await operationEnded(.chatDeleted) appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory()) } catch let error { @@ -468,6 +463,19 @@ struct DatabaseView: View { } } +func stopChatAsync() async throws { + try await apiStopChat() + ChatReceiver.shared.stop() + await MainActor.run { ChatModel.shared.chatRunning = false } + appStateGroupDefault.set(.stopped) +} + +func deleteChatAsync() async throws { + try await apiDeleteStorage() + _ = kcDatabasePassword.remove() + storeDBPassphraseGroupDefault.set(true) +} + struct DatabaseView_Previews: PreviewProvider { static var previews: some View { DatabaseView(showSettings: Binding.constant(false), chatItemTTL: .none) diff --git a/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift b/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift index e06ae61acf..d9b1bfed34 100644 --- a/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift +++ b/apps/ios/Shared/Views/Helpers/LocalAuthenticationUtils.swift @@ -30,19 +30,26 @@ struct LocalAuthRequest { var title: LocalizedStringKey? // if title is null, reason is shown var reason: String var password: String + var selfDestruct: Bool var completed: (LAResult) -> Void - static var sample = LocalAuthRequest(title: "Enter Passcode", reason: "Authenticate", password: "", completed: { _ in }) + static var sample = LocalAuthRequest(title: "Enter Passcode", reason: "Authenticate", password: "", selfDestruct: false, completed: { _ in }) } -func authenticate(title: LocalizedStringKey? = nil, reason: String, completed: @escaping (LAResult) -> Void) { +func authenticate(title: LocalizedStringKey? = nil, reason: String, selfDestruct: Bool = false, completed: @escaping (LAResult) -> Void) { logger.debug("authenticate") switch privacyLocalAuthModeDefault.get() { case .system: systemAuthenticate(reason, completed) case .passcode: if let password = kcAppPassword.get() { DispatchQueue.main.async { - ChatModel.shared.laRequest = LocalAuthRequest(title: title, reason: reason, password: password, completed: completed) + ChatModel.shared.laRequest = LocalAuthRequest( + title: title, + reason: reason, + password: password, + selfDestruct: selfDestruct && UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT), + completed: completed + ) } } else { completed(.unavailable(authError: NSLocalizedString("No app password", comment: "Authentication unavailable"))) diff --git a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift index 5cf74bafdd..8200a2c0f5 100644 --- a/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift +++ b/apps/ios/Shared/Views/LocalAuth/LocalAuthView.swift @@ -7,6 +7,7 @@ // import SwiftUI +import SimpleXChat struct LocalAuthView: View { @EnvironmentObject var m: ChatModel @@ -15,6 +16,13 @@ struct LocalAuthView: View { var body: some View { PasscodeView(passcode: $password, title: authRequest.title ?? "Enter Passcode", reason: authRequest.reason, submitLabel: "Submit") { + if let sdPassword = kcSelfDestructPassword.get(), authRequest.selfDestruct && password == sdPassword { + deleteStorageAndRestart(sdPassword) { r in + m.laRequest = nil + authRequest.completed(r) + } + return + } let r: LAResult = password == authRequest.password ? .success : .failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry")) @@ -25,6 +33,41 @@ struct LocalAuthView: View { authRequest.completed(.failed(authError: NSLocalizedString("Authentication cancelled", comment: "PIN entry"))) } } + + private func deleteStorageAndRestart(_ password: String, completed: @escaping (LAResult) -> Void) { + Task { + do { + try await stopChatAsync() + try await deleteChatAsync() + _ = kcAppPassword.set(password) + _ = kcSelfDestructPassword.remove() + await NtfManager.shared.removeAllNotifications() + let displayName = UserDefaults.standard.string(forKey: DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) + UserDefaults.standard.removeObject(forKey: DEFAULT_LA_SELF_DESTRUCT) + UserDefaults.standard.removeObject(forKey: DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) + await MainActor.run { + m.chatDbChanged = true + m.chatInitialized = false + } + resetChatCtrl() + try initializeChat(start: true) + m.chatDbChanged = false + appStateGroupDefault.set(.active) + if m.currentUser != nil { return } + var profile: Profile? = nil + if let displayName = displayName, displayName != "" { + profile = Profile(displayName: displayName, fullName: "") + } + m.currentUser = try apiCreateActiveUser(profile) + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete + try startChat() + completed(.success) + } catch { + completed(.failed(authError: NSLocalizedString("Incorrect passcode", comment: "PIN entry"))) + } + } + } } struct LocalAuthView_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift index 5b95ab0cc5..76cd3e279a 100644 --- a/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift +++ b/apps/ios/Shared/Views/LocalAuth/SetAppPasscodeView.swift @@ -10,6 +10,9 @@ import SwiftUI import SimpleXChat struct SetAppPasscodeView: View { + var passcodeKeychain: KeyChainItem = kcAppPassword + var title: LocalizedStringKey = "New Passcode" + var reason: String? var submit: () -> Void var cancel: () -> Void @Environment(\.dismiss) var dismiss: DismissAction @@ -27,7 +30,7 @@ struct SetAppPasscodeView: View { submitEnabled: { pwd in pwd == enteredPassword } ) { if passcode == enteredPassword { - if kcAppPassword.set(passcode) { + if passcodeKeychain.set(passcode) { enteredPassword = "" passcode = "" dismiss() @@ -38,7 +41,7 @@ struct SetAppPasscodeView: View { } } } else { - setPasswordView(title: "New Passcode", submitLabel: "Save") { + setPasswordView(title: title, submitLabel: "Save") { enteredPassword = passcode passcode = "" confirming = true @@ -51,7 +54,7 @@ struct SetAppPasscodeView: View { } private func setPasswordView(title: LocalizedStringKey, submitLabel: LocalizedStringKey, submitEnabled: (((String) -> Bool))? = nil, submit: @escaping () -> Void) -> some View { - PasscodeView(passcode: $passcode, title: title, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) { + PasscodeView(passcode: $passcode, title: title, reason: reason, submitLabel: submitLabel, submitEnabled: submitEnabled, submit: submit) { dismiss() cancel() } diff --git a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift index 316eb2b78b..0066022098 100644 --- a/apps/ios/Shared/Views/Onboarding/CreateProfile.swift +++ b/apps/ios/Shared/Views/Onboarding/CreateProfile.swift @@ -129,6 +129,8 @@ struct CreateProfile: View { m.onboardingStage = .step3_CreateSimpleXAddress } } else { + onboardingStageDefault.set(.onboardingComplete) + m.onboardingStage = .onboardingComplete dismiss() m.users = try listUsers() try getUserChatData() diff --git a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift index 92f0f8c201..3652dec054 100644 --- a/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift +++ b/apps/ios/Shared/Views/UserSettings/IncognitoHelp.swift @@ -13,6 +13,7 @@ struct IncognitoHelp: View { VStack(alignment: .leading) { Text("Incognito mode") .font(.largeTitle) + .bold() .padding(.vertical) ScrollView { VStack(alignment: .leading) { diff --git a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift index dd21ea26c0..a6f12cb275 100644 --- a/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift +++ b/apps/ios/Shared/Views/UserSettings/PrivacySettings.swift @@ -102,9 +102,13 @@ struct SimplexLockView: View { @AppStorage(DEFAULT_LA_NOTICE_SHOWN) private var prefLANoticeShown = false @State private var laMode: LAMode = privacyLocalAuthModeDefault.get() @AppStorage(DEFAULT_LA_LOCK_DELAY) private var laLockDelay = 30 - @State var performLA: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) + @State private var performLA: Bool = UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) + @State private var selfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT) + @State private var currentSelfDestruct: Bool = UserDefaults.standard.bool(forKey: DEFAULT_LA_SELF_DESTRUCT) + @AppStorage(DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME) private var selfDestructDisplayName = "" @State private var performLAToggleReset = false @State private var performLAModeReset = false + @State private var performLASelfDestructReset = false @State private var showPasswordAction: PasswordAction? = nil @State private var showChangePassword = false @State var laAlert: LASettingViewAlert? = nil @@ -116,6 +120,8 @@ struct SimplexLockView: View { case laUnavailableTurningOffAlert case laPasscodeSetAlert case laPasscodeChangedAlert + case laSeldDestructPasscodeSetAlert + case laSeldDestructPasscodeChangedAlert case laPasscodeNotChangedAlert var id: Self { self } @@ -124,7 +130,10 @@ struct SimplexLockView: View { enum PasswordAction: Identifiable { case enableAuth case toggleMode - case changePassword + case changePasscode + case enableSelfDestruct + case changeSelfDestructPasscode + case selfDestructInfo var id: Self { self } } @@ -159,12 +168,34 @@ struct SimplexLockView: View { } } if showChangePassword && laMode == .passcode { - Button("Change Passcode") { + Button("Change passcode") { changeLAPassword() } } } } + + if performLA && laMode == .passcode { + Section("Self-destruct passcode") { + Toggle(isOn: $selfDestruct) { + HStack(spacing: 6) { + Text("Enable self-destruct") + Image(systemName: "info.circle") + .foregroundColor(.accentColor) + .font(.system(size: 14)) + } + .onTapGesture { + showPasswordAction = .selfDestructInfo + } + } + if selfDestruct { + TextField("New display name", text: $selfDestructDisplayName) + Button("Change self-destruct passcode") { + changeSelfDestructPassword() + } + } + } + } } } .onChange(of: performLA) { performLAToggle in @@ -192,6 +223,13 @@ struct SimplexLockView: View { updateLAMode() } } + .onChange(of: selfDestruct) { _ in + if performLASelfDestructReset { + performLASelfDestructReset = false + } else if prefPerformLA { + toggleSelfDestruct() + } + } .alert(item: $laAlert) { alertItem in switch alertItem { case .laTurnedOnAlert: return laTurnedOnAlert() @@ -200,6 +238,8 @@ struct SimplexLockView: View { case .laUnavailableTurningOffAlert: return laUnavailableTurningOffAlert() case .laPasscodeSetAlert: return passcodeAlert("Passcode set!") case .laPasscodeChangedAlert: return passcodeAlert("Passcode changed!") + case .laSeldDestructPasscodeSetAlert: return selfDestructPasscodeAlert("Self-destruct passcode enabled!") + case .laSeldDestructPasscodeChangedAlert: return selfDestructPasscodeAlert("Self-destruct passcode changed!") case .laPasscodeNotChangedAlert: return mkAlert(title: "Passcode not changed!") } } @@ -223,12 +263,27 @@ struct SimplexLockView: View { } cancel: { revertLAMode() } - case .changePassword: + case .changePasscode: SetAppPasscodeView { showLAAlert(.laPasscodeChangedAlert) } cancel: { showLAAlert(.laPasscodeNotChangedAlert) } + case .enableSelfDestruct: + SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, title: "Set passcode", reason: NSLocalizedString("Enable self-destruct passcode", comment: "set passcode view")) { + updateSelfDestruct() + showLAAlert(.laSeldDestructPasscodeSetAlert) + } cancel: { + revertSelfDestruct() + } + case .changeSelfDestructPasscode: + SetAppPasscodeView(passcodeKeychain: kcSelfDestructPassword, reason: NSLocalizedString("Change self-destruct passcode", comment: "set passcode view")) { + showLAAlert(.laSeldDestructPasscodeChangedAlert) + } cancel: { + showLAAlert(.laPasscodeNotChangedAlert) + } + case .selfDestructInfo: + selfDestructInfoView() } } .onAppear { @@ -239,6 +294,30 @@ struct SimplexLockView: View { } } + private func selfDestructInfoView() -> some View { + VStack(alignment: .leading) { + Text("Self-desctruct") + .font(.largeTitle) + .bold() + .padding(.vertical) + ScrollView { + VStack(alignment: .leading) { + Group { + Text("If you enter your self-desctruct passcode while opening the app:") + VStack(spacing: 8) { + textListItem("1.", "All app data is deleted.") + textListItem("2.", "App passcode is replaced with self-desctruct passcode.") + textListItem("3.", "An empty chat profile with the provided name is created, and the app opens as usual.") + } + } + .padding(.bottom) + } + } + } + .frame(maxWidth: .infinity) + .padding() + } + private func showLAAlert(_ a: LASettingViewAlert) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { laAlert = a @@ -276,11 +355,39 @@ struct SimplexLockView: View { } } + private func toggleSelfDestruct() { + authenticate(reason: NSLocalizedString("Change self-destruct mode", comment: "authentication reason")) { laResult in + switch laResult { + case .failed: + revertSelfDestruct() + laAlert = .laFailedAlert + case .success: + if selfDestruct { + showPasswordAction = .enableSelfDestruct + } else { + resetSelfDestruct() + } + case .unavailable: + disableUnavailableLA() + } + } + } + private func changeLAPassword() { authenticate(title: "Current Passcode", reason: NSLocalizedString("Change passcode", comment: "authentication reason")) { laResult in switch laResult { case .failed: laAlert = .laFailedAlert - case .success: showPasswordAction = .changePassword + case .success: showPasswordAction = .changePasscode + case .unavailable: disableUnavailableLA() + } + } + } + + private func changeSelfDestructPassword() { + authenticate(reason: NSLocalizedString("Change self-destruct passcode", comment: "authentication reason")) { laResult in + switch laResult { + case .failed: laAlert = .laFailedAlert + case .success: showPasswordAction = .changeSelfDestructPasscode case .unavailable: disableUnavailableLA() } } @@ -329,6 +436,7 @@ struct SimplexLockView: View { _ = kcAppPassword.remove() laLockDelay = 30 showChangePassword = false + resetSelfDestruct() } private func resetLAEnabled(_ onOff: Bool) { @@ -347,9 +455,29 @@ struct SimplexLockView: View { privacyLocalAuthModeDefault.set(laMode) } + private func resetSelfDestruct() { + _ = kcSelfDestructPassword.remove() + selfDestruct = false + updateSelfDestruct() + } + + private func revertSelfDestruct() { + performLASelfDestructReset = true + withAnimation { selfDestruct = currentSelfDestruct } + } + + private func updateSelfDestruct() { + UserDefaults.standard.set(selfDestruct, forKey: DEFAULT_LA_SELF_DESTRUCT) + currentSelfDestruct = selfDestruct + } + private func passcodeAlert(_ title: LocalizedStringKey) -> Alert { mkAlert(title: title, message: "Please remember or store it securely - there is no way to recover a lost passcode!") } + + private func selfDestructPasscodeAlert(_ title: LocalizedStringKey) -> Alert { + mkAlert(title: title, message: "If you enter this passcode when opening the app, all app data will be irreversibly removed!") + } } struct PrivacySettings_Previews: PreviewProvider { diff --git a/apps/ios/Shared/Views/UserSettings/SettingsView.swift b/apps/ios/Shared/Views/UserSettings/SettingsView.swift index 5f5c1334b6..714de5b2b2 100644 --- a/apps/ios/Shared/Views/UserSettings/SettingsView.swift +++ b/apps/ios/Shared/Views/UserSettings/SettingsView.swift @@ -21,6 +21,8 @@ let DEFAULT_LA_NOTICE_SHOWN = "localAuthenticationNoticeShown" let DEFAULT_PERFORM_LA = "performLocalAuthentication" let DEFAULT_LA_MODE = "localAuthenticationMode" let DEFAULT_LA_LOCK_DELAY = "localAuthenticationLockDelay" +let DEFAULT_LA_SELF_DESTRUCT = "localAuthenticationSelfDestruct" +let DEFAULT_LA_SELF_DESTRUCT_DISPLAY_NAME = "localAuthenticationSelfDestructDisplayName" let DEFAULT_NOTIFICATION_ALERT_SHOWN = "notificationAlertShown" let DEFAULT_WEBRTC_POLICY_RELAY = "webrtcPolicyRelay" let DEFAULT_WEBRTC_ICE_SERVERS = "webrtcICEServers" @@ -53,6 +55,7 @@ let appDefaults: [String: Any] = [ DEFAULT_PERFORM_LA: false, DEFAULT_LA_MODE: LAMode.system.rawValue, DEFAULT_LA_LOCK_DELAY: 30, + DEFAULT_LA_SELF_DESTRUCT: false, DEFAULT_NOTIFICATION_ALERT_SHOWN: false, DEFAULT_WEBRTC_POLICY_RELAY: true, DEFAULT_CALL_KIT_CALLS_IN_RECENTS: false, @@ -298,9 +301,8 @@ struct SettingsView: View { .frame(maxWidth: 24, maxHeight: 24, alignment: .center) .foregroundColor(chatModel.incognito ? Color.indigo : .secondary) Toggle(isOn: $chatModel.incognito) { - HStack { + HStack(spacing: 6) { Text("Incognito") - Spacer().frame(width: 4) Image(systemName: "info.circle") .foregroundColor(.accentColor) .font(.system(size: 14)) diff --git a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff index 52d88f00c6..faf4fb391d 100644 --- a/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff +++ b/apps/ios/SimpleX Localizations/cs.xcloc/Localized Contents/cs.xliff @@ -730,8 +730,8 @@ Dostupné ve verzi 5.1 Změnit No comment provided by engineer. - - Change Passcode + + Change passcode Změna hesla No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff index b735149017..d90797dce3 100644 --- a/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff +++ b/apps/ios/SimpleX Localizations/de.xcloc/Localized Contents/de.xliff @@ -730,8 +730,8 @@ Verfügbar ab v5.1 Ändern No comment provided by engineer. - - Change Passcode + + Change passcode Passwort ändern No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff index ce52791721..8759953c90 100644 --- a/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff +++ b/apps/ios/SimpleX Localizations/el.xcloc/Localized Contents/el.xliff @@ -569,8 +569,8 @@ Available in v5.1 Change No comment provided by engineer. - - Change Passcode + + Change passcode No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff index 66db8126e2..341f77da69 100644 --- a/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff +++ b/apps/ios/SimpleX Localizations/en.xcloc/Localized Contents/en.xliff @@ -739,9 +739,9 @@ Available in v5.1 Change No comment provided by engineer. - - Change Passcode - Change Passcode + + Change passcode + Change passcode No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff index 6e20eade0d..dfd5c780bc 100644 --- a/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff +++ b/apps/ios/SimpleX Localizations/es.xcloc/Localized Contents/es.xliff @@ -730,8 +730,8 @@ Disponible en v5.1 Cambiar No comment provided by engineer. - - Change Passcode + + Change passcode Cambiar el código de acceso No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff index cf17e8a993..63cace370b 100644 --- a/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff +++ b/apps/ios/SimpleX Localizations/fr.xcloc/Localized Contents/fr.xliff @@ -730,8 +730,8 @@ Disponible dans la v5.1 Changer No comment provided by engineer. - - Change Passcode + + Change passcode Modifier le code d'accès No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff index 74495a0d22..1665c5220e 100644 --- a/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff +++ b/apps/ios/SimpleX Localizations/he.xcloc/Localized Contents/he.xliff @@ -569,8 +569,8 @@ Available in v5.1 Change No comment provided by engineer. - - Change Passcode + + Change passcode No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff index 0433429359..7b3534d026 100644 --- a/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff +++ b/apps/ios/SimpleX Localizations/it.xcloc/Localized Contents/it.xliff @@ -730,8 +730,8 @@ Disponibile nella v5.1 Cambia No comment provided by engineer. - - Change Passcode + + Change passcode Cambia codice di accesso No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff index 11c0de1dc7..6b1f2f2e21 100644 --- a/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff +++ b/apps/ios/SimpleX Localizations/nl.xcloc/Localized Contents/nl.xliff @@ -730,8 +730,8 @@ Beschikbaar in v5.1 Veranderen No comment provided by engineer. - - Change Passcode + + Change passcode Toegangscode wijzigen No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff index f711d0c08a..dce3f7b509 100644 --- a/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff +++ b/apps/ios/SimpleX Localizations/pl.xcloc/Localized Contents/pl.xliff @@ -730,8 +730,8 @@ Dostępny w v5.1 Zmień No comment provided by engineer. - - Change Passcode + + Change passcode Zmień kod dostępu No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff index e5a11e3a13..d016a4649e 100644 --- a/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff +++ b/apps/ios/SimpleX Localizations/pt.xcloc/Localized Contents/pt.xliff @@ -569,8 +569,8 @@ Available in v5.1 Change No comment provided by engineer. - - Change Passcode + + Change passcode No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff index 576f6a2378..74995c5233 100644 --- a/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff +++ b/apps/ios/SimpleX Localizations/ru.xcloc/Localized Contents/ru.xliff @@ -730,8 +730,8 @@ Available in v5.1 Поменять No comment provided by engineer. - - Change Passcode + + Change passcode Изменить код доступа No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff index af58442ef9..5d8dbf1776 100644 --- a/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hans.xcloc/Localized Contents/zh-Hans.xliff @@ -730,8 +730,8 @@ Available in v5.1 更改 No comment provided by engineer. - - Change Passcode + + Change passcode 更改密码 No comment provided by engineer. diff --git a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff index 36cbc3076c..d25b9a4c06 100644 --- a/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff +++ b/apps/ios/SimpleX Localizations/zh-Hant.xcloc/Localized Contents/zh-Hant.xliff @@ -4207,8 +4207,8 @@ SimpleX servers cannot see your profile. 已取消認證 PIN entry - - Change Passcode + + Change passcode 修改密碼 No comment provided by engineer. diff --git a/apps/ios/SimpleXChat/APITypes.swift b/apps/ios/SimpleXChat/APITypes.swift index 53ae7e8849..561bbc48d0 100644 --- a/apps/ios/SimpleXChat/APITypes.swift +++ b/apps/ios/SimpleXChat/APITypes.swift @@ -14,7 +14,7 @@ let jsonEncoder = getJSONEncoder() public enum ChatCommand { case showActiveUser - case createActiveUser(profile: Profile) + case createActiveUser(profile: Profile?) case listUsers case apiSetActiveUser(userId: Int64, viewPwd: String?) case apiHideUser(userId: Int64, viewPwd: String) @@ -110,7 +110,11 @@ public enum ChatCommand { get { switch self { case .showActiveUser: return "/u" - case let .createActiveUser(profile): return "/create user \(profile.displayName) \(profile.fullName)" + case let .createActiveUser(profile): + if let profile = profile { + return "/create user \(profile.displayName) \(profile.fullName)" + } + return "/create user" case .listUsers: return "/users" case let .apiSetActiveUser(userId, viewPwd): return "/_user \(userId)\(maybePwd(viewPwd))" case let .apiHideUser(userId, viewPwd): return "/_hide user \(userId) \(encodeJSON(viewPwd))" diff --git a/apps/ios/SimpleXChat/KeyChain.swift b/apps/ios/SimpleXChat/KeyChain.swift index 91d94146a0..50a5946462 100644 --- a/apps/ios/SimpleXChat/KeyChain.swift +++ b/apps/ios/SimpleXChat/KeyChain.swift @@ -13,11 +13,14 @@ private let ACCESS_POLICY: CFString = kSecAttrAccessibleAfterFirstUnlockThisDevi private let ACCESS_GROUP: String = "5NN7GUYB6T.chat.simplex.app" private let DATABASE_PASSWORD_ITEM: String = "databasePassword" private let APP_PASSWORD_ITEM: String = "appPassword" +private let SELF_DESTRUCT_PASSWORD_ITEM: String = "selfDestructPassword" public let kcDatabasePassword = KeyChainItem(forKey: DATABASE_PASSWORD_ITEM) public let kcAppPassword = KeyChainItem(forKey: APP_PASSWORD_ITEM) +public let kcSelfDestructPassword = KeyChainItem(forKey: SELF_DESTRUCT_PASSWORD_ITEM) + public struct KeyChainItem { var forKey: String diff --git a/apps/ios/de.lproj/Localizable.strings b/apps/ios/de.lproj/Localizable.strings index 0674ca7f1b..5721b2bd6d 100644 --- a/apps/ios/de.lproj/Localizable.strings +++ b/apps/ios/de.lproj/Localizable.strings @@ -477,7 +477,7 @@ "Change passcode" = "Passwort ändern"; /* No comment provided by engineer. */ -"Change Passcode" = "Passwort ändern"; +"Change passcode" = "Passwort ändern"; /* No comment provided by engineer. */ "Change receiving address" = "Wechseln der Empfängeradresse"; diff --git a/apps/ios/es.lproj/Localizable.strings b/apps/ios/es.lproj/Localizable.strings index ebad345675..520da70c7b 100644 --- a/apps/ios/es.lproj/Localizable.strings +++ b/apps/ios/es.lproj/Localizable.strings @@ -477,7 +477,7 @@ "Change passcode" = "Cambiar el código de acceso"; /* No comment provided by engineer. */ -"Change Passcode" = "Cambiar el código de acceso"; +"Change passcode" = "Cambiar el código de acceso"; /* No comment provided by engineer. */ "Change receiving address" = "Cambiar servidor de recepción"; diff --git a/apps/ios/fr.lproj/Localizable.strings b/apps/ios/fr.lproj/Localizable.strings index a4e25219b9..d5c0ade54e 100644 --- a/apps/ios/fr.lproj/Localizable.strings +++ b/apps/ios/fr.lproj/Localizable.strings @@ -477,7 +477,7 @@ "Change passcode" = "Modifier le code d'accès"; /* No comment provided by engineer. */ -"Change Passcode" = "Modifier le code d'accès"; +"Change passcode" = "Modifier le code d'accès"; /* No comment provided by engineer. */ "Change receiving address" = "Changer d'adresse de réception"; diff --git a/apps/ios/it.lproj/Localizable.strings b/apps/ios/it.lproj/Localizable.strings index 26e61d858a..0ccf4153c8 100644 --- a/apps/ios/it.lproj/Localizable.strings +++ b/apps/ios/it.lproj/Localizable.strings @@ -477,7 +477,7 @@ "Change passcode" = "Cambia codice di accesso"; /* No comment provided by engineer. */ -"Change Passcode" = "Cambia codice di accesso"; +"Change passcode" = "Cambia codice di accesso"; /* No comment provided by engineer. */ "Change receiving address" = "Cambia indirizzo di ricezione"; diff --git a/apps/ios/nl.lproj/Localizable.strings b/apps/ios/nl.lproj/Localizable.strings index b3a809f714..c6232ffb12 100644 --- a/apps/ios/nl.lproj/Localizable.strings +++ b/apps/ios/nl.lproj/Localizable.strings @@ -477,7 +477,7 @@ "Change passcode" = "Toegangscode wijzigen"; /* No comment provided by engineer. */ -"Change Passcode" = "Toegangscode wijzigen"; +"Change passcode" = "Toegangscode wijzigen"; /* No comment provided by engineer. */ "Change receiving address" = "Ontvangst adres wijzigen"; diff --git a/apps/ios/pl.lproj/Localizable.strings b/apps/ios/pl.lproj/Localizable.strings index 15233d206b..8fec32c91e 100644 --- a/apps/ios/pl.lproj/Localizable.strings +++ b/apps/ios/pl.lproj/Localizable.strings @@ -477,7 +477,7 @@ "Change passcode" = "Zmień pin"; /* No comment provided by engineer. */ -"Change Passcode" = "Zmień kod dostępu"; +"Change passcode" = "Zmień kod dostępu"; /* No comment provided by engineer. */ "Change receiving address" = "Zmień adres odbioru"; diff --git a/apps/ios/ru.lproj/Localizable.strings b/apps/ios/ru.lproj/Localizable.strings index 9e19a5bd04..fd8b15e672 100644 --- a/apps/ios/ru.lproj/Localizable.strings +++ b/apps/ios/ru.lproj/Localizable.strings @@ -477,7 +477,7 @@ "Change passcode" = "Изменить код доступа"; /* No comment provided by engineer. */ -"Change Passcode" = "Изменить код доступа"; +"Change passcode" = "Изменить код доступа"; /* No comment provided by engineer. */ "Change receiving address" = "Поменять адрес получения"; diff --git a/apps/ios/zh-Hans.lproj/Localizable.strings b/apps/ios/zh-Hans.lproj/Localizable.strings index 2499415b9b..25a5d98b85 100644 --- a/apps/ios/zh-Hans.lproj/Localizable.strings +++ b/apps/ios/zh-Hans.lproj/Localizable.strings @@ -477,7 +477,7 @@ "Change passcode" = "更改密码"; /* No comment provided by engineer. */ -"Change Passcode" = "更改密码"; +"Change passcode" = "更改密码"; /* No comment provided by engineer. */ "Change receiving address" = "更改接收地址"; diff --git a/src/Simplex/Chat.hs b/src/Simplex/Chat.hs index 497be8f6cb..e049efcc1c 100644 --- a/src/Simplex/Chat.hs +++ b/src/Simplex/Chat.hs @@ -83,6 +83,7 @@ import Simplex.Messaging.Util import System.Exit (exitFailure, exitSuccess) import System.FilePath (combine, splitExtensions, takeFileName, ()) import System.IO (Handle, IOMode (..), SeekMode (..), hFlush, openFile, stdout) +import System.Random (randomRIO) import Text.Read (readMaybe) import UnliftIO.Async import UnliftIO.Concurrent (forkFinally, forkIO, mkWeakThreadId, threadDelay) @@ -318,7 +319,8 @@ toView event = do processChatCommand :: forall m. ChatMonad m => ChatCommand -> m ChatResponse processChatCommand = \case ShowActiveUser -> withUser' $ pure . CRActiveUser - CreateActiveUser p@Profile {displayName} sameServers -> do + CreateActiveUser NewUser {profile, sameServers, pastTimestamp} -> do + p@Profile {displayName} <- liftIO $ maybe generateRandomProfile pure profile u <- asks currentUser (smp, smpServers) <- chooseServers SPSMP (xftp, xftpServers) <- chooseServers SPXFTP @@ -329,7 +331,8 @@ processChatCommand = \case when (any (\User {localDisplayName = n} -> n == displayName) users) $ throwChatError $ CEUserExists displayName withAgent (\a -> createUser a smp xftp) - user <- withStore $ \db -> createUserRecord db (AgentUserId auId) p True + ts <- liftIO $ getCurrentTime >>= if pastTimestamp then coupleDaysAgo else pure + user <- withStore $ \db -> createUserRecordAt db (AgentUserId auId) p True ts storeServers user smpServers storeServers user xftpServers setActive ActiveNone @@ -351,6 +354,8 @@ processChatCommand = \case storeServers user servers = unless (null servers) $ withStore $ \db -> overwriteProtocolServers db user servers + coupleDaysAgo t = (`addUTCTime` t) . fromInteger . (+ (2 * day)) <$> randomRIO (0, day) + day = 86400 ListUsers -> CRUsersList <$> withStore' getUsersInfo APISetActiveUser userId' viewPwd_ -> withUser $ \user -> do user' <- privateGetUser userId' @@ -4584,12 +4589,8 @@ chatCommandP = choice [ "/mute " *> ((`ShowMessages` False) <$> chatNameP), "/unmute " *> ((`ShowMessages` True) <$> chatNameP), - "/create user" - *> ( do - sameSmp <- (A.space *> "same_smp=" *> onOffP) <|> pure False - uProfile <- A.space *> userProfile - pure $ CreateActiveUser uProfile sameSmp - ), + "/_create user " *> (CreateActiveUser <$> jsonP), + "/create user " *> (CreateActiveUser <$> newUserP), "/users" $> ListUsers, "/_user " *> (APISetActiveUser <$> A.decimal <*> optional (A.space *> jsonP)), ("/user " <|> "/u ") *> (SetActiveUser <$> displayName <*> optional (A.space *> pwdP)), @@ -4784,7 +4785,7 @@ chatCommandP = ("/welcome" <|> "/w") $> Welcome, "/profile_image " *> (UpdateProfileImage . Just . ImageData <$> imageP), "/profile_image" $> UpdateProfileImage Nothing, - ("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> userNames), + ("/profile " <|> "/p ") *> (uncurry UpdateProfile <$> profileNames), ("/profile" <|> "/p") $> ShowProfile, "/set voice #" *> (SetGroupFeature (AGF SGFVoice) <$> displayName <*> (A.space *> strP)), "/set voice @" *> (SetContactFeature (ACF SCFVoice) <$> displayName <*> optional (A.space *> strP)), @@ -4823,23 +4824,19 @@ chatCommandP = refChar c = c > ' ' && c /= '#' && c /= '@' liveMessageP = " live=" *> onOffP <|> pure False onOffP = ("on" $> True) <|> ("off" $> False) - userNames = do - cName <- displayName - fullName <- fullNameP cName - pure (cName, fullName) - userProfile = do - (cName, fullName) <- userNames - pure Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing} + profileNames = (,) <$> displayName <*> fullNameP + newUserP = do + sameServers <- "same_smp=" *> onOffP <* A.space <|> pure False + (cName, fullName) <- profileNames + let profile = Just Profile {displayName = cName, fullName, image = Nothing, contactLink = Nothing, preferences = Nothing} + pure NewUser {profile, sameServers, pastTimestamp = False} jsonP :: J.FromJSON a => Parser a jsonP = J.eitherDecodeStrict' <$?> A.takeByteString groupProfile = do - gName <- displayName - fullName <- fullNameP gName + (gName, fullName) <- profileNames let groupPreferences = Just (emptyGroupPrefs :: GroupPreferences) {directMessages = Just DirectMessagesGroupPreference {enable = FEOn}} pure GroupProfile {displayName = gName, fullName, description = Nothing, image = Nothing, groupPreferences} - fullNameP name = do - n <- (A.space *> A.takeByteString) <|> pure "" - pure $ if B.null n then name else safeDecodeUtf8 n + fullNameP = A.space *> textP <|> pure "" textP = safeDecodeUtf8 <$> A.takeByteString pwdP = jsonP <|> (UserPwd . safeDecodeUtf8 <$> A.takeTill (== ' ')) msgTextP = jsonP <|> textP diff --git a/src/Simplex/Chat/Controller.hs b/src/Simplex/Chat/Controller.hs index 5537524945..88f62b6791 100644 --- a/src/Simplex/Chat/Controller.hs +++ b/src/Simplex/Chat/Controller.hs @@ -180,7 +180,7 @@ instance ToJSON HelpSection where data ChatCommand = ShowActiveUser - | CreateActiveUser Profile Bool + | CreateActiveUser NewUser | ListUsers | APISetActiveUser UserId (Maybe UserPwd) | SetActiveUser UserName (Maybe UserPwd) diff --git a/src/Simplex/Chat/Store.hs b/src/Simplex/Chat/Store.hs index e2aa28ad2c..d670c52dc2 100644 --- a/src/Simplex/Chat/Store.hs +++ b/src/Simplex/Chat/Store.hs @@ -27,6 +27,7 @@ module Simplex.Chat.Store chatStoreFile, agentStoreFile, createUserRecord, + createUserRecordAt, getUsersInfo, getUsers, setActiveUser, @@ -490,9 +491,11 @@ insertedRowId :: DB.Connection -> IO Int64 insertedRowId db = fromOnly . head <$> DB.query_ db "SELECT last_insert_rowid()" createUserRecord :: DB.Connection -> AgentUserId -> Profile -> Bool -> ExceptT StoreError IO User -createUserRecord db (AgentUserId auId) Profile {displayName, fullName, image, preferences = userPreferences} activeUser = +createUserRecord db auId p activeUser = createUserRecordAt db auId p activeUser =<< liftIO getCurrentTime + +createUserRecordAt :: DB.Connection -> AgentUserId -> Profile -> Bool -> UTCTime -> ExceptT StoreError IO User +createUserRecordAt db (AgentUserId auId) Profile {displayName, fullName, image, preferences = userPreferences} activeUser currentTs = checkConstraint SEDuplicateName . liftIO $ do - currentTs <- getCurrentTime when activeUser $ DB.execute_ db "UPDATE users SET active_user = 0" DB.execute db diff --git a/src/Simplex/Chat/Types.hs b/src/Simplex/Chat/Types.hs index de89599f2d..0999998861 100644 --- a/src/Simplex/Chat/Types.hs +++ b/src/Simplex/Chat/Types.hs @@ -120,6 +120,13 @@ instance ToJSON User where toEncoding = J.genericToEncoding J.defaultOptions {J.omitNothingFields = True} toJSON = J.genericToJSON J.defaultOptions {J.omitNothingFields = True} +data NewUser = NewUser + { profile :: Maybe Profile, + sameServers :: Bool, + pastTimestamp :: Bool + } + deriving (Show, Generic, FromJSON) + newtype B64UrlByteString = B64UrlByteString ByteString deriving (Eq, Show) diff --git a/tests/ChatTests/Groups.hs b/tests/ChatTests/Groups.hs index 32e4ff05bd..e2cf4e2e61 100644 --- a/tests/ChatTests/Groups.hs +++ b/tests/ChatTests/Groups.hs @@ -457,7 +457,7 @@ testGroupSameName = alice <## "group #team is created" alice <## "to add members use /a team or /create link #team" alice ##> "/g team" - alice <## "group #team_1 (team) is created" + alice <## "group #team_1 is created" alice <## "to add members use /a team_1 or /create link #team_1" testGroupDeleteWhenInvited :: HasCallStack => FilePath -> IO () @@ -518,7 +518,7 @@ testGroupReAddInvited = concurrentlyN_ [ alice <## "invitation to join the group #team sent to bob", do - bob <## "#team_1 (team): alice invites you to join the group as admin" + bob <## "#team_1: alice invites you to join the group as admin" bob <## "use /j team_1 to accept" ] @@ -678,7 +678,7 @@ testGroupRemoveAdd = ] alice ##> "/a team bob" alice <## "invitation to join the group #team sent to bob" - bob <## "#team_1 (team): alice invites you to join the group as admin" + bob <## "#team_1: alice invites you to join the group as admin" bob <## "use /j team_1 to accept" bob ##> "/j team_1" concurrentlyN_ @@ -1058,13 +1058,13 @@ testGroupLiveMessage = alice <## "message history:" alice .<## ": hello 2" alice .<## ":" - bobItemId <- lastItemId bob - bob ##> ("/_get item info " <> bobItemId) - bob <##. "sent at: " - bob <##. "received at: " - bob <## "message history:" - bob .<## ": hello 2" - bob .<## ":" + -- bobItemId <- lastItemId bob + -- bob ##> ("/_get item info " <> bobItemId) + -- bob <##. "sent at: " + -- bob <##. "received at: " + -- bob <## "message history:" + -- bob .<## ": hello 2" + -- bob .<## ":" testUpdateGroupProfile :: HasCallStack => FilePath -> IO () testUpdateGroupProfile = diff --git a/tests/ChatTests/Profiles.hs b/tests/ChatTests/Profiles.hs index a5dc083f71..6bbd4b1447 100644 --- a/tests/ChatTests/Profiles.hs +++ b/tests/ChatTests/Profiles.hs @@ -1147,7 +1147,7 @@ testUpdateGroupPrefs = alice #$> ("/_get chat #1 count=100", chat, [(0, "connected")]) threadDelay 500000 bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected")]) - alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}" + alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}" alice <## "updated group preferences:" alice <## "Full deletion: on" alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on")]) @@ -1156,7 +1156,7 @@ testUpdateGroupPrefs = bob <## "Full deletion: on" threadDelay 500000 bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on")]) - alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"off\"}, \"directMessages\": {\"enable\": \"on\"}}}" + alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"off\"}, \"directMessages\": {\"enable\": \"on\"}}}" alice <## "updated group preferences:" alice <## "Full deletion: off" alice <## "Voice messages: off" @@ -1167,7 +1167,7 @@ testUpdateGroupPrefs = bob <## "Voice messages: off" threadDelay 500000 bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off")]) - -- alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}" + -- alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}}}" alice ##> "/set voice #team on" alice <## "updated group preferences:" alice <## "Voice messages: on" @@ -1178,7 +1178,7 @@ testUpdateGroupPrefs = threadDelay 500000 bob #$> ("/_get chat #1 count=100", chat, groupFeatures <> [(0, "connected"), (0, "Full deletion: on"), (0, "Full deletion: off"), (0, "Voice messages: off"), (0, "Voice messages: on")]) threadDelay 500000 - alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}" + alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"fullDelete\": {\"enable\": \"off\"}, \"voice\": {\"enable\": \"on\"}, \"directMessages\": {\"enable\": \"on\"}}}" -- no update threadDelay 500000 alice #$> ("/_get chat #1 count=100", chat, [(0, "connected"), (1, "Full deletion: on"), (1, "Full deletion: off"), (1, "Voice messages: off"), (1, "Voice messages: on")]) @@ -1343,7 +1343,7 @@ testEnableTimedMessagesGroup = \alice bob -> do createGroup2 "team" alice bob threadDelay 1000000 - alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"team\", \"groupPreferences\": {\"timedMessages\": {\"enable\": \"on\", \"ttl\": 1}, \"directMessages\": {\"enable\": \"on\"}}}" + alice ##> "/_group_profile #1 {\"displayName\": \"team\", \"fullName\": \"\", \"groupPreferences\": {\"timedMessages\": {\"enable\": \"on\", \"ttl\": 1}, \"directMessages\": {\"enable\": \"on\"}}}" alice <## "updated group preferences:" alice <## "Disappearing messages: on (1 sec)" bob <## "alice updated group #team:"