ios: migration via link

This commit is contained in:
Avently
2024-02-15 01:05:19 +07:00
parent edc5a4c31b
commit 701f5d7cdc
6 changed files with 382 additions and 32 deletions

View File

@@ -1701,34 +1701,48 @@ func processReceivedMsg(_ res: ChatResponse) async {
}
case let .rcvFileAccepted(user, aChatItem): // usually rcvFileAccepted is a response, but it's also an event for XFTP files auto-accepted from NSE
await chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileStart(user, aChatItem):
await chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileStart(user, aChatItem, _):
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
}
case let .rcvFileComplete(user, aChatItem):
await chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileSndCancelled(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
case let .rcvFileProgressXFTP(user, aChatItem, _, _):
await chatItemSimpleUpdate(user, aChatItem)
case let .rcvFileError(user, aChatItem):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
case let .rcvFileProgressXFTP(user, aChatItem, _, _, _):
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
}
case let .rcvFileError(user, aChatItem, _):
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
}
case let .sndFileStart(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
case let .sndFileComplete(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupDirectFile(aChatItem) }
case let .sndFileRcvCancelled(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupDirectFile(aChatItem) }
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupDirectFile(aChatItem) }
}
case let .sndFileProgressXFTP(user, aChatItem, _, _, _):
await chatItemSimpleUpdate(user, aChatItem)
case let .sndFileCompleteXFTP(user, aChatItem, _):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
case let .sndFileError(user, aChatItem):
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
}
case let .sndFileCompleteXFTP(user, aChatItem, _, _):
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
}
case let .sndFileError(user, aChatItem, _):
if let aChatItem = aChatItem {
await chatItemSimpleUpdate(user, aChatItem)
Task { cleanupFile(aChatItem) }
}
case let .callInvitation(invitation):
await MainActor.run {
m.callInvitations[invitation.contact.id] = invitation

View File

@@ -0,0 +1,296 @@
//
// MigrateToAnotherDevice.swift
// SimpleX (iOS)
//
// Created by Avently on 14.02.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
public enum MigrationState: Equatable {
case initial
case chatStopInProgress
case chatStopFailed(reason: String)
case passphraseNotSet
case passphraseConfirmation
case uploadConfirmation
case uploadProgress(uploadedKb: Int64, totalKb: Int64)
case uploadDone(link: String)
}
struct MigrateToAnotherDevice: View {
@EnvironmentObject var m: ChatModel
@State private var migrationState: MigrationState = .initial
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State private var alert: MigrateToAnotherDeviceViewAlert?
private let chatWasStoppedInitially: Bool = AppChatState.shared.value == .stopped
enum MigrateToAnotherDeviceViewAlert: Identifiable {
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
case let .error(title, _): return "error \(title)"
}
}
}
var body: some View {
VStack {
List {
switch migrationState {
case .initial: EmptyView()
case .chatStopInProgress:
progressView("Stopping chat")
case let .chatStopFailed(reason):
chatStopFailedView(reason)
case .passphraseNotSet:
passphraseNotSetView()
case .passphraseConfirmation:
PassphraseConfirmationView(migrationState: $migrationState)
case .uploadConfirmation:
uploadConfirmationView()
case let .uploadProgress(uploaded, total):
uploadProgressView(uploaded, totalKb: total)
case let .uploadDone(link):
uploadDoneView(link)
}
}
}
.onAppear {
if case .initial = migrationState {
if AppChatState.shared.value == .stopped {
migrationState = initialRandomDBPassphraseGroupDefault.get() ? .passphraseNotSet : .passphraseConfirmation
} else {
migrationState = .chatStopInProgress
stopChat()
}
}
}
.onDisappear {
if !chatWasStoppedInitially {
Task {
try? startChat(refreshInvitations: true)
}
}
}
.alert(item: $alert) { alert in
switch alert {
case let .error(title, error):
return Alert(title: Text(title), message: Text(error))
}
}
}
private func chatStopFailedView(_ reason: String) -> some View {
Section {
Text(reason)
Button(action: stopChat) {
settingsRow("stop.fill", color: .red) {
Text("Stop chat")
}
}
} header: {
Text("Error stopping chat")
} footer: {
Text("In order to continue, chat should be stopped")
}
}
private func passphraseNotSetView() -> some View {
Section {
Text("Database is encrypted using a random passphrase. Please set your own password before migrating.")
NavigationLink {
DatabaseEncryptionView(useKeychain: $useKeychain)
.navigationTitle("Database passphrase")
} label: {
settingsRow("lock.open", color: .secondary) {
Text("Set passphrase")
}
}
} header: {
Text("Set passphrase to export")
}
.onAppear {
if !initialRandomDBPassphraseGroupDefault.get() {
migrationState = .uploadConfirmation
}
}
}
private func uploadConfirmationView() -> some View {
Section {
Button(action: startUploading) {
settingsRow("tray.and.arrow.up", color: .secondary) {
Text("Start uploading")
}
}
} header: {
Text("Confirm upload")
} footer: {
Text("Do you want to start uploading now?")
}
}
private func uploadProgressView(_ uploadedKb: Int64, totalKb: Int64) -> some View {
progressView("Uploaded \(ByteCountFormatter.string(fromByteCount: uploadedKb, countStyle: .binary)) from \(ByteCountFormatter.string(fromByteCount: totalKb, countStyle: .binary))")
}
private func uploadDoneView(_ link: String) -> some View {
Section {
SimpleXLinkQRCode(uri: link)
shareLinkButton(link)
} header: {
Text("Link to uploaded archive")
}
}
private func shareLinkButton(_ link: String) -> some View {
Button {
showShareSheet(items: [simplexChatLink(link)])
} label: {
Label("Share", systemImage: "square.and.arrow.up")
}
}
private func stopChat() {
Task {
do {
try await stopChatAsync()
migrationState = initialRandomDBPassphraseGroupDefault.get() ? .passphraseNotSet : .passphraseConfirmation
} catch let e {
migrationState = .chatStopFailed(reason: e.localizedDescription)
}
}
}
private func startUploading() {
Task {
for i in 1...10 {
try? await Task.sleep(nanoseconds: 100_000000)
await MainActor.run {
migrationState = .uploadProgress(uploadedKb: Int64(100 * i), totalKb: 1000)
if i == 10 {
migrationState = .uploadDone(link: "https://simplex.chat")
}
}
}
}
}
}
private struct PassphraseConfirmationView: View {
@Binding var migrationState: MigrationState
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State private var currentKey: String = ""
@State private var verifyingPassphrase: Bool = false
@State private var alert: PassphraseConfirmationViewAlert?
enum PassphraseConfirmationViewAlert: Identifiable {
case wrongPassphrase(title: LocalizedStringKey = "Wrong passphrase!", message: LocalizedStringKey = "Enter correct passphrase.")
case invalidConfirmation(title: LocalizedStringKey = "Invalid migration confirmation")
case keychainError(_ title: LocalizedStringKey = "Keychain error")
case databaseError(_ title: LocalizedStringKey = "Database error", message: String)
case unknownError(_ title: LocalizedStringKey = "Unknown error", message: String)
case error(title: LocalizedStringKey, error: LocalizedStringKey = "")
var id: String {
switch self {
case .wrongPassphrase: return "wrongPassphrase"
case .invalidConfirmation: return "invalidConfirmation"
case .keychainError: return "keychainError"
case let .databaseError(title, message): return "\(title) \(message)"
case let .unknownError(title, message): return "\(title) \(message)"
case let .error(title, _): return "error \(title)"
}
}
}
var body: some View {
ZStack(alignment: .topLeading) {
VStack {
Section {
VStack {
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
Button(action: { checkDatabasePassphrase(currentKey, $verifyingPassphrase) }) {
settingsRow(useKeychain ? "key" : "lock", color: .secondary) {
Text("Check passphrase")
}
}
}
} header: {
Text("Enter database passphrase")
} footer: {
Text("Make sure you remember database passphrase before migrating")
}
}
if (verifyingPassphrase) {
progressView("Checking passphrase…")
}
}
.alert(item: $alert) { alert in
switch alert {
case let .wrongPassphrase(title, message):
return Alert(title: Text(title), message: Text(message))
case let .invalidConfirmation(title):
return Alert(title: Text(title))
case let .keychainError(title):
return Alert(title: Text(title))
case let .databaseError(title, message):
return Alert(title: Text(title), message: Text(message))
case let .unknownError(title, message):
return Alert(title: Text(title), message: Text(message))
case let .error(title, error):
return Alert(title: Text(title), message: Text(error))
}
}
}
private func checkDatabasePassphrase(_ dbKey: String, _ verifyingPassphrase: Binding<Bool>) {
verifyingPassphrase.wrappedValue = true
defer {
verifyingPassphrase.wrappedValue = false
}
do {
resetChatCtrl()
try initializeChat(start: false, confirmStart: false, dbKey: dbKey, confirmMigrations: nil)
if let s = ChatModel.shared.chatDbStatus {
let am = AlertManager.shared
switch s {
case .invalidConfirmation:
am.showAlert(Alert(title: Text(String("Invalid migration confirmation"))))
case .errorNotADatabase:
alert = .wrongPassphrase()
case .errorKeychain:
alert = .keychainError()
case let .errorSQL(_, error):
alert = .databaseError(message: error)
case let .unknown(error):
alert = .unknownError(message: error)
case .errorMigration: ()
case .ok:
migrationState = .uploadConfirmation
}
}
} catch let error {
logger.error("initializeChat \(responseError(error))")
}
}
}
private func progressView(_ text: LocalizedStringKey) -> some View {
VStack {
ProgressView().scaleEffect(2)
Text(text)
.padding()
}
.frame(maxWidth: .infinity, maxHeight: .infinity )
}
struct MigrateToAnotherDevice_Previews: PreviewProvider {
static var previews: some View {
MigrateToAnotherDevice()
}
}

