From 6bbfb2d06e7c5d374c0be828db4e8a44b97c80d6 Mon Sep 17 00:00:00 2001 From: Justin Mazzocchi <2831158+jzzocc@users.noreply.github.com> Date: Sun, 7 Feb 2021 13:00:17 -0800 Subject: [PATCH] Image pasting support --- Metatext.xcodeproj/project.pbxproj | 6 ++ .../View Models/CompositionViewModel.swift | 60 +++++++++---------- Views/UIKit/CompositionView.swift | 14 ++++- Views/UIKit/ImagePastableTextView.swift | 28 +++++++++ 4 files changed, 76 insertions(+), 32 deletions(-) create mode 100644 Views/UIKit/ImagePastableTextView.swift diff --git a/Metatext.xcodeproj/project.pbxproj b/Metatext.xcodeproj/project.pbxproj index 580675b..3f688fb 100644 --- a/Metatext.xcodeproj/project.pbxproj +++ b/Metatext.xcodeproj/project.pbxproj @@ -131,6 +131,8 @@ D0B5FE9B251583DB00478838 /* ProfileCollection+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B5FE9A251583DB00478838 /* ProfileCollection+Extensions.swift */; }; D0B8510C25259E56004E0744 /* LoadMoreTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B8510B25259E56004E0744 /* LoadMoreTableViewCell.swift */; }; D0BE97A325CF44310057E161 /* CGRect+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE97A225CF44310057E161 /* CGRect+Extensions.swift */; }; + D0BE97D725D0863E0057E161 /* ImagePastableTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */; }; + D0BE97E025D086F80057E161 /* ImagePastableTextView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */; }; D0BEB1F324F8EE8C001B0F04 /* AttachmentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */; }; D0BEB1F724F9A84B001B0F04 /* LoadingTableFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */; }; D0BEB1FF24F9E5BB001B0F04 /* ListsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */; }; @@ -327,6 +329,7 @@ D0B8510B25259E56004E0744 /* LoadMoreTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadMoreTableViewCell.swift; sourceTree = ""; }; D0BDF66524FD7A6400C7FA1C /* ServiceLayer */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ServiceLayer; sourceTree = ""; }; D0BE97A225CF44310057E161 /* CGRect+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "CGRect+Extensions.swift"; sourceTree = ""; }; + D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePastableTextView.swift; sourceTree = ""; }; D0BEB1F224F8EE8C001B0F04 /* AttachmentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentView.swift; sourceTree = ""; }; D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoadingTableFooterView.swift; sourceTree = ""; }; D0BEB1FE24F9E5BB001B0F04 /* ListsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListsView.swift; sourceTree = ""; }; @@ -450,6 +453,7 @@ D07EC7FC25B16994006DF726 /* EmojiCategoryHeaderView.swift */, D0DDA77E25C6058300FA0F91 /* ExploreSectionHeaderView.swift */, D0477F4525C72E50005C5368 /* FollowsYouLabel.swift */, + D0BE97D625D0863E0057E161 /* ImagePastableTextView.swift */, D0D2AC6625BD0484003D5DF2 /* LineChartView.swift */, D0BEB1F624F9A84B001B0F04 /* LoadingTableFooterView.swift */, D05936FE25AA94EA00754FDF /* MarkAttachmentsSensitiveView.swift */, @@ -1064,6 +1068,7 @@ D035F86F25B7F30E00DC75ED /* MainNavigationView.swift in Sources */, D08E512125786A6600FA2C5F /* UIButton+Extensions.swift in Sources */, D0D2AC4725BCD289003D5DF2 /* TagView.swift in Sources */, + D0BE97D725D0863E0057E161 /* ImagePastableTextView.swift in Sources */, D05936F425AA66A600754FDF /* UIView+Extensions.swift in Sources */, D05936E925AA3F3D00754FDF /* EditAttachmentView.swift in Sources */, D035F8C725B96A4000DC75ED /* SecondaryNavigationButton.swift in Sources */, @@ -1152,6 +1157,7 @@ D025B14725C4D26B001C69A8 /* ImageCacheSerializer.swift in Sources */, D036EBB8259FE29800EC1CFC /* Status+Extensions.swift in Sources */, D021A6A625C3E584008A0C0D /* EditAttachmentView.swift in Sources */, + D0BE97E025D086F80057E161 /* ImagePastableTextView.swift in Sources */, D05936DF25A937EC00754FDF /* EditThumbnailView.swift in Sources */, D021A69525C3E4C1008A0C0D /* EmojiView.swift in Sources */, ); diff --git a/ViewModels/Sources/ViewModels/View Models/CompositionViewModel.swift b/ViewModels/Sources/ViewModels/View Models/CompositionViewModel.swift index b4dfcc8..b3b077d 100644 --- a/ViewModels/Sources/ViewModels/View Models/CompositionViewModel.swift +++ b/ViewModels/Sources/ViewModels/View Models/CompositionViewModel.swift @@ -171,6 +171,35 @@ public extension CompositionViewModel { pollOptions.removeAll { $0 === pollOption } } + func attach(itemProvider: NSItemProvider, parentViewModel: NewStatusViewModel) { + attachmentUploadCancellable = MediaProcessingService.dataAndMimeType(itemProvider: itemProvider) + .flatMap { [weak self] data, mimeType -> AnyPublisher in + guard let self = self else { return Empty().eraseToAnyPublisher() } + + let progress = Progress(totalUnitCount: 1) + + DispatchQueue.main.async { + self.attachmentUpload = AttachmentUpload(progress: progress, data: data, mimeType: mimeType) + } + + return parentViewModel.identityContext.service.uploadAttachment( + data: data, + mimeType: mimeType, + progress: progress) + } + .receive(on: DispatchQueue.main) + .assignErrorsToAlertItem(to: \.alertItem, on: parentViewModel) + .handleEvents(receiveCancel: { [weak self] in self?.attachmentUpload = nil }) + .sink { [weak self] _ in + self?.attachmentUpload = nil + } receiveValue: { [weak self] in + self?.attachmentViewModels.append( + AttachmentViewModel( + attachment: $0, + identityContext: parentViewModel.identityContext)) + } + } + func cancelUpload() { attachmentUploadCancellable?.cancel() } @@ -203,37 +232,6 @@ public extension CompositionViewModel.PollOption { typealias Id = UUID } -extension CompositionViewModel { - func attach(itemProvider: NSItemProvider, parentViewModel: NewStatusViewModel) { - attachmentUploadCancellable = MediaProcessingService.dataAndMimeType(itemProvider: itemProvider) - .flatMap { [weak self] data, mimeType -> AnyPublisher in - guard let self = self else { return Empty().eraseToAnyPublisher() } - - let progress = Progress(totalUnitCount: 1) - - DispatchQueue.main.async { - self.attachmentUpload = AttachmentUpload(progress: progress, data: data, mimeType: mimeType) - } - - return parentViewModel.identityContext.service.uploadAttachment( - data: data, - mimeType: mimeType, - progress: progress) - } - .receive(on: DispatchQueue.main) - .assignErrorsToAlertItem(to: \.alertItem, on: parentViewModel) - .handleEvents(receiveCancel: { [weak self] in self?.attachmentUpload = nil }) - .sink { [weak self] _ in - self?.attachmentUpload = nil - } receiveValue: { [weak self] in - self?.attachmentViewModels.append( - AttachmentViewModel( - attachment: $0, - identityContext: parentViewModel.identityContext)) - } - } -} - private extension CompositionViewModel { static let maxAttachmentCount = 4 } diff --git a/Views/UIKit/CompositionView.swift b/Views/UIKit/CompositionView.swift index d9d1323..f439c08 100644 --- a/Views/UIKit/CompositionView.swift +++ b/Views/UIKit/CompositionView.swift @@ -9,7 +9,7 @@ final class CompositionView: UIView { let avatarImageView = AnimatedImageView() let changeIdentityButton = UIButton() let spoilerTextField = UITextField() - let textView = UITextView() + let textView = ImagePastableTextView() let textViewPlaceholder = UILabel() let removeButton = UIButton(type: .close) let inReplyToView = UIView() @@ -220,6 +220,18 @@ private extension CompositionView { } .store(in: &cancellables) + viewModel.$canAddAttachment + .sink { [weak self] in self?.textView.canPasteImage = $0 } + .store(in: &cancellables) + + textView.pastedImagesPublisher.sink { [weak self] in + guard let self = self else { return } + + self.viewModel.attach(itemProvider: NSItemProvider(object: $0), + parentViewModel: self.parentViewModel) + } + .store(in: &cancellables) + viewModel.$displayPoll .throttle(for: .seconds(TimeInterval.zeroIfReduceMotion(.shortAnimationDuration)), scheduler: DispatchQueue.main, diff --git a/Views/UIKit/ImagePastableTextView.swift b/Views/UIKit/ImagePastableTextView.swift new file mode 100644 index 0000000..c59d5f3 --- /dev/null +++ b/Views/UIKit/ImagePastableTextView.swift @@ -0,0 +1,28 @@ +// Copyright © 2021 Metabolist. All rights reserved. + +import Combine +import UIKit + +final class ImagePastableTextView: UITextView { + var canPasteImage = true + private(set) lazy var pastedImagesPublisher: AnyPublisher = + pastedImagesSubject.eraseToAnyPublisher() + + private let pastedImagesSubject = PassthroughSubject() + + override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { + if action == #selector(paste(_:)) { + return UIPasteboard.general.hasStrings || (UIPasteboard.general.hasImages && canPasteImage) + } + + return super.canPerformAction(action, withSender: sender) + } + + override func paste(_ sender: Any?) { + if UIPasteboard.general.hasImages, let image = UIPasteboard.general.image { + pastedImagesSubject.send(image) + } else { + super.paste(sender) + } + } +}