From 66de0593df5ecfefa1a16a33b00abffadcead3e0 Mon Sep 17 00:00:00 2001 From: CMK Date: Tue, 6 Jul 2021 19:53:47 +0800 Subject: [PATCH] feat: add copy photo action for status --- Localization/app.json | 7 +- .../Diffiable/Section/StatusSection.swift | 3 +- Mastodon/Generated/Strings.swift | 8 ++- .../StatusProvider+UITableViewDelegate.swift | 31 ++++++++- .../Resources/ar.lproj/Localizable.strings | 7 +- .../Resources/en.lproj/Localizable.strings | 7 +- Mastodon/Service/PhotoLibraryService.swift | 64 +++++++++++-------- 7 files changed, 87 insertions(+), 40 deletions(-) diff --git a/Localization/app.json b/Localization/app.json index fa2af093..28119a48 100644 --- a/Localization/app.json +++ b/Localization/app.json @@ -70,15 +70,16 @@ "cancel": "Cancel", "discard": "Discard", "try_again": "Try Again", - "take_photo": "Take photo", - "save_photo": "Save photo", + "take_photo": "Take Photo", + "save_photo": "Save Photo", + "copy_photo": "Copy Photo", "sign_in": "Sign In", "sign_up": "Sign Up", "see_more": "See More", "preview": "Preview", "share": "Share", "share_user": "Share %s", - "share_post": "Share post", + "share_post": "Share Post", "open_in_safari": "Open in Safari", "find_people": "Find people to follow", "manually_search": "Manually search instead", diff --git a/Mastodon/Diffiable/Section/StatusSection.swift b/Mastodon/Diffiable/Section/StatusSection.swift index d0d37a45..d3c5ba80 100644 --- a/Mastodon/Diffiable/Section/StatusSection.swift +++ b/Mastodon/Diffiable/Section/StatusSection.swift @@ -1005,7 +1005,8 @@ extension StatusSection { private static func setupStatusMoreButtonMenu( cell: StatusTableViewCell, dependency: NeedsDependency, - status: Status) { + status: Status + ) { guard let userProvider = dependency as? UserProvider else { fatalError() } diff --git a/Mastodon/Generated/Strings.swift b/Mastodon/Generated/Strings.swift index 163b13da..28c732b2 100644 --- a/Mastodon/Generated/Strings.swift +++ b/Mastodon/Generated/Strings.swift @@ -110,6 +110,8 @@ internal enum L10n { internal static let confirm = L10n.tr("Localizable", "Common.Controls.Actions.Confirm") /// Continue internal static let `continue` = L10n.tr("Localizable", "Common.Controls.Actions.Continue") + /// Copy Photo + internal static let copyPhoto = L10n.tr("Localizable", "Common.Controls.Actions.CopyPhoto") /// Delete internal static let delete = L10n.tr("Localizable", "Common.Controls.Actions.Delete") /// Discard @@ -144,7 +146,7 @@ internal enum L10n { } /// Save internal static let save = L10n.tr("Localizable", "Common.Controls.Actions.Save") - /// Save photo + /// Save Photo internal static let savePhoto = L10n.tr("Localizable", "Common.Controls.Actions.SavePhoto") /// See More internal static let seeMore = L10n.tr("Localizable", "Common.Controls.Actions.SeeMore") @@ -152,7 +154,7 @@ internal enum L10n { internal static let settings = L10n.tr("Localizable", "Common.Controls.Actions.Settings") /// Share internal static let share = L10n.tr("Localizable", "Common.Controls.Actions.Share") - /// Share post + /// Share Post internal static let sharePost = L10n.tr("Localizable", "Common.Controls.Actions.SharePost") /// Share %@ internal static func shareUser(_ p1: Any) -> String { @@ -164,7 +166,7 @@ internal enum L10n { internal static let signUp = L10n.tr("Localizable", "Common.Controls.Actions.SignUp") /// Skip internal static let skip = L10n.tr("Localizable", "Common.Controls.Actions.Skip") - /// Take photo + /// Take Photo internal static let takePhoto = L10n.tr("Localizable", "Common.Controls.Actions.TakePhoto") /// Try Again internal static let tryAgain = L10n.tr("Localizable", "Common.Controls.Actions.TryAgain") diff --git a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift index d7ce7858..dae79d22 100644 --- a/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift +++ b/Mastodon/Protocol/StatusProvider/StatusProvider+UITableViewDelegate.swift @@ -189,6 +189,35 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { }) .store(in: &self.context.disposeBag) } + let copyPhotoAction = UIAction( + title: L10n.Common.Controls.Actions.copyPhoto, + image: UIImage(systemName: "doc.on.doc"), identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off + ) { [weak self] _ in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: copy photo", ((#file as NSString).lastPathComponent), #line, #function) + guard let self = self else { return } + self.attachment(of: status, index: i) + .setFailureType(to: Error.self) + .compactMap { attachment -> AnyPublisher? in + guard let attachment = attachment, let url = URL(string: attachment.url) else { return nil } + return self.context.photoLibraryService.copyImage(url: url) + } + .switchToLatest() + .sink(receiveCompletion: { [weak self] completion in + guard let self = self else { return } + switch completion { + case .failure(let error): + guard let error = error as? PhotoLibraryService.PhotoLibraryError, + case .noPermission = error else { return } + let alertController = SettingService.openSettingsAlertController(title: L10n.Common.Alerts.SavePhotoFailure.title, message: L10n.Common.Alerts.SavePhotoFailure.message) + self.coordinator.present(scene: .alertController(alertController: alertController), from: self, transition: .alertController(animated: true, completion: nil)) + case .finished: + break + } + }, receiveValue: { _ in + // do nothing + }) + .store(in: &self.context.disposeBag) + } let shareAction = UIAction( title: L10n.Common.Controls.Actions.share, image: UIImage(systemName: "square.and.arrow.up")!, identifier: nil, discoverabilityTitle: nil, attributes: [], state: .off ) { [weak self] _ in @@ -210,7 +239,7 @@ extension StatusTableViewCellDelegate where Self: StatusProvider { }) .store(in: &self.context.disposeBag) } - let children = [savePhotoAction, shareAction] + let children = [savePhotoAction, copyPhotoAction, shareAction] return UIMenu(title: "", image: nil, children: children) } contextMenuConfiguration.indexPath = indexPath diff --git a/Mastodon/Resources/ar.lproj/Localizable.strings b/Mastodon/Resources/ar.lproj/Localizable.strings index 82745b78..ce34268c 100644 --- a/Mastodon/Resources/ar.lproj/Localizable.strings +++ b/Mastodon/Resources/ar.lproj/Localizable.strings @@ -30,6 +30,7 @@ Please check your internet connection."; "Common.Controls.Actions.Cancel" = "Cancel"; "Common.Controls.Actions.Confirm" = "Confirm"; "Common.Controls.Actions.Continue" = "Continue"; +"Common.Controls.Actions.CopyPhoto" = "Copy Photo"; "Common.Controls.Actions.Delete" = "Delete"; "Common.Controls.Actions.Discard" = "Discard"; "Common.Controls.Actions.Done" = "Done"; @@ -46,16 +47,16 @@ Please check your internet connection."; "Common.Controls.Actions.Reply" = "Reply"; "Common.Controls.Actions.ReportUser" = "Report %@"; "Common.Controls.Actions.Save" = "Save"; -"Common.Controls.Actions.SavePhoto" = "Save photo"; +"Common.Controls.Actions.SavePhoto" = "Save Photo"; "Common.Controls.Actions.SeeMore" = "See More"; "Common.Controls.Actions.Settings" = "Settings"; "Common.Controls.Actions.Share" = "Share"; -"Common.Controls.Actions.SharePost" = "Share post"; +"Common.Controls.Actions.SharePost" = "Share Post"; "Common.Controls.Actions.ShareUser" = "Share %@"; "Common.Controls.Actions.SignIn" = "Sign In"; "Common.Controls.Actions.SignUp" = "Sign Up"; "Common.Controls.Actions.Skip" = "Skip"; -"Common.Controls.Actions.TakePhoto" = "Take photo"; +"Common.Controls.Actions.TakePhoto" = "Take Photo"; "Common.Controls.Actions.TryAgain" = "Try Again"; "Common.Controls.Actions.UnblockDomain" = "Unblock %@"; "Common.Controls.Friendship.Block" = "Block"; diff --git a/Mastodon/Resources/en.lproj/Localizable.strings b/Mastodon/Resources/en.lproj/Localizable.strings index 82745b78..ce34268c 100644 --- a/Mastodon/Resources/en.lproj/Localizable.strings +++ b/Mastodon/Resources/en.lproj/Localizable.strings @@ -30,6 +30,7 @@ Please check your internet connection."; "Common.Controls.Actions.Cancel" = "Cancel"; "Common.Controls.Actions.Confirm" = "Confirm"; "Common.Controls.Actions.Continue" = "Continue"; +"Common.Controls.Actions.CopyPhoto" = "Copy Photo"; "Common.Controls.Actions.Delete" = "Delete"; "Common.Controls.Actions.Discard" = "Discard"; "Common.Controls.Actions.Done" = "Done"; @@ -46,16 +47,16 @@ Please check your internet connection."; "Common.Controls.Actions.Reply" = "Reply"; "Common.Controls.Actions.ReportUser" = "Report %@"; "Common.Controls.Actions.Save" = "Save"; -"Common.Controls.Actions.SavePhoto" = "Save photo"; +"Common.Controls.Actions.SavePhoto" = "Save Photo"; "Common.Controls.Actions.SeeMore" = "See More"; "Common.Controls.Actions.Settings" = "Settings"; "Common.Controls.Actions.Share" = "Share"; -"Common.Controls.Actions.SharePost" = "Share post"; +"Common.Controls.Actions.SharePost" = "Share Post"; "Common.Controls.Actions.ShareUser" = "Share %@"; "Common.Controls.Actions.SignIn" = "Sign In"; "Common.Controls.Actions.SignUp" = "Sign Up"; "Common.Controls.Actions.Skip" = "Skip"; -"Common.Controls.Actions.TakePhoto" = "Take photo"; +"Common.Controls.Actions.TakePhoto" = "Take Photo"; "Common.Controls.Actions.TryAgain" = "Try Again"; "Common.Controls.Actions.UnblockDomain" = "Unblock %@"; "Common.Controls.Friendship.Block" = "Block"; diff --git a/Mastodon/Service/PhotoLibraryService.swift b/Mastodon/Service/PhotoLibraryService.swift index 2dcc8f99..f6cb7548 100644 --- a/Mastodon/Service/PhotoLibraryService.swift +++ b/Mastodon/Service/PhotoLibraryService.swift @@ -9,7 +9,7 @@ import os.log import UIKit import Combine import Photos -import AlamofireImage +import Nuke final class PhotoLibraryService: NSObject { @@ -26,39 +26,51 @@ extension PhotoLibraryService { extension PhotoLibraryService { func saveImage(url: URL) -> AnyPublisher { + return processImage(url: url) + .handleEvents(receiveOutput: { image in + self.save(image: image) + }) + .eraseToAnyPublisher() + } + + func copyImage(url: URL) -> AnyPublisher { + return processImage(url: url) + .handleEvents(receiveOutput: { image in + UIPasteboard.general.image = image + }) + .eraseToAnyPublisher() + } + + func processImage(url: URL) -> AnyPublisher { let impactFeedbackGenerator = UIImpactFeedbackGenerator(style: .light) let notificationFeedbackGenerator = UINotificationFeedbackGenerator() - - return Future { promise in - guard PHPhotoLibrary.authorizationStatus(for: .addOnly) != .denied else { - promise(.failure(PhotoLibraryError.noPermission)) - return - } - - ImageDownloader.default.download(URLRequest(url: url), completion: { [weak self] response in - guard let self = self else { return } - switch response.result { + + guard PHPhotoLibrary.authorizationStatus(for: .addOnly) != .denied else { + return Fail(error: PhotoLibraryError.noPermission).eraseToAnyPublisher() + } + + return ImagePipeline.shared.imagePublisher(with: url) + .handleEvents(receiveSubscription: { _ in + impactFeedbackGenerator.impactOccurred() + }, receiveOutput: { response in + self.save(image: response.image) + }, receiveCompletion: { completion in + switch completion { case .failure(let error): os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s fail: %s", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription, error.localizedDescription) - promise(.failure(error)) - case .success(let image): + + notificationFeedbackGenerator.notificationOccurred(.error) + case .finished: os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: download image %s success", ((#file as NSString).lastPathComponent), #line, #function, url.debugDescription) - self.save(image: image) - promise(.success(image)) + + notificationFeedbackGenerator.notificationOccurred(.success) } }) - } - .handleEvents(receiveSubscription: { _ in - impactFeedbackGenerator.impactOccurred() - }, receiveCompletion: { completion in - switch completion { - case .failure: - notificationFeedbackGenerator.notificationOccurred(.error) - case .finished: - notificationFeedbackGenerator.notificationOccurred(.success) + .map { response in + return response.image } - }) - .eraseToAnyPublisher() + .mapError { error in error as Error } + .eraseToAnyPublisher() } func save(image: UIImage, withNotificationFeedback: Bool = false) {