core, iOS: support for self-destruct password (#2412)
* core, iOS: support for self-destruct password * disable test logging * core: fix tests, iOS: remove notifications on removal * change alerts
This commit is contained in:
parent
57801fde1f
commit
0b8d9d11e2
@ -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:
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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")))
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -13,6 +13,7 @@ struct IncognitoHelp: View {
|
||||
VStack(alignment: .leading) {
|
||||
Text("Incognito mode")
|
||||
.font(.largeTitle)
|
||||
.bold()
|
||||
.padding(.vertical)
|
||||
ScrollView {
|
||||
VStack(alignment: .leading) {
|
||||
|
@ -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 {
|
||||
|
@ -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))
|
||||
|
@ -730,8 +730,8 @@ Dostupné ve verzi 5.1</target>
|
||||
<target>Změnit</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>Změna hesla</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -730,8 +730,8 @@ Verfügbar ab v5.1</target>
|
||||
<target>Ändern</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>Passwort ändern</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -569,8 +569,8 @@ Available in v5.1</source>
|
||||
<source>Change</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change database passphrase?" xml:space="preserve">
|
||||
|
@ -739,9 +739,9 @@ Available in v5.1</target>
|
||||
<target>Change</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<target>Change Passcode</target>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>Change passcode</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change database passphrase?" xml:space="preserve">
|
||||
|
@ -730,8 +730,8 @@ Disponible en v5.1</target>
|
||||
<target>Cambiar</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>Cambiar el código de acceso</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -730,8 +730,8 @@ Disponible dans la v5.1</target>
|
||||
<target>Changer</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>Modifier le code d'accès</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -569,8 +569,8 @@ Available in v5.1</source>
|
||||
<source>Change</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change database passphrase?" xml:space="preserve">
|
||||
|
@ -730,8 +730,8 @@ Disponibile nella v5.1</target>
|
||||
<target>Cambia</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>Cambia codice di accesso</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -730,8 +730,8 @@ Beschikbaar in v5.1</target>
|
||||
<target>Veranderen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>Toegangscode wijzigen</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -730,8 +730,8 @@ Dostępny w v5.1</target>
|
||||
<target>Zmień</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>Zmień kod dostępu</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -569,8 +569,8 @@ Available in v5.1</source>
|
||||
<source>Change</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change database passphrase?" xml:space="preserve">
|
||||
|
@ -730,8 +730,8 @@ Available in v5.1</source>
|
||||
<target>Поменять</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>Изменить код доступа</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -730,8 +730,8 @@ Available in v5.1</source>
|
||||
<target>更改</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve">
|
||||
<source>Change passcode</source>
|
||||
<target>更改密码</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -4207,8 +4207,8 @@ SimpleX servers cannot see your profile.</source>
|
||||
<target state="translated">已取消認證</target>
|
||||
<note>PIN entry</note>
|
||||
</trans-unit>
|
||||
<trans-unit id="Change Passcode" xml:space="preserve" approved="no">
|
||||
<source>Change Passcode</source>
|
||||
<trans-unit id="Change passcode" xml:space="preserve" approved="no">
|
||||
<source>Change passcode</source>
|
||||
<target state="translated">修改密碼</target>
|
||||
<note>No comment provided by engineer.</note>
|
||||
</trans-unit>
|
||||
|
@ -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))"
|
||||
|
@ -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
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -477,7 +477,7 @@
|
||||
"Change passcode" = "Изменить код доступа";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change Passcode" = "Изменить код доступа";
|
||||
"Change passcode" = "Изменить код доступа";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address" = "Поменять адрес получения";
|
||||
|
@ -477,7 +477,7 @@
|
||||
"Change passcode" = "更改密码";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change Passcode" = "更改密码";
|
||||
"Change passcode" = "更改密码";
|
||||
|
||||
/* No comment provided by engineer. */
|
||||
"Change receiving address" = "更改接收地址";
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
||||
|
@ -457,7 +457,7 @@ testGroupSameName =
|
||||
alice <## "group #team is created"
|
||||
alice <## "to add members use /a team <name> 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 <name> 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 =
|
||||
|
@ -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:"
|
||||
|
Loading…
Reference in New Issue
Block a user