Skip to content

Commit

Permalink
Handle NSItemProvider public.image data types. (#3541)
Browse files Browse the repository at this point in the history
  • Loading branch information
stefanceriu authored Nov 20, 2024
1 parent acd670a commit b2137ad
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 95 deletions.
121 changes: 117 additions & 4 deletions ElementX/Sources/Other/Extensions/NSItemProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,136 @@
//

import Foundation
import UIKit
import UniformTypeIdentifiers

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 {
preferredContentType != nil
}

var preferredContentType: UTType? {
var preferredContentType: PreferredContentType? {
let supportedContentTypes = registeredContentTypes
.filter { isMimeTypeSupported($0.preferredMIMEType) }
.filter { isMimeTypeSupported($0.preferredMIMEType) || isIdentifierSupported($0.identifier) }

// Have .jpeg take priority over .heic
if supportedContentTypes.contains(.jpeg) {
return .jpeg
guard let fileExtension = preferredFileExtension(for: .jpeg) else {
return nil
}

return .init(type: .jpeg, fileExtension: fileExtension)
}

guard let preferredContentType = supportedContentTypes.first,
let fileExtension = preferredFileExtension(for: preferredContentType) else {
return nil
}

return supportedContentTypes.first
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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ struct PhotoLibraryPicker: UIViewControllerRepresentable {
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 {
Task { @MainActor in
self?.photoLibraryPicker.callback(.error(.failedLoadingFileRepresentation(error)))
Expand Down
60 changes: 13 additions & 47 deletions ElementX/Sources/Screens/Timeline/TimelineInteractionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -248,54 +248,20 @@ class TimelineInteractionHandler {
// MARK: Pasting and dropping

func handlePasteOrDrop(_ provider: NSItemProvider) {
guard let contentType = provider.preferredContentType,
let preferredExtension = contentType.preferredFilenameExtension else {
MXLog.error("Invalid NSItemProvider: \(provider)")
actionsSubject.send(.displayErrorToast(L10n.screenRoomErrorFailedProcessingMedia))
return
}

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)")
}
Task {
let loadingIndicatorIdentifier = UUID().uuidString
self.userIndicatorController.submitIndicator(UserIndicator(id: loadingIndicatorIdentifier, type: .modal, title: L10n.commonLoading, persistent: true))
defer {
self.userIndicatorController.retractIndicatorWithId(loadingIndicatorIdentifier)
}

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))
}
}

Expand Down
46 changes: 3 additions & 43 deletions ShareExtension/Sources/ShareExtensionViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,54 +42,14 @@ class ShareExtensionViewController: UIViewController {
return nil
}

guard let contentType = itemProvider.preferredContentType,
let preferredExtension = contentType.preferredFilenameExtension else {
MXLog.error("Invalid NSItemProvider: \(itemProvider)")
guard let fileURL = await itemProvider.storeData() else {
MXLog.error("Failed storing NSItemProvider data \(itemProvider)")
return nil
}

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 {
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
}
return .mediaFile(roomID: roomID, mediaFile: .init(url: fileURL, suggestedName: fileURL.lastPathComponent))
}

private func openMainApp(payload: ShareExtensionPayload) async {
Expand Down

0 comments on commit b2137ad

Please sign in to comment.