migrating to device

This commit is contained in:
Avently 2024-02-23 23:04:29 +07:00
parent ac7a3e5b96
commit fa2bdaf477
9 changed files with 642 additions and 69 deletions

View File

@ -276,8 +276,8 @@ func apiStorageEncryption(currentKey: String = "", newKey: String = "") async th
try await sendCommandOkResp(.apiStorageEncryption(config: DBEncryptionConfig(currentKey: currentKey, newKey: newKey)))
}
func testStorageEncryption(key: String) async throws {
try await sendCommandOkResp(.testStorageEncryption(key: key))
func testStorageEncryption(key: String, _ ctrl: chat_ctrl? = nil) async throws {
try await sendCommandOkResp(.testStorageEncryption(key: key), ctrl)
}
func apiGetChats() throws -> [ChatData] {
@ -878,8 +878,8 @@ func uploadStandaloneFile(user: any UserLike, file: CryptoFile, ctrl: chat_ctrl?
}
}
func downloadStandaloneFile(user: any UserLike, url: String, file: CryptoFile) async -> (RcvFileTransfer?, String?) {
let r = await chatSendCmd(.apiDownloadStandaloneFile(userId: user.userId, url: url, file: file))
func downloadStandaloneFile(user: any UserLike, url: String, file: CryptoFile, ctrl: chat_ctrl? = nil) async -> (RcvFileTransfer?, String?) {
let r = await chatSendCmd(.apiDownloadStandaloneFile(userId: user.userId, url: url, file: file), ctrl)
if case let .rcvStandaloneFileCreated(_, rcvFileTransfer) = r {
return (rcvFileTransfer, nil)
} else {
@ -1106,8 +1106,8 @@ func apiMarkChatItemRead(_ cInfo: ChatInfo, _ cItem: ChatItem) async {
}
}
private func sendCommandOkResp(_ cmd: ChatCommand) async throws {
let r = await chatSendCmd(cmd)
private func sendCommandOkResp(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) async throws {
let r = await chatSendCmd(cmd, ctrl)
if case .cmdOk = r { return }
throw r
}

View File

@ -0,0 +1,515 @@
//
// MigrateFromAnotherDevice.swift
// SimpleX (iOS)
//
// Created by Avently on 23.02.2024.
// Copyright © 2024 SimpleX Chat. All rights reserved.
//
import SwiftUI
import SimpleXChat
private enum MigrationState: Equatable {
case pasteOrScanLink(link: String)
case linkDownloading(link: String)
case downloadProgress(downloadedBytes: Int64, totalBytes: Int64, fileId: Int64, link: String, archivePath: URL, ctrl: chat_ctrl?)
case downloadFailed(totalBytes: Int64, link: String, archivePath: URL)
case archiveImport(archivePath: String)
case passphraseEntering(passphrase: String)
case migration(passphrase: String)
}
private enum MigrateFromAnotherDeviceViewAlert: 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.")
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: String = "")
var id: String {
switch self {
case .chatImportedWithErrors: return "chatImportedWithErrors"
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)"
}
}
}
struct MigrateFromAnotherDevice: View {
@EnvironmentObject var m: ChatModel
@Environment(\.dismiss) var dismiss: DismissAction
@State private var migrationState: MigrationState = .pasteOrScanLink(link: "")
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State private var alert: MigrateFromAnotherDeviceViewAlert?
private let tempDatabaseUrl = urlForTemporaryDatabase()
@State private var chatReceiver: MigrationChatReceiver? = nil
@State private var backDisabled: Bool = false
@State private var showQRCodeScanner: Bool = true
var body: some View {
VStack {
switch migrationState {
case let .pasteOrScanLink(link):
pasteOrScanLinkView(link)
case let .linkDownloading(link):
linkDownloadingView(link)
case let .downloadProgress(downloaded, total, _, link, archivePath, _):
downloadProgressView(downloaded, totalBytes: total, link, archivePath)
case let .downloadFailed(total, link, archivePath):
downloadFailedView(totalBytes: total, link, archivePath)
case let .archiveImport(archivePath):
archiveImportView(archivePath)
case let .passphraseEntering(passphrase):
PassphraseEnteringView(migrationState: $migrationState, currentKey: passphrase, alert: $alert)
case let .migration(passphrase):
migrationView(passphrase)
}
}
.modifier(BackButton(label: "Back") {
if !backDisabled {
dismiss()
}
})
.onChange(of: migrationState) { state in
backDisabled = switch migrationState {
case .passphraseEntering: true
case .migration: true
default: false
}
}
.onDisappear {
Task {
if case let .downloadProgress(_, _, fileId, _, _, ctrl) = migrationState, let ctrl {
await stopArchiveDownloading(fileId, ctrl)
}
chatReceiver?.stop()
try? FileManager.default.removeItem(atPath: "\(tempDatabaseUrl.path)_chat.db")
try? FileManager.default.removeItem(atPath: "\(tempDatabaseUrl.path)_agent.db")
try? FileManager.default.removeItem(at: getMigrationTempFilesDirectory())
}
}
.alert(item: $alert) { alert in
switch alert {
case let .chatImportedWithErrors(title, text):
return Alert(title: Text(title), message: Text(text))
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))
}
}
.interactiveDismissDisabled(backDisabled)
}
private func pasteOrScanLinkView(_ link: String) -> some View {
ZStack {
List {
Section("Paste link to an archive") {
pasteLinkView()
}
Section("Or scan QR code") {
ScannerInView(showQRCodeScanner: $showQRCodeScanner) { resp in
switch resp {
case let .success(r):
let link = r.string
if strHasSimplexFileLink(link.trimmingCharacters(in: .whitespaces)) {
migrationState = .linkDownloading(link: link.trimmingCharacters(in: .whitespaces))
} else {
alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.")
}
case let .failure(e):
logger.error("processQRCode QR code error: \(e.localizedDescription)")
alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.")
}
}
}
}
}
}
private func pasteLinkView() -> some View {
Button {
if let str = UIPasteboard.general.string {
if strHasSimplexFileLink(str.trimmingCharacters(in: .whitespaces)) {
migrationState = .linkDownloading(link: str.trimmingCharacters(in: .whitespaces))
} else {
alert = .error(title: "Invalid link", error: "The text you pasted is not a SimpleX link.")
}
}
} label: {
Text("Tap to paste link")
}
.disabled(!ChatModel.shared.pasteboardHasStrings)
.frame(maxWidth: .infinity, alignment: .center)
}
private func linkDownloadingView(_ link: String) -> some View {
ZStack {
List {
Section {} header: {
Text("Downloading link details…")
}
}
progressView()
}
.onAppear {
downloadLinkDetails(link)
}
}
private func downloadProgressView(_ downloadedBytes: Int64, totalBytes: Int64, _ link: String, _ archivePath: URL) -> some View {
ZStack {
List {
Section {} header: {
Text("Downloading archive…")
}
}
let ratio = Float(downloadedBytes) / Float(totalBytes)
largeProgressView(ratio, "\(Int(ratio * 100))%", "\(ByteCountFormatter.string(fromByteCount: downloadedBytes, countStyle: .binary)) downloaded")
}
}
private func downloadFailedView(totalBytes: Int64, _ link: String, _ archivePath: URL) -> some View {
List {
Section {
Button(action: {
migrationState = .downloadProgress(downloadedBytes: 0, totalBytes: totalBytes, fileId: 0, link: link, archivePath: archivePath, ctrl: nil)
}) {
settingsRow("tray.and.arrow.down") {
Text("Repeat download").foregroundColor(.accentColor)
}
}
} header: {
Text("Download failed")
} footer: {
Text("You can give another try")
.font(.callout)
}
}
.onAppear {
chatReceiver?.stop()
try? FileManager.default.removeItem(atPath: "\(tempDatabaseUrl.path)_chat.db")
try? FileManager.default.removeItem(atPath: "\(tempDatabaseUrl.path)_agent.db")
}
}
private func archiveImportView(_ archivePath: String) -> some View {
ZStack {
List {
Section {} header: {
Text("Importing archive…")
}
}
progressView()
}
.onAppear {
importArchive(archivePath)
}
}
private func migrationView(_ passphrase: String) -> some View {
ZStack {
List {
Section {} header: {
Text("Migrating…")
}
}
progressView()
}
.onAppear {
startChat(passphrase)
}
}
private func largeProgressView(_ value: Float, _ title: String, _ description: LocalizedStringKey) -> some View {
ZStack {
VStack {
Text(description)
.font(.title3)
.hidden()
Text(title)
.font(.system(size: 60))
.foregroundColor(.accentColor)
Text(description)
.font(.title3)
}
Circle()
.trim(from: 0, to: CGFloat(value))
.stroke(
Color.accentColor,
style: StrokeStyle(lineWidth: 30)
)
.rotationEffect(.degrees(-90))
.animation(.linear, value: value)
.frame(maxWidth: .infinity)
.padding(.horizontal)
.padding(.horizontal)
}
.frame(maxWidth: .infinity)
}
private func downloadLinkDetails(_ link: String) {
let archiveTime = Date.now
let ts = archiveTime.ISO8601Format(Date.ISO8601FormatStyle(timeSeparator: .omitted))
let archiveName = "simplex-chat.\(ts).zip"
let archivePath = getMigrationTempFilesDirectory().appendingPathComponent(archiveName)
startDownloading(0, link, archivePath)
}
private func initTemporaryDatabase() -> (chat_ctrl, User)? {
let (status, ctrl) = chatInitTemporaryDatabase(url: tempDatabaseUrl)
showErrorOnMigrationIfNeeded(status, $alert)
do {
if let ctrl, let user = try startChatWithTemporaryDatabase(ctrl: ctrl) {
return (ctrl, user)
}
} catch let error {
logger.error("Error while starting chat in temporary database: \(error.localizedDescription)")
}
return nil
}
private func startDownloading(_ totalBytes: Int64, _ link: String, _ archivePath: URL) {
Task {
guard let ctrlAndUser = initTemporaryDatabase() else {
return migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
}
let (ctrl, user) = ctrlAndUser
chatReceiver = MigrationChatReceiver(ctrl: ctrl) { msg in
Task {
await TerminalItems.shared.add(.resp(.now, msg))
}
logger.debug("processReceivedMsg: \(msg.responseType)")
await MainActor.run {
switch msg {
case let .rcvFileProgressXFTP(_, _, receivedSize, totalSize, rcvFileTransfer):
migrationState = .downloadProgress(downloadedBytes: receivedSize, totalBytes: totalSize, fileId: rcvFileTransfer.fileId, link: link, archivePath: archivePath, ctrl: ctrl)
case .rcvStandaloneFileComplete:
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
migrationState = .archiveImport(archivePath: archivePath.path)
}
default:
logger.debug("unsupported event: \(msg.responseType)")
}
}
}
chatReceiver?.start()
let (res, error) = await downloadStandaloneFile(user: user, url: link, file: CryptoFile.plain(archivePath.lastPathComponent), ctrl: ctrl)
if res == nil {
migrationState = .downloadFailed(totalBytes: totalBytes, link: link, archivePath: archivePath)
return alert = .error(title: "Error downloading the archive", error: error ?? "")
}
}
}
private func importArchive(_ archivePath: String) {
Task {
do {
try await apiDeleteStorage()
do {
let config = ArchiveConfig(archivePath: archivePath)
let archiveErrors = try await apiImportArchive(config: config)
if !archiveErrors.isEmpty {
alert = .chatImportedWithErrors()
}
migrationState = .passphraseEntering(passphrase: "")
} catch let error {
alert = .error(title: "Error importing chat database", error: responseError(error))
}
} catch let error {
alert = .error(title: "Error deleting chat database", error: responseError(error))
}
}
}
private func stopArchiveDownloading(_ fileId: Int64, _ ctrl: chat_ctrl) async {
_ = await apiCancelFile(fileId: fileId, ctrl: ctrl)
}
private func cancelMigration(_ fileId: Int64, _ ctrl: chat_ctrl) {
Task {
await stopArchiveDownloading(fileId, ctrl)
await MainActor.run {
dismiss()
}
}
}
private func startChat(_ passphrase: String) {
_ = kcDatabasePassword.set(passphrase)
storeDBPassphraseGroupDefault.set(true)
initialRandomDBPassphraseGroupDefault.set(false)
AppChatState.shared.set(.active)
Task {
do {
// resetChatCtrl()
try initializeChat(start: true, confirmStart: false, dbKey: passphrase, refreshInvitations: true)
await MainActor.run {
hideView()
AlertManager.shared.showAlertMsg(title: "Chat migrated!", message: "Notify another device")
}
} catch let error {
hideView()
AlertManager.shared.showAlert(Alert(title: Text("Error starting chat"), message: Text(responseError(error))))
}
}
}
private func hideView() {
onboardingStageDefault.set(.onboardingComplete)
m.onboardingStage = .onboardingComplete
dismiss()
}
private func strHasSimplexFileLink(_ text: String) -> Bool {
text.starts(with: "simplex:/file") || text.starts(with: "https://simplex.chat/file")
}
private static func urlForTemporaryDatabase() -> URL {
URL(fileURLWithPath: generateNewFileName(getMigrationTempFilesDirectory().path + "/" + "migration", "db", fullPath: true))
}
}
private struct PassphraseEnteringView: View {
@Binding var migrationState: MigrationState
@State private var useKeychain = storeDBPassphraseGroupDefault.get()
@State var currentKey: String
@State private var verifyingPassphrase: Bool = false
@Binding var alert: MigrateFromAnotherDeviceViewAlert?
var body: some View {
ZStack {
List {
Section {
PassphraseField(key: $currentKey, placeholder: "Current passphrase…", valid: validKey(currentKey))
Button(action: {
verifyingPassphrase = true
hideKeyboard()
Task {
let (status, ctrl) = chatInitTemporaryDatabase(url: getAppDatabasePath(), key: currentKey)
let success = switch status {
case .ok, .invalidConfirmation: true
default: false
}
if success {
// if let ctrl {
// chat_close_store(ctrl)
// }
applyChatCtrl(ctrl: ctrl, result: (currentKey != "", status))
migrationState = .migration(passphrase: currentKey)
} else {
showErrorOnMigrationIfNeeded(status, $alert)
}
verifyingPassphrase = false
}
}) {
settingsRow("key", color: .secondary) {
Text("Open chat")
}
}
} header: {
Text("Enter passphrase")
} footer: {
Text("Passphrase will be stored on device in Keychain. It's required for notifications to work. You can change it later in settings")
.font(.callout)
}
}
if verifyingPassphrase {
progressView()
}
}
}
}
private func showErrorOnMigrationIfNeeded(_ status: DBMigrationResult, _ alert: Binding<MigrateFromAnotherDeviceViewAlert?>) {
switch status {
case .invalidConfirmation:
alert.wrappedValue = .invalidConfirmation()
case .errorNotADatabase:
alert.wrappedValue = .wrongPassphrase()
case .errorKeychain:
alert.wrappedValue = .keychainError()
case let .errorSQL(_, error):
alert.wrappedValue = .databaseError(message: error)
case let .unknown(error):
alert.wrappedValue = .unknownError(message: error)
case .errorMigration: ()
case .ok: ()
}
}
private func progressView() -> some View {
VStack {
ProgressView().scaleEffect(2)
}
.frame(maxWidth: .infinity, maxHeight: .infinity )
}
private class MigrationChatReceiver {
let ctrl: chat_ctrl
let processReceivedMsg: (ChatResponse) async -> Void
private var receiveLoop: Task<Void, Never>?
private var receiveMessages = true
init(ctrl: chat_ctrl, _ processReceivedMsg: @escaping (ChatResponse) async -> Void) {
self.ctrl = ctrl
self.processReceivedMsg = processReceivedMsg
}
func start() {
logger.debug("MigrationChatReceiver.start")
receiveMessages = true
if receiveLoop != nil { return }
receiveLoop = Task { await receiveMsgLoop() }
}
func receiveMsgLoop() async {
// TODO use function that has timeout
if let msg = await chatRecvMsg(ctrl) {
await processReceivedMsg(msg)
}
if self.receiveMessages {
_ = try? await Task.sleep(nanoseconds: 7_500_000)
await receiveMsgLoop()
}
}
func stop() {
logger.debug("MigrationChatReceiver.stop")
receiveMessages = false
receiveLoop?.cancel()
receiveLoop = nil
chat_close_store(ctrl)
}
}
struct MigrateFromAnotherDevice_Previews: PreviewProvider {
static var previews: some View {
MigrateFromAnotherDevice()
}
}

