diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaContainer.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaContainer.swift index be8451fd..5f9d10a7 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaContainer.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaContainer.swift @@ -8,6 +8,7 @@ struct StatusEditorMediaContainer: Identifiable { let id = UUID().uuidString let image: UIImage? let movieTransferable: MovieFileTranseferable? + let gifTransferable: GifFileTranseferable? let mediaAttachment: MediaAttachment? let error: Error? } diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift index 09088980..8eca5d89 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorMediaView.swift @@ -24,7 +24,7 @@ struct StatusEditorMediaView: View { makeLazyImage(mediaAttachement: attachement) } else if container.image != nil { makeLocalImage(container: container) - } else if container.movieTransferable != nil { + } else if container.movieTransferable != nil || container.gifTransferable != nil { makeVideoAttachement(container: container) } else if let error = container.error as? ServerError { makeErrorView(error: error) diff --git a/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift b/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift index 7a21a1c0..05d9a027 100644 --- a/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift +++ b/Packages/Status/Sources/Status/Editor/Components/StatusEditorUTTypeSupported.swift @@ -13,20 +13,31 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { case image = "public.image" case jpeg = "public.jpeg" case png = "public.png" + case tiff = "public.tiff" case video = "public.video" case movie = "public.movie" case mp4 = "public.mpeg-4" case gif = "public.gif" + case gif2 = "com.compuserve.gif" case quickTimeMovie = "com.apple.quicktime-movie" static func types() -> [UTType] { - [.url, .text, .plainText, .image, .jpeg, .png, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie] + [.url, .text, .plainText, .image, .jpeg, .png, .tiff, .video, .mpeg4Movie, .gif, .movie, .quickTimeMovie] } var isVideo: Bool { switch self { - case .video, .movie, .mp4, .gif, .quickTimeMovie: + case .video, .movie, .mp4, .quickTimeMovie: + return true + default: + return false + } + } + + var isGif: Bool { + switch self { + case .gif, .gif2: return true default: return false @@ -37,13 +48,18 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { let result = try await item.loadItem(forTypeIdentifier: rawValue) if isVideo, let transferable = await getVideoTransferable(item: item) { return transferable + } else if isGif, let transferable = await getGifTransferable(item: item) { + return transferable } - if self == .jpeg || self == .png, - let imageURL = result as? URL, - let data = try? Data(contentsOf: imageURL), - let image = UIImage(data: data) - { - return image + if self == .jpeg || self == .png || self == .tiff || self == .image { + if let imageURL = result as? URL, + let data = try? Data(contentsOf: imageURL), + let image = UIImage(data: data) { + return image + } else if let data = result as? Data, + let image = UIImage(data: data) { + return image + } } if let url = result as? URL { return url.absoluteString @@ -68,6 +84,19 @@ enum StatusEditorUTTypeSupported: String, CaseIterable { } } } + + private func getGifTransferable(item: NSItemProvider) async -> GifFileTranseferable? { + return await withCheckedContinuation { continuation in + _ = item.loadTransferable(type: GifFileTranseferable.self) { result in + switch result { + case let .success(success): + continuation.resume(with: .success(success)) + case .failure: + continuation.resume(with: .success(nil)) + } + } + } + } } struct MovieFileTranseferable: Transferable { @@ -95,9 +124,7 @@ struct MovieFileTranseferable: Transferable { FileRepresentation(contentType: .movie) { movie in SentTransferredFile(movie.url) } importing: { received in - let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)") - try FileManager.default.copyItem(at: received.file, to: copy) - return Self(url: copy) + return Self(url: localURLFor(received: received)) } } } @@ -113,13 +140,33 @@ struct ImageFileTranseferable: Transferable { FileRepresentation(contentType: .image) { image in SentTransferredFile(image.url) } importing: { received in - let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)") - try FileManager.default.copyItem(at: received.file, to: copy) - return Self(url: copy) + return Self(url: localURLFor(received: received)) } } } +struct GifFileTranseferable: Transferable { + let url: URL + + var data: Data? { + try? Data(contentsOf: url) + } + + static var transferRepresentation: some TransferRepresentation { + FileRepresentation(contentType: .gif) { gif in + SentTransferredFile(gif.url) + } importing: { received in + return Self(url: localURLFor(received: received)) + } + } +} + +fileprivate func localURLFor(received: ReceivedTransferredFile) -> URL { + let copy = URL.temporaryDirectory.appending(path: "\(UUID().uuidString).\(received.file.pathExtension)") + try? FileManager.default.copyItem(at: received.file, to: copy) + return copy +} + public extension URL { func mimeType() -> String { if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType { diff --git a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift index 6a686594..90d08001 100644 --- a/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift +++ b/Packages/Status/Sources/Status/Editor/StatusEditorViewModel.swift @@ -7,7 +7,7 @@ import PhotosUI import SwiftUI @MainActor -public class StatusEditorViewModel: ObservableObject { +public class StatusEditorViewModel: NSObject, ObservableObject { var mode: Mode var client: Client? @@ -15,7 +15,11 @@ public class StatusEditorViewModel: ObservableObject { var theme: Theme? var preferences: UserPreferences? - var textView: UITextView? + var textView: UITextView? { + didSet { + textView?.pasteDelegate = self + } + } var selectedRange: NSRange { get { guard let textView else { @@ -261,6 +265,7 @@ public class StatusEditorViewModel: ObservableObject { visibility = status.visibility mediasImages = status.mediaAttachments.map { .init(image: nil, movieTransferable: nil, + gifTransferable: nil, mediaAttachment: $0, error: nil) } case let .quote(status): @@ -333,24 +338,11 @@ public class StatusEditorViewModel: ObservableObject { urlLengthAdjustments = totalUrlLength - (maxLengthOfUrl * numUrls) - var mediaAdded = false statusText.enumerateAttributes(in: range) { attributes, range, _ in - if let attachment = attributes[.attachment] as? NSTextAttachment, let image = attachment.image { - mediasImages.append(.init(image: image, - movieTransferable: nil, - mediaAttachment: nil, - error: nil)) - statusText.removeAttribute(.attachment, range: range) - statusText.mutableString.deleteCharacters(in: range) - mediaAdded = true - } else if attributes[.link] != nil { + if attributes[.link] != nil { statusText.removeAttribute(.link, range: range) } } - - if mediaAdded { - processMediasToUpload() - } } catch {} } @@ -370,11 +362,19 @@ public class StatusEditorViewModel: ObservableObject { } else if let image = content as? UIImage { mediasImages.append(.init(image: image, movieTransferable: nil, + gifTransferable: nil, mediaAttachment: nil, error: nil)) } else if let video = content as? MovieFileTranseferable { mediasImages.append(.init(image: nil, movieTransferable: video, + gifTransferable: nil, + mediaAttachment: nil, + error: nil)) + } else if let gif = content as? GifFileTranseferable { + mediasImages.append(.init(image: nil, + movieTransferable: nil, + gifTransferable: gif, mediaAttachment: nil, error: nil)) } @@ -492,17 +492,17 @@ public class StatusEditorViewModel: ObservableObject { Task { var medias: [StatusEditorMediaContainer] = [] for media in selectedMedias { + print(media.supportedContentTypes) var file: (any Transferable)? - do { - file = try await media.loadTransferable(type: ImageFileTranseferable.self) - if file == nil { - file = try await media.loadTransferable(type: MovieFileTranseferable.self) - } - } catch { - medias.append(.init(image: nil, - movieTransferable: nil, - mediaAttachment: nil, - error: error)) + + if file == nil { + file = try? await media.loadTransferable(type: GifFileTranseferable.self) + } + if file == nil { + file = try? await media.loadTransferable(type: MovieFileTranseferable.self) + } + if file == nil { + file = try? await media.loadTransferable(type: ImageFileTranseferable.self) } if var imageFile = file as? ImageFileTranseferable, @@ -510,11 +510,19 @@ public class StatusEditorViewModel: ObservableObject { { medias.append(.init(image: image, movieTransferable: nil, + gifTransferable: nil, mediaAttachment: nil, error: nil)) } else if let videoFile = file as? MovieFileTranseferable { medias.append(.init(image: nil, movieTransferable: videoFile, + gifTransferable: nil, + mediaAttachment: nil, + error: nil)) + } else if let gifFile = file as? GifFileTranseferable { + medias.append(.init(image: nil, + movieTransferable: nil, + gifTransferable: gifFile, mediaAttachment: nil, error: nil)) } @@ -543,8 +551,10 @@ public class StatusEditorViewModel: ObservableObject { func upload(container: StatusEditorMediaContainer) async { if let index = indexOf(container: container) { let originalContainer = mediasImages[index] + guard originalContainer.mediaAttachment == nil else { return } let newContainer = StatusEditorMediaContainer(image: originalContainer.image, movieTransferable: originalContainer.movieTransferable, + gifTransferable: nil, mediaAttachment: nil, error: nil) mediasImages[index] = newContainer @@ -556,6 +566,7 @@ public class StatusEditorViewModel: ObservableObject { let uploadedMedia = try await uploadMedia(data: data, mimeType: "image/jpeg") mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil, movieTransferable: nil, + gifTransferable: nil, mediaAttachment: uploadedMedia, error: nil) if let uploadedMedia, uploadedMedia.url == nil { @@ -567,6 +578,17 @@ public class StatusEditorViewModel: ObservableObject { let uploadedMedia = try await uploadMedia(data: data, mimeType: videoURL.mimeType()) mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil, movieTransferable: originalContainer.movieTransferable, + gifTransferable: nil, + mediaAttachment: uploadedMedia, + error: nil) + if let uploadedMedia, uploadedMedia.url == nil { + scheduleAsyncMediaRefresh(mediaAttachement: uploadedMedia) + } + } else if let gifData = originalContainer.gifTransferable?.data { + let uploadedMedia = try await uploadMedia(data: gifData, mimeType: "image/gif") + mediasImages[index] = .init(image: mode.isInShareExtension ? originalContainer.image : nil, + movieTransferable: nil, + gifTransferable: originalContainer.gifTransferable, mediaAttachment: uploadedMedia, error: nil) if let uploadedMedia, uploadedMedia.url == nil { @@ -578,6 +600,7 @@ public class StatusEditorViewModel: ObservableObject { if let index = indexOf(container: newContainer) { mediasImages[index] = .init(image: originalContainer.image, movieTransferable: nil, + gifTransferable: nil, mediaAttachment: nil, error: error) } @@ -601,6 +624,7 @@ public class StatusEditorViewModel: ObservableObject { let oldContainer = mediasImages[index] mediasImages[index] = .init(image: oldContainer.image, movieTransferable: oldContainer.movieTransferable, + gifTransferable: oldContainer.gifTransferable, mediaAttachment: newAttachement, error: nil) } @@ -619,6 +643,7 @@ public class StatusEditorViewModel: ObservableObject { description: description)) mediasImages[index] = .init(image: nil, movieTransferable: nil, + gifTransferable: nil, mediaAttachment: media, error: nil) } catch {} @@ -636,7 +661,6 @@ public class StatusEditorViewModel: ObservableObject { } // MARK: - Custom emojis - func fetchCustomEmojis() async { guard let client else { return } do { @@ -645,6 +669,7 @@ public class StatusEditorViewModel: ObservableObject { } } +//MARK: - DropDelegate extension StatusEditorViewModel: DropDelegate { public func performDrop(info: DropInfo) -> Bool { let item = info.itemProviders(for: StatusEditorUTTypeSupported.types()) @@ -652,3 +677,19 @@ extension StatusEditorViewModel: DropDelegate { return true } } + +// MARK: - UITextPasteDelegate +extension StatusEditorViewModel: UITextPasteDelegate { + public func textPasteConfigurationSupporting( + _ textPasteConfigurationSupporting: UITextPasteConfigurationSupporting, + transform item: UITextPasteItem) { + if !item.itemProvider.registeredContentTypes(conformingTo: .image).isEmpty || + !item.itemProvider.registeredContentTypes(conformingTo: .video).isEmpty || + !item.itemProvider.registeredContentTypes(conformingTo: .gif).isEmpty { + processItemsProvider(items: [item.itemProvider]) + item.setNoResult() + } else { + item.setDefaultResult() + } + } +}