View File

@@ -205,6 +205,16 @@ struct SettingsView: View {
}
.disabled(chatModel.chatRunning != true)
Section {
NavigationLink {
MigrateToAnotherDevice()
.navigationTitle("Migrate to another device")
.navigationBarTitleDisplayMode(.large)
} label: {
settingsRow("tray.and.arrow.up") { Text("Migrate to another device") }
}
}
Section("Settings") {
NavigationLink {
NotificationsView()

View File

@@ -642,10 +642,14 @@ func receivedMsgNtf(_ res: ChatResponse) async -> (String, NSENotification)? {
cleanupDirectFile(aChatItem)
return nil
case let .sndFileRcvCancelled(_, aChatItem, _):
cleanupDirectFile(aChatItem)
if let aChatItem = aChatItem {
cleanupDirectFile(aChatItem)
}
return nil
case let .sndFileCompleteXFTP(_, aChatItem, _):
cleanupFile(aChatItem)
case let .sndFileCompleteXFTP(_, aChatItem, _, _):
if let aChatItem = aChatItem {
cleanupFile(aChatItem)
}
return nil
case let .callInvitation(invitation):
// Do not post it without CallKit support, iOS will stop launching the app without showing CallKit

View File

@@ -185,6 +185,7 @@
64E972072881BB22008DBC02 /* CIGroupInvitationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */; };
64F1CC3B28B39D8600CD1FB1 /* IncognitoHelp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64F1CC3A28B39D8600CD1FB1 /* IncognitoHelp.swift */; };
8C05382E2B39887E006436DC /* VideoUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C05382D2B39887E006436DC /* VideoUtils.swift */; };
8C7DF3202B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7DF31F2B7CDB0A00C886D0 /* MigrateToAnotherDevice.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 */; };
@@ -473,6 +474,7 @@
64E972062881BB22008DBC02 /* CIGroupInvitationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIGroupInvitationView.swift; sourceTree = "<group>"; };
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>"; };
8C7DF31F2B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MigrateToAnotherDevice.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; };
@@ -766,6 +768,7 @@
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */,
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */,
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */,
8C7DF31F2B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift */,
);
path = UserSettings;
sourceTree = "<group>";
@@ -1220,6 +1223,7 @@
5CB0BA92282713FD00B3292C /* CreateProfile.swift in Sources */,
5C5F2B7027EBC704006A9D5F /* ProfileImage.swift in Sources */,
5C9329412929248A0090FFF9 /* ScanProtocolServer.swift in Sources */,
8C7DF3202B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift in Sources */,
64AA1C6C27F3537400AC7277 /* DeletedItemView.swift in Sources */,
5C93293F2928E0FD0090FFF9 /* AudioRecPlay.swift in Sources */,
5C029EA82837DBB3004A9677 /* CICallItemView.swift in Sources */,

