Merge branch 'master' into master-android

This commit is contained in:
Evgeny Poberezkin
2024-03-12 18:29:34 +00:00
22 changed files with 571 additions and 497 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ final class ChatModel: ObservableObject {
@Published var remoteCtrlSession: RemoteCtrlSession?
// currently showing invitation
@Published var showingInvitation: ShowingInvitation?
@Published var migrationState: MigrationFromAnotherDeviceState? = MigrationFromAnotherDeviceState.transform()
@Published var migrationState: MigrationToState? = MigrationToDeviceState.makeMigrationState()
// audio recording and playback
@Published var stopPreviousRecPlay: URL? = nil // coordinates currently playing source
@Published var draft: ComposeState?
@@ -264,7 +264,9 @@ struct ChatListView: View {
}
func filtered(_ chat: Chat) -> Bool {
(chat.chatInfo.chatSettings?.favorite ?? false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
(chat.chatInfo.chatSettings?.favorite ?? false) ||
chat.chatStats.unreadChat ||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
}
func viewNameContains(_ cInfo: ChatInfo, _ s: String) -> Bool {
@@ -1,5 +1,5 @@
//
// MigrateToAnotherDevice.swift
// MigrateFromDevice.swift
// SimpleX (iOS)
//
// Created by Avently on 14.02.2024.
@@ -9,7 +9,7 @@
import SwiftUI
import SimpleXChat
private enum MigrationToState: Equatable {
private enum MigrationFromState: Equatable {
case chatStopInProgress
case chatStopFailed(reason: String)
case passphraseNotSet
@@ -23,7 +23,7 @@ private enum MigrationToState: Equatable {
case finished(chatDeletion: Bool)
}
private enum MigrateToAnotherDeviceViewAlert: Identifiable {
private enum MigrateFromDeviceViewAlert: Identifiable {
case deleteChat(_ title: LocalizedStringKey = "Delete chat profile?", _ text: LocalizedStringKey = "This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost.")
case startChat(_ title: LocalizedStringKey = "Start chat?", _ text: LocalizedStringKey = "Warning: starting chat on multiple devices is not supported and will cause message delivery failures")
@@ -51,15 +51,15 @@ private enum MigrateToAnotherDeviceViewAlert: Identifiable {
}
}
struct MigrateToAnotherDevice: View {
struct MigrateFromDevice: View {
@EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
@Binding var showSettings: Bool
@Binding var showProgressOnSettings: Bool
@State private var migrationState: MigrationToState = .chatStopInProgress
@State private var migrationState: MigrationFromState = .chatStopInProgress
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@AppStorage(GROUP_DEFAULT_INITIAL_RANDOM_DB_PASSPHRASE, store: groupDefaults) private var initialRandomDBPassphrase: Bool = false
@State private var alert: MigrateToAnotherDeviceViewAlert?
@State private var alert: MigrateFromDeviceViewAlert?
@State private var authorized = !UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA)
private let tempDatabaseUrl = urlForTemporaryDatabase()
@State private var chatReceiver: MigrationChatReceiver? = nil
@@ -108,11 +108,8 @@ struct MigrateToAnotherDevice: View {
})
.onChange(of: migrationState) { state in
backDisabled = switch migrationState {
case .archiving: true
case .linkCreation: true
case .linkShown: true
case .finished: true
default: false
case .chatStopInProgress, .archiving, .linkShown, .finished: true
case .chatStopFailed, .passphraseNotSet, .passphraseConfirmation, .uploadConfirmation, .uploadProgress, .uploadFailed, .linkCreation: false
}
}
.onAppear {
@@ -120,7 +117,7 @@ struct MigrateToAnotherDevice: View {
}
.onDisappear {
Task {
if case .linkCreation = migrationState {} else if case .linkShown = migrationState {} else if case .finished = migrationState {} else {
if !backDisabled {
await MainActor.run {
showProgressOnSettings = true
}
@@ -252,7 +249,7 @@ struct MigrateToAnotherDevice: View {
}
}
let ratio = Float(uploadedBytes) / Float(totalBytes)
MigrateToAnotherDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded")
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: uploadedBytes, countStyle: .binary)) uploaded")
}
.onAppear {
startUploading(totalBytes, archivePath)
@@ -306,7 +303,10 @@ struct MigrateToAnotherDevice: View {
}
}
} footer: {
Text("Choose _Migrate from another device_ on the new device and scan QR code.")
VStack(alignment: .leading, spacing: 16) {
Text("**Warning**: the archive will be removed.")
Text("Choose _Migrate from another device_ on the new device and scan QR code.")
}
.font(.callout)
}
Section("Show QR code") {
@@ -498,6 +498,9 @@ struct MigrateToAnotherDevice: View {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
migrationState = .linkShown(fileId: fileTransferMeta.fileId, link: data.addToLink(link: rcvURIs[0]), archivePath: archivePath, ctrl: ctrl)
}
case .sndFileError:
alert = .error(title: "Upload failed", error: "Check your internet connection and try again")
migrationState = .uploadFailed(totalBytes: totalBytes, archivePath: archivePath)
default:
logger.debug("unsupported event: \(msg.responseType)")
}
@@ -587,12 +590,12 @@ struct MigrateToAnotherDevice: View {
}
private struct PassphraseConfirmationView: View {
@Binding var migrationState: MigrationToState
@Binding var migrationState: MigrationFromState
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State private var currentKey: String = ""
@State private var verifyingPassphrase: Bool = false
@FocusState private var keyboardVisible: Bool
@Binding var alert: MigrateToAnotherDeviceViewAlert?
@Binding var alert: MigrateFromDeviceViewAlert?
var body: some View {
ZStack {
@@ -638,13 +641,17 @@ private struct PassphraseConfirmationView: View {
await MainActor.run {
migrationState = .uploadConfirmation
}
} catch {
showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert)
} catch let error {
if case .chatCmdError(_, .errorDatabase(.errorOpen(.errorNotADatabase))) = error as? ChatResponse {
showErrorOnMigrationIfNeeded(.errorNotADatabase(dbFile: ""), $alert)
} else {
alert = .error(title: "Error", error: NSLocalizedString("Error verifying passphrase:", comment: "") + " " + String(String(describing: error)))
}
}
}
}
private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateToAnotherDeviceViewAlert?>) {
private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateFromDeviceViewAlert?>) {
switch status {
case .invalidConfirmation:
alert.wrappedValue = .invalidConfirmation()
@@ -720,8 +727,8 @@ private class MigrationChatReceiver {
}
}
struct MigrateToAnotherDevice_Previews: PreviewProvider {
struct MigrateFromDevice_Previews: PreviewProvider {
static var previews: some View {
MigrateToAnotherDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false))
MigrateFromDevice(showSettings: Binding.constant(true), showProgressOnSettings: Binding.constant(false))
}
}
@@ -1,5 +1,5 @@
//
// MigrateFromAnotherDevice.swift
// MigrateToDevice.swift
// SimpleX (iOS)
//
// Created by Avently on 23.02.2024.
@@ -9,56 +9,47 @@
import SwiftUI
import SimpleXChat
enum MigrationFromAnotherDeviceState: Codable, Equatable {
enum MigrationToDeviceState: Codable, Equatable {
case downloadProgress(link: String, archiveName: String)
case archiveImport(archiveName: String)
case passphrase
func makeMigrationState() -> MigrationFromState {
var initial: MigrationFromState = .pasteOrScanLink
//logger.debug("Inited with migrationState: \(String(describing: self))")
switch self {
case let .downloadProgress(link, archiveName):
// iOS changes absolute directory every launch, check this way
let archivePath = getMigrationTempFilesDirectory().path + "/" + archiveName
initial = .downloadFailed(totalBytes: 0, link: link, archivePath: archivePath)
// Here we check whether it's needed to show migration process after app restart or not
// It's important to NOT show the process when archive was corrupted/not fully downloaded
static func makeMigrationState() -> MigrationToState? {
let state: MigrationToDeviceState? = UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_TO_STAGE) != nil ? decodeJSON(UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_TO_STAGE)!) : nil
var initial: MigrationToState? = .pasteOrScanLink
//logger.debug("Inited with migrationState: \(String(describing: state))")
switch state {
case nil:
initial = nil
case .downloadProgress:
// No migration happens at the moment actually since archive were not downloaded fully
logger.debug("MigrateToDevice: archive wasn't fully downloaded, removed broken file")
initial = nil
case let .archiveImport(archiveName):
let archivePath = getMigrationTempFilesDirectory().path + "/" + archiveName
initial = .archiveImportFailed(archivePath: archivePath)
case .passphrase:
initial = .passphrase(passphrase: "")
}
if initial == nil {
UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_TO_STAGE)
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
}
return initial
}
// Here we check whether it's needed to show migration process after app restart or not
// It's important to NOT show the process when archive was corrupted/not fully downloaded
static func transform() -> MigrationFromAnotherDeviceState? {
let state: MigrationFromAnotherDeviceState? = UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_STAGE) != nil ? decodeJSON(UserDefaults.standard.string(forKey: DEFAULT_MIGRATION_STAGE)!) : nil
if case let .downloadProgress(_, archiveName) = state {
// iOS changes absolute directory every launch, check this way
let archivePath = getMigrationTempFilesDirectory().path + "/" + archiveName
try? FileManager.default.removeItem(atPath: archivePath)
UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_STAGE)
// No migration happens at the moment actually since archive were not downloaded fully
logger.debug("MigrateFromDevice: archive wasn't fully downloaded, removed broken file")
return nil
}
return state
}
static func save(_ state: MigrationFromAnotherDeviceState?, apply: (MigrationFromAnotherDeviceState?) -> Void) {
static func save(_ state: MigrationToDeviceState?) {
if let state {
UserDefaults.standard.setValue(encodeJSON(state), forKey: DEFAULT_MIGRATION_STAGE)
UserDefaults.standard.setValue(encodeJSON(state), forKey: DEFAULT_MIGRATION_TO_STAGE)
} else {
UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_STAGE)
UserDefaults.standard.removeObject(forKey: DEFAULT_MIGRATION_TO_STAGE)
}
apply(state)
}
}
enum MigrationFromState: Equatable {
enum MigrationToState: Equatable {
case pasteOrScanLink
case linkDownloading(link: String)
case downloadProgress(downloadedBytes: Int64, totalBytes: Int64, fileId: Int64, link: String, archivePath: String, ctrl: chat_ctrl?)
@@ -71,7 +62,7 @@ enum MigrationFromState: Equatable {
case onion(appSettings: AppSettings)
}
private enum MigrateFromAnotherDeviceViewAlert: Identifiable {
private enum MigrateToDeviceViewAlert: Identifiable {
case chatImportedWithErrors(title: LocalizedStringKey = "Chat database imported",
text: LocalizedStringKey = "Some non-fatal errors occurred during import - you may see Chat console for more details.")
@@ -98,13 +89,13 @@ private enum MigrateFromAnotherDeviceViewAlert: Identifiable {
}
}
struct MigrateFromAnotherDevice: View {
struct MigrateToDevice: View {
@EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
@AppStorage(DEFAULT_DEVELOPER_TOOLS) private var developerTools = false
@State var migrationState: MigrationFromState
@Binding var migrationState: MigrationToState?
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State private var alert: MigrateFromAnotherDeviceViewAlert?
@State private var alert: MigrateToDeviceViewAlert?
private let tempDatabaseUrl = urlForTemporaryDatabase()
@State private var chatReceiver: MigrationChatReceiver? = nil
// Prevent from hiding the view until migration is finished or app deleted
@@ -114,6 +105,7 @@ struct MigrateFromAnotherDevice: View {
var body: some View {
VStack {
switch migrationState {
case nil: EmptyView()
case .pasteOrScanLink:
pasteOrScanLinkView()
case let .linkDownloading(link):
@@ -138,18 +130,14 @@ struct MigrateFromAnotherDevice: View {
}
.onAppear {
backDisabled = switch migrationState {
case .linkDownloading: false
case .downloadProgress: false
case .archiveImportFailed: false
default: m.migrationState != nil
case nil, .pasteOrScanLink, .linkDownloading, .downloadProgress, .downloadFailed, .archiveImportFailed: false
case .archiveImport, .passphrase, .migrationConfirmation, .migration, .onion: true
}
}
.onChange(of: migrationState) { state in
backDisabled = switch state {
case .linkDownloading: false
case .downloadProgress: false
case .archiveImportFailed: false
default: m.migrationState != nil
case nil, .pasteOrScanLink, .linkDownloading, .downloadProgress, .downloadFailed, .archiveImportFailed: false
case .archiveImport, .passphrase, .migrationConfirmation, .migration, .onion: true
}
}
.onDisappear {
@@ -164,7 +152,7 @@ struct MigrateFromAnotherDevice: View {
chatReceiver?.stopAndCleanUp()
if !backDisabled {
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
MigrationFromAnotherDeviceState.save(nil) { m.migrationState = $0 }
MigrationToDeviceState.save(nil)
}
}
}
@@ -255,7 +243,7 @@ struct MigrateFromAnotherDevice: View {
}
}
let ratio = Float(downloadedBytes) / Float(max(totalBytes, 1))
MigrateToAnotherDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded")
MigrateFromDevice.largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded")
}
}
@@ -280,7 +268,7 @@ struct MigrateFromAnotherDevice: View {
.onAppear {
chatReceiver?.stopAndCleanUp()
try? FileManager.default.removeItem(atPath: archivePath)
MigrationFromAnotherDeviceState.save(nil) { m.migrationState = $0 }
MigrationToDeviceState.save(nil)
}
}
@@ -446,11 +434,16 @@ struct MigrateFromAnotherDevice: View {
switch msg {
case let .rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer):
migrationState = .downloadProgress(downloadedBytes: receivedSize, totalBytes: totalSize, fileId: rcvFileTransfer.fileId, link: link, archivePath: archivePath, ctrl: ctrl)
MigrationFromAnotherDeviceState.save(.downloadProgress(link: link, archiveName: URL(fileURLWithPath: archivePath).lastPathComponent)) { m.migrationState = $0 }
MigrationToDeviceState.save(.downloadProgress(link: link, archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
case .rcvStandaloneFileComplete:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
migrationState = .archiveImport(archivePath: archivePath)
MigrationFromAnotherDeviceState.save(.archiveImport(archiveName: URL(fileURLWithPath: archivePath).lastPathComponent)) { m.migrationState = $0 }
// User closed the whole screen before new state was saved
if migrationState == nil {
MigrationToDeviceState.save(nil)
} else {
migrationState = .archiveImport(archivePath: archivePath)
MigrationToDeviceState.save(.archiveImport(archiveName: URL(fileURLWithPath: archivePath).lastPathComponent))
}
}
case .rcvFileError:
alert = .error(title: "Download failed", error: "File was deleted or link is invalid")
@@ -487,7 +480,7 @@ struct MigrateFromAnotherDevice: View {
}
await MainActor.run {
migrationState = .passphrase(passphrase: "")
MigrationFromAnotherDeviceState.save(.passphrase) { m.migrationState = $0 }
MigrationToDeviceState.save(.passphrase)
}
} catch let error {
await MainActor.run {
@@ -523,11 +516,12 @@ struct MigrateFromAnotherDevice: View {
resetChatCtrl()
try initializeChat(start: false, confirmStart: false, dbKey: passphrase, refreshInvitations: true, confirmMigrations: confirmation)
var appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport())
let hasOnionConfigured = appSettings.networkConfig?.socksProxy != nil || appSettings.networkConfig?.hostMode == .onionHost
appSettings.networkConfig?.socksProxy = nil
appSettings.networkConfig?.hostMode = .publicHost
appSettings.networkConfig?.requiredHostMode = true
await MainActor.run {
if appSettings.networkConfig?.hostMode == .onionViaSocks || appSettings.networkConfig?.hostMode == .onionHost || appSettings.networkConfig?.socksProxy != nil {
appSettings.networkConfig?.socksProxy = nil
appSettings.networkConfig?.hostMode = .publicHost
appSettings.networkConfig?.requiredHostMode = true
if hasOnionConfigured {
migrationState = .onion(appSettings: appSettings)
} else {
finishMigration(appSettings)
@@ -543,7 +537,7 @@ struct MigrateFromAnotherDevice: View {
private func finishMigration(_ appSettings: AppSettings) {
do {
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
MigrationFromAnotherDeviceState.save(nil) { m.migrationState = $0 }
MigrationToDeviceState.save(nil)
appSettings.importIntoApp()
try SimpleX.startChat(refreshInvitations: true)
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Finalize migration on another device.")
@@ -569,12 +563,12 @@ struct MigrateFromAnotherDevice: View {
}
private struct PassphraseEnteringView: View {
@Binding var migrationState: MigrationFromState
@Binding var migrationState: MigrationToState?
@State private var useKeychain = true
@State var currentKey: String
@State private var verifyingPassphrase: Bool = false
@FocusState private var keyboardVisible: Bool
@Binding var alert: MigrateFromAnotherDeviceViewAlert?
@Binding var alert: MigrateToDeviceViewAlert?
var body: some View {
ZStack {
@@ -643,7 +637,7 @@ private struct PassphraseEnteringView: View {
}
}
private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateFromAnotherDeviceViewAlert?>) {
private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateToDeviceViewAlert?>) {
switch status {
case .invalidConfirmation:
alert.wrappedValue = .invalidConfirmation()
@@ -713,8 +707,8 @@ private class MigrationChatReceiver {
}
}
struct MigrateFromAnotherDevice_Previews: PreviewProvider {
struct MigrateToDevice_Previews: PreviewProvider {
static var previews: some View {
MigrateFromAnotherDevice(migrationState: .pasteOrScanLink)
MigrateToDevice(migrationState: Binding.constant(.pasteOrScanLink))
}
}
@@ -13,8 +13,6 @@ struct SimpleXInfo: View {
@EnvironmentObject var m: ChatModel
@Environment(\.colorScheme) var colorScheme: ColorScheme
@State private var showHowItWorks = false
@State private var migrationState: MigrationFromState? = nil
@State private var migrateFromAnotherDevice: Bool = false
var onboarding: Bool
var body: some View {
@@ -49,8 +47,7 @@ struct SimpleXInfo: View {
Spacer()
Button {
migrationState = nil
migrateFromAnotherDevice = true
m.migrationState = .pasteOrScanLink
} label: {
Label("Migrate from another device", systemImage: "tray.and.arrow.down")
.font(.subheadline)
@@ -71,16 +68,15 @@ struct SimpleXInfo: View {
}
.frame(minHeight: g.size.height)
}
.onAppear {
if m.migrationState != nil {
migrationState = m.migrationState?.makeMigrationState()
migrateFromAnotherDevice = true
}
}
.sheet(isPresented: $migrateFromAnotherDevice) {
.sheet(isPresented: Binding(
get: { m.migrationState != nil },
set: { _ in
m.migrationState = nil
MigrationToDeviceState.save(nil) }
)) {
NavigationView {
VStack(alignment: .leading) {
MigrateFromAnotherDevice(migrationState: migrationState ?? .pasteOrScanLink)
MigrateToDevice(migrationState: $m.migrationState)
}
.navigationTitle("Migrate here")
.background(colorScheme == .light ? Color(uiColor: .tertiarySystemGroupedBackground) : .clear)
@@ -51,7 +51,8 @@ let DEFAULT_SHOW_HIDDEN_PROFILES_NOTICE = "showHiddenProfilesNotice"
let DEFAULT_SHOW_MUTE_PROFILE_ALERT = "showMuteProfileAlert"
let DEFAULT_WHATS_NEW_VERSION = "defaultWhatsNewVersion"
let DEFAULT_ONBOARDING_STAGE = "onboardingStage"
let DEFAULT_MIGRATION_STAGE = "migrationStage"
let DEFAULT_MIGRATION_TO_STAGE = "migrationToStage"
let DEFAULT_MIGRATION_FROM_STAGE = "migrationFromStage"
let DEFAULT_CUSTOM_DISAPPEARING_MESSAGE_TIME = "customDisappearingMessageTime"
let DEFAULT_SHOW_UNREAD_AND_FAVORITES = "showUnreadAndFavorites"
let DEFAULT_DEVICE_NAME_FOR_REMOTE_ACCESS = "deviceNameForRemoteAccess"
@@ -212,7 +213,7 @@ struct SettingsView: View {
}
NavigationLink {
MigrateToAnotherDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
MigrateFromDevice(showSettings: $showSettings, showProgressOnSettings: $showProgress)
.navigationTitle("Migrate device")
.navigationBarTitleDisplayMode(.large)
} label: {
+8 -8
View File
@@ -191,8 +191,8 @@
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; };
8C69FE7D2B8C7D2700267E38 /* AppSettings.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */; };
8C7D949A2B88952700B7B9E1 /* MigrateFromAnotherDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateFromAnotherDevice.swift */; };
8C7DF3202B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift */; };
8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */; };
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */; };
D7197A1829AE89660055C05A /* WebRTC in Frameworks */ = {isa = PBXBuildFile; productRef = D7197A1729AE89660055C05A /* WebRTC */; };
D72A9088294BD7A70047C86D /* NativeTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = D72A9087294BD7A70047C86D /* NativeTextEditor.swift */; };
D741547829AF89AF0022400A /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D741547729AF89AF0022400A /* StoreKit.framework */; };
@@ -487,8 +487,8 @@
64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IncognitoHelp.swift; sourceTree = "<group>"; };
8C05382D2B39887E006436DC /* VideoUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VideoUtils.swift; sourceTree = "<group>"; };
8C69FE7C2B8C7D2700267E38 /* AppSettings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppSettings.swift; sourceTree = "<group>"; };
8C7D94992B88952700B7B9E1 /* MigrateFromAnotherDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromAnotherDevice.swift; sourceTree = "<group>"; };
8C7DF31F2B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAnotherDevice.swift; sourceTree = "<group>"; };
8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToDevice.swift; sourceTree = "<group>"; };
8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateFromDevice.swift; sourceTree = "<group>"; };
D72A9087294BD7A70047C86D /* NativeTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeTextEditor.swift; sourceTree = "<group>"; };
D741547729AF89AF0022400A /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/StoreKit.framework; sourceTree = DEVELOPER_DIR; };
D741547929AF90B00022400A /* PushKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = PushKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS16.1.sdk/System/Library/Frameworks/PushKit.framework; sourceTree = DEVELOPER_DIR; };
@@ -914,8 +914,8 @@
8C7D94982B8894D300B7B9E1 /* Migration */ = {
isa = PBXGroup;
children = (
8C7DF31F2B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift */,
8C7D94992B88952700B7B9E1 /* MigrateFromAnotherDevice.swift */,
8C7DF31F2B7CDB0A00C886D0 /* MigrateFromDevice.swift */,
8C7D94992B88952700B7B9E1 /* MigrateToDevice.swift */,
);
path = Migration;
sourceTree = "<group>";
@@ -1151,7 +1151,7 @@
5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */,
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */,
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
8C7D949A2B88952700B7B9E1 /* MigrateFromAnotherDevice.swift in Sources */,
8C7D949A2B88952700B7B9E1 /* MigrateToDevice.swift in Sources */,
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */,
@@ -1249,7 +1249,7 @@
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
8C7DF3202B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift in Sources */,
8C7DF3202B7CDB0A00C886D0 /* MigrateFromDevice.swift in Sources */,
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,
+6 -1
View File
@@ -68,14 +68,19 @@ public func chatInitTemporaryDatabase(url: URL, key: String? = nil, confirmation
public func chatInitControllerRemovingDatabases() {
let dbPath = getAppDatabasePath().path
let fm = FileManager.default
// Remove previous databases, otherwise, can be .errorNotADatabase with nil controller
try? fm.removeItem(atPath: dbPath + CHAT_DB)
try? fm.removeItem(atPath: dbPath + AGENT_DB)
let dbKey = randomDatabasePassword()
logger.debug("chatInitControllerRemovingDatabases path: \(dbPath)")
var cPath = dbPath.cString(using: .utf8)!
var cKey = dbKey.cString(using: .utf8)!
var cConfirm = MigrationConfirmation.error.rawValue.cString(using: .utf8)!
chat_migrate_init_key(&cPath, &cKey, 1, &cConfirm, 0, &chatController)
// We need only controller, not databases
let fm = FileManager.default
try? fm.removeItem(atPath: dbPath + CHAT_DB)
try? fm.removeItem(atPath: dbPath + AGENT_DB)
}
@@ -13,7 +13,8 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.call.*
import chat.simplex.common.views.chat.ComposeState
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrationFromAnotherDeviceState
import chat.simplex.common.views.migration.MigrationToDeviceState
import chat.simplex.common.views.migration.MigrationToState
import chat.simplex.res.MR
import dev.icerock.moko.resources.ImageResource
import dev.icerock.moko.resources.StringResource
@@ -105,7 +106,7 @@ object ChatModel {
// currently showing invitation
val showingInvitation = mutableStateOf(null as ShowingInvitation?)
val migrationState: MutableState<MigrationFromAnotherDeviceState?> by lazy { mutableStateOf(MigrationFromAnotherDeviceState.transform()) }
val migrationState: MutableState<MigrationToState?> by lazy { mutableStateOf(MigrationToDeviceState.makeMigrationState()) }
var draft = mutableStateOf(null as ComposeState?)
var draftChatId = mutableStateOf(null as String?)
@@ -147,7 +147,8 @@ class AppPreferences {
val appLanguage = mkStrPreference(SHARED_PREFS_APP_LANGUAGE, null)
val onboardingStage = mkEnumPreference(SHARED_PREFS_ONBOARDING_STAGE, OnboardingStage.OnboardingComplete) { OnboardingStage.values().firstOrNull { it.name == this } }
val migrationStage = mkStrPreference(SHARED_PREFS_MIGRATION_STAGE, null)
val migrationToStage = mkStrPreference(SHARED_PREFS_MIGRATION_TO_STAGE, null)
val migrationFromStage = mkStrPreference(SHARED_PREFS_MIGRATION_FROM_STAGE, null)
val storeDBPassphrase = mkBoolPreference(SHARED_PREFS_STORE_DB_PASSPHRASE, true)
val initialRandomDBPassphrase = mkBoolPreference(SHARED_PREFS_INITIAL_RANDOM_DB_PASSPHRASE, false)
val encryptedDBPassphrase = mkStrPreference(SHARED_PREFS_ENCRYPTED_DB_PASSPHRASE, null)
@@ -286,7 +287,8 @@ class AppPreferences {
private const val SHARED_PREFS_CHAT_ARCHIVE_TIME = "ChatArchiveTime"
private const val SHARED_PREFS_APP_LANGUAGE = "AppLanguage"
private const val SHARED_PREFS_ONBOARDING_STAGE = "OnboardingStage"
const val SHARED_PREFS_MIGRATION_STAGE = "MigrationStage"
const val SHARED_PREFS_MIGRATION_TO_STAGE = "MigrationToStage"
const val SHARED_PREFS_MIGRATION_FROM_STAGE = "MigrationFromStage"
private const val SHARED_PREFS_CHAT_LAST_START = "ChatLastStart"
private const val SHARED_PREFS_CHAT_STOPPED = "ChatStopped"
private const val SHARED_PREFS_DEVELOPER_TOOLS = "DeveloperTools"
@@ -704,9 +706,11 @@ object ChatController {
throw Exception("failed to set storage encryption: ${r.responseType} ${r.details}")
}
suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): Boolean {
suspend fun testStorageEncryption(key: String, ctrl: ChatCtrl? = null): CR.ChatCmdError? {
val r = sendCmd(null, CC.TestStorageEncryption(key), ctrl)
return r is CR.CmdOk
if (r is CR.CmdOk) return null
else if (r is CR.ChatCmdError) return r
throw Exception("failed to test storage encryption: ${r.responseType} ${r.details}")
}
suspend fun apiGetChats(rh: Long?): List<Chat> {
@@ -152,6 +152,10 @@ fun chatInitTemporaryDatabase(dbPath: String, key: String? = null, confirmation:
fun chatInitControllerRemovingDatabases() {
val dbPath = dbAbsolutePrefixPath
// Remove previous databases, otherwise, can be .errorNotADatabase with null controller
File(dbPath + "_chat.db").delete()
File(dbPath + "_agent.db").delete()
val dbKey = randomDatabasePassword()
Log.d(TAG, "chatInitControllerRemovingDatabases path: $dbPath")
val migrated = chatMigrateInit(dbPath, dbKey, MigrationConfirmation.Error.value)
@@ -535,7 +535,9 @@ private fun filteredChats(
}
private fun filtered(chat: Chat): Boolean =
(chat.chatInfo.chatSettings?.favorite ?: false) || chat.chatStats.unreadCount > 0 || chat.chatStats.unreadChat
(chat.chatInfo.chatSettings?.favorite ?: false) ||
chat.chatStats.unreadChat ||
(chat.chatInfo.ntfsEnabled && chat.chatStats.unreadCount > 0)
private fun viewNameContains(cInfo: ChatInfo, s: String): Boolean =
cInfo.chatViewName.lowercase().contains(s.lowercase())
@@ -79,39 +79,49 @@ data class MigrationFileLinkData(
@Serializable
private sealed class MigrationToState {
@Serializable object ChatStopInProgress: MigrationToState()
@Serializable data class ChatStopFailed(val reason: String): MigrationToState()
@Serializable object PassphraseNotSet: MigrationToState()
@Serializable object PassphraseConfirmation: MigrationToState()
@Serializable object UploadConfirmation: MigrationToState()
@Serializable object Archiving: MigrationToState()
@Serializable data class DatabaseInit(val totalBytes: Long, val archivePath: String): MigrationToState()
@Serializable data class UploadProgress(val uploadedBytes: Long, val totalBytes: Long, val fileId: Long, val archivePath: String, val ctrl: ChatCtrl, val user: User): MigrationToState()
@Serializable data class UploadFailed(val totalBytes: Long, val archivePath: String): MigrationToState()
@Serializable object LinkCreation: MigrationToState()
@Serializable data class LinkShown(val fileId: Long, val link: String, val ctrl: ChatCtrl): MigrationToState()
@Serializable data class Finished(val chatDeletion: Boolean): MigrationToState()
private sealed class MigrationFromState {
@Serializable object ChatStopInProgress: MigrationFromState()
@Serializable data class ChatStopFailed(val reason: String): MigrationFromState()
@Serializable object PassphraseNotSet: MigrationFromState()
@Serializable object PassphraseConfirmation: MigrationFromState()
@Serializable object UploadConfirmation: MigrationFromState()
@Serializable object Archiving: MigrationFromState()
@Serializable data class DatabaseInit(val totalBytes: Long, val archivePath: String): MigrationFromState()
@Serializable data class UploadProgress(val uploadedBytes: Long, val totalBytes: Long, val fileId: Long, val archivePath: String, val ctrl: ChatCtrl, val user: User): MigrationFromState()
@Serializable data class UploadFailed(val totalBytes: Long, val archivePath: String): MigrationFromState()
@Serializable object LinkCreation: MigrationFromState()
@Serializable data class LinkShown(val fileId: Long, val link: String, val ctrl: ChatCtrl): MigrationFromState()
@Serializable data class Finished(val chatDeletion: Boolean): MigrationFromState()
}
private var MutableState<MigrationToState>.state: MigrationToState
private var MutableState<MigrationFromState>.state: MigrationFromState
get() = value
set(v) { value = v }
@Composable
fun MigrateToAnotherDeviceView(close: () -> Unit) {
val migrationState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf<MigrationToState>(MigrationToState.ChatStopInProgress) }
fun MigrateFromDeviceView(close: () -> Unit) {
val migrationState = rememberSaveable(stateSaver = serializableSaver()) { mutableStateOf<MigrationFromState>(MigrationFromState.ChatStopInProgress) }
// Prevent from hiding the view until migration is finished or app deleted
val backDisabled = remember {
derivedStateOf {
migrationState.value is MigrationToState.DatabaseInit ||
migrationState.value is MigrationToState.Archiving ||
migrationState.value is MigrationToState.LinkCreation ||
migrationState.value is MigrationToState.LinkShown ||
migrationState.value is MigrationToState.Finished
when (migrationState.value) {
is MigrationFromState.ChatStopInProgress,
is MigrationFromState.DatabaseInit,
is MigrationFromState.Archiving,
is MigrationFromState.LinkShown,
is MigrationFromState.Finished -> true
is MigrationFromState.ChatStopFailed,
is MigrationFromState.PassphraseNotSet,
is MigrationFromState.PassphraseConfirmation,
is MigrationFromState.UploadConfirmation,
is MigrationFromState.UploadProgress,
is MigrationFromState.UploadFailed,
is MigrationFromState.LinkCreation -> false
}
}
}
val chatReceiver = remember { mutableStateOf(null as MigrationToChatReceiver?) }
val chatReceiver = remember { mutableStateOf(null as MigrationFromChatReceiver?) }
ModalView(
enableClose = !backDisabled.value,
close = {
@@ -121,7 +131,7 @@ fun MigrateToAnotherDeviceView(close: () -> Unit) {
close()
},
) {
MigrateToAnotherDeviceLayout(
MigrateFromDeviceLayout(
migrationState = migrationState,
chatReceiver = chatReceiver
)
@@ -129,16 +139,16 @@ fun MigrateToAnotherDeviceView(close: () -> Unit) {
}
@Composable
private fun MigrateToAnotherDeviceLayout(
migrationState: MutableState<MigrationToState>,
chatReceiver: MutableState<MigrationToChatReceiver?>
private fun MigrateFromDeviceLayout(
migrationState: MutableState<MigrationFromState>,
chatReceiver: MutableState<MigrationFromChatReceiver?>
) {
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max),
) {
AppBarTitle(stringResource(MR.strings.migrate_to_device))
AppBarTitle(stringResource(MR.strings.migrate_from_device_title))
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver)
SectionBottomSpacer()
}
@@ -147,30 +157,30 @@ private fun MigrateToAnotherDeviceLayout(
@Composable
private fun SectionByState(
migrationState: MutableState<MigrationToState>,
migrationState: MutableState<MigrationFromState>,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationToChatReceiver?>
chatReceiver: MutableState<MigrationFromChatReceiver?>
) {
when (val s = migrationState.value) {
is MigrationToState.ChatStopInProgress -> migrationState.ChatStopInProgressView()
is MigrationToState.ChatStopFailed -> migrationState.ChatStopFailedView(s.reason)
is MigrationToState.PassphraseNotSet -> migrationState.PassphraseNotSetView()
is MigrationToState.PassphraseConfirmation -> migrationState.PassphraseConfirmationView()
is MigrationToState.UploadConfirmation -> migrationState.UploadConfirmationView()
is MigrationToState.Archiving -> migrationState.ArchivingView()
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(tempDatabaseFile, s.totalBytes, s.archivePath)
is MigrationToState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath)
is MigrationToState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value)
is MigrationToState.LinkCreation -> LinkCreationView()
is MigrationToState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl)
is MigrationToState.Finished -> migrationState.FinishedView(s.chatDeletion)
is MigrationFromState.ChatStopInProgress -> migrationState.ChatStopInProgressView()
is MigrationFromState.ChatStopFailed -> migrationState.ChatStopFailedView(s.reason)
is MigrationFromState.PassphraseNotSet -> migrationState.PassphraseNotSetView()
is MigrationFromState.PassphraseConfirmation -> migrationState.PassphraseConfirmationView()
is MigrationFromState.UploadConfirmation -> migrationState.UploadConfirmationView()
is MigrationFromState.Archiving -> migrationState.ArchivingView()
is MigrationFromState.DatabaseInit -> migrationState.DatabaseInitView(tempDatabaseFile, s.totalBytes, s.archivePath)
is MigrationFromState.UploadProgress -> migrationState.UploadProgressView(s.uploadedBytes, s.totalBytes, s.ctrl, s.user, tempDatabaseFile, chatReceiver, s.archivePath)
is MigrationFromState.UploadFailed -> migrationState.UploadFailedView(s.totalBytes, s.archivePath, chatReceiver.value)
is MigrationFromState.LinkCreation -> LinkCreationView()
is MigrationFromState.LinkShown -> migrationState.LinkShownView(s.fileId, s.link, s.ctrl)
is MigrationFromState.Finished -> migrationState.FinishedView(s.chatDeletion)
}
}
@Composable
private fun MutableState<MigrationToState>.ChatStopInProgressView() {
private fun MutableState<MigrationFromState>.ChatStopInProgressView() {
Box {
SectionView(stringResource(MR.strings.migration_to_device_stopping_chat).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_from_device_stopping_chat).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -179,7 +189,7 @@ private fun MutableState<MigrationToState>.ChatStopInProgressView() {
}
@Composable
private fun MutableState<MigrationToState>.ChatStopFailedView(reason: String) {
private fun MutableState<MigrationFromState>.ChatStopFailedView(reason: String) {
SectionView(stringResource(MR.strings.error_stopping_chat).uppercase()) {
Text(reason)
SectionSpacer()
@@ -189,22 +199,22 @@ private fun MutableState<MigrationToState>.ChatStopFailedView(reason: String) {
textColor = MaterialTheme.colors.error,
click = ::stopChat
){}
SectionTextFooter(stringResource(MR.strings.migration_to_device_chat_should_be_stopped))
SectionTextFooter(stringResource(MR.strings.migrate_from_device_chat_should_be_stopped))
}
}
@Composable
private fun MutableState<MigrationToState>.PassphraseNotSetView() {
private fun MutableState<MigrationFromState>.PassphraseNotSetView() {
DatabaseEncryptionView(chatModel, true)
KeyChangeEffect(appPreferences.initialRandomDBPassphrase.state.value) {
if (!appPreferences.initialRandomDBPassphrase.get()) {
state = MigrationToState.UploadConfirmation
state = MigrationFromState.UploadConfirmation
}
}
}
@Composable
private fun MutableState<MigrationToState>.PassphraseConfirmationView() {
private fun MutableState<MigrationFromState>.PassphraseConfirmationView() {
val useKeychain = remember { appPreferences.storeDBPassphrase.get() }
val currentKey = rememberSaveable { mutableStateOf("") }
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
@@ -214,12 +224,12 @@ private fun MutableState<MigrationToState>.PassphraseConfirmationView() {
ChatStoppedView()
SectionSpacer()
SectionView(stringResource(MR.strings.migration_to_device_verify_database_passphrase).uppercase()) {
SectionView(stringResource(MR.strings.migrate_from_device_verify_database_passphrase).uppercase()) {
PassphraseField(currentKey, placeholder = stringResource(MR.strings.current_passphrase), Modifier.padding(horizontal = DEFAULT_PADDING), isValid = ::validKey, requestFocus = true)
SettingsActionItemWithContent(
icon = painterResource(if (useKeychain) MR.images.ic_vpn_key_filled else MR.images.ic_lock),
text = stringResource(MR.strings.migration_to_device_verify_passphrase),
text = stringResource(MR.strings.migrate_from_device_verify_passphrase),
textColor = MaterialTheme.colors.primary,
disabled = verifyingPassphrase.value || currentKey.value.isEmpty(),
click = {
@@ -231,7 +241,7 @@ private fun MutableState<MigrationToState>.PassphraseConfirmationView() {
}
}
) {}
SectionTextFooter(stringResource(MR.strings.migration_to_device_confirm_you_remember_passphrase))
SectionTextFooter(stringResource(MR.strings.migrate_from_device_confirm_you_remember_passphrase))
}
}
if (verifyingPassphrase.value) {
@@ -241,22 +251,22 @@ private fun MutableState<MigrationToState>.PassphraseConfirmationView() {
}
@Composable
private fun MutableState<MigrationToState>.UploadConfirmationView() {
SectionView(stringResource(MR.strings.migration_to_device_confirm_upload).uppercase()) {
private fun MutableState<MigrationFromState>.UploadConfirmationView() {
SectionView(stringResource(MR.strings.migrate_from_device_confirm_upload).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_ios_share),
text = stringResource(MR.strings.migration_to_device_archive_and_upload),
text = stringResource(MR.strings.migrate_from_device_archive_and_upload),
textColor = MaterialTheme.colors.primary,
click = { state = MigrationToState.Archiving }
click = { state = MigrationFromState.Archiving }
){}
SectionTextFooter(stringResource(MR.strings.migration_to_device_all_data_will_be_uploaded))
SectionTextFooter(stringResource(MR.strings.migrate_from_device_all_data_will_be_uploaded))
}
}
@Composable
private fun MutableState<MigrationToState>.ArchivingView() {
private fun MutableState<MigrationFromState>.ArchivingView() {
Box {
SectionView(stringResource(MR.strings.migration_to_device_archiving_database).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_from_device_archiving_database).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -265,9 +275,9 @@ private fun MutableState<MigrationToState>.ArchivingView() {
}
@Composable
private fun MutableState<MigrationToState>.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) {
private fun MutableState<MigrationFromState>.DatabaseInitView(tempDatabaseFile: File, totalBytes: Long, archivePath: String) {
Box {
SectionView(stringResource(MR.strings.migration_to_device_database_init).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_from_device_database_init).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -276,19 +286,19 @@ private fun MutableState<MigrationToState>.DatabaseInitView(tempDatabaseFile: Fi
}
@Composable
private fun MutableState<MigrationToState>.UploadProgressView(
private fun MutableState<MigrationFromState>.UploadProgressView(
uploadedBytes: Long,
totalBytes: Long,
ctrl: ChatCtrl,
user: User,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationToChatReceiver?>,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
archivePath: String,
) {
Box {
SectionView(stringResource(MR.strings.migration_to_device_uploading_archive).uppercase()) {
SectionView(stringResource(MR.strings.migrate_from_device_uploading_archive).uppercase()) {
val ratio = uploadedBytes.toFloat() / max(totalBytes, 1)
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migration_to_device_bytes_uploaded).format(formatBytes(uploadedBytes)))
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_from_device_bytes_uploaded).format(formatBytes(uploadedBytes)))
}
}
LaunchedEffect(Unit) {
@@ -297,17 +307,17 @@ private fun MutableState<MigrationToState>.UploadProgressView(
}
@Composable
private fun MutableState<MigrationToState>.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationToChatReceiver?) {
SectionView(stringResource(MR.strings.migration_to_device_upload_failed).uppercase()) {
private fun MutableState<MigrationFromState>.UploadFailedView(totalBytes: Long, archivePath: String, chatReceiver: MigrationFromChatReceiver?) {
SectionView(stringResource(MR.strings.migrate_from_device_upload_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_ios_share),
text = stringResource(MR.strings.migration_to_device_repeat_upload),
text = stringResource(MR.strings.migrate_from_device_repeat_upload),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationToState.DatabaseInit(totalBytes, archivePath)
state = MigrationFromState.DatabaseInit(totalBytes, archivePath)
}
) {}
SectionTextFooter(stringResource(MR.strings.migration_to_device_try_again))
SectionTextFooter(stringResource(MR.strings.migrate_from_device_try_again))
}
LaunchedEffect(Unit) {
chatReceiver?.stopAndCleanUp()
@@ -317,17 +327,17 @@ private fun MutableState<MigrationToState>.UploadFailedView(totalBytes: Long, ar
@Composable
private fun LinkCreationView() {
Box {
SectionView(stringResource(MR.strings.migration_to_device_creating_archive_link).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_from_device_creating_archive_link).uppercase()) {}
ProgressView()
}
}
@Composable
private fun MutableState<MigrationToState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) {
private fun MutableState<MigrationFromState>.LinkShownView(fileId: Long, link: String, ctrl: ChatCtrl) {
SectionView {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_close),
text = stringResource(MR.strings.migration_to_device_cancel_migration),
text = stringResource(MR.strings.migrate_from_device_cancel_migration),
textColor = MaterialTheme.colors.error,
click = {
cancelMigration(fileId, ctrl)
@@ -335,31 +345,32 @@ private fun MutableState<MigrationToState>.LinkShownView(fileId: Long, link: Str
) {}
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_check),
text = stringResource(MR.strings.migration_to_device_finalize_migration),
text = stringResource(MR.strings.migrate_from_device_finalize_migration),
textColor = MaterialTheme.colors.primary,
click = {
finishMigration(fileId, ctrl)
}
) {}
SectionTextFooter(annotatedStringResource(MR.strings.migration_to_device_choose_migrate_from_another_device))
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_archive_will_be_deleted))
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_choose_migrate_from_another_device))
}
SectionSpacer()
SectionView(stringResource(MR.strings.show_QR_code).uppercase()) {
SimpleXLinkQRCode(link, onShare = {})
}
SectionSpacer()
SectionView(stringResource(MR.strings.migration_to_device_or_share_this_file_link).uppercase()) {
SectionView(stringResource(MR.strings.migrate_from_device_or_share_this_file_link).uppercase()) {
LinkTextView(link, true)
}
}
@Composable
private fun MutableState<MigrationToState>.FinishedView(chatDeletion: Boolean) {
private fun MutableState<MigrationFromState>.FinishedView(chatDeletion: Boolean) {
Box {
SectionView(stringResource(MR.strings.migration_to_device_migration_complete).uppercase()) {
SectionView(stringResource(MR.strings.migrate_from_device_migration_complete).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_delete_forever),
text = stringResource(MR.strings.migration_to_device_delete_database_from_device),
text = stringResource(MR.strings.migrate_from_device_delete_database_from_device),
textColor = MaterialTheme.colors.primary,
click = {
AlertManager.shared.showAlertDialog(
@@ -375,21 +386,21 @@ private fun MutableState<MigrationToState>.FinishedView(chatDeletion: Boolean) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_play_arrow_filled),
text = stringResource(MR.strings.migration_to_device_start_chat),
text = stringResource(MR.strings.migrate_from_device_start_chat),
textColor = MaterialTheme.colors.error,
click = {
AlertManager.shared.showAlertDialog(
title = generalGetString(MR.strings.start_chat_question),
text = generalGetString(MR.strings.migration_to_device_starting_chat_on_multiple_devices_unsupported),
confirmText = generalGetString(MR.strings.migration_to_device_start_chat),
text = generalGetString(MR.strings.migrate_from_device_starting_chat_on_multiple_devices_unsupported),
confirmText = generalGetString(MR.strings.migrate_from_device_start_chat),
onConfirm = {
withLongRunningApi { startChatAndDismiss() }
}
)
}
) {}
SectionTextFooter(annotatedStringResource(MR.strings.migration_to_device_you_must_not_start_database_on_two_device))
SectionTextFooter(annotatedStringResource(MR.strings.migration_to_device_using_on_two_device_breaks_encryption))
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_you_must_not_start_database_on_two_device))
SectionTextFooter(annotatedStringResource(MR.strings.migrate_from_device_using_on_two_device_breaks_encryption))
}
if (chatDeletion) {
ProgressView()
@@ -420,52 +431,58 @@ fun LargeProgressView(value: Float, title: String, description: String) {
}
}
private fun MutableState<MigrationToState>.stopChat() {
private fun MutableState<MigrationFromState>.stopChat() {
withBGApi {
try {
stopChatAsync(chatModel)
try {
controller.apiSaveAppSettings(AppSettings.current.prepareForExport())
state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationToState.PassphraseNotSet else MigrationToState.PassphraseConfirmation
state = if (appPreferences.initialRandomDBPassphrase.get()) MigrationFromState.PassphraseNotSet else MigrationFromState.PassphraseConfirmation
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.migrate_to_device_error_saving_settings),
title = generalGetString(MR.strings.migrate_from_device_error_saving_settings),
text = e.stackTraceToString()
)
state = MigrationToState.ChatStopFailed(reason = generalGetString(MR.strings.migrate_to_device_error_saving_settings))
state = MigrationFromState.ChatStopFailed(reason = generalGetString(MR.strings.migrate_from_device_error_saving_settings))
}
} catch (e: Exception) {
state = MigrationToState.ChatStopFailed(reason = e.stackTraceToString().take(10))
state = MigrationFromState.ChatStopFailed(reason = e.stackTraceToString().take(10))
}
}
}
private suspend fun MutableState<MigrationToState>.verifyDatabasePassphrase(dbKey: String) {
if (controller.testStorageEncryption(dbKey)) {
state = MigrationToState.UploadConfirmation
} else {
private suspend fun MutableState<MigrationFromState>.verifyDatabasePassphrase(dbKey: String) {
val error = controller.testStorageEncryption(dbKey)
if (error == null) {
state = MigrationFromState.UploadConfirmation
} else if (((error.chatError as? ChatError.ChatErrorDatabase)?.databaseError as? DatabaseError.ErrorOpen)?.sqliteError is SQLiteError.ErrorNotADatabase) {
showErrorOnMigrationIfNeeded(DBMigrationResult.ErrorNotADatabase(""))
} else {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.error),
text = generalGetString(MR.strings.migrate_from_device_error_verifying_passphrase) + " " + error.details
)
}
}
private fun MutableState<MigrationToState>.exportArchive() {
private fun MutableState<MigrationFromState>.exportArchive() {
withLongRunningApi {
try {
getMigrationTempFilesDirectory().mkdir()
val archivePath = exportChatArchive(chatModel, getMigrationTempFilesDirectory(), mutableStateOf(""), mutableStateOf(Instant.DISTANT_PAST), mutableStateOf(""))
val totalBytes = File(archivePath).length()
if (totalBytes > 0L) {
state = MigrationToState.DatabaseInit(totalBytes, archivePath)
state = MigrationFromState.DatabaseInit(totalBytes, archivePath)
} else {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_exported_file_doesnt_exist))
state = MigrationToState.UploadConfirmation
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_from_device_exported_file_doesnt_exist))
state = MigrationFromState.UploadConfirmation
}
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.migrate_to_device_error_exporting_archive),
title = generalGetString(MR.strings.migrate_from_device_error_exporting_archive),
text = e.stackTraceToString()
)
state = MigrationToState.UploadConfirmation
state = MigrationFromState.UploadConfirmation
}
}
}
@@ -484,7 +501,7 @@ suspend fun initTemporaryDatabase(tempDatabaseFile: File, netCfg: NetCfg): Pair<
return null
}
private fun MutableState<MigrationToState>.prepareDatabase(
private fun MutableState<MigrationFromState>.prepareDatabase(
tempDatabaseFile: File,
totalBytes: Long,
archivePath: String,
@@ -492,35 +509,35 @@ private fun MutableState<MigrationToState>.prepareDatabase(
withLongRunningApi {
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, getNetCfg())
if (ctrlAndUser == null) {
state = MigrationToState.UploadFailed(totalBytes, archivePath)
state = MigrationFromState.UploadFailed(totalBytes, archivePath)
return@withLongRunningApi
}
val (ctrl, user) = ctrlAndUser
state = MigrationToState.UploadProgress(0L, totalBytes, 0L, archivePath, ctrl, user)
state = MigrationFromState.UploadProgress(0L, totalBytes, 0L, archivePath, ctrl, user)
}
}
private fun MutableState<MigrationToState>.startUploading(
private fun MutableState<MigrationFromState>.startUploading(
totalBytes: Long,
ctrl: ChatCtrl,
user: User,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationToChatReceiver?>,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
archivePath: String,
) {
withBGApi {
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) {
is CR.SndFileProgressXFTP -> {
val s = state
if (s is MigrationToState.UploadProgress && s.uploadedBytes != s.totalBytes) {
state = MigrationToState.UploadProgress(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user)
if (s is MigrationFromState.UploadProgress && s.uploadedBytes != s.totalBytes) {
state = MigrationFromState.UploadProgress(msg.sentSize, msg.totalSize, msg.fileTransferMeta.fileId, archivePath, ctrl, user)
}
}
is CR.SndFileRedirectStartXFTP -> {
delay(500)
state = MigrationToState.LinkCreation
state = MigrationFromState.LinkCreation
}
is CR.SndStandaloneFileComplete -> {
delay(500)
@@ -532,7 +549,14 @@ private fun MutableState<MigrationToState>.startUploading(
requiredHostMode = cfg.requiredHostMode
)
)
state = MigrationToState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl)
state = MigrationFromState.LinkShown(msg.fileTransferMeta.fileId, data.addToLink(msg.rcvURIs[0]), ctrl)
}
is CR.SndFileError -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_from_device_upload_failed),
generalGetString(MR.strings.migrate_from_device_check_connection_and_try_again)
)
state = MigrationFromState.UploadFailed(totalBytes, archivePath)
}
else -> {
Log.d(TAG, "unsupported event: ${msg.responseType}")
@@ -544,13 +568,13 @@ private fun MutableState<MigrationToState>.startUploading(
val (res, error) = controller.uploadStandaloneFile(user, CryptoFile.plain(File(archivePath).name), ctrl)
if (res == null) {
state = MigrationToState.UploadFailed(totalBytes, archivePath)
state = MigrationFromState.UploadFailed(totalBytes, archivePath)
return@withBGApi AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migration_to_device_error_uploading_archive),
generalGetString(MR.strings.migrate_from_device_error_uploading_archive),
error
)
}
state = MigrationToState.UploadProgress(0, res.fileSize, res.fileId, archivePath, ctrl, user)
state = MigrationFromState.UploadProgress(0, res.fileSize, res.fileId, archivePath, ctrl, user)
}
}
@@ -565,19 +589,19 @@ private fun cancelMigration(fileId: Long, ctrl: ChatCtrl) {
}
}
private fun MutableState<MigrationToState>.finishMigration(fileId: Long, ctrl: ChatCtrl) {
private fun MutableState<MigrationFromState>.finishMigration(fileId: Long, ctrl: ChatCtrl) {
withBGApi {
cancelUploadedArchive(fileId, ctrl)
state = MigrationToState.Finished(false)
state = MigrationFromState.Finished(false)
}
}
private fun MutableState<MigrationToState>.deleteChatAndDismiss() {
private fun MutableState<MigrationFromState>.deleteChatAndDismiss() {
withBGApi {
try {
deleteChatAsync(chatModel)
chatModel.chatDbChanged.value = true
state = MigrationToState.Finished(true)
state = MigrationFromState.Finished(true)
try {
initChatController(startChat = { CompletableDeferred(false) })
chatModel.chatDbChanged.value = false
@@ -587,7 +611,7 @@ private fun MutableState<MigrationToState>.deleteChatAndDismiss() {
}
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(
title = generalGetString(MR.strings.migration_to_device_error_deleting_database),
title = generalGetString(MR.strings.migrate_from_device_error_deleting_database),
text = e.stackTraceToString()
)
}
@@ -615,14 +639,14 @@ private suspend fun startChatAndDismiss(dismiss: Boolean = true) {
}
}
private suspend fun MutableState<MigrationToState>.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) {
private suspend fun MutableState<MigrationFromState>.cleanUpOnBack(chatReceiver: MigrationFromChatReceiver?) {
val s = state
if (s !is MigrationToState.LinkShown && s !is MigrationToState.Finished) {
if (s !is MigrationFromState.LinkShown && s !is MigrationFromState.Finished) {
chatModel.switchingUsersAndHosts.value = true
startChatAndDismiss(false)
chatModel.switchingUsersAndHosts.value = false
}
if (s is MigrationToState.UploadProgress) {
if (s is MigrationFromState.UploadProgress) {
cancelUploadedArchive(s.fileId, s.ctrl)
}
chatReceiver?.stopAndCleanUp()
@@ -632,7 +656,7 @@ private suspend fun MutableState<MigrationToState>.cleanUpOnBack(chatReceiver: M
private fun fileForTemporaryDatabase(): File =
File(getMigrationTempFilesDirectory(), generateNewFileName("migration", "db", getMigrationTempFilesDirectory()))
private class MigrationToChatReceiver(
private class MigrationFromChatReceiver(
val ctrl: ChatCtrl,
val databaseUrl: File,
var receiveMessages: Boolean = true,
@@ -13,7 +13,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import chat.simplex.common.model.*
import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_STAGE
import chat.simplex.common.model.AppPreferences.Companion.SHARED_PREFS_MIGRATION_TO_STAGE
import chat.simplex.common.model.ChatController.getNetCfg
import chat.simplex.common.model.ChatController.startChat
import chat.simplex.common.model.ChatCtrl
@@ -39,100 +39,97 @@ import java.util.*
import kotlin.math.max
@Serializable
sealed class MigrationFromAnotherDeviceState {
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationFromAnotherDeviceState()
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationFromAnotherDeviceState()
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationFromAnotherDeviceState()
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationFromAnotherDeviceState()
sealed class MigrationToDeviceState {
@Serializable @SerialName("onion") data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToDeviceState()
@Serializable @SerialName("downloadProgress") data class DownloadProgress(val link: String, val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("archiveImport") data class ArchiveImport(val archiveName: String, val netCfg: NetCfg): MigrationToDeviceState()
@Serializable @SerialName("passphrase") data class Passphrase(val netCfg: NetCfg): MigrationToDeviceState()
companion object {
// Here we check whether it's needed to show migration process after app restart or not
// It's important to NOT show the process when archive was corrupted/not fully downloaded
fun transform(): MigrationFromAnotherDeviceState? {
val stage = settings.getStringOrNull(SHARED_PREFS_MIGRATION_STAGE)
var state: MigrationFromAnotherDeviceState? = if (stage != null) json.decodeFromString(stage) else null
if (state is DownloadProgress) {
// No migration happens at the moment actually since archive were not downloaded fully
Log.e(TAG, "MigrateFromDevice: archive wasn't fully downloaded, removed broken file")
state = null
} else if (state is Onion) {
state = null
} else if (state is ArchiveImport && !File(getMigrationTempFilesDirectory(), state.archiveName).exists()) {
Log.e(TAG, "MigrateFromDevice: archive was removed unintentionally or state is broken, dropping migration")
state = null
fun makeMigrationState(): MigrationToState? {
val stage = settings.getStringOrNull(SHARED_PREFS_MIGRATION_TO_STAGE)
val state: MigrationToDeviceState? = if (stage != null) json.decodeFromString(stage) else null
val initial: MigrationToState? = when(state) {
null -> null
is DownloadProgress -> {
// No migration happens at the moment actually since archive were not downloaded fully
Log.e(TAG, "MigrateToDevice: archive wasn't fully downloaded, removed broken file")
null
}
is Onion -> null
is ArchiveImport -> {
if (!File(getMigrationTempFilesDirectory(), state.archiveName).exists()) {
Log.e(TAG, "MigrateToDevice: archive was removed unintentionally or state is broken, dropping migration")
null
} else {
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
MigrationToState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg)
}
}
is Passphrase -> MigrationToState.Passphrase("", state.netCfg)
}
if (state == null) {
settings.remove(SHARED_PREFS_MIGRATION_STAGE)
if (initial == null) {
settings.remove(SHARED_PREFS_MIGRATION_TO_STAGE)
getMigrationTempFilesDirectory().deleteRecursively()
}
return state
return initial
}
fun save(state: MigrationFromAnotherDeviceState?) {
fun save(state: MigrationToDeviceState?) {
if (state != null) {
appPreferences.migrationStage.set(json.encodeToString(state))
appPreferences.migrationToStage.set(json.encodeToString(state))
} else {
appPreferences.migrationStage.set(null)
appPreferences.migrationToStage.set(null)
}
chatModel.migrationState.value = state
}
}
}
@Serializable
private sealed class MigrationState {
@Serializable object PasteOrScanLink: MigrationState()
@Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationState()
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationState()
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationState()
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationState()
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationState()
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationState()
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationState()
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationState()
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationState()
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationState()
sealed class MigrationToState {
@Serializable object PasteOrScanLink: MigrationToState()
@Serializable data class Onion(val link: String, val socksProxy: String?, val hostMode: HostMode, val requiredHostMode: Boolean): MigrationToState()
@Serializable data class DatabaseInit(val link: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class LinkDownloading(val link: String, val ctrl: ChatCtrl, val user: User, val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class DownloadProgress(val downloadedBytes: Long, val totalBytes: Long, val fileId: Long, val link: String, val archivePath: String, val netCfg: NetCfg, val ctrl: ChatCtrl?): MigrationToState()
@Serializable data class DownloadFailed(val totalBytes: Long, val link: String, val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class ArchiveImport(val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class ArchiveImportFailed(val archivePath: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class Passphrase(val passphrase: String, val netCfg: NetCfg): MigrationToState()
@Serializable data class MigrationConfirmation(val status: DBMigrationResult, val passphrase: String, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
@Serializable data class Migration(val passphrase: String, val confirmation: chat.simplex.common.views.helpers.MigrationConfirmation, val useKeychain: Boolean, val netCfg: NetCfg): MigrationToState()
}
private var MutableState<MigrationState>.state: MigrationState
private var MutableState<MigrationToState?>.state: MigrationToState?
get() = value
set(v) { value = v }
@Composable
fun ModalData.MigrateFromAnotherDeviceView(state: MigrationFromAnotherDeviceState? = null, close: () -> Unit) {
val migrationState = rememberSaveable(stateSaver = serializableSaver()) {
mutableStateOf(
when (state) {
null -> MigrationState.PasteOrScanLink
is MigrationFromAnotherDeviceState.Onion -> {
MigrationState.Onion(state.link, state.socksProxy, state.hostMode, state.requiredHostMode)
}
is MigrationFromAnotherDeviceState.DownloadProgress -> {
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
// SHOULDN'T BE HERE because the app checks this before opening migration screen and will not open it in this case.
// See analyzeMigrationState()
MigrationState.DownloadFailed(totalBytes = 0, link = state.link, archivePath = archivePath.absolutePath, state.netCfg)
}
is MigrationFromAnotherDeviceState.ArchiveImport -> {
val archivePath = File(getMigrationTempFilesDirectory(), state.archiveName)
MigrationState.ArchiveImportFailed(archivePath.absolutePath, state.netCfg)
}
is MigrationFromAnotherDeviceState.Passphrase -> {
MigrationState.Passphrase("", state.netCfg)
}
}
)
}
fun ModalData.MigrateToDeviceView(close: () -> Unit) {
val migrationState = remember { chatModel.migrationState }
// Prevent from hiding the view until migration is finished or app deleted
val backDisabled = remember {
derivedStateOf {
val s = chatModel.migrationState.value
s is MigrationFromAnotherDeviceState.ArchiveImport ||
s is MigrationFromAnotherDeviceState.Passphrase ||
migrationState.value is MigrationState.DatabaseInit
when (chatModel.migrationState.value) {
null,
is MigrationToState.PasteOrScanLink,
is MigrationToState.Onion,
is MigrationToState.LinkDownloading,
is MigrationToState.DownloadProgress,
is MigrationToState.DownloadFailed,
is MigrationToState.ArchiveImportFailed -> false
is MigrationToState.ArchiveImport,
is MigrationToState.DatabaseInit,
is MigrationToState.Migration,
is MigrationToState.MigrationConfirmation,
is MigrationToState.Passphrase -> true
}
}
}
val chatReceiver = remember { mutableStateOf(null as MigrationFromChatReceiver?) }
val chatReceiver = remember { mutableStateOf(null as MigrationToChatReceiver?) }
ModalView(
enableClose = !backDisabled.value,
close = {
@@ -142,7 +139,7 @@ fun ModalData.MigrateFromAnotherDeviceView(state: MigrationFromAnotherDeviceStat
}
},
) {
MigrateFromAnotherDeviceLayout(
MigrateToDeviceLayout(
migrationState = migrationState,
chatReceiver = chatReceiver,
close = close,
@@ -151,9 +148,9 @@ fun ModalData.MigrateFromAnotherDeviceView(state: MigrationFromAnotherDeviceStat
}
@Composable
private fun ModalData.MigrateFromAnotherDeviceLayout(
migrationState: MutableState<MigrationState>,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
private fun ModalData.MigrateToDeviceLayout(
migrationState: MutableState<MigrationToState?>,
chatReceiver: MutableState<MigrationToChatReceiver?>,
close: () -> Unit,
) {
val tempDatabaseFile = rememberSaveable { mutableStateOf(fileForTemporaryDatabase()) }
@@ -161,7 +158,7 @@ private fun ModalData.MigrateFromAnotherDeviceLayout(
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).height(IntrinsicSize.Max),
) {
AppBarTitle(stringResource(MR.strings.migrate_here))
AppBarTitle(stringResource(MR.strings.migrate_to_device_title))
SectionByState(migrationState, tempDatabaseFile.value, chatReceiver, close)
SectionBottomSpacer()
}
@@ -170,28 +167,29 @@ private fun ModalData.MigrateFromAnotherDeviceLayout(
@Composable
private fun ModalData.SectionByState(
migrationState: MutableState<MigrationState>,
migrationState: MutableState<MigrationToState?>,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
chatReceiver: MutableState<MigrationToChatReceiver?>,
close: () -> Unit
) {
when (val s = migrationState.value) {
is MigrationState.PasteOrScanLink -> migrationState.PasteOrScanLinkView()
is MigrationState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState)
is MigrationState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg)
is MigrationState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg)
is MigrationState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes)
is MigrationState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg)
is MigrationState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg)
is MigrationState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg)
is MigrationState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg)
is MigrationState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg)
is MigrationState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close)
null -> {}
is MigrationToState.PasteOrScanLink -> migrationState.PasteOrScanLinkView()
is MigrationToState.Onion -> OnionView(s.link, s.socksProxy, s.hostMode, s.requiredHostMode, migrationState)
is MigrationToState.DatabaseInit -> migrationState.DatabaseInitView(s.link, tempDatabaseFile, s.netCfg)
is MigrationToState.LinkDownloading -> migrationState.LinkDownloadingView(s.link, s.ctrl, s.user, s.archivePath, tempDatabaseFile, chatReceiver, s.netCfg)
is MigrationToState.DownloadProgress -> DownloadProgressView(s.downloadedBytes, totalBytes = s.totalBytes)
is MigrationToState.DownloadFailed -> migrationState.DownloadFailedView(s.link, chatReceiver.value, s.archivePath, s.netCfg)
is MigrationToState.ArchiveImport -> migrationState.ArchiveImportView(s.archivePath, s.netCfg)
is MigrationToState.ArchiveImportFailed -> migrationState.ArchiveImportFailedView(s.archivePath, s.netCfg)
is MigrationToState.Passphrase -> migrationState.PassphraseEnteringView(currentKey = s.passphrase, s.netCfg)
is MigrationToState.MigrationConfirmation -> migrationState.MigrationConfirmationView(s.status, s.passphrase, s.useKeychain, s.netCfg)
is MigrationToState.Migration -> MigrationView(s.passphrase, s.confirmation, s.useKeychain, s.netCfg, close)
}
}
@Composable
private fun MutableState<MigrationState>.PasteOrScanLinkView() {
private fun MutableState<MigrationToState?>.PasteOrScanLinkView() {
if (appPlatform.isAndroid) {
SectionView(stringResource(MR.strings.scan_QR_code).replace('\n', ' ').uppercase()) {
QRCodeScanner(showQRCodeScanner = remember { mutableStateOf(true) }) { text ->
@@ -209,7 +207,7 @@ private fun MutableState<MigrationState>.PasteOrScanLinkView() {
}
@Composable
private fun MutableState<MigrationState>.PasteLinkView() {
private fun MutableState<MigrationToState?>.PasteLinkView() {
val clipboard = LocalClipboardManager.current
SectionItemView({
val str = clipboard.getText()?.text ?: return@SectionItemView
@@ -220,7 +218,7 @@ private fun MutableState<MigrationState>.PasteLinkView() {
}
@Composable
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationState>) {
private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: HostMode, requiredHostMode: Boolean, state: MutableState<MigrationToState?>) {
val onionHosts = remember { stateGetOrPut("onionHosts") {
getNetCfg().copy(socksProxy = socksProxy, hostMode = hostMode, requiredHostMode = requiredHostMode).onionHosts
} }
@@ -238,10 +236,10 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
mutableStateOf(getNetCfg().withOnionHosts(onionHosts.value).copy(socksProxy = socksProxy, sessionMode = sessionMode.value))
}
SectionView(stringResource(MR.strings.migration_from_device_confirm_network_settings).uppercase()) {
SectionView(stringResource(MR.strings.migrate_to_device_confirm_network_settings).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_check),
text = stringResource(MR.strings.migration_from_device_apply_onion),
text = stringResource(MR.strings.migrate_to_device_apply_onion),
textColor = MaterialTheme.colors.primary,
click = {
val updated = netCfg.value
@@ -251,11 +249,11 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
sessionMode = sessionMode.value
)
withBGApi {
state.value = MigrationState.DatabaseInit(link, updated)
state.value = MigrationToState.DatabaseInit(link, updated)
}
}
){}
SectionTextFooter(stringResource(MR.strings.migration_from_device_confirm_network_settings_footer))
SectionTextFooter(stringResource(MR.strings.migrate_to_device_confirm_network_settings_footer))
}
SectionSpacer()
@@ -285,9 +283,9 @@ private fun ModalData.OnionView(link: String, socksProxy: String?, hostMode: Hos
}
@Composable
private fun MutableState<MigrationState>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.DatabaseInitView(link: String, tempDatabaseFile: File, netCfg: NetCfg) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_database_init).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_to_device_database_init).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -296,17 +294,17 @@ private fun MutableState<MigrationState>.DatabaseInitView(link: String, tempData
}
@Composable
private fun MutableState<MigrationState>.LinkDownloadingView(
private fun MutableState<MigrationToState?>.LinkDownloadingView(
link: String,
ctrl: ChatCtrl,
user: User,
archivePath: String,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
chatReceiver: MutableState<MigrationToChatReceiver?>,
netCfg: NetCfg
) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_downloading_details).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_to_device_downloading_details).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -317,37 +315,37 @@ private fun MutableState<MigrationState>.LinkDownloadingView(
@Composable
private fun DownloadProgressView(downloadedBytes: Long, totalBytes: Long) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_downloading_archive).uppercase()) {
SectionView(stringResource(MR.strings.migrate_to_device_downloading_archive).uppercase()) {
val ratio = downloadedBytes.toFloat() / max(totalBytes, 1)
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migration_from_device_bytes_downloaded).format(formatBytes(downloadedBytes)))
LargeProgressView(ratio, "${(ratio * 100).toInt()}%", stringResource(MR.strings.migrate_to_device_bytes_downloaded).format(formatBytes(downloadedBytes)))
}
}
}
@Composable
private fun MutableState<MigrationState>.DownloadFailedView(link: String, chatReceiver: MigrationFromChatReceiver?, archivePath: String, netCfg: NetCfg) {
SectionView(stringResource(MR.strings.migration_from_device_download_failed).uppercase()) {
private fun MutableState<MigrationToState?>.DownloadFailedView(link: String, chatReceiver: MigrationToChatReceiver?, archivePath: String, netCfg: NetCfg) {
SectionView(stringResource(MR.strings.migrate_to_device_download_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_download),
text = stringResource(MR.strings.migration_from_device_repeat_download),
text = stringResource(MR.strings.migrate_to_device_repeat_download),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationState.DatabaseInit(link, netCfg)
state = MigrationToState.DatabaseInit(link, netCfg)
}
) {}
SectionTextFooter(stringResource(MR.strings.migration_from_device_try_again))
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
}
LaunchedEffect(Unit) {
chatReceiver?.stopAndCleanUp()
File(archivePath).delete()
MigrationFromAnotherDeviceState.save(null)
MigrationToDeviceState.save(null)
}
}
@Composable
private fun MutableState<MigrationState>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.ArchiveImportView(archivePath: String, netCfg: NetCfg) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_importing_archive).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_to_device_importing_archive).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -356,29 +354,29 @@ private fun MutableState<MigrationState>.ArchiveImportView(archivePath: String,
}
@Composable
private fun MutableState<MigrationState>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
SectionView(stringResource(MR.strings.migration_from_device_import_failed).uppercase()) {
private fun MutableState<MigrationToState?>.ArchiveImportFailedView(archivePath: String, netCfg: NetCfg) {
SectionView(stringResource(MR.strings.migrate_to_device_import_failed).uppercase()) {
SettingsActionItemWithContent(
icon = painterResource(MR.images.ic_download),
text = stringResource(MR.strings.migration_from_device_repeat_import),
text = stringResource(MR.strings.migrate_to_device_repeat_import),
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationState.ArchiveImport(archivePath, netCfg)
state = MigrationToState.ArchiveImport(archivePath, netCfg)
}
) {}
SectionTextFooter(stringResource(MR.strings.migration_from_device_try_again))
SectionTextFooter(stringResource(MR.strings.migrate_to_device_try_again))
}
}
@Composable
private fun MutableState<MigrationState>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.PassphraseEnteringView(currentKey: String, netCfg: NetCfg) {
val currentKey = rememberSaveable { mutableStateOf(currentKey) }
val verifyingPassphrase = rememberSaveable { mutableStateOf(false) }
val useKeychain = rememberSaveable { mutableStateOf(appPreferences.storeDBPassphrase.get()) }
Box {
val view = LocalMultiplatformView()
SectionView(stringResource(MR.strings.migration_from_device_enter_passphrase).uppercase()) {
SectionView(stringResource(MR.strings.migrate_to_device_enter_passphrase).uppercase()) {
SavePassphraseSetting(
useKeychain.value,
false,
@@ -401,9 +399,9 @@ private fun MutableState<MigrationState>.PassphraseEnteringView(currentKey: Stri
val (status, _) = chatInitTemporaryDatabase(dbAbsolutePrefixPath, key = currentKey.value, confirmation = MigrationConfirmation.YesUp)
val success = status == DBMigrationResult.OK || status == DBMigrationResult.InvalidConfirmation
if (success) {
state = MigrationState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg)
state = MigrationToState.Migration(currentKey.value, MigrationConfirmation.YesUp, useKeychain.value, netCfg)
} else if (status is DBMigrationResult.ErrorMigration) {
state = MigrationState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
state = MigrationToState.MigrationConfirmation(status, currentKey.value, useKeychain.value, netCfg)
} else {
showErrorOnMigrationIfNeeded(status)
}
@@ -420,7 +418,7 @@ private fun MutableState<MigrationState>.PassphraseEnteringView(currentKey: Stri
}
@Composable
private fun MutableState<MigrationState>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.MigrationConfirmationView(status: DBMigrationResult, passphrase: String, useKeychain: Boolean, netCfg: NetCfg) {
data class Tuple4<A,B,C,D>(val a: A, val b: B, val c: C, val d: D)
val (header: String, button: String?, footer: String, confirmation: MigrationConfirmation?) = when (status) {
is DBMigrationResult.ErrorMigration -> when (val err = status.migrationError) {
@@ -455,7 +453,7 @@ private fun MutableState<MigrationState>.MigrationConfirmationView(status: DBMig
text = button,
textColor = MaterialTheme.colors.primary,
click = {
state = MigrationState.Migration(passphrase, confirmation, useKeychain, netCfg)
state = MigrationToState.Migration(passphrase, confirmation, useKeychain, netCfg)
}
) {}
}
@@ -466,7 +464,7 @@ private fun MutableState<MigrationState>.MigrationConfirmationView(status: DBMig
@Composable
private fun MigrationView(passphrase: String, confirmation: MigrationConfirmation, useKeychain: Boolean, netCfg: NetCfg, close: () -> Unit) {
Box {
SectionView(stringResource(MR.strings.migration_from_device_migrating).uppercase()) {}
SectionView(stringResource(MR.strings.migrate_to_device_migrating).uppercase()) {}
ProgressView()
}
LaunchedEffect(Unit) {
@@ -479,18 +477,18 @@ private fun ProgressView() {
DefaultProgressView(null)
}
private suspend fun MutableState<MigrationState>.checkUserLink(link: String) {
private suspend fun MutableState<MigrationToState?>.checkUserLink(link: String) {
if (strHasSimplexFileLink(link.trim())) {
val data = MigrationFileLinkData.readFromLink(link)
val hasOnionConfigured = data?.networkConfig?.hasOnionConfigured() ?: false
val networkConfig = data?.networkConfig?.transformToPlatformSupported()
// If any of iOS or Android had onion enabled, show onion screen
if (hasOnionConfigured && networkConfig?.hostMode != null && networkConfig.requiredHostMode != null) {
state = MigrationState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
state = MigrationToState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode)
MigrationToDeviceState.save(MigrationToDeviceState.Onion(link.trim(), networkConfig.socksProxy, networkConfig.hostMode, networkConfig.requiredHostMode))
} else {
val current = getNetCfg()
state = MigrationState.DatabaseInit(link.trim(), current.copy(
state = MigrationToState.DatabaseInit(link.trim(), current.copy(
socksProxy = networkConfig?.socksProxy,
hostMode = networkConfig?.hostMode ?: current.hostMode,
requiredHostMode = networkConfig?.requiredHostMode ?: current.requiredHostMode
@@ -504,7 +502,7 @@ private suspend fun MutableState<MigrationState>.checkUserLink(link: String) {
}
}
private fun MutableState<MigrationState>.prepareDatabase(
private fun MutableState<MigrationToState?>.prepareDatabase(
link: String,
tempDatabaseFile: File,
netCfg: NetCfg,
@@ -512,43 +510,59 @@ private fun MutableState<MigrationState>.prepareDatabase(
withLongRunningApi {
val ctrlAndUser = initTemporaryDatabase(tempDatabaseFile, netCfg)
if (ctrlAndUser == null) {
state = MigrationState.DownloadFailed(0, link, archivePath(), netCfg)
state = MigrationToState.DownloadFailed(0, link, archivePath(), netCfg)
return@withLongRunningApi
}
val (ctrl, user) = ctrlAndUser
state = MigrationState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
state = MigrationToState.LinkDownloading(link, ctrl, user, archivePath(), netCfg)
}
}
private fun MutableState<MigrationState>.startDownloading(
private fun MutableState<MigrationToState?>.startDownloading(
totalBytes: Long,
ctrl: ChatCtrl,
user: User,
tempDatabaseFile: File,
chatReceiver: MutableState<MigrationFromChatReceiver?>,
chatReceiver: MutableState<MigrationToChatReceiver?>,
link: String,
archivePath: String,
netCfg: NetCfg,
) {
withBGApi {
chatReceiver.value = MigrationFromChatReceiver(ctrl, tempDatabaseFile) { msg ->
chatReceiver.value = MigrationToChatReceiver(ctrl, tempDatabaseFile) { msg ->
when (msg) {
is CR.RcvFileProgressXFTP -> {
state = MigrationState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl)
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.DownloadProgress(link, File(archivePath).name, netCfg))
state = MigrationToState.DownloadProgress(msg.receivedSize, msg.totalSize, msg.rcvFileTransfer.fileId, link, archivePath, netCfg, ctrl)
MigrationToDeviceState.save(MigrationToDeviceState.DownloadProgress(link, File(archivePath).name, netCfg))
}
is CR.RcvStandaloneFileComplete -> {
delay(500)
state = MigrationState.ArchiveImport(archivePath, netCfg)
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.ArchiveImport(File(archivePath).name, netCfg))
// User closed the whole screen before new state was saved
if (state == null) {
MigrationToDeviceState.save(null)
} else {
state = MigrationToState.ArchiveImport(archivePath, netCfg)
MigrationToDeviceState.save(MigrationToDeviceState.ArchiveImport(File(archivePath).name, netCfg))
}
}
is CR.RcvFileError -> {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migration_from_device_download_failed),
generalGetString(MR.strings.migration_from_device_file_delete_or_link_invalid)
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
}
is CR.ChatRespError -> {
if (msg.chatError is ChatError.ChatErrorChat && msg.chatError.errorType is ChatErrorType.NoRcvFileUser) {
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migrate_to_device_download_failed),
generalGetString(MR.strings.migrate_to_device_file_delete_or_link_invalid)
)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
} else {
Log.d(TAG, "unsupported error: ${msg.responseType}")
}
}
else -> Log.d(TAG, "unsupported event: ${msg.responseType}")
}
@@ -557,16 +571,16 @@ private fun MutableState<MigrationState>.startDownloading(
val (res, error) = controller.downloadStandaloneFile(user, link, CryptoFile.plain(File(archivePath).path), ctrl)
if (res == null) {
state = MigrationState.DownloadFailed(totalBytes, link, archivePath, netCfg)
state = MigrationToState.DownloadFailed(totalBytes, link, archivePath, netCfg)
AlertManager.shared.showAlertMsg(
generalGetString(MR.strings.migration_from_device_error_downloading_archive),
generalGetString(MR.strings.migrate_to_device_error_downloading_archive),
error
)
}
}
}
private fun MutableState<MigrationState>.importArchive(archivePath: String, netCfg: NetCfg) {
private fun MutableState<MigrationToState?>.importArchive(archivePath: String, netCfg: NetCfg) {
withLongRunningApi {
try {
if (ChatController.ctrl == null || ChatController.ctrl == -1L) {
@@ -582,14 +596,14 @@ private fun MutableState<MigrationState>.importArchive(archivePath: String, netC
generalGetString(MR.strings.non_fatal_errors_occured_during_import)
)
}
state = MigrationState.Passphrase("", netCfg)
MigrationFromAnotherDeviceState.save(MigrationFromAnotherDeviceState.Passphrase(netCfg))
state = MigrationToState.Passphrase("", netCfg)
MigrationToDeviceState.save(MigrationToDeviceState.Passphrase(netCfg))
} catch (e: Exception) {
state = MigrationState.ArchiveImportFailed(archivePath, netCfg)
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_importing_database), e.stackTraceToString())
}
} catch (e: Exception) {
state = MigrationState.ArchiveImportFailed(archivePath, netCfg)
state = MigrationToState.ArchiveImportFailed(archivePath, netCfg)
AlertManager.shared.showAlertMsg (generalGetString(MR.strings.error_deleting_database), e.stackTraceToString())
}
}
@@ -630,30 +644,32 @@ private suspend fun finishMigration(appSettings: AppSettings, close: () -> Unit)
startChat(user)
}
hideView(close)
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migration_from_device_chat_migrated), generalGetString(MR.strings.migration_from_device_finalize_migration))
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.migrate_to_device_chat_migrated), generalGetString(MR.strings.migrate_to_device_finalize_migration))
} catch (e: Exception) {
AlertManager.shared.showAlertMsg(generalGetString(MR.strings.error_starting_chat), e.stackTraceToString())
}
MigrationFromAnotherDeviceState.save(null)
MigrationToDeviceState.save(null)
}
private fun hideView(close: () -> Unit) {
appPreferences.onboardingStage.set(OnboardingStage.OnboardingComplete)
chatModel.migrationState.value = null
close()
}
private suspend fun MutableState<MigrationState>.cleanUpOnBack(chatReceiver: MigrationFromChatReceiver?) {
private suspend fun MutableState<MigrationToState?>.cleanUpOnBack(chatReceiver: MigrationToChatReceiver?) {
val state = state
if (state is MigrationState.ArchiveImportFailed) {
if (state is MigrationToState.ArchiveImportFailed) {
// Original database is not exist, nothing is set up correctly for showing to a user yet. Return to clean state
deleteChatDatabaseFilesAndState()
initChatControllerAndRunMigrations()
} else if (state is MigrationState.DownloadProgress && state.ctrl != null) {
} else if (state is MigrationToState.DownloadProgress && state.ctrl != null) {
stopArchiveDownloading(state.fileId, state.ctrl)
}
chatReceiver?.stopAndCleanUp()
getMigrationTempFilesDirectory().deleteRecursively()
MigrationFromAnotherDeviceState.save(null)
MigrationToDeviceState.save(null)
chatModel.migrationState.value = null
}
private fun strHasSimplexFileLink(text: String): Boolean =
@@ -670,7 +686,7 @@ private fun archivePath(): String {
return archivePath.absolutePath
}
private class MigrationFromChatReceiver(
private class MigrationToChatReceiver(
val ctrl: ChatCtrl,
val databaseUrl: File,
var receiveMessages: Boolean = true,
@@ -19,7 +19,8 @@ import chat.simplex.common.model.*
import chat.simplex.common.platform.chatModel
import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrateFromAnotherDeviceView
import chat.simplex.common.views.migration.MigrateToDeviceView
import chat.simplex.common.views.migration.MigrationToState
import chat.simplex.res.MR
import dev.icerock.moko.resources.StringResource
@@ -71,7 +72,9 @@ fun SimpleXInfoLayout(
.padding(top = DEFAULT_PADDING), contentAlignment = Alignment.Center
) {
SimpleButtonDecorated(text = stringResource(MR.strings.migrate_from_another_device), icon = painterResource(MR.images.ic_download),
click = { ModalManager.fullscreen.showCustomModal { close -> MigrateFromAnotherDeviceView(chatModel.migrationState.value, close) } })
click = {
chatModel.migrationState.value = MigrationToState.PasteOrScanLink
ModalManager.fullscreen.showCustomModal { close -> MigrateToDeviceView(close) } })
}
}
@@ -85,9 +88,8 @@ fun SimpleXInfoLayout(
}
}
LaunchedEffect(Unit) {
val state = chatModel.migrationState.value
if (state != null && !ModalManager.fullscreen.hasModalsOpen()) {
ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateFromAnotherDeviceView(state, close) }
if (chatModel.migrationState.value != null && !ModalManager.fullscreen.hasModalsOpen()) {
ModalManager.fullscreen.showCustomModal(animated = false) { close -> MigrateToDeviceView(close) }
}
}
}
@@ -28,8 +28,7 @@ import chat.simplex.common.ui.theme.*
import chat.simplex.common.views.CreateProfile
import chat.simplex.common.views.database.DatabaseView
import chat.simplex.common.views.helpers.*
import chat.simplex.common.views.migration.MigrateFromAnotherDeviceView
import chat.simplex.common.views.migration.MigrateToAnotherDeviceView
import chat.simplex.common.views.migration.MigrateFromDeviceView
import chat.simplex.common.views.onboarding.SimpleXInfo
import chat.simplex.common.views.onboarding.WhatsNewView
import chat.simplex.common.views.remote.ConnectDesktopView
@@ -137,7 +136,7 @@ fun SettingsLayout(
} else {
SettingsActionItem(painterResource(MR.images.ic_desktop), stringResource(MR.strings.settings_section_title_use_from_desktop), showCustomModal{ it, close -> ConnectDesktopView(close) }, disabled = stopped, extraPadding = true)
}
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_to_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateToAnotherDeviceView(close) } }}, disabled = stopped, extraPadding = true)
SettingsActionItem(painterResource(MR.images.ic_ios_share), stringResource(MR.strings.migrate_from_device_to_another_device), { withAuth(generalGetString(MR.strings.auth_open_migration_to_another_device), generalGetString(MR.strings.auth_log_in_using_credential)) { ModalManager.fullscreen.showCustomModal { close -> MigrateFromDeviceView(close) } }}, disabled = stopped, extraPadding = true)
}
SectionDividerSpaced()
@@ -1846,63 +1846,66 @@
<string name="agent_internal_error_desc">Please report it to the developers: \n%s</string>
<string name="restart_chat_button">Restart chat</string>
<!-- MigrateFromAnotherDevice.kt -->
<string name="migrate_here">Migrate here</string>
<!-- MigrateToDevice.kt -->
<string name="migrate_to_device_title">Migrate here</string>
<string name="or_paste_archive_link">Or paste archive link</string>
<string name="paste_archive_link">Paste archive link</string>
<string name="invalid_file_link">Invalid link</string>
<string name="migration_from_device_migrating">Migrating</string>
<string name="migration_from_device_database_init">Preparing download</string>
<string name="migration_from_device_downloading_details">Downloading link details</string>
<string name="migration_from_device_downloading_archive">Downloading archive</string>
<string name="migration_from_device_bytes_downloaded">%s downloaded</string>
<string name="migration_from_device_download_failed">Download failed</string>
<string name="migration_from_device_repeat_download">Repeat download</string>
<string name="migration_from_device_try_again">You can give another try.</string>
<string name="migration_from_device_importing_archive">Importing archive</string>
<string name="migration_from_device_import_failed">Import failed</string>
<string name="migration_from_device_repeat_import">Repeat import</string>
<string name="migration_from_device_enter_passphrase">Enter passphrase</string>
<string name="migration_from_device_file_delete_or_link_invalid">File was deleted or link is invalid</string>
<string name="migration_from_device_error_downloading_archive">Error downloading the archive</string>
<string name="migration_from_device_chat_migrated">Chat migrated!</string>
<string name="migration_from_device_finalize_migration">Finalize migration on another device.</string>
<string name="migration_from_device_confirm_network_settings">Confirm network settings</string>
<string name="migration_from_device_confirm_network_settings_footer">Please confirm that network settings are correct for this device.</string>
<string name="migration_from_device_apply_onion">Apply</string>
<string name="migrate_to_device_migrating">Migrating</string>
<string name="migrate_to_device_database_init">Preparing download</string>
<string name="migrate_to_device_downloading_details">Downloading link details</string>
<string name="migrate_to_device_downloading_archive">Downloading archive</string>
<string name="migrate_to_device_bytes_downloaded">%s downloaded</string>
<string name="migrate_to_device_download_failed">Download failed</string>
<string name="migrate_to_device_repeat_download">Repeat download</string>
<string name="migrate_to_device_try_again">You can give another try.</string>
<string name="migrate_to_device_importing_archive">Importing archive</string>
<string name="migrate_to_device_import_failed">Import failed</string>
<string name="migrate_to_device_repeat_import">Repeat import</string>
<string name="migrate_to_device_enter_passphrase">Enter passphrase</string>
<string name="migrate_to_device_file_delete_or_link_invalid">File was deleted or link is invalid</string>
<string name="migrate_to_device_error_downloading_archive">Error downloading the archive</string>
<string name="migrate_to_device_chat_migrated">Chat migrated!</string>
<string name="migrate_to_device_finalize_migration">Finalize migration on another device.</string>
<string name="migrate_to_device_confirm_network_settings">Confirm network settings</string>
<string name="migrate_to_device_confirm_network_settings_footer">Please confirm that network settings are correct for this device.</string>
<string name="migrate_to_device_apply_onion">Apply</string>
<!-- MigrateToAnotherDevice.kt -->
<string name="migrate_to_device">Migrate to another device</string>
<string name="migrate_to_device_error_saving_settings">Error saving settings</string>
<string name="migrate_to_device_exported_file_doesnt_exist">Exported file doesn\'t exist</string>
<string name="migrate_to_device_error_exporting_archive">Error exporting chat database</string>
<string name="migration_to_device_database_init">Preparing upload</string>
<string name="migration_to_device_error_uploading_archive">Error uploading the archive</string>
<string name="migration_to_device_error_deleting_database">Error deleting database</string>
<string name="migration_to_device_stopping_chat">Stopping chat</string>
<string name="migration_to_device_chat_should_be_stopped">In order to continue, chat should be stopped.</string>
<string name="migration_to_device_archive_and_upload">Archive and upload</string>
<string name="migration_to_device_confirm_upload">Confirm upload</string>
<string name="migration_to_device_all_data_will_be_uploaded">All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</string>
<string name="migration_to_device_archiving_database">Archiving database</string>
<string name="migration_to_device_bytes_uploaded">%s uploaded</string>
<string name="migration_to_device_uploading_archive">Uploading archive</string>
<string name="migration_to_device_upload_failed">Upload failed</string>
<string name="migration_to_device_repeat_upload">Repeat upload</string>
<string name="migration_to_device_try_again">You can give another try.</string>
<string name="migration_to_device_creating_archive_link">Creating archive link</string>
<string name="migration_to_device_cancel_migration">Cancel migration</string>
<string name="migration_to_device_finalize_migration">Finalize migration</string>
<string name="migration_to_device_choose_migrate_from_another_device"><![CDATA[Choose <i>Migrate from another device</i> on the new device and scan QR code.]]></string>
<string name="migration_to_device_or_share_this_file_link">Or securely share this file link</string>
<string name="migration_to_device_delete_database_from_device">Delete database from this device</string>
<string name="migration_to_device_starting_chat_on_multiple_devices_unsupported">Warning: starting chat on multiple devices is not supported and will cause message delivery failures</string>
<string name="migration_to_device_start_chat">Start chat</string>
<string name="migration_to_device_migration_complete">Migration complete</string>
<string name="migration_to_device_you_must_not_start_database_on_two_device"><![CDATA[You <b>must not</b> use the same database on two devices.]]></string>
<string name="migration_to_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Please note</b>: using the same database on two devices will break the decryption of messages from your connections, as a security protection.]]></string>
<string name="migration_to_device_verify_database_passphrase">Verify database passphrase</string>
<string name="migration_to_device_verify_passphrase">Verify passphrase</string>
<string name="migration_to_device_confirm_you_remember_passphrase">Confirm that you remember database passphrase to migrate it.</string>
<!-- MigrateFromDevice.kt -->
<string name="migrate_from_device_title">Migrate device</string>
<string name="migrate_from_device_to_another_device">Migrate to another device</string>
<string name="migrate_from_device_error_saving_settings">Error saving settings</string>
<string name="migrate_from_device_exported_file_doesnt_exist">Exported file doesn\'t exist</string>
<string name="migrate_from_device_error_exporting_archive">Error exporting chat database</string>
<string name="migrate_from_device_database_init">Preparing upload</string>
<string name="migrate_from_device_error_uploading_archive">Error uploading the archive</string>
<string name="migrate_from_device_error_deleting_database">Error deleting database</string>
<string name="migrate_from_device_stopping_chat">Stopping chat</string>
<string name="migrate_from_device_chat_should_be_stopped">In order to continue, chat should be stopped.</string>
<string name="migrate_from_device_archive_and_upload">Archive and upload</string>
<string name="migrate_from_device_confirm_upload">Confirm upload</string>
<string name="migrate_from_device_all_data_will_be_uploaded">All your contacts, conversations and files will be securely encrypted and uploaded in chunks to configured XFTP relays.</string>
<string name="migrate_from_device_archiving_database">Archiving database</string>
<string name="migrate_from_device_bytes_uploaded">%s uploaded</string>
<string name="migrate_from_device_uploading_archive">Uploading archive</string>
<string name="migrate_from_device_upload_failed">Upload failed</string>
<string name="migrate_from_device_repeat_upload">Repeat upload</string>
<string name="migrate_from_device_try_again">You can give another try.</string>
<string name="migrate_from_device_creating_archive_link">Creating archive link</string>
<string name="migrate_from_device_cancel_migration">Cancel migration</string>
<string name="migrate_from_device_finalize_migration">Finalize migration</string>
<string name="migrate_from_device_choose_migrate_from_another_device"><![CDATA[Choose <i>Migrate from another device</i> on the new device and scan QR code.]]></string>
<string name="migrate_from_device_or_share_this_file_link">Or securely share this file link</string>
<string name="migrate_from_device_delete_database_from_device">Delete database from this device</string>
<string name="migrate_from_device_starting_chat_on_multiple_devices_unsupported">Warning: starting chat on multiple devices is not supported and will cause message delivery failures</string>
<string name="migrate_from_device_start_chat">Start chat</string>
<string name="migrate_from_device_migration_complete">Migration complete</string>
<string name="migrate_from_device_you_must_not_start_database_on_two_device"><![CDATA[You <b>must not</b> use the same database on two devices.]]></string>
<string name="migrate_from_device_using_on_two_device_breaks_encryption"><![CDATA[<b>Please note</b>: using the same database on two devices will break the decryption of messages from your connections, as a security protection.]]></string>
<string name="migrate_from_device_verify_database_passphrase">Verify database passphrase</string>
<string name="migrate_from_device_verify_passphrase">Verify passphrase</string>
<string name="migrate_from_device_confirm_you_remember_passphrase">Confirm that you remember database passphrase to migrate it.</string>
<string name="migrate_from_device_check_connection_and_try_again">Check your internet connection and try again</string>
<string name="migrate_from_device_archive_will_be_deleted"><![CDATA[<b>Warning</b>: the archive will be deleted.]]></string>
<string name="migrate_from_device_error_verifying_passphrase">Error verifying passphrase:</string>
</resources>
+1 -1
View File
@@ -1,5 +1,5 @@
name: simplex-chat
version: 5.6.0.1
version: 5.6.0.2
#synopsis:
#description:
homepage: https://github.com/simplex-chat/simplex-chat#readme
+1 -1
View File
@@ -5,7 +5,7 @@ cabal-version: 1.12
-- see: https://github.com/sol/hpack
name: simplex-chat
version: 5.6.0.1
version: 5.6.0.2
category: Web, System, Services, Cryptography
homepage: https://github.com/simplex-chat/simplex-chat#readme
author: simplex.chat
+45 -28
View File
@@ -3291,10 +3291,24 @@ processAgentMessage _ connId DEL_CONN =
toView $ CRAgentConnDeleted (AgentConnId connId)
processAgentMessage corrId connId msg = do
vr <- chatVersionRange
withStore' (`getUserByAConnId` AgentConnId connId) >>= \case
-- getUserByAConnId never throws logical errors, only SEDBBusyError can be thrown here
critical (withStore' (`getUserByAConnId` AgentConnId connId)) >>= \case
Just user -> processAgentMessageConn vr user corrId connId msg `catchChatError` (toView . CRChatError (Just user))
_ -> throwChatError $ CENoConnectionUser (AgentConnId connId)
-- CRITICAL error will be shown to the user as alert with restart button in Android/desktop apps.
-- SEDBBusyError will only be thrown on IO exceptions or SQLError during DB queries,
-- e.g. when database is locked or busy for longer than 3s.
-- In this case there is no better mitigation than showing alert:
-- - without ACK the message delivery will be stuck,
-- - with ACK message will be lost, as it failed to be saved.
-- Full app restart is likely to resolve database condition and the message will be received and processed again.
critical :: ChatMonad m => m a -> m a
critical a =
a `catchChatError` \case
ChatErrorStore SEDBBusyError {message} -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing
e -> throwError e
processAgentMessageNoConn :: forall m. ChatMonad m => ACommand 'Agent 'AENone -> m ()
processAgentMessageNoConn = \case
CONNECT p h -> hostEvent $ CRHostConnected p h
@@ -3482,9 +3496,13 @@ processAgentMsgRcvFile _corrId aFileId msg =
agentXFTPDeleteRcvFile aFileId fileId
toView $ CRRcvFileError user ci e ft
processAgentMessageConn :: forall m. ChatMonad m => (PQSupport -> VersionRangeChat) -> User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
processAgentMessageConn :: forall m . ChatMonad m => (PQSupport -> VersionRangeChat) -> User -> ACorrId -> ConnId -> ACommand 'Agent 'AEConn -> m ()
processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage = do
entity <- withStore (\db -> getConnectionEntity db vr user $ AgentConnId agentConnId) >>= updateConnStatus
-- Missing connection/entity errors here will be sent to the view but not shown as CRITICAL alert,
-- as in this case no need to ACK message - we can't process messages for this connection anyway.
-- SEDBException will be re-trown as CRITICAL as it is likely to indicate a temporary database condition
-- that will be resolved with app restart.
entity <- critical $ withStore (\db -> getConnectionEntity db vr user $ AgentConnId agentConnId) >>= updateConnStatus
case agentMessage of
END -> case entity of
RcvDirectMsgConnection _ (Just ct) -> toView $ CRContactAnotherClient user ct
@@ -3547,12 +3565,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
processINFOpqSupport conn pqSupport
_conn' <- saveConnInfo conn connInfo
pure ()
MSG meta _msgFlags msgBody -> do
cmdId <- createAckCmd conn
MSG meta _msgFlags msgBody ->
-- TODO only acknowledge without saving message?
-- probably this branch is never executed, so there should be no reason
-- to save message if contact hasn't been created yet - chat item isn't created anyway
withAckMessage agentConnId cmdId meta $ do
withAckMessage agentConnId conn meta False $ \cmdId -> do
(_conn', _) <- saveDirectRcvMSG conn meta cmdId msgBody
pure False
SENT msgId ->
@@ -3584,12 +3601,11 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
forM_ contData $ \(hostConnId, xGrpMemIntroCont) ->
sendXGrpMemInv hostConnId (Just directConnReq) xGrpMemIntroCont
CRContactUri _ -> throwChatError $ CECommandError "unexpected ConnectionRequestUri type"
MSG msgMeta _msgFlags msgBody -> do
let MsgMeta {pqEncryption} = msgMeta
(ct', conn') <- updateContactPQRcv user ct conn pqEncryption
checkIntegrityCreateItem (CDDirectRcv ct') msgMeta
cmdId <- createAckCmd conn'
withAckMessage agentConnId cmdId msgMeta $ do
MSG msgMeta _msgFlags msgBody ->
withAckMessage agentConnId conn msgMeta True $ \cmdId -> do
let MsgMeta {pqEncryption} = msgMeta
(ct', conn') <- updateContactPQRcv user ct conn pqEncryption
checkIntegrityCreateItem (CDDirectRcv ct') msgMeta `catchChatError` \_ -> pure ()
(conn'', msg@RcvMessage {chatMsgEvent = ACME _ event}) <- saveDirectRcvMSG conn' msgMeta cmdId msgBody
let ct'' = ct' {activeConn = Just conn''} :: Contact
assertDirectAllowed user MDRcv ct'' $ toCMEventTag event
@@ -3995,10 +4011,8 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
void $ sendDirectMemberMessage imConn (XGrpMemCon memberId) groupId
_ -> messageWarning "sendXGrpMemCon: member category GCPreMember or GCPostMember is expected"
MSG msgMeta _msgFlags msgBody -> do
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta
cmdId <- createAckCmd conn
let aChatMsgs = parseChatMessages msgBody
withAckMessage agentConnId cmdId msgMeta $ do
withAckMessage agentConnId conn msgMeta True $ \cmdId -> do
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
forM_ aChatMsgs $ \case
Right (ACMsg _ chatMsg) ->
processEvent cmdId chatMsg `catchChatError` \e -> toView $ CRChatError (Just user) e
@@ -4010,6 +4024,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
[Right (ACMsg _ chatMsg)] -> forwardMsg_ chatMsg
_ -> pure ()
where
aChatMsgs = parseChatMessages msgBody
brokerTs = metaBrokerTs msgMeta
processEvent :: MsgEncodingI e => CommandId -> ChatMessage e -> m ()
processEvent cmdId chatMsg = do
@@ -4046,12 +4061,12 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
BFileChunk sharedMsgId chunk -> bFileChunkGroup gInfo sharedMsgId chunk msgMeta
_ -> messageError $ "unsupported message: " <> T.pack (show event)
checkSendRcpt :: [AChatMessage] -> m Bool
checkSendRcpt aChatMsgs = do
checkSendRcpt aMsgs = do
currentMemCount <- withStore' $ \db -> getGroupCurrentMembersCount db user gInfo
let GroupInfo {chatSettings = ChatSettings {sendRcpts}} = gInfo
pure $
fromMaybe (sendRcptsSmallGroups user) sendRcpts
&& any aChatMsgHasReceipt aChatMsgs
&& any aChatMsgHasReceipt aMsgs
&& currentMemCount <= smallGroupsRcptsMemLimit
where
aChatMsgHasReceipt (ACMsg _ ChatMessage {chatMsgEvent}) =
@@ -4241,6 +4256,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
_ -> pure ()
CON _ -> startReceivingFile user fileId
MSG meta _ msgBody -> do
-- XXX: not all branches do ACK
parseFileChunk msgBody >>= receiveFileChunk ft (Just conn) meta
OK ->
-- [async agent commands] continuation on receiving OK
@@ -4384,19 +4400,22 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
withAckMessage' :: ConnId -> Connection -> MsgMeta -> m () -> m ()
withAckMessage' cId conn msgMeta action = do
cmdId <- createAckCmd conn
withAckMessage cId cmdId msgMeta $ action $> False
withAckMessage cId conn msgMeta False $ \_cmdId -> action $> False
withAckMessage :: ConnId -> CommandId -> MsgMeta -> m Bool -> m ()
withAckMessage cId cmdId msgMeta action = do
withAckMessage :: ConnId -> Connection -> MsgMeta -> Bool -> (CommandId -> m Bool) -> m ()
withAckMessage cId conn msgMeta showCritical action = do
cmdId <- createAckCmd conn `catchChatError` \e -> throwError $ ChatErrorAgent (CRITICAL True $ show e) Nothing
-- [async agent commands] command should be asynchronous, continuation is ackMsgDeliveryEvent
-- TODO catching error and sending ACK after an error, particularly if it is a database error, will result in the message not processed (and no notification to the user).
-- Possible solutions are:
-- 1) retry processing several times
-- 2) stabilize database
-- 3) show screen of death to the user asking to restart
tryChatError action >>= \case
tryChatError (action cmdId) >>= \case
Right withRcpt -> ackMsg cId cmdId msgMeta $ if withRcpt then Just "" else Nothing
-- If showCritical is True, then these errors don't result in ACK and show user visible alert
-- This prevents losing the message that failed to be processed.
Left (ChatErrorStore SEDBBusyError {message}) | showCritical -> throwError $ ChatErrorAgent (CRITICAL True message) Nothing
Left e -> ackMsg cId cmdId msgMeta Nothing >> throwError e
ackMsg :: ConnId -> CommandId -> MsgMeta -> Maybe MsgReceiptInfo -> m ()
@@ -4997,9 +5016,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
checkIntegrityCreateItem :: forall c. ChatTypeI c => ChatDirection c 'MDRcv -> MsgMeta -> m ()
checkIntegrityCreateItem cd MsgMeta {integrity, broker = (_, brokerTs)} = case integrity of
MsgOk -> pure ()
MsgError e ->
createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs)
`catchChatError` \_ -> pure ()
MsgError e -> createInternalChatItem user cd (CIRcvIntegrityError e) (Just brokerTs)
xInfo :: Contact -> Profile -> m ()
xInfo c p' = void $ processContactProfileUpdate c p' True
@@ -5719,7 +5736,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
directMsgReceived :: Contact -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m ()
directMsgReceived ct conn@Connection {connId} msgMeta msgRcpts = do
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta
checkIntegrityCreateItem (CDDirectRcv ct) msgMeta `catchChatError` \_ -> pure ()
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do
withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus
updateDirectItemStatus ct conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete
@@ -5731,7 +5748,7 @@ processAgentMessageConn vr user@User {userId} corrId agentConnId agentMessage =
-- - getChatItemIdByAgentMsgId to return [ChatItemId]
groupMsgReceived :: GroupInfo -> GroupMember -> Connection -> MsgMeta -> NonEmpty MsgReceipt -> m ()
groupMsgReceived gInfo m conn@Connection {connId} msgMeta msgRcpts = do
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta
checkIntegrityCreateItem (CDGroupRcv gInfo m) msgMeta `catchChatError` \_ -> pure ()
forM_ msgRcpts $ \MsgReceipt {agentMsgId, msgRcptStatus} -> do
withStore' $ \db -> updateSndMsgDeliveryStatus db connId agentMsgId $ MDSSndRcvd msgRcptStatus
updateGroupItemStatus gInfo m conn agentMsgId $ CISSndRcvd msgRcptStatus SSPComplete
+15 -20
View File
@@ -46,6 +46,8 @@ import Data.Time (NominalDiffTime, UTCTime)
import Data.Time.Clock.System (systemToUTCTime)
import Data.Version (showVersion)
import Data.Word (Word16)
import Database.SQLite.Simple (SQLError)
import qualified Database.SQLite.Simple as SQL
import Language.Haskell.TH (Exp, Q, runIO)
import Numeric.Natural
import qualified Paths_simplex_chat as SC
@@ -80,7 +82,7 @@ import Simplex.Messaging.Protocol (AProtoServerWithAuth, AProtocolType (..), Cor
import Simplex.Messaging.TMap (TMap)
import Simplex.Messaging.Transport (TLS, simplexMQVersion)
import Simplex.Messaging.Transport.Client (TransportHost)
import Simplex.Messaging.Util (allFinally, catchAllErrors, liftEitherError, tryAllErrors, (<$$>))
import Simplex.Messaging.Util (allFinally, catchAllErrors, liftIOEither, tryAllErrors, (<$$>))
import Simplex.RemoteControl.Client
import Simplex.RemoteControl.Invitation (RCSignedInvitation, RCVerifiedInvitation)
import Simplex.RemoteControl.Types
@@ -1296,30 +1298,23 @@ withStoreCtx' :: ChatMonad m => Maybe String -> (DB.Connection -> IO a) -> m a
withStoreCtx' ctx_ action = withStoreCtx ctx_ $ liftIO . action
withStoreCtx :: ChatMonad m => Maybe String -> (DB.Connection -> ExceptT StoreError IO a) -> m a
withStoreCtx ctx_ action = do
withStoreCtx _ctx action = do
ChatController {chatStore} <- ask
liftEitherError ChatErrorStore $ case ctx_ of
Nothing -> withTransaction chatStore (runExceptT . action) `catch` handleInternal ""
-- uncomment to debug store performance
-- Just ctx -> do
-- t1 <- liftIO getCurrentTime
-- putStrLn $ "withStoreCtx start :: " <> show t1 <> " :: " <> ctx
-- r <- withTransactionCtx ctx_ chatStore (runExceptT . action) `E.catch` handleInternal (" (" <> ctx <> ")")
-- t2 <- liftIO getCurrentTime
-- putStrLn $ "withStoreCtx end :: " <> show t2 <> " :: " <> ctx <> " :: duration=" <> show (diffToMilliseconds $ diffUTCTime t2 t1)
-- pure r
Just _ -> withTransaction chatStore (runExceptT . action) `catch` handleInternal ""
where
handleInternal :: String -> SomeException -> IO (Either StoreError a)
handleInternal ctxStr e = pure . Left . SEInternalError $ show e <> ctxStr
liftIOEither $ withTransaction chatStore (runExceptT . withExceptT ChatErrorStore . action) `E.catches` handleDBErrors
withStoreBatch :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO (Either ChatError a))) -> m (t (Either ChatError a))
withStoreBatch actions = do
ChatController {chatStore} <- ask
liftIO $ withTransaction chatStore $ mapM (`E.catch` handleInternal) . actions
where
handleInternal :: E.SomeException -> IO (Either ChatError a)
handleInternal = pure . Left . ChatError . CEInternalError . show
liftIO $ withTransaction chatStore $ mapM (`E.catches` handleDBErrors) . actions
handleDBErrors :: [E.Handler IO (Either ChatError a)]
handleDBErrors =
[ E.Handler $ \(e :: SQLError) ->
let se = SQL.sqlError e
busy = se == SQL.ErrorBusy || se == SQL.ErrorLocked
in pure . Left . ChatErrorStore $ if busy then SEDBBusyError $ show se else SEDBException $ show e,
E.Handler $ \(E.SomeException e) -> pure . Left . ChatErrorStore . SEDBException $ show e
]
withStoreBatch' :: (ChatMonad' m, Traversable t) => (DB.Connection -> t (IO a)) -> m (t (Either ChatError a))
withStoreBatch' actions = withStoreBatch $ fmap (fmap Right) . actions
+2
View File
@@ -95,6 +95,8 @@ data StoreError
| SEUniqueID
| SELargeMsg
| SEInternalError {message :: String}
| SEDBException {message :: String}
| SEDBBusyError {message :: String}
| SEBadChatItem {itemId :: ChatItemId, itemTs :: Maybe ChatItemTs}
| SEChatItemNotFound {itemId :: ChatItemId}
| SEChatItemNotFoundByText {text :: Text}