View File

@ -9,7 +9,7 @@
import SwiftUI
import SimpleXChat
public enum MigrationState: Equatable {
private enum MigrationState: Equatable {
case initial
case chatStopInProgress
case chatStopFailed(reason: String)
@ -24,7 +24,7 @@ public enum MigrationState: Equatable {
case finished
}
enum MigrateToAnotherDeviceViewAlert: Identifiable {
private enum MigrateToAnotherDeviceViewAlert: 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")
@ -132,6 +132,7 @@ struct MigrateToAnotherDevice: View {
.onDisappear {
if case .linkShown = migrationState {} else if case .finished = migrationState {} else if !chatWasStoppedInitially {
Task {
AppChatState.shared.set(.active)
try? startChat(refreshInvitations: true)
}
}
@ -535,6 +536,7 @@ struct MigrateToAnotherDevice: View {
private func startChatAndDismiss() {
Task {
AppChatState.shared.set(.active)
try? startChat(refreshInvitations: true)
dismiss()
}
@ -623,7 +625,7 @@ func chatStoppedView() -> some View {
}
}
class MigrationChatReceiver {
private class MigrationChatReceiver {
let ctrl: chat_ctrl
let processReceivedMsg: (ChatResponse) async -> Void
private var receiveLoop: Task<Void, Never>?

View File

@ -86,7 +86,7 @@ struct NewChatView: View {
}
}
if case .connect = selection {
ConnectView(showQRCodeScanner: showQRCodeScanner, pastedLink: $pastedLink, alert: $alert)
ConnectView(showQRCodeScanner: $showQRCodeScanner, pastedLink: $pastedLink, alert: $alert)
.transition(.move(edge: .trailing))
}
}
@ -284,8 +284,7 @@ private struct InviteView: View {
private struct ConnectView: View {
@Environment(\.dismiss) var dismiss: DismissAction
@State var showQRCodeScanner = false
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
@Binding var showQRCodeScanner: Bool
@Binding var pastedLink: String
@Binding var alert: NewChatViewAlert?
@State private var sheet: PlanAndConnectActionSheet?
@ -295,32 +294,13 @@ private struct ConnectView: View {
Section("Paste the link you received") {
pasteLinkView()
}
scanCodeView()
Section("Or scan QR code") {
ScannerInView(showQRCodeScanner: $showQRCodeScanner, processQRCode: processQRCode)
}
}
.actionSheet(item: $sheet) { s in
planAndConnectActionSheet(s, dismiss: true, cleanup: { pastedLink = "" })
}
.onAppear {
let status = AVCaptureDevice.authorizationStatus(for: .video)
cameraAuthorizationStatus = status
if showQRCodeScanner {
switch status {
case .notDetermined: askCameraAuthorization()
case .restricted: showQRCodeScanner = false
case .denied: showQRCodeScanner = false
case .authorized: ()
@unknown default: askCameraAuthorization()
}
}
}
}
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
AVCaptureDevice.requestAccess(for: .video) { allowed in
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
if allowed { cb?() }
}
}
@ViewBuilder private func pasteLinkView() -> some View {
@ -351,8 +331,45 @@ private struct ConnectView: View {
}
}
private func scanCodeView() -> some View {
Section("Or scan QR code") {
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
switch resp {
case let .success(r):
let link = r.string
if strIsSimplexLink(r.string) {
connect(link)
} else {
alert = .newChatSomeAlert(alert: .someAlert(
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
id: "processQRCode: code is not a SimpleX link"
))
}
case let .failure(e):
logger.error("processQRCode QR code error: \(e.localizedDescription)")
alert = .newChatSomeAlert(alert: .someAlert(
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
id: "processQRCode: failure"
))
}
}
private func connect(_ link: String) {
planAndConnect(
link,
showAlert: { alert = .planAndConnectAlert(alert: $0) },
showActionSheet: { sheet = $0 },
dismiss: true,
incognito: nil
)
}
}
struct ScannerInView: View {
@Binding var showQRCodeScanner: Bool
let processQRCode: (_ resp: Result<ScanResult, ScanError>) -> Void
@State private var cameraAuthorizationStatus: AVAuthorizationStatus?
var body: some View {
Group {
if showQRCodeScanner, case .authorized = cameraAuthorizationStatus {
CodeScannerView(codeTypes: [.qr], scanMode: .continuous, completion: processQRCode)
.aspectRatio(1, contentMode: .fit)
@ -396,37 +413,26 @@ private struct ConnectView: View {
.disabled(cameraAuthorizationStatus == .restricted)
}
}
}
private func processQRCode(_ resp: Result<ScanResult, ScanError>) {
switch resp {
case let .success(r):
let link = r.string
if strIsSimplexLink(r.string) {
connect(link)
} else {
alert = .newChatSomeAlert(alert: .someAlert(
alert: mkAlert(title: "Invalid QR code", message: "The code you scanned is not a SimpleX link QR code."),
id: "processQRCode: code is not a SimpleX link"
))
.onAppear {
let status = AVCaptureDevice.authorizationStatus(for: .video)
cameraAuthorizationStatus = status
if showQRCodeScanner {
switch status {
case .notDetermined: askCameraAuthorization()
case .restricted: showQRCodeScanner = false
case .denied: showQRCodeScanner = false
case .authorized: ()
@unknown default: askCameraAuthorization()
}
}
case let .failure(e):
logger.error("processQRCode QR code error: \(e.localizedDescription)")
alert = .newChatSomeAlert(alert: .someAlert(
alert: mkAlert(title: "Invalid QR code", message: "Error scanning code: \(e.localizedDescription)"),
id: "processQRCode: failure"
))
}
}
private func connect(_ link: String) {
planAndConnect(
link,
showAlert: { alert = .planAndConnectAlert(alert: $0) },
showActionSheet: { sheet = $0 },
dismiss: true,
incognito: nil
)
func askCameraAuthorization(_ cb: (() -> Void)? = nil) {
AVCaptureDevice.requestAccess(for: .video) { allowed in
cameraAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video)
if allowed { cb?() }
}
}
}

View File

@ -37,7 +37,7 @@ struct HowItWorks: View {
Spacer()
if onboarding {
OnboardingActionButton()
OnboardingActionButton(hideMigrate: true)
.padding(.bottom, 8)
}
}

View File

@ -42,7 +42,7 @@ struct SimpleXInfo: View {
Spacer()
if onboarding {
OnboardingActionButton()
OnboardingActionButton(hideMigrate: false)
Spacer()
}
@ -87,10 +87,28 @@ struct SimpleXInfo: View {
struct OnboardingActionButton: View {
@EnvironmentObject var m: ChatModel
let hideMigrate: Bool
@State private var migrateFromAnotherDevice: Bool = false
var body: some View {
if m.currentUser == nil {
actionButton("Create your profile", onboarding: .step2_CreateProfile)
if !hideMigrate {
actionButton("Migrate from another device") {
migrateFromAnotherDevice = true
}
.sheet(isPresented: $migrateFromAnotherDevice) {
VStack(alignment: .leading) {
Text("Migrate here")
.font(.largeTitle)
.padding([.leading, .top, .trailing])
.padding(.top)
MigrateFromAnotherDevice()
}
.background(Color(uiColor: .tertiarySystemGroupedBackground))
}
}
} else {
actionButton("Make a private connection", onboarding: .onboardingComplete)
}
@ -111,6 +129,21 @@ struct OnboardingActionButton: View {
.frame(maxWidth: .infinity)
.padding(.bottom)
}
private func actionButton(_ label: LocalizedStringKey, action: @escaping () -> Void) -> some View {
Button {
withAnimation {
action()
}
} label: {
HStack {
Text(label).font(.title2)
Image(systemName: "greaterthan")
}
}
.frame(maxWidth: .infinity)
.padding(.bottom)
}
}
struct SimpleXInfo_Previews: PreviewProvider {

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 */; };
8C7D949A2B88952700B7B9E1 /* MigrateFromAnotherDevice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8C7D94992B88952700B7B9E1 /* MigrateFromAnotherDevice.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 */; };
@ -474,6 +475,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>"; };
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>"; };
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; };
@ -555,6 +557,7 @@
5CB924DD27A8622200ACCCDD /* NewChat */,
5CFA59C22860B04D00863A68 /* Database */,
5CB634AB29E46CDB0066AD6B /* LocalAuth */,
8C7D94982B8894D300B7B9E1 /* Migration */,
5CA8D01B2AD9B076001FD661 /* RemoteAccess */,
5CB924DF27A8678B00ACCCDD /* UserSettings */,
5C2E261127A30FEA00F70299 /* TerminalView.swift */,
@ -768,7 +771,6 @@
64D0C2BF29F9688300B38D5F /* UserAddressView.swift */,
64D0C2C129FA57AB00B38D5F /* UserAddressLearnMore.swift */,
5CEBD7472A5F115D00665FE2 /* SetDeliveryReceiptsView.swift */,
8C7DF31F2B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift */,
);
path = UserSettings;
sourceTree = "<group>";
@ -896,6 +898,15 @@
path = Group;
sourceTree = "<group>";
};
8C7D94982B8894D300B7B9E1 /* Migration */ = {
isa = PBXGroup;
children = (
8C7DF31F2B7CDB0A00C886D0 /* MigrateToAnotherDevice.swift */,
8C7D94992B88952700B7B9E1 /* MigrateFromAnotherDevice.swift */,
);
path = Migration;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXHeadersBuildPhase section */
@ -1127,6 +1138,7 @@
5CBD285A295711D700EC2CF4 /* ImageUtils.swift in Sources */,
6419EC562AB8BC8B004A607A /* ContextInvitingContactMemberView.swift in Sources */,
5CE4407927ADB701007B033A /* EmojiItemView.swift in Sources */,
8C7D949A2B88952700B7B9E1 /* MigrateFromAnotherDevice.swift in Sources */,
5C3F1D562842B68D00EC8A82 /* IntegrityErrorItemView.swift in Sources */,
5C029EAA283942EA004A9677 /* CallController.swift in Sources */,
5CBE6C142944CC12002D9531 /* ScanCodeView.swift in Sources */,

View File

@ -54,9 +54,9 @@ public func chatMigrateInit(_ useKey: String? = nil, confirmMigrations: Migratio
return result
}
public func chatInitTemporaryDatabase(url: URL) -> (DBMigrationResult, chat_ctrl?) {
public func chatInitTemporaryDatabase(url: URL, key: String? = nil) -> (DBMigrationResult, chat_ctrl?) {
let dbPath = url.path
let dbKey = randomDatabasePassword()
let dbKey = key ?? randomDatabasePassword()
logger.debug("chatInitTemporaryDatabase path: \(dbPath)")
var temporaryController: chat_ctrl? = nil
var cPath = dbPath.cString(using: .utf8)!
@ -85,6 +85,11 @@ public func resetChatCtrl() {
migrationResult = nil
}
public func applyChatCtrl(ctrl: chat_ctrl?, result: (Bool, DBMigrationResult)) {
chatController = ctrl
migrationResult = result
}
public func sendSimpleXCmd(_ cmd: ChatCommand, _ ctrl: chat_ctrl? = nil) -> ChatResponse {
var c = cmd.cmdString.cString(using: .utf8)!
let cjson = chat_send_cmd(ctrl ?? getChatCtrl(), &c)!

View File

@ -3378,7 +3378,7 @@ public struct SndFileTransfer: Decodable {
}
public struct RcvFileTransfer: Decodable {
public let fileId: Int64
}
public struct FileTransferMeta: Decodable {