From 6fa3695ad68172f10f0d1576d8967e5ddd990402 Mon Sep 17 00:00:00 2001 From: Arturs Krumins Date: Wed, 31 Jul 2024 20:07:11 +0300 Subject: [PATCH] ios: add database password prompt to share-sheet, fix sharing screenshot (#4546) * ios: add password prompt to share-sheet * fix sharing screenshots * s/Password/Passphrase/ * alert title --------- Co-authored-by: Evgeny Poberezkin --- apps/ios/SimpleX SE/ShareModel.swift | 101 ++++++++++++++++----------- apps/ios/SimpleX SE/ShareView.swift | 12 +++- 2 files changed, 70 insertions(+), 43 deletions(-) diff --git a/apps/ios/SimpleX SE/ShareModel.swift b/apps/ios/SimpleX SE/ShareModel.swift index 8d1443608a..49c48afc5c 100644 --- a/apps/ios/SimpleX SE/ShareModel.swift +++ b/apps/ios/SimpleX SE/ShareModel.swift @@ -27,6 +27,7 @@ class ShareModel: ObservableObject { @Published var isLoaded = false @Published var bottomBar: BottomBar = .loadingSpinner @Published var errorAlert: ErrorAlert? + @Published var alertRequiresPassword = false enum BottomBar { case sendButton @@ -71,43 +72,47 @@ class ShareModel: ObservableObject { apiSuspendChat(expired: $0) } } - // Init Chat - Task { - if let e = initChat() { - await MainActor.run { errorAlert = e } - } else { - // Load Chats - Task { - switch fetchChats() { - case let .success(chats): - // Decode base64 images on background thread - let profileImages = chats.reduce(into: Dictionary()) { dict, chatData in - if let profileImage = chatData.chatInfo.image, - let uiImage = UIImage(base64Encoded: profileImage) { - dict[chatData.id] = uiImage - } + setup() + } + } + + func setup(with dbKey: String? = nil) { + // Init Chat + Task { + if let e = initChat(with: dbKey) { + await MainActor.run { errorAlert = e } + } else { + // Load Chats + Task { + switch fetchChats() { + case let .success(chats): + // Decode base64 images on background thread + let profileImages = chats.reduce(into: Dictionary()) { dict, chatData in + if let profileImage = chatData.chatInfo.image, + let uiImage = UIImage(base64Encoded: profileImage) { + dict[chatData.id] = uiImage } - await MainActor.run { - self.chats = chats - self.profileImages = profileImages - withAnimation { isLoaded = true } - } - case let .failure(error): - await MainActor.run { errorAlert = error } } + await MainActor.run { + self.chats = chats + self.profileImages = profileImages + withAnimation { isLoaded = true } + } + case let .failure(error): + await MainActor.run { errorAlert = error } } - // Process Attachment - Task { - switch await self.itemProvider!.sharedContent() { - case let .success(chatItemContent): - await MainActor.run { - self.sharedContent = chatItemContent - self.bottomBar = .sendButton - if case let .text(string) = chatItemContent { comment = string } - } - case let .failure(errorAlert): - await MainActor.run { self.errorAlert = errorAlert } + } + // Process Attachment + Task { + switch await self.itemProvider!.sharedContent() { + case let .success(chatItemContent): + await MainActor.run { + self.sharedContent = chatItemContent + self.bottomBar = .sendButton + if case let .text(string) = chatItemContent { comment = string } } + case let .failure(errorAlert): + await MainActor.run { self.errorAlert = errorAlert } } } } @@ -149,14 +154,15 @@ class ShareModel: ObservableObject { } } - private func initChat() -> ErrorAlert? { + private func initChat(with dbKey: String? = nil) -> ErrorAlert? { do { - if hasChatCtrl() { + if hasChatCtrl() && dbKey == nil { try apiActivateChat() } else { + resetChatCtrl() // Clears retained migration result registerGroupDefaults() haskell_init_se() - let (_, result) = chatMigrateInit(confirmMigrations: defaultMigrationConfirmation()) + let (_, result) = chatMigrateInit(dbKey, confirmMigrations: defaultMigrationConfirmation()) if let e = migrationError(result) { return e } try apiSetAppFilePaths( filesFolder: getAppFilesDirectory().path, @@ -175,8 +181,9 @@ class ShareModel: ObservableObject { private func migrationError(_ r: DBMigrationResult) -> ErrorAlert? { let useKeychain = storeDBPassphraseGroupDefault.get() let storedDBKey = kcDatabasePassword.get() - // This switch duplicates DatabaseErrorView. - // TODO allow entering passphrase and make messages the same as in DatabaseErrorView. + if case .errorNotADatabase = r { + Task { await MainActor.run { self.alertRequiresPassword = true } } + } return switch r { case .errorNotADatabase: if useKeychain && storedDBKey != nil && storedDBKey != "" { @@ -186,8 +193,8 @@ class ShareModel: ObservableObject { ) } else { ErrorAlert( - title: "Encrypted database", - message: "Sharing is not supported when passphrase is not stored in KeyChain." + title: "Database encrypted!", + message: "Database passphrase is required to open chat." ) } case let .errorMigration(_, migrationError): @@ -393,8 +400,7 @@ extension NSItemProvider { // Static } else { - if let url = try? await inPlaceUrl(type: type), - let image = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE), + if let image = await staticImage(), let cryptoFile = saveImage(image), let preview = resizeImageToStrSize(image, maxDataSize: MAX_DATA_SIZE) { .success(.image(preview: preview, cryptoFile: cryptoFile)) @@ -477,6 +483,17 @@ extension NSItemProvider { } return nil } + + private func staticImage() async -> UIImage? { + if let url = try? await inPlaceUrl(type: .image), + let downsampledImage = downsampleImage(at: url, to: MAX_DOWNSAMPLE_SIZE) { + downsampledImage + } else { + /// Fallback to loading image directly from `ItemProvider` + /// in case loading from disk is not possible. Required for sharing screenshots. + try? await loadItem(forTypeIdentifier: UTType.image.identifier) as? UIImage + } + } } diff --git a/apps/ios/SimpleX SE/ShareView.swift b/apps/ios/SimpleX SE/ShareView.swift index 395492bf47..a213a4ab23 100644 --- a/apps/ios/SimpleX SE/ShareView.swift +++ b/apps/ios/SimpleX SE/ShareView.swift @@ -12,6 +12,7 @@ import SimpleXChat struct ShareView: View { @ObservedObject var model: ShareModel @Environment(\.colorScheme) var colorScheme + @State private var password = String() @AppStorage(GROUP_DEFAULT_PROFILE_IMAGE_CORNER_RADIUS, store: groupDefaults) private var radius = defaultProfileImageCorner var body: some View { @@ -54,7 +55,16 @@ struct ShareView: View { placement: .navigationBarDrawer(displayMode: .always) ) .alert($model.errorAlert) { alert in - Button("Ok") { model.completion() } + if model.alertRequiresPassword { + SecureField("Passphrase", text: $password) + Button("Ok") { + model.setup(with: password) + password = String() + } + Button("Cancel", role: .cancel) { model.completion() } + } else { + Button("Ok") { model.completion() } + } } }