Handle NSItemProvider public.image data types. (#3541)
This commit is contained in:
@@ -6,23 +6,136 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import UIKit
|
||||||
import UniformTypeIdentifiers
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
extension NSItemProvider {
|
extension NSItemProvider {
|
||||||
|
struct PreferredContentType {
|
||||||
|
let type: UTType
|
||||||
|
let fileExtension: String
|
||||||
|
}
|
||||||
|
|
||||||
|
func storeData() async -> URL? {
|
||||||
|
guard let contentType = preferredContentType else {
|
||||||
|
MXLog.error("Invalid NSItemProvider: \(self)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType.type.identifier == UTType.image.identifier {
|
||||||
|
return await generateURLForUIImage(contentType)
|
||||||
|
} else {
|
||||||
|
return await generateURLForGenericData(contentType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateURLForUIImage(_ contentType: PreferredContentType) async -> URL? {
|
||||||
|
guard let uiImage = try? await loadItem(forTypeIdentifier: contentType.type.identifier) as? UIImage else {
|
||||||
|
MXLog.error("Failed casting UIImage, invalid NSItemProvider: \(self)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let pngData = uiImage.pngData() else {
|
||||||
|
MXLog.error("Failed extracting PNG data out of the UIImage")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
if let suggestedName = suggestedName as? NSString,
|
||||||
|
// Suggestions are nice but their extension is `jpeg`
|
||||||
|
let filename = (suggestedName.deletingPathExtension as NSString).appendingPathExtension(contentType.fileExtension) {
|
||||||
|
return try FileManager.default.writeDataToTemporaryDirectory(data: pngData, fileName: filename)
|
||||||
|
} else {
|
||||||
|
let filename = "\(UUID().uuidString).\(contentType.fileExtension)"
|
||||||
|
return try FileManager.default.writeDataToTemporaryDirectory(data: pngData, fileName: filename)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func generateURLForGenericData(_ contentType: PreferredContentType) async -> URL? {
|
||||||
|
let providerDescription = description
|
||||||
|
let shareData: Data? = await withCheckedContinuation { continuation in
|
||||||
|
_ = loadDataRepresentation(for: contentType.type) { data, error in
|
||||||
|
if let error {
|
||||||
|
MXLog.error("Failed processing NSItemProvider: \(providerDescription) with error: \(error)")
|
||||||
|
continuation.resume(returning: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data else {
|
||||||
|
MXLog.error("Invalid NSItemProvider data: \(providerDescription)")
|
||||||
|
continuation.resume(returning: nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.resume(returning: data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let shareData else {
|
||||||
|
MXLog.error("Invalid share data")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
|
if let filename = suggestedName {
|
||||||
|
let hasExtension = !(filename as NSString).pathExtension.isEmpty
|
||||||
|
let filename = hasExtension ? filename : "\(filename).\(contentType.fileExtension)"
|
||||||
|
return try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename)
|
||||||
|
} else {
|
||||||
|
let filename = "\(UUID().uuidString).\(contentType.fileExtension)"
|
||||||
|
return try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
MXLog.error("Failed storing NSItemProvider data \(self) with error: \(error)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var isSupportedForPasteOrDrop: Bool {
|
var isSupportedForPasteOrDrop: Bool {
|
||||||
preferredContentType != nil
|
preferredContentType != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var preferredContentType: UTType? {
|
var preferredContentType: PreferredContentType? {
|
||||||
let supportedContentTypes = registeredContentTypes
|
let supportedContentTypes = registeredContentTypes
|
||||||
.filter { isMimeTypeSupported($0.preferredMIMEType) }
|
.filter { isMimeTypeSupported($0.preferredMIMEType) || isIdentifierSupported($0.identifier) }
|
||||||
|
|
||||||
// Have .jpeg take priority over .heic
|
// Have .jpeg take priority over .heic
|
||||||
if supportedContentTypes.contains(.jpeg) {
|
if supportedContentTypes.contains(.jpeg) {
|
||||||
return .jpeg
|
guard let fileExtension = preferredFileExtension(for: .jpeg) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return .init(type: .jpeg, fileExtension: fileExtension)
|
||||||
}
|
}
|
||||||
|
|
||||||
return supportedContentTypes.first
|
guard let preferredContentType = supportedContentTypes.first,
|
||||||
|
let fileExtension = preferredFileExtension(for: preferredContentType) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return .init(type: preferredContentType, fileExtension: fileExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func preferredFileExtension(for contentType: UTType) -> String? {
|
||||||
|
if let fileExtension = contentType.preferredFilenameExtension {
|
||||||
|
return fileExtension
|
||||||
|
}
|
||||||
|
|
||||||
|
switch contentType.identifier {
|
||||||
|
case UTType.image.identifier:
|
||||||
|
return "png"
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isIdentifierSupported(_ identifier: String?) -> Bool {
|
||||||
|
// Don't filter out generic public.image content as screenshots are in this format
|
||||||
|
// and we can convert them to a PNG ourselves.
|
||||||
|
identifier == UTType.image.identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isMimeTypeSupported(_ mimeType: String?) -> Bool {
|
private func isMimeTypeSupported(_ mimeType: String?) -> Bool {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ struct PhotoLibraryPicker: UIViewControllerRepresentable {
|
|||||||
photoLibraryPicker.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
photoLibraryPicker.userIndicatorController.retractIndicatorWithId(Self.loadingIndicatorIdentifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
provider.loadFileRepresentation(forTypeIdentifier: contentType.identifier) { [weak self] url, error in
|
provider.loadFileRepresentation(forTypeIdentifier: contentType.type.identifier) { [weak self] url, error in
|
||||||
guard let url else {
|
guard let url else {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
self?.photoLibraryPicker.callback(.error(.failedLoadingFileRepresentation(error)))
|
self?.photoLibraryPicker.callback(.error(.failedLoadingFileRepresentation(error)))
|
||||||
|
|||||||
@@ -248,54 +248,20 @@ class TimelineInteractionHandler {
|
|||||||
// MARK: Pasting and dropping
|
// MARK: Pasting and dropping
|
||||||
|
|
||||||
func handlePasteOrDrop(_ provider: NSItemProvider) {
|
func handlePasteOrDrop(_ provider: NSItemProvider) {
|
||||||
guard let contentType = provider.preferredContentType,
|
Task {
|
||||||
let preferredExtension = contentType.preferredFilenameExtension else {
|
let loadingIndicatorIdentifier = UUID().uuidString
|
||||||
MXLog.error("Invalid NSItemProvider: \(provider)")
|
self.userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
||||||
actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia))
|
defer {
|
||||||
return
|
self.userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
|
||||||
}
|
|
||||||
|
|
||||||
let providerSuggestedName = provider.suggestedName
|
|
||||||
let providerDescription = provider.description
|
|
||||||
|
|
||||||
_ = provider.loadDataRepresentation(for: contentType) { data, error in
|
|
||||||
Task { @MainActor in
|
|
||||||
let loadingIndicatorIdentifier = UUID().uuidString
|
|
||||||
self.userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
|
|
||||||
defer {
|
|
||||||
self.userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let error {
|
|
||||||
self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia))
|
|
||||||
MXLog.error("Failed processing NSItemProvider: \(providerDescription) with error: \(error)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let data else {
|
|
||||||
self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia))
|
|
||||||
MXLog.error("Invalid NSItemProvider data: \(providerDescription)")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let url = try await Task.detached {
|
|
||||||
if let filename = providerSuggestedName {
|
|
||||||
let hasExtension = !(filename as NSString).pathExtension.isEmpty
|
|
||||||
let filename = hasExtension ? filename : "\(filename).\(preferredExtension)"
|
|
||||||
return try FileManager.default.writeDataToTemporaryDirectory(data: data, fileName: filename)
|
|
||||||
} else {
|
|
||||||
let filename = "\(UUID().uuidString).\(preferredExtension)"
|
|
||||||
return try FileManager.default.writeDataToTemporaryDirectory(data: data, fileName: filename)
|
|
||||||
}
|
|
||||||
}.value
|
|
||||||
|
|
||||||
self.actionsSubject.send(.displayMediaUploadPreviewScreen(url: url))
|
|
||||||
} catch {
|
|
||||||
self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia))
|
|
||||||
MXLog.error("Failed storing NSItemProvider data \(providerDescription) with error: \(error)")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
guard let fileURL = await provider.storeData() else {
|
||||||
|
MXLog.error("Failed storing NSItemProvider data \(provider)")
|
||||||
|
self.actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.actionsSubject.send(.displayMediaUploadPreviewScreen(url: fileURL))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,54 +42,14 @@ class ShareExtensionViewController: UIViewController {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let contentType = itemProvider.preferredContentType,
|
guard let fileURL = await itemProvider.storeData() else {
|
||||||
let preferredExtension = contentType.preferredFilenameExtension else {
|
MXLog.error("Failed storing NSItemProvider data \(itemProvider)")
|
||||||
MXLog.error("Invalid NSItemProvider: \(itemProvider)")
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier
|
let roomID = (extensionContext?.intent as? INSendMessageIntent)?.conversationIdentifier
|
||||||
let providerSuggestedName = itemProvider.suggestedName
|
|
||||||
let providerDescription = itemProvider.description
|
|
||||||
|
|
||||||
let shareData: Data? = await withCheckedContinuation { continuation in
|
|
||||||
_ = itemProvider.loadDataRepresentation(for: contentType) { data, error in
|
|
||||||
if let error {
|
|
||||||
MXLog.error("Failed processing NSItemProvider: \(providerDescription) with error: \(error)")
|
|
||||||
continuation.resume(returning: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let data else {
|
return .mediaFile(roomID: roomID, mediaFile: .init(url: fileURL, suggestedName: fileURL.lastPathComponent))
|
||||||
MXLog.error("Invalid NSItemProvider data: \(providerDescription)")
|
|
||||||
continuation.resume(returning: nil)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
continuation.resume(returning: data)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let shareData else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
do {
|
|
||||||
let url: URL
|
|
||||||
if let filename = providerSuggestedName {
|
|
||||||
let hasExtension = !(filename as NSString).pathExtension.isEmpty
|
|
||||||
let filename = hasExtension ? filename : "\(filename).\(preferredExtension)"
|
|
||||||
url = try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename)
|
|
||||||
} else {
|
|
||||||
let filename = "\(UUID().uuidString).\(preferredExtension)"
|
|
||||||
url = try FileManager.default.writeDataToTemporaryDirectory(data: shareData, fileName: filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
return .mediaFile(roomID: roomID, mediaFile: .init(url: url, suggestedName: providerSuggestedName))
|
|
||||||
} catch {
|
|
||||||
MXLog.error("Failed storing NSItemProvider data \(providerDescription) with error: \(error)")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private func openMainApp(payload: ShareExtensionPayload) async {
|
private func openMainApp(payload: ShareExtensionPayload) async {
|
||||||
|
|||||||
Reference in New Issue
Block a user