View File

@@ -131,6 +131,8 @@ public enum ChatCommand {
case listRemoteCtrls
case stopRemoteCtrl
case deleteRemoteCtrl(remoteCtrlId: Int64)
case apiUploadFileDirectly(userId: Int64, file: CryptoFile)
case apiDownloadFileDirectly(userId: Int64, url: String, file: CryptoFile)
// misc
case showVersion
case string(String)
@@ -284,6 +286,8 @@ public enum ChatCommand {
case .listRemoteCtrls: return "/list remote ctrls"
case .stopRemoteCtrl: return "/stop remote ctrl"
case let .deleteRemoteCtrl(rcId): return "/delete remote ctrl \(rcId)"
case let .apiUploadFileDirectly(userId, file): return "/_upload \(userId) \(encodeJSON(file))"
case let .apiDownloadFileDirectly(userId, link, file): return "/_download \(userId) \(link) \(encodeJSON(file))"
case .showVersion: return "/version"
case let .string(str): return str
}
@@ -409,6 +413,8 @@ public enum ChatCommand {
case .listRemoteCtrls: return "listRemoteCtrls"
case .stopRemoteCtrl: return "stopRemoteCtrl"
case .deleteRemoteCtrl: return "deleteRemoteCtrl"
case .apiUploadFileDirectly: return "apiUploadFileDirectly"
case .apiDownloadFileDirectly: return "apiDownloadFileDirectly"
case .showVersion: return "showVersion"
case .string: return "console command"
}
@@ -591,20 +597,25 @@ public enum ChatResponse: Decodable, Error {
// receiving file events
case rcvFileAccepted(user: UserRef, chatItem: AChatItem)
case rcvFileAcceptedSndCancelled(user: UserRef, rcvFileTransfer: RcvFileTransfer)
case rcvFileStart(user: UserRef, chatItem: AChatItem)
case rcvFileProgressXFTP(user: UserRef, chatItem: AChatItem, receivedSize: Int64, totalSize: Int64)
case rcvFileStart(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
case rcvFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, receivedSize: Int64, totalSize: Int64, rcvFileTransfer: RcvFileTransfer)
case rcvFileComplete(user: UserRef, chatItem: AChatItem)
case rcvFileCompleteXFTP(user: UserRef, targetPath: String, rcvFileTransfer: RcvFileTransfer)
case rcvFileCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
case rcvFileSndCancelled(user: UserRef, chatItem: AChatItem, rcvFileTransfer: RcvFileTransfer)
case rcvFileError(user: UserRef, chatItem: AChatItem)
case rcvFileError(user: UserRef, chatItem_: AChatItem?, rcvFileTransfer: RcvFileTransfer)
// sending file events
case sndFileStart(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileComplete(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileCancelled(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sndFileTransfers: [SndFileTransfer])
case sndFileRcvCancelled(user: UserRef, chatItem: AChatItem, sndFileTransfer: SndFileTransfer)
case sndFileProgressXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
case sndFileCompleteXFTP(user: UserRef, chatItem: AChatItem, fileTransferMeta: FileTransferMeta)
case sndFileError(user: UserRef, chatItem: AChatItem)
case sndFileRcvCancelled(user: UserRef, chatItem_: AChatItem?, sndFileTransfer: SndFileTransfer)
case sndFileStartXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
case sndFileStartXFTPDirect(user: UserRef, fileTransferMeta: FileTransferMeta)
case sndFileProgressXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, sentSize: Int64, totalSize: Int64)
case sndFileRedirectXFTP(user: UserRef, fileTransferMeta: FileTransferMeta, redirectMeta: FileTransferMeta)
case sndFileCompleteXFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta, rcvURIs: [String])
case sndFileCancelledFTP(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
case sndFileError(user: UserRef, chatItem_: AChatItem?, fileTransferMeta: FileTransferMeta)
// call events
case callInvitation(callInvitation: RcvCallInvitation)
case callOffer(user: UserRef, contact: Contact, callType: CallType, offer: WebRTCSession, sharedKey: String?, askConfirmation: Bool)
@@ -745,15 +756,20 @@ public enum ChatResponse: Decodable, Error {
case .rcvFileStart: return "rcvFileStart"
case .rcvFileProgressXFTP: return "rcvFileProgressXFTP"
case .rcvFileComplete: return "rcvFileComplete"
case .rcvFileCompleteXFTP: return "rcvFileCompleteXFTP"
case .rcvFileCancelled: return "rcvFileCancelled"
case .rcvFileSndCancelled: return "rcvFileSndCancelled"
case .rcvFileError: return "rcvFileError"
case .sndFileStart: return "sndFileStart"
case .sndFileComplete: return "sndFileComplete"
case .sndFileCancelled: return "sndFileCancelled"
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
case .sndFileStartXFTP: return "sndFileStartXFTP"
case .sndFileStartXFTPDirect: return "sndFileStartXFTPDirect"
case .sndFileProgressXFTP: return "sndFileProgressXFTP"
case .sndFileRedirectXFTP: return "sndFileRedirectXFTP"
case .sndFileRcvCancelled: return "sndFileRcvCancelled"
case .sndFileCompleteXFTP: return "sndFileCompleteXFTP"
case .sndFileCancelledFTP: return "sndFileCancelledFTP"
case .sndFileError: return "sndFileError"
case .callInvitation: return "callInvitation"
case .callOffer: return "callOffer"
@@ -892,19 +908,24 @@ public enum ChatResponse: Decodable, Error {
case let .newMemberContactReceivedInv(u, contact, groupInfo, member): return withUser(u, "contact: \(contact)\ngroupInfo: \(groupInfo)\nmember: \(member)")
case let .rcvFileAccepted(u, chatItem): return withUser(u, String(describing: chatItem))
case .rcvFileAcceptedSndCancelled: return noDetails
case let .rcvFileStart(u, chatItem): return withUser(u, String(describing: chatItem))
case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)")
case let .rcvFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .rcvFileProgressXFTP(u, chatItem, receivedSize, totalSize, _): return withUser(u, "chatItem: \(String(describing: chatItem))\nreceivedSize: \(receivedSize)\ntotalSize: \(totalSize)")
case let .rcvFileComplete(u, chatItem): return withUser(u, String(describing: chatItem))
case let .rcvFileCompleteXFTP(u, targetPath, _): return withUser(u, targetPath)
case let .rcvFileCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .rcvFileSndCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .rcvFileError(u, chatItem): return withUser(u, String(describing: chatItem))
case let .rcvFileError(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileStart(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileComplete(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileCancelled(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
case let .sndFileStartXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
case .sndFileStartXFTPDirect(_, _): return noDetails
case let .sndFileRcvCancelled(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileProgressXFTP(u, chatItem, _, sentSize, totalSize): return withUser(u, "chatItem: \(String(describing: chatItem))\nsentSize: \(sentSize)\ntotalSize: \(totalSize)")
case let .sndFileCompleteXFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileError(u, chatItem): return withUser(u, String(describing: chatItem))
case let .sndFileRedirectXFTP(u, _, redirectMeta): return withUser(u, String(describing: redirectMeta))
case let .sndFileCompleteXFTP(u, chatItem, _, _): return withUser(u, String(describing: chatItem))
case let .sndFileCancelledFTP(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .sndFileError(u, chatItem, _): return withUser(u, String(describing: chatItem))
case let .callInvitation(inv): return String(describing: inv)
case let .callOffer(u, contact, callType, offer, sharedKey, askConfirmation): return withUser(u, "contact: \(contact.id)\ncallType: \(String(describing: callType))\nsharedKey: \(sharedKey ?? "")\naskConfirmation: \(askConfirmation)\noffer: \(String(describing: offer))")
case let .callAnswer(u, contact, answer): return withUser(u, "contact: \(contact.id)\nanswer: \(String(describing: answer))")
@@ -1732,6 +1753,7 @@ public enum StoreError: Decodable {
case fileIdNotFoundBySharedMsgId(sharedMsgId: String)
case sndFileNotFoundXFTP(agentSndFileId: String)
case rcvFileNotFoundXFTP(agentRcvFileId: String)
case extraFileDescrNotFoundXFTP(fileId: Int64)
case connectionNotFound(agentConnId: String)
case connectionNotFoundById(connId: Int64)
case connectionNotFoundByMemberId(groupMemberId: Int64)