mirror of
https://github.com/simplex-chat/simplex-chat.git
synced 2026-03-30 18:35:49 +00:00
ios: start/stop chat toggle refactoring (#5275)
* ios: start/stop chat toggle refactoring * changes * changes * return back * reduce diff * better * update button * ios: do not start chat after export, always show run toggle (#5284) --------- Co-authored-by: Evgeny Poberezkin <evgeny@poberezkin.com>
This commit is contained in:
committed by
GitHub
parent
879c117269
commit
961bdbfc59
@@ -17,6 +17,7 @@ class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
application.registerForRemoteNotifications()
|
||||
removePasscodesIfReinstalled()
|
||||
prepareForLaunch()
|
||||
deleteOldChatArchive()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1600,6 +1600,15 @@ func initializeChat(start: Bool, confirmStart: Bool = false, dbKey: String? = ni
|
||||
m.chatInitialized = true
|
||||
m.currentUser = try apiGetActiveUser()
|
||||
m.conditions = try getServerOperators()
|
||||
if shouldImportAppSettingsDefault.get() {
|
||||
do {
|
||||
let appSettings = try apiGetAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
appSettings.importIntoApp()
|
||||
shouldImportAppSettingsDefault.set(false)
|
||||
} catch {
|
||||
logger.error("Error while importing app settings: \(error)")
|
||||
}
|
||||
}
|
||||
if m.currentUser == nil {
|
||||
onboardingStageDefault.set(.step1_SimpleXInfo)
|
||||
privacyDeliveryReceiptsSet.set(true)
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
//
|
||||
// ChatArchiveView.swift
|
||||
// SimpleXChat
|
||||
//
|
||||
// Created by Evgeny on 23/06/2022.
|
||||
// Copyright © 2022 SimpleX Chat. All rights reserved.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import SimpleXChat
|
||||
|
||||
struct ChatArchiveView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
var archiveName: String
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_NAME) private var chatArchiveName: String?
|
||||
@AppStorage(DEFAULT_CHAT_ARCHIVE_TIME) private var chatArchiveTime: Double = 0
|
||||
@State private var showDeleteAlert = false
|
||||
|
||||
var body: some View {
|
||||
let fileUrl = getDocumentsDirectory().appendingPathComponent(archiveName)
|
||||
let fileTs = chatArchiveTimeDefault.get()
|
||||
List {
|
||||
Section {
|
||||
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
||||
Button {
|
||||
showShareSheet(items: [fileUrl])
|
||||
} label: {
|
||||
Text("Save archive")
|
||||
}
|
||||
}
|
||||
settingsRow("trash", color: theme.colors.secondary) {
|
||||
Button {
|
||||
showDeleteAlert = true
|
||||
} label: {
|
||||
Text("Delete archive").foregroundColor(.red)
|
||||
}
|
||||
}
|
||||
} header: {
|
||||
Text("Chat archive")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text("Created on \(fileTs)")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
}
|
||||
.alert(isPresented: $showDeleteAlert) {
|
||||
Alert(
|
||||
title: Text("Delete chat archive?"),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: fileUrl.path)
|
||||
chatArchiveName = nil
|
||||
chatArchiveTime = 0
|
||||
} catch let error {
|
||||
logger.error("removeItem error \(String(describing: error))")
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ChatArchiveView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ChatArchiveView(archiveName: "")
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,8 @@ struct DatabaseEncryptionView: View {
|
||||
@State private var confirmNewKey = ""
|
||||
@State private var currentKeyShown = false
|
||||
|
||||
let stopChatRunBlockStartChat: (Binding<Bool>, @escaping () async throws -> Bool) -> Void
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
List {
|
||||
@@ -134,46 +136,61 @@ struct DatabaseEncryptionView: View {
|
||||
.onAppear {
|
||||
if initialRandomDBPassphrase { currentKey = kcDatabasePassword.get() ?? "" }
|
||||
}
|
||||
.disabled(m.chatRunning != false)
|
||||
.disabled(progressIndicator)
|
||||
.alert(item: $alert) { item in databaseEncryptionAlert(item) }
|
||||
}
|
||||
|
||||
private func encryptDatabase() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
encryptionStartedDefault.set(true)
|
||||
encryptionStartedAtDefault.set(Date.now)
|
||||
if !m.chatDbChanged {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
}
|
||||
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
||||
encryptionStartedDefault.set(false)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
if migration {
|
||||
storeDBPassphraseGroupDefault.set(useKeychain)
|
||||
}
|
||||
if useKeychain {
|
||||
if kcDatabasePassword.set(newKey) {
|
||||
await resetFormAfterEncryption(true)
|
||||
await operationEnded(.databaseEncrypted)
|
||||
} else {
|
||||
await resetFormAfterEncryption()
|
||||
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
||||
}
|
||||
} else {
|
||||
if migration {
|
||||
removePassphraseFromKeyChain()
|
||||
}
|
||||
await resetFormAfterEncryption()
|
||||
private func encryptDatabaseAsync() async -> Bool {
|
||||
await MainActor.run {
|
||||
progressIndicator = true
|
||||
}
|
||||
do {
|
||||
encryptionStartedDefault.set(true)
|
||||
encryptionStartedAtDefault.set(Date.now)
|
||||
if !m.chatDbChanged {
|
||||
try apiSaveAppSettings(settings: AppSettings.current.prepareForExport())
|
||||
}
|
||||
try await apiStorageEncryption(currentKey: currentKey, newKey: newKey)
|
||||
encryptionStartedDefault.set(false)
|
||||
initialRandomDBPassphraseGroupDefault.set(false)
|
||||
if migration {
|
||||
storeDBPassphraseGroupDefault.set(useKeychain)
|
||||
}
|
||||
if useKeychain {
|
||||
if kcDatabasePassword.set(newKey) {
|
||||
await resetFormAfterEncryption(true)
|
||||
await operationEnded(.databaseEncrypted)
|
||||
}
|
||||
} catch let error {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||
await operationEnded(.currentPassphraseError)
|
||||
} else {
|
||||
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
|
||||
await resetFormAfterEncryption()
|
||||
await operationEnded(.error(title: "Keychain error", error: "Error saving passphrase to keychain"))
|
||||
}
|
||||
} else {
|
||||
if migration {
|
||||
removePassphraseFromKeyChain()
|
||||
}
|
||||
await resetFormAfterEncryption()
|
||||
await operationEnded(.databaseEncrypted)
|
||||
}
|
||||
return true
|
||||
} catch let error {
|
||||
if case .chatCmdError(_, .errorDatabase(.errorExport(.errorNotADatabase))) = error as? ChatResponse {
|
||||
await operationEnded(.currentPassphraseError)
|
||||
} else {
|
||||
await operationEnded(.error(title: "Error encrypting database", error: "\(responseError(error))"))
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func encryptDatabase() {
|
||||
// it will try to stop and start the chat in case of: non-migration && successful encryption. In migration the chat will remain stopped
|
||||
if migration {
|
||||
Task {
|
||||
await encryptDatabaseAsync()
|
||||
}
|
||||
} else {
|
||||
stopChatRunBlockStartChat($progressIndicator) {
|
||||
return await encryptDatabaseAsync()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,6 +388,6 @@ func validKey(_ s: String) -> Bool {
|
||||
|
||||
struct DatabaseEncryptionView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false)
|
||||
DatabaseEncryptionView(useKeychain: Binding.constant(true), migration: false, stopChatRunBlockStartChat: { _, _ in true })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ struct DatabaseView: View {
|
||||
@EnvironmentObject var theme: AppTheme
|
||||
let dismissSettingsSheet: DismissAction
|
||||
@State private var runChat = false
|
||||
@State private var stoppingChat = false
|
||||
@State private var alert: DatabaseAlert? = nil
|
||||
@State private var showFileImporter = false
|
||||
@State private var importedArchivePath: URL?
|
||||
@@ -57,6 +58,8 @@ struct DatabaseView: View {
|
||||
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
|
||||
@State private var appFilesCountAndSize: (Int, Int)?
|
||||
|
||||
@State private var showDatabaseEncryptionView = false
|
||||
|
||||
@State var chatItemTTL: ChatItemTTL
|
||||
@State private var currentChatItemTTL: ChatItemTTL = .none
|
||||
|
||||
@@ -69,7 +72,20 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func chatDatabaseView() -> some View {
|
||||
NavigationLink(isActive: $showDatabaseEncryptionView) {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in
|
||||
stopChatRunBlockStartChat(false, progressIndicator, block)
|
||||
})
|
||||
.navigationTitle("Database passphrase")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
EmptyView()
|
||||
}
|
||||
.frame(width: 1, height: 1)
|
||||
.hidden()
|
||||
|
||||
List {
|
||||
let stopped = m.chatRunning == false
|
||||
Section {
|
||||
@@ -101,9 +117,10 @@ struct DatabaseView: View {
|
||||
isOn: $runChat
|
||||
)
|
||||
.onChange(of: runChat) { _ in
|
||||
if (runChat) {
|
||||
startChat()
|
||||
} else {
|
||||
if runChat {
|
||||
DatabaseView.startChat($runChat, $progressIndicator)
|
||||
} else if !stoppingChat {
|
||||
stoppingChat = false
|
||||
alert = .stopChat
|
||||
}
|
||||
}
|
||||
@@ -123,7 +140,9 @@ struct DatabaseView: View {
|
||||
let color: Color = unencrypted ? .orange : theme.colors.secondary
|
||||
settingsRow(unencrypted ? "lock.open" : useKeychain ? "key" : "lock", color: color) {
|
||||
NavigationLink {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false)
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: false, stopChatRunBlockStartChat: { progressIndicator, block in
|
||||
stopChatRunBlockStartChat(false, progressIndicator, block)
|
||||
})
|
||||
.navigationTitle("Database passphrase")
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
@@ -133,9 +152,14 @@ struct DatabaseView: View {
|
||||
settingsRow("square.and.arrow.up", color: theme.colors.secondary) {
|
||||
Button("Export database") {
|
||||
if initialRandomDBPassphraseGroupDefault.get() && !unencrypted {
|
||||
alert = .exportProhibited
|
||||
showDatabaseEncryptionView = true
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
alert = .exportProhibited
|
||||
}
|
||||
} else {
|
||||
exportArchive()
|
||||
stopChatRunBlockStartChat(stopped, $progressIndicator) {
|
||||
await exportArchive()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -144,20 +168,6 @@ struct DatabaseView: View {
|
||||
showFileImporter = true
|
||||
}
|
||||
}
|
||||
if let archiveName = chatArchiveName {
|
||||
let title: LocalizedStringKey = chatArchiveTimeDefault.get() < chatLastStartGroupDefault.get()
|
||||
? "Old database archive"
|
||||
: "New database archive"
|
||||
settingsRow("archivebox", color: theme.colors.secondary) {
|
||||
NavigationLink {
|
||||
ChatArchiveView(archiveName: archiveName)
|
||||
.navigationTitle(title)
|
||||
.modifier(ThemedBackground(grouped: true))
|
||||
} label: {
|
||||
Text(title)
|
||||
}
|
||||
}
|
||||
}
|
||||
settingsRow("trash.slash", color: theme.colors.secondary) {
|
||||
Button("Delete database", role: .destructive) {
|
||||
alert = .deleteChat
|
||||
@@ -167,14 +177,10 @@ struct DatabaseView: View {
|
||||
Text("Chat database")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
} footer: {
|
||||
Text(
|
||||
stopped
|
||||
? "You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts."
|
||||
: "Stop chat to enable database actions"
|
||||
)
|
||||
Text("You must use the most recent version of your chat database on one device ONLY, otherwise you may stop receiving the messages from some contacts.")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
}
|
||||
.disabled(!stopped)
|
||||
.disabled(progressIndicator)
|
||||
|
||||
if case .group = dbContainer, legacyDatabase {
|
||||
Section(header: Text("Old database").foregroundColor(theme.colors.secondary)) {
|
||||
@@ -190,7 +196,7 @@ struct DatabaseView: View {
|
||||
Button(m.users.count > 1 ? "Delete files for all chat profiles" : "Delete all files", role: .destructive) {
|
||||
alert = .deleteFilesAndMedia
|
||||
}
|
||||
.disabled(!stopped || appFilesCountAndSize?.0 == 0)
|
||||
.disabled(progressIndicator || appFilesCountAndSize?.0 == 0)
|
||||
} header: {
|
||||
Text("Files & media")
|
||||
.foregroundColor(theme.colors.secondary)
|
||||
@@ -255,7 +261,10 @@ struct DatabaseView: View {
|
||||
title: Text("Import chat database?"),
|
||||
message: Text("Your current chat database will be DELETED and REPLACED with the imported one.") + Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
||||
primaryButton: .destructive(Text("Import")) {
|
||||
importArchive(fileURL)
|
||||
stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) {
|
||||
_ = await DatabaseView.importArchive(fileURL, $progressIndicator, $alert)
|
||||
return true
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
@@ -263,19 +272,15 @@ struct DatabaseView: View {
|
||||
return Alert(title: Text("Error: no database file"))
|
||||
}
|
||||
case .archiveImported:
|
||||
return Alert(
|
||||
title: Text("Chat database imported"),
|
||||
message: Text("Restart the app to use imported chat database")
|
||||
)
|
||||
let (title, message) = archiveImportedAlertText()
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .archiveImportedWithErrors(errs):
|
||||
return Alert(
|
||||
title: Text("Chat database imported"),
|
||||
message: Text("Restart the app to use imported chat database") + Text(verbatim: "\n") + Text("Some non-fatal errors occurred during import:") + archiveErrorsText(errs)
|
||||
)
|
||||
let (title, message) = archiveImportedWithErrorsAlertText(errs: errs)
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case let .archiveExportedWithErrors(archivePath, errs):
|
||||
return Alert(
|
||||
title: Text("Chat database exported"),
|
||||
message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
|
||||
message: Text("You may save the exported archive.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)),
|
||||
dismissButton: .default(Text("Continue")) {
|
||||
showShareSheet(items: [archivePath])
|
||||
}
|
||||
@@ -285,15 +290,17 @@ struct DatabaseView: View {
|
||||
title: Text("Delete chat profile?"),
|
||||
message: Text("This action cannot be undone - your profile, contacts, messages and files will be irreversibly lost."),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
deleteChat()
|
||||
let wasStopped = m.chatRunning == false
|
||||
stopChatRunBlockStartChat(wasStopped, $progressIndicator) {
|
||||
_ = await deleteChat()
|
||||
return true
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
case .chatDeleted:
|
||||
return Alert(
|
||||
title: Text("Chat database deleted"),
|
||||
message: Text("Restart the app to create a new chat profile")
|
||||
)
|
||||
let (title, message) = chatDeletedAlertText()
|
||||
return Alert(title: Text(title), message: Text(message))
|
||||
case .deleteLegacyDatabase:
|
||||
return Alert(
|
||||
title: Text("Delete old database?"),
|
||||
@@ -308,7 +315,10 @@ struct DatabaseView: View {
|
||||
title: Text("Delete files and media?"),
|
||||
message: Text("This action cannot be undone - all received and sent files and media will be deleted. Low resolution pictures will remain."),
|
||||
primaryButton: .destructive(Text("Delete")) {
|
||||
deleteFiles()
|
||||
stopChatRunBlockStartChat(m.chatRunning == false, $progressIndicator) {
|
||||
deleteFiles()
|
||||
return true
|
||||
}
|
||||
},
|
||||
secondaryButton: .cancel()
|
||||
)
|
||||
@@ -328,95 +338,180 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func authStopChat() {
|
||||
private func authStopChat(_ onStop: (() -> Void)? = nil) {
|
||||
if UserDefaults.standard.bool(forKey: DEFAULT_PERFORM_LA) {
|
||||
authenticate(reason: NSLocalizedString("Stop SimpleX", comment: "authentication reason")) { laResult in
|
||||
switch laResult {
|
||||
case .success: stopChat()
|
||||
case .unavailable: stopChat()
|
||||
case .success: stopChat(onStop)
|
||||
case .unavailable: stopChat(onStop)
|
||||
case .failed: withAnimation { runChat = true }
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stopChat()
|
||||
stopChat(onStop)
|
||||
}
|
||||
}
|
||||
|
||||
private func stopChat() {
|
||||
private func stopChat(_ onStop: (() -> Void)? = nil) {
|
||||
Task {
|
||||
do {
|
||||
try await stopChatAsync()
|
||||
onStop?()
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
runChat = true
|
||||
alert = .error(title: "Error stopping chat", error: responseError(error))
|
||||
showAlert("Error stopping chat", message: responseError(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func exportArchive() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
let (archivePath, archiveErrors) = try await exportChatArchive()
|
||||
if archiveErrors.isEmpty {
|
||||
showShareSheet(items: [archivePath])
|
||||
await MainActor.run { progressIndicator = false }
|
||||
} else {
|
||||
await MainActor.run {
|
||||
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors)
|
||||
progressIndicator = false
|
||||
func stopChatRunBlockStartChat(
|
||||
_ stopped: Bool,
|
||||
_ progressIndicator: Binding<Bool>,
|
||||
_ block: @escaping () async throws -> Bool
|
||||
) {
|
||||
// if the chat was running, the sequence is: stop chat, run block, start chat.
|
||||
// Otherwise, just run block and do nothing - the toggle will be visible anyway and the user can start the chat or not
|
||||
if stopped {
|
||||
Task {
|
||||
do {
|
||||
_ = try await block()
|
||||
} catch {
|
||||
logger.error("Error while executing block: \(error)")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
authStopChat {
|
||||
stoppingChat = true
|
||||
runChat = false
|
||||
Task {
|
||||
// if it throws, let's start chat again anyway
|
||||
var canStart = false
|
||||
do {
|
||||
canStart = try await block()
|
||||
} catch {
|
||||
logger.error("Error executing block: \(error)")
|
||||
canStart = true
|
||||
}
|
||||
if canStart {
|
||||
await MainActor.run {
|
||||
DatabaseView.startChat($runChat, $progressIndicator)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static func startChat(_ runChat: Binding<Bool>, _ progressIndicator: Binding<Bool>) {
|
||||
progressIndicator.wrappedValue = true
|
||||
let m = ChatModel.shared
|
||||
if m.chatDbChanged {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resetChatCtrl()
|
||||
do {
|
||||
let hadDatabase = hasDatabase()
|
||||
try initializeChat(start: true)
|
||||
m.chatDbChanged = false
|
||||
AppChatState.shared.set(.active)
|
||||
if m.chatDbStatus != .ok || !hadDatabase {
|
||||
// Hide current view and show `DatabaseErrorView`
|
||||
dismissAllSheets(animated: true)
|
||||
}
|
||||
} catch let error {
|
||||
fatalError("Error starting chat \(responseError(error))")
|
||||
}
|
||||
progressIndicator.wrappedValue = false
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
_ = try apiStartChat()
|
||||
runChat.wrappedValue = true
|
||||
m.chatRunning = true
|
||||
ChatReceiver.shared.start()
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
AppChatState.shared.set(.active)
|
||||
} catch let error {
|
||||
runChat.wrappedValue = false
|
||||
showAlert(NSLocalizedString("Error starting chat", comment: ""), message: responseError(error))
|
||||
}
|
||||
progressIndicator.wrappedValue = false
|
||||
}
|
||||
}
|
||||
|
||||
private func exportArchive() async -> Bool {
|
||||
await MainActor.run {
|
||||
progressIndicator = true
|
||||
}
|
||||
do {
|
||||
let (archivePath, archiveErrors) = try await exportChatArchive()
|
||||
if archiveErrors.isEmpty {
|
||||
showShareSheet(items: [archivePath])
|
||||
await MainActor.run { progressIndicator = false }
|
||||
} else {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
||||
alert = .archiveExportedWithErrors(archivePath: archivePath, archiveErrors: archiveErrors)
|
||||
progressIndicator = false
|
||||
}
|
||||
}
|
||||
} catch let error {
|
||||
await MainActor.run {
|
||||
alert = .error(title: "Error exporting chat database", error: responseError(error))
|
||||
progressIndicator = false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func importArchive(_ archivePath: URL) {
|
||||
static func importArchive(
|
||||
_ archivePath: URL,
|
||||
_ progressIndicator: Binding<Bool>,
|
||||
_ alert: Binding<DatabaseAlert?>
|
||||
) async -> Bool {
|
||||
if archivePath.startAccessingSecurityScopedResource() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
let archiveErrors = try await apiImportArchive(config: config)
|
||||
_ = kcDatabasePassword.remove()
|
||||
if archiveErrors.isEmpty {
|
||||
await operationEnded(.archiveImported)
|
||||
} else {
|
||||
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors))
|
||||
}
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)))
|
||||
}
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)))
|
||||
}
|
||||
archivePath.stopAccessingSecurityScopedResource()
|
||||
await MainActor.run {
|
||||
progressIndicator.wrappedValue = true
|
||||
}
|
||||
do {
|
||||
try await apiDeleteStorage()
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
do {
|
||||
let config = ArchiveConfig(archivePath: archivePath.path)
|
||||
let archiveErrors = try await apiImportArchive(config: config)
|
||||
shouldImportAppSettingsDefault.set(true)
|
||||
_ = kcDatabasePassword.remove()
|
||||
if archiveErrors.isEmpty {
|
||||
await operationEnded(.archiveImported, progressIndicator, alert)
|
||||
} else {
|
||||
await operationEnded(.archiveImportedWithErrors(archiveErrors: archiveErrors), progressIndicator, alert)
|
||||
}
|
||||
return true
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error importing chat database", error: responseError(error)), progressIndicator, alert)
|
||||
}
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting chat database", error: responseError(error)), progressIndicator, alert)
|
||||
}
|
||||
archivePath.stopAccessingSecurityScopedResource()
|
||||
} else {
|
||||
alert = .error(title: "Error accessing database file")
|
||||
showAlert("Error accessing database file")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private func deleteChat() {
|
||||
progressIndicator = true
|
||||
Task {
|
||||
do {
|
||||
try await deleteChatAsync()
|
||||
await operationEnded(.chatDeleted)
|
||||
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
|
||||
} catch let error {
|
||||
await operationEnded(.error(title: "Error deleting database", error: responseError(error)))
|
||||
}
|
||||
private func deleteChat() async -> Bool {
|
||||
await MainActor.run {
|
||||
progressIndicator = true
|
||||
}
|
||||
do {
|
||||
try await deleteChatAsync()
|
||||
appFilesCountAndSize = directoryFileCountAndSize(getAppFilesDirectory())
|
||||
await DatabaseView.operationEnded(.chatDeleted, $progressIndicator, $alert)
|
||||
return true
|
||||
} catch let error {
|
||||
await DatabaseView.operationEnded(.error(title: "Error deleting database", error: responseError(error)), $progressIndicator, $alert)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,39 +523,28 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func operationEnded(_ dbAlert: DatabaseAlert) async {
|
||||
private static func operationEnded(_ dbAlert: DatabaseAlert, _ progressIndicator: Binding<Bool>, _ alert: Binding<DatabaseAlert?>) async {
|
||||
await MainActor.run {
|
||||
let m = ChatModel.shared
|
||||
m.chatDbChanged = true
|
||||
m.chatInitialized = false
|
||||
progressIndicator = false
|
||||
alert = dbAlert
|
||||
progressIndicator.wrappedValue = false
|
||||
}
|
||||
}
|
||||
|
||||
private func startChat() {
|
||||
if m.chatDbChanged {
|
||||
dismissSettingsSheet()
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
|
||||
resetChatCtrl()
|
||||
do {
|
||||
try initializeChat(start: true)
|
||||
m.chatDbChanged = false
|
||||
AppChatState.shared.set(.active)
|
||||
} catch let error {
|
||||
fatalError("Error starting chat \(responseError(error))")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
do {
|
||||
_ = try apiStartChat()
|
||||
runChat = true
|
||||
m.chatRunning = true
|
||||
ChatReceiver.shared.start()
|
||||
chatLastStartGroupDefault.set(Date.now)
|
||||
AppChatState.shared.set(.active)
|
||||
} catch let error {
|
||||
runChat = false
|
||||
alert = .error(title: "Error starting chat", error: responseError(error))
|
||||
await withCheckedContinuation { cont in
|
||||
let okAlertActionWaiting = UIAlertAction(title: NSLocalizedString("Ok", comment: "alert button"), style: .default, handler: { _ in cont.resume() })
|
||||
// show these alerts globally so they are visible when all sheets will be hidden
|
||||
if case .archiveImported = dbAlert {
|
||||
let (title, message) = archiveImportedAlertText()
|
||||
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||
} else if case .archiveImportedWithErrors(let errs) = dbAlert {
|
||||
let (title, message) = archiveImportedWithErrorsAlertText(errs: errs)
|
||||
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||
} else if case .chatDeleted = dbAlert {
|
||||
let (title, message) = chatDeletedAlertText()
|
||||
showAlert(title, message: message, actions: { [okAlertActionWaiting] })
|
||||
} else {
|
||||
alert.wrappedValue = dbAlert
|
||||
cont.resume()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -503,8 +587,28 @@ struct DatabaseView: View {
|
||||
}
|
||||
}
|
||||
|
||||
func archiveErrorsText(_ errs: [ArchiveError]) -> Text {
|
||||
return Text("\n" + errs.map(showArchiveError).joined(separator: "\n"))
|
||||
private func archiveImportedAlertText() -> (String, String) {
|
||||
(
|
||||
NSLocalizedString("Chat database imported", comment: ""),
|
||||
NSLocalizedString("Restart the app to use imported chat database", comment: "")
|
||||
)
|
||||
}
|
||||
private func archiveImportedWithErrorsAlertText(errs: [ArchiveError]) -> (String, String) {
|
||||
(
|
||||
NSLocalizedString("Chat database imported", comment: ""),
|
||||
NSLocalizedString("Restart the app to use imported chat database", comment: "") + "\n" + NSLocalizedString("Some non-fatal errors occurred during import:", comment: "") + archiveErrorsText(errs)
|
||||
)
|
||||
}
|
||||
|
||||
private func chatDeletedAlertText() -> (String, String) {
|
||||
(
|
||||
NSLocalizedString("Chat database deleted", comment: ""),
|
||||
NSLocalizedString("Restart the app to create a new chat profile", comment: "")
|
||||
)
|
||||
}
|
||||
|
||||
func archiveErrorsText(_ errs: [ArchiveError]) -> String {
|
||||
return "\n" + errs.map(showArchiveError).joined(separator: "\n")
|
||||
|
||||
func showArchiveError(_ err: ArchiveError) -> String {
|
||||
switch err {
|
||||
|
||||
@@ -117,7 +117,7 @@ struct MigrateToAppGroupView: View {
|
||||
setV3DBMigration(.migration_error)
|
||||
migrationError = "Error starting chat: \(responseError(error))"
|
||||
}
|
||||
deleteOldArchive()
|
||||
deleteOldChatArchive()
|
||||
} label: {
|
||||
Text("Start chat")
|
||||
.font(.title)
|
||||
@@ -235,14 +235,16 @@ func exportChatArchive(_ storagePath: URL? = nil) async throws -> (URL, [Archive
|
||||
try? FileManager.default.createDirectory(at: getWallpaperDirectory(), withIntermediateDirectories: true)
|
||||
let errs = try await apiExportArchive(config: config)
|
||||
if storagePath == nil {
|
||||
deleteOldArchive()
|
||||
deleteOldChatArchive()
|
||||
UserDefaults.standard.set(archiveName, forKey: DEFAULT_CHAT_ARCHIVE_NAME)
|
||||
chatArchiveTimeDefault.set(archiveTime)
|
||||
}
|
||||
return (archivePath, errs)
|
||||
}
|
||||
|
||||
func deleteOldArchive() {
|
||||
/// Deprecated. Remove in the end of 2025. All unused archives should be deleted for the most users til then.
|
||||
/// Remove DEFAULT_CHAT_ARCHIVE_NAME and DEFAULT_CHAT_ARCHIVE_TIME as well
|
||||
func deleteOldChatArchive() {
|
||||
let d = UserDefaults.standard
|
||||
if let archiveName = d.string(forKey: DEFAULT_CHAT_ARCHIVE_NAME) {
|
||||
do {
|
||||
|
||||
@@ -177,7 +177,7 @@ struct MigrateFromDevice: View {
|
||||
case let .archiveExportedWithErrors(archivePath, errs):
|
||||
return Alert(
|
||||
title: Text("Chat database exported"),
|
||||
message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + archiveErrorsText(errs),
|
||||
message: Text("You may migrate the exported database.") + Text(verbatim: "\n") + Text("Some file(s) were not exported:") + Text(archiveErrorsText(errs)),
|
||||
dismissButton: .default(Text("Continue")) {
|
||||
Task { await uploadArchive(path: archivePath) }
|
||||
}
|
||||
@@ -222,7 +222,8 @@ struct MigrateFromDevice: View {
|
||||
}
|
||||
|
||||
private func passphraseNotSetView() -> some View {
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true)
|
||||
DatabaseEncryptionView(useKeychain: $useKeychain, migration: true, stopChatRunBlockStartChat: { _, _ in
|
||||
})
|
||||
.onChange(of: initialRandomDBPassphrase) { initial in
|
||||
if !initial {
|
||||
migrationState = .uploadConfirmation
|
||||
|
||||
@@ -103,6 +103,9 @@ struct MigrateToDevice: View {
|
||||
@State private var showQRCodeScanner: Bool = true
|
||||
@State private var pasteboardHasStrings = UIPasteboard.general.hasStrings
|
||||
|
||||
@State private var importingArchiveFromFileProgressIndicator = false
|
||||
@State private var showFileImporter = false
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
switch migrationState {
|
||||
@@ -200,6 +203,12 @@ struct MigrateToDevice: View {
|
||||
Section(header: Text("Or paste archive link").foregroundColor(theme.colors.secondary)) {
|
||||
pasteLinkView()
|
||||
}
|
||||
Section(header: Text("Or import archive file").foregroundColor(theme.colors.secondary)) {
|
||||
archiveImportFromFileView()
|
||||
}
|
||||
}
|
||||
if importingArchiveFromFileProgressIndicator {
|
||||
progressView()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -220,6 +229,34 @@ struct MigrateToDevice: View {
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
}
|
||||
|
||||
private func archiveImportFromFileView() -> some View {
|
||||
Button {
|
||||
showFileImporter = true
|
||||
} label: {
|
||||
Label("Import database", systemImage: "square.and.arrow.down")
|
||||
}
|
||||
.disabled(importingArchiveFromFileProgressIndicator)
|
||||
.fileImporter(
|
||||
isPresented: $showFileImporter,
|
||||
allowedContentTypes: [.zip],
|
||||
allowsMultipleSelection: false
|
||||
) { result in
|
||||
if case let .success(files) = result, let fileURL = files.first {
|
||||
Task {
|
||||
let success = await DatabaseView.importArchive(fileURL, $importingArchiveFromFileProgressIndicator, Binding.constant(nil))
|
||||
if success {
|
||||
DatabaseView.startChat(
|
||||
Binding.constant(false),
|
||||
$importingArchiveFromFileProgressIndicator
|
||||
)
|
||||
hideView()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func linkDownloadingView(_ link: String) -> some View {
|
||||
ZStack {
|
||||
List {
|
||||
|
||||
@@ -39,6 +39,7 @@ let DEFAULT_EXPERIMENTAL_CALLS = "experimentalCalls"
|
||||
let DEFAULT_CHAT_ARCHIVE_NAME = "chatArchiveName"
|
||||
let DEFAULT_CHAT_ARCHIVE_TIME = "chatArchiveTime"
|
||||
let DEFAULT_CHAT_V3_DB_MIGRATION = "chatV3DBMigration"
|
||||
let DEFAULT_SHOULD_IMPORT_APP_SETTINGS = "shouldImportAppSettings"
|
||||
let DEFAULT_DEVELOPER_TOOLS = "developerTools"
|
||||
let DEFAULT_ENCRYPTION_STARTED = "encryptionStarted"
|
||||
let DEFAULT_ENCRYPTION_STARTED_AT = "encryptionStartedAt"
|
||||
@@ -192,6 +193,8 @@ let customDisappearingMessageTimeDefault = IntDefault(defaults: UserDefaults.sta
|
||||
let showDeleteConversationNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONVERSATION_NOTICE)
|
||||
let showDeleteContactNoticeDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOW_DELETE_CONTACT_NOTICE)
|
||||
|
||||
/// after importing new database, this flag will be set and unset only after importing app settings in `initializeChat` */
|
||||
let shouldImportAppSettingsDefault = BoolDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SHOULD_IMPORT_APP_SETTINGS)
|
||||
let currentThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME, withDefault: DefaultTheme.SYSTEM_THEME_NAME)
|
||||
let systemDarkThemeDefault = StringDefault(defaults: UserDefaults.standard, forKey: DEFAULT_SYSTEM_DARK_THEME, withDefault: DefaultTheme.DARK.themeName)
|
||||
let currentThemeIdsDefault = CodableDefault<[String: String]>(defaults: UserDefaults.standard, forKey: DEFAULT_CURRENT_THEME_IDS, withDefault: [:] )
|
||||
|
||||
@@ -139,7 +139,6 @@
|
||||
5CF937202B24DE8C00E1D781 /* SharedFileSubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */; };
|
||||
5CF937232B2503D000E1D781 /* NSESubscriber.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CF937212B25034A00E1D781 /* NSESubscriber.swift */; };
|
||||
5CFA59C42860BC6200863A68 /* MigrateToAppGroupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */; };
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFA59CF286477B400863A68 /* ChatArchiveView.swift */; };
|
||||
5CFE0921282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
5CFE0922282EEAF60002594B /* ZoomableScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */; };
|
||||
640417CD2B29B8C200CCB412 /* NewChatMenuButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */; };
|
||||
@@ -489,7 +488,6 @@
|
||||
5CF9371F2B24DE8C00E1D781 /* SharedFileSubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedFileSubscriber.swift; sourceTree = "<group>"; };
|
||||
5CF937212B25034A00E1D781 /* NSESubscriber.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSESubscriber.swift; sourceTree = "<group>"; };
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAppGroupView.swift; sourceTree = "<group>"; };
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChatArchiveView.swift; sourceTree = "<group>"; };
|
||||
5CFE0920282EEAF60002594B /* ZoomableScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = ZoomableScrollView.swift; path = Shared/Views/ZoomableScrollView.swift; sourceTree = SOURCE_ROOT; };
|
||||
640417CB2B29B8C200CCB412 /* NewChatMenuButton.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatMenuButton.swift; sourceTree = "<group>"; };
|
||||
640417CC2B29B8C200CCB412 /* NewChatView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewChatView.swift; sourceTree = "<group>"; };
|
||||
@@ -1058,7 +1056,6 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5C4B3B09285FB130003915F2 /* DatabaseView.swift */,
|
||||
5CFA59CF286477B400863A68 /* ChatArchiveView.swift */,
|
||||
5CFA59C32860BC6200863A68 /* MigrateToAppGroupView.swift */,
|
||||
5C9CC7A828C532AB00BEF955 /* DatabaseErrorView.swift */,
|
||||
5C9CC7AC28C55D7800BEF955 /* DatabaseEncryptionView.swift */,
|
||||
@@ -1511,7 +1508,6 @@
|
||||
8C9BC2652C240D5200875A27 /* ThemeModeEditor.swift in Sources */,
|
||||
5CB346E92869E8BA001FD2EF /* PushEnvironment.swift in Sources */,
|
||||
5C55A91F283AD0E400C4E99E /* CallManager.swift in Sources */,
|
||||
5CFA59D12864782E00863A68 /* ChatArchiveView.swift in Sources */,
|
||||
649BCDA22805D6EF00C3A862 /* CIImageView.swift in Sources */,
|
||||
5CADE79C292131E900072E13 /* ContactPreferencesView.swift in Sources */,
|
||||
CEA6E91C2CBD21B0002B5DB4 /* UserDefault.swift in Sources */,
|
||||
|
||||
Reference in New Issue
Block a user