mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-08-06 23:39:54 +00:00
Merge branch 'master' into master-android
This commit is contained in:
@@ -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 {
|
||||
|
||||
+28
-21
@@ -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))
|
||||
}
|
||||
}
|
||||
+55
-61
@@ -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: {
|
||||
|
||||
@@ -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 */,
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+3
-2
@@ -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?)
|
||||
|
||||
+8
-4
@@ -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)
|
||||
|
||||
+3
-1
@@ -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())
|
||||
|
||||
+144
-120
@@ -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,
|
||||
+166
-150
@@ -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,
|
||||
+7
-5
@@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-3
@@ -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
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user