diff --git a/apps/ios/Shared/Model/ImageUtils.swift b/apps/ios/Shared/Model/ImageUtils.swift index 90070e74d..41d741e7e 100644 --- a/apps/ios/Shared/Model/ImageUtils.swift +++ b/apps/ios/Shared/Model/ImageUtils.swift @@ -195,18 +195,18 @@ func moveTempFileFromURL(_ url: URL) -> CryptoFile? { } } -func generateNewFileName(_ prefix: String, _ ext: String) -> String { - uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)") +func generateNewFileName(_ prefix: String, _ ext: String, fullPath: Bool = false) -> String { + uniqueCombine("\(prefix)_\(getTimestamp()).\(ext)", fullPath: fullPath) } -private func uniqueCombine(_ fileName: String) -> String { +private func uniqueCombine(_ fileName: String, fullPath: Bool = false) -> String { func tryCombine(_ fileName: String, _ n: Int) -> String { let ns = fileName as NSString let name = ns.deletingPathExtension let ext = ns.pathExtension let suffix = (n == 0) ? "" : "_\(n)" let f = "\(name)\(suffix).\(ext)" - return (FileManager.default.fileExists(atPath: getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f + return (FileManager.default.fileExists(atPath: fullPath ? f : getAppFilePath(f).path)) ? tryCombine(fileName, n + 1) : f } return tryCombine(fileName, 0) } diff --git a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift index 057282177..4001edffb 100644 --- a/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift +++ b/apps/ios/Shared/Views/Chat/ComposeMessage/ComposeView.swift @@ -384,10 +384,10 @@ struct ComposeView: View { } } .sheet(isPresented: $showMediaPicker) { - LibraryMediaListPicker(media: $chosenMedia, selectionLimit: 10) { itemsSelected in - showMediaPicker = false - if itemsSelected { - DispatchQueue.main.async { + LibraryMediaListPicker(addMedia: addMediaContent, selectionLimit: 10) { itemsSelected in + await MainActor.run { + showMediaPicker = false + if itemsSelected { composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: [])) } } @@ -488,6 +488,21 @@ struct ComposeView: View { } } + private func addMediaContent(_ content: UploadContent) async { + if let img = resizeImageToStrSize(content.uiImage, maxDataSize: 14000) { + var newMedia: [(String, UploadContent?)] = [] + if case var .mediaPreviews(media) = composeState.preview { + media.append((img, content)) + newMedia = media + } else { + newMedia = [(img, content)] + } + await MainActor.run { + composeState = composeState.copy(preview: .mediaPreviews(mediaPreviews: newMedia)) + } + } + } + private var maxFileSize: Int64 { getMaxFileSize(.xftp) } diff --git a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift index 7e123c389..18cc3f4d8 100644 --- a/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift +++ b/apps/ios/Shared/Views/Chat/Group/GroupProfileView.swift @@ -103,8 +103,10 @@ struct GroupProfileView: View { } } .sheet(isPresented: $showImagePicker) { - LibraryImagePicker(image: $chosenImage) { - didSelectItem in showImagePicker = false + LibraryImagePicker(image: $chosenImage) { _ in + await MainActor.run { + showImagePicker = false + } } } .onChange(of: chosenImage) { image in diff --git a/apps/ios/Shared/Views/Helpers/ImagePicker.swift b/apps/ios/Shared/Views/Helpers/ImagePicker.swift index 1b44c2313..0e3f8082b 100644 --- a/apps/ios/Shared/Views/Helpers/ImagePicker.swift +++ b/apps/ios/Shared/Views/Helpers/ImagePicker.swift @@ -13,112 +13,122 @@ import SimpleXChat struct LibraryImagePicker: View { @Binding var image: UIImage? - var didFinishPicking: (_ didSelectItems: Bool) -> Void - @State var images: [UploadContent] = [] + var didFinishPicking: (_ didSelectImage: Bool) async -> Void + @State var mediaAdded = false var body: some View { - LibraryMediaListPicker(media: $images, selectionLimit: 1, didFinishPicking: didFinishPicking) - .onChange(of: images) { _ in - if let img = images.first { - image = img.uiImage - } - } + LibraryMediaListPicker(addMedia: addMedia, selectionLimit: 1, didFinishPicking: didFinishPicking) + } + + private func addMedia(_ content: UploadContent) async { + if mediaAdded { return } + await MainActor.run { + mediaAdded = true + image = content.uiImage + } } } struct LibraryMediaListPicker: UIViewControllerRepresentable { typealias UIViewControllerType = PHPickerViewController - @Binding var media: [UploadContent] + var addMedia: (_ content: UploadContent) async -> Void var selectionLimit: Int - var didFinishPicking: (_ didSelectItems: Bool) -> Void + var didFinishPicking: (_ didSelectItems: Bool) async -> Void class Coordinator: PHPickerViewControllerDelegate { let parent: LibraryMediaListPicker let dispatchQueue = DispatchQueue(label: "chat.simplex.app.LibraryMediaListPicker") - var media: [UploadContent] = [] - var mediaCount: Int = 0 init(_ parent: LibraryMediaListPicker) { self.parent = parent } func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { - parent.didFinishPicking(!results.isEmpty) - guard !results.isEmpty else { - return + Task { + await parent.didFinishPicking(!results.isEmpty) + if results.isEmpty { return } + for r in results { + await loadItem(r.itemProvider) + } } + } - parent.media = [] - media = [] - mediaCount = results.count - for result in results { - logger.log("LibraryMediaListPicker result") - let p = result.itemProvider - if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { - p.loadFileRepresentation(forTypeIdentifier: UTType.movie.identifier) { url, error in - if let url = url { - let tempUrl = URL(fileURLWithPath: getTempFilesDirectory().path + "/" + generateNewFileName("video", url.pathExtension)) - if ((try? FileManager.default.copyItem(at: url, to: tempUrl)) != nil) { - ChatModel.shared.filesToDelete.insert(tempUrl) - self.loadVideo(url: tempUrl, error: error) + private func loadItem(_ p: NSItemProvider) async { + logger.debug("LibraryMediaListPicker result") + if p.hasItemConformingToTypeIdentifier(UTType.movie.identifier) { + if let video = await loadVideo(p) { + await self.parent.addMedia(video) + logger.debug("LibraryMediaListPicker: added video") + } + } else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) { + if let img = await loadImageData(p) { + await self.parent.addMedia(img) + logger.debug("LibraryMediaListPicker: added image") + } + } else if p.canLoadObject(ofClass: UIImage.self) { + if let img = await loadImage(p) { + await self.parent.addMedia(.simpleImage(image: img)) + logger.debug("LibraryMediaListPicker: added image") + } + } + } + + private func loadImageData(_ p: NSItemProvider) async -> UploadContent? { + await withCheckedContinuation { cont in + loadFileURL(p, type: UTType.data) { url in + if let url = url { + let img = UploadContent.loadFromURL(url: url) + cont.resume(returning: img) + } else { + cont.resume(returning: nil) + } + } + } + } + + private func loadImage(_ p: NSItemProvider) async -> UIImage? { + await withCheckedContinuation { cont in + p.loadObject(ofClass: UIImage.self) { obj, err in + if let err = err { + logger.error("LibraryMediaListPicker result image error: \(err.localizedDescription)") + cont.resume(returning: nil) + } else { + cont.resume(returning: obj as? UIImage) + } + } + } + } + + private func loadVideo(_ p: NSItemProvider) async -> UploadContent? { + await withCheckedContinuation { cont in + loadFileURL(p, type: UTType.movie) { url in + if let url = url { + let tempUrl = URL(fileURLWithPath: generateNewFileName(getTempFilesDirectory().path + "/" + "video", url.pathExtension, fullPath: true)) + do { +// logger.debug("LibraryMediaListPicker copyItem \(url) to \(tempUrl)") + try FileManager.default.copyItem(at: url, to: tempUrl) + DispatchQueue.main.async { + _ = ChatModel.shared.filesToDelete.insert(tempUrl) } + let video = UploadContent.loadVideoFromURL(url: tempUrl) + cont.resume(returning: video) + return + } catch let err { + logger.error("LibraryMediaListPicker copyItem error: \(err.localizedDescription)") } } - } else if p.hasItemConformingToTypeIdentifier(UTType.data.identifier) { - p.loadFileRepresentation(forTypeIdentifier: UTType.data.identifier) { url, error in - self.loadImage(object: url, error: error) - } - } else if p.canLoadObject(ofClass: UIImage.self) { - p.loadObject(ofClass: UIImage.self) { image, error in - DispatchQueue.main.async { - self.loadImage(object: image, error: error) - } - } + cont.resume(returning: nil) + } + } + } + + private func loadFileURL(_ p: NSItemProvider, type: UTType, completion: @escaping (URL?) -> Void) { + p.loadFileRepresentation(forTypeIdentifier: type.identifier) { url, err in + if let err = err { + logger.error("LibraryMediaListPicker loadFileURL error: \(err.localizedDescription)") + completion(nil) } else { - dispatchQueue.sync { self.mediaCount -= 1} - } - } - DispatchQueue.main.asyncAfter(deadline: .now() + 10) { - self.dispatchQueue.sync { - if self.parent.media.count == 0 { - logger.log("LibraryMediaListPicker: added \(self.media.count) images out of \(results.count)") - self.parent.media = self.media - } - } - } - } - - func loadImage(object: Any?, error: Error? = nil) { - if let error = error { - logger.error("LibraryMediaListPicker: couldn't load image with error: \(error.localizedDescription)") - } else if let image = object as? UIImage { - media.append(.simpleImage(image: image)) - logger.log("LibraryMediaListPicker: added image") - } else if let url = object as? URL, let image = UploadContent.loadFromURL(url: url) { - media.append(image) - } - dispatchQueue.sync { - self.mediaCount -= 1 - if self.mediaCount == 0 && self.parent.media.count == 0 { - logger.log("LibraryMediaListPicker: added all media") - self.parent.media = self.media - self.media = [] - } - } - } - - func loadVideo(url: URL?, error: Error? = nil) { - if let error = error { - logger.error("LibraryMediaListPicker: couldn't load video with error: \(error.localizedDescription)") - } else if let url = url as URL?, let video = UploadContent.loadVideoFromURL(url: url) { - media.append(video) - } - dispatchQueue.sync { - self.mediaCount -= 1 - if self.mediaCount == 0 && self.parent.media.count == 0 { - logger.log("LibraryMediaListPicker: added all media") - self.parent.media = self.media - self.media = [] + completion(url) } } } diff --git a/apps/ios/Shared/Views/NewChat/AddGroupView.swift b/apps/ios/Shared/Views/NewChat/AddGroupView.swift index 2d7f31c58..6c7919669 100644 --- a/apps/ios/Shared/Views/NewChat/AddGroupView.swift +++ b/apps/ios/Shared/Views/NewChat/AddGroupView.swift @@ -130,8 +130,10 @@ struct AddGroupView: View { } } .sheet(isPresented: $showImagePicker) { - LibraryImagePicker(image: $chosenImage) { - didSelectItem in showImagePicker = false + LibraryImagePicker(image: $chosenImage) { _ in + await MainActor.run { + showImagePicker = false + } } } .alert(isPresented: $showInvalidNameAlert) { diff --git a/apps/ios/Shared/Views/UserSettings/UserProfile.swift b/apps/ios/Shared/Views/UserSettings/UserProfile.swift index b64ec21de..e5ec23178 100644 --- a/apps/ios/Shared/Views/UserSettings/UserProfile.swift +++ b/apps/ios/Shared/Views/UserSettings/UserProfile.swift @@ -120,8 +120,10 @@ struct UserProfile: View { } } .sheet(isPresented: $showImagePicker) { - LibraryImagePicker(image: $chosenImage) { - didSelectItem in showImagePicker = false + LibraryImagePicker(image: $chosenImage) { _ in + await MainActor.run { + showImagePicker = false + } } } .onChange(of: chosenImage) { image in