From 19c6e1697be998081a4604ba179554c7ae9fe74d Mon Sep 17 00:00:00 2001 From: kean Date: Thu, 17 Aug 2023 17:56:33 -0400 Subject: [PATCH 1/4] Integrate PHPicker in site selection flow --- .../SiteIconPickerPresenter.swift | 49 +++++++++++++++++-- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift index e7cb5b55823d..925c8570a52c 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift @@ -4,10 +4,11 @@ import WPMediaPicker import WordPressShared import MobileCoreServices import UniformTypeIdentifiers +import PhotosUI /// Encapsulates the interactions required to capture a new site icon image, crop it and resize it. /// -class SiteIconPickerPresenter: NSObject { +final class SiteIconPickerPresenter: NSObject { // MARK: - Public Properties @@ -68,8 +69,17 @@ class SiteIconPickerPresenter: NSObject { /// Presents a new WPMediaPickerViewController instance. /// @objc func presentPickerFrom(_ viewController: UIViewController) { - viewController.present(mediaPickerViewController, animated: true) - registerChangeObserver(forPicker: mediaPickerViewController.mediaPicker) + if FeatureFlag.nativePhotoPicker.enabled { + var configuration = PHPickerConfiguration() + configuration.filter = .images + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self + viewController.present(picker, animated: true) + } else { + viewController.present(mediaPickerViewController, animated: true) + registerChangeObserver(forPicker: mediaPickerViewController.mediaPicker) + } } // MARK: - Private Methods @@ -87,7 +97,7 @@ class SiteIconPickerPresenter: NSObject { /// Shows a new ImageCropViewController for the given image. /// - fileprivate func showImageCropViewController(_ image: UIImage) { + fileprivate func showImageCropViewController(_ image: UIImage, picker: PHPickerViewController? = nil) { DispatchQueue.main.async { SVProgressHUD.dismiss() let imageCropViewController = ImageCropViewController(image: image) @@ -131,7 +141,17 @@ class SiteIconPickerPresenter: NSObject { } } } - self.mediaPickerViewController.show(after: imageCropViewController) + if let picker { + imageCropViewController.shouldShowCancelButton = true + imageCropViewController.onCancel = { [weak picker] in + // Dismiss the crop controller but not the picker + picker?.dismiss(animated: true) + } + let navigationController = UINavigationController(rootViewController: imageCropViewController) + picker.present(navigationController, animated: true) + } else { + self.mediaPickerViewController.show(after: imageCropViewController) + } } } @@ -173,6 +193,25 @@ class SiteIconPickerPresenter: NSObject { } } +extension SiteIconPickerPresenter: PHPickerViewControllerDelegate { + func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) { + guard let result = results.first else { + picker.presentingViewController?.dismiss(animated: true) + return + } + WPAnalytics.track(.siteSettingsSiteIconGalleryPicked) + self.showLoadingMessage() + self.originalMedia = nil + result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in + if let image = image as? UIImage { + self?.showImageCropViewController(image, picker: picker) + } else { + self?.showErrorLoadingImageMessage() + } + } + } +} + extension SiteIconPickerPresenter: WPMediaPickerViewControllerDelegate { func mediaPickerControllerWillBeginLoadingData(_ picker: WPMediaPickerViewController) { From 417d143a8de079477d2f37bab589393961e08772 Mon Sep 17 00:00:00 2001 From: kean Date: Fri, 18 Aug 2023 10:46:15 -0400 Subject: [PATCH 2/4] Switch to UIMenu in site icon picker flow --- .../Detail Header/BlogDetailHeaderView.swift | 17 ++- .../Detail Header/SiteIconView.swift | 8 ++ .../SitePickerViewController+QuickStart.swift | 2 +- .../SitePickerViewController+SiteIcon.swift | 128 ++++++++++-------- .../SitePickerViewController.swift | 22 +-- .../SiteIconPickerPresenter.swift | 82 ++++++++--- .../ViewRelated/Media/MediaPicker.swift | 37 +++++ WordPress/WordPress.xcodeproj/project.pbxproj | 6 + 8 files changed, 203 insertions(+), 99 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift index 9e3592c4e1cb..e2c6dc879fa0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/BlogDetailHeaderView.swift @@ -2,7 +2,8 @@ import Gridicons import UIKit @objc protocol BlogDetailHeaderViewDelegate { - func siteIconTapped() + func makeSiteIconMenu() -> UIMenu? + func didShowSiteIconMenu() func siteIconReceivedDroppedImage(_ image: UIImage?) func siteIconShouldAllowDroppedImages() -> Bool func siteTitleTapped() @@ -107,11 +108,12 @@ class BlogDetailHeaderView: UIView { // MARK: - Initializers - required init(items: [ActionRow.Item]) { + required init(items: [ActionRow.Item], delegate: BlogDetailHeaderViewDelegate) { titleView = TitleView(frame: .zero) super.init(frame: .zero) + self.delegate = delegate setupChildViews(items: items) } @@ -122,11 +124,14 @@ class BlogDetailHeaderView: UIView { // MARK: - Child View Initialization private func setupChildViews(items: [ActionRow.Item]) { - titleView.siteIconView.tapped = { [weak self] in - QuickStartTourGuide.shared.visited(.siteIcon) - self?.titleView.siteIconView.spotlightIsShown = false + assert(delegate != nil) - self?.delegate?.siteIconTapped() + if let menu = delegate?.makeSiteIconMenu() { + titleView.siteIconView.setMenu(menu) { [weak self] in + self?.delegate?.didShowSiteIconMenu() + WPAnalytics.track(.siteSettingsSiteIconTapped) + self?.titleView.siteIconView.spotlightIsShown = false + } } titleView.siteIconView.dropped = { [weak self] images in diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift index 09bb894a7532..de0d79922397 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift @@ -40,6 +40,14 @@ class SiteIconView: UIView { private var dropInteraction: UIDropInteraction? + /// Set the menu to be displayed when the button is tapped. The menu replaces + /// teh default on tap action. + func setMenu(_ menu: UIMenu, onMenuTriggerd: @escaping () -> Void) { + button.menu = menu + button.showsMenuAsPrimaryAction = true + button.addAction(UIAction { _ in onMenuTriggerd() }, for: .menuActionTriggered) + } + private let button: UIButton = { let button = UIButton(frame: .zero) button.backgroundColor = UIColor.secondaryButtonBackground diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+QuickStart.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+QuickStart.swift index b828d41e7085..509d7c3684e4 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+QuickStart.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+QuickStart.swift @@ -40,7 +40,7 @@ extension SitePickerViewController { blogDetailHeaderView.toggleSpotlightOnSiteIcon() } - private func showNoticeAsNeeded() { + func showNoticeAsNeeded() { if let tourToSuggest = QuickStartTourGuide.shared.tourToSuggest(for: blog) { QuickStartTourGuide.shared.suggest(tourToSuggest, for: blog) } diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift index 9a6556bb30de..b4794d76a662 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift @@ -3,71 +3,84 @@ import WordPressFlux import WordPressShared import SwiftUI import SVProgressHUD +import Gridicons extension SitePickerViewController { - func showSiteIconSelectionAlert() { - let alert = UIAlertController(title: SiteIconAlertStrings.title, - message: nil, - preferredStyle: .actionSheet) - - alert.popoverPresentationController?.sourceView = blogDetailHeaderView.blavatarImageView.superview - alert.popoverPresentationController?.sourceRect = blogDetailHeaderView.blavatarImageView.frame - alert.popoverPresentationController?.permittedArrowDirections = .any - - alert.addDefaultActionWithTitle(SiteIconAlertStrings.Actions.chooseImage) { [weak self] _ in - NoticesDispatch.unlock() - self?.updateSiteIcon() - } - - alert.addDefaultActionWithTitle(SiteIconAlertStrings.Actions.createWithEmoji) { [weak self] _ in - NoticesDispatch.unlock() - self?.showEmojiPicker() - } - - alert.addDestructiveActionWithTitle(SiteIconAlertStrings.Actions.removeSiteIcon) { [weak self] _ in - NoticesDispatch.unlock() - self?.removeSiteIcon() + func makeSiteIconMenu() -> UIMenu? { + guard siteIconShouldAllowDroppedImages() else { + return nil } - - alert.addCancelActionWithTitle(SiteIconAlertStrings.Actions.cancel) { [weak self] _ in - NoticesDispatch.unlock() - self?.startAlertTimer() - } - - present(alert, animated: true) + return UIMenu(children: [ + UIDeferredMenuElement.uncached { [weak self] in + $0(self?.makeUpdateSiteIconActions() ?? []) + } + ]) } - func showUpdateSiteIconAlert() { - let alert = UIAlertController(title: nil, - message: nil, - preferredStyle: .actionSheet) - - alert.popoverPresentationController?.sourceView = blogDetailHeaderView.blavatarImageView.superview - alert.popoverPresentationController?.sourceRect = blogDetailHeaderView.blavatarImageView.frame - alert.popoverPresentationController?.permittedArrowDirections = .any - - alert.addDefaultActionWithTitle(SiteIconAlertStrings.Actions.changeSiteIcon) { [weak self] _ in - NoticesDispatch.unlock() - self?.updateSiteIcon() - } - - if blog.hasIcon { - alert.addDestructiveActionWithTitle(SiteIconAlertStrings.Actions.removeSiteIcon) { [weak self] _ in - NoticesDispatch.unlock() - self?.removeSiteIcon() + func didShowSiteIconMenu() { + if QuickStartTourGuide.shared.isCurrentElement(.siteIcon) { + // There is no good way to determine when `UIMenu` is cancelled, + // so we wait until nothing is presented by the site picker. + NoticesDispatch.lock() + Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in + if self.presentedViewController == nil { + NoticesDispatch.unlock() + self.showNoticeAsNeeded() + timer.invalidate() + } } } + QuickStartTourGuide.shared.visited(.siteIcon) + } - alert.addCancelActionWithTitle(SiteIconAlertStrings.Actions.cancel) { [weak self] _ in - NoticesDispatch.unlock() - self?.startAlertTimer() + private func makeUpdateSiteIconActions() -> [UIAction] { + var actions: [UIAction] = [] + if FeatureFlag.nativePhotoPicker.enabled { + actions += [ + MediaPickerMenu.makePickFromPhotosAction { [weak self] in + self?.updateSiteIcon(source: .photosLibrary) + }, + MediaPickerMenu.makeTakePhotoAction { [weak self] in + self?.updateSiteIcon(source: .camera) + }, + MediaPickerMenu.makePickFromMediaAction { [weak self] in + self?.updateSiteIcon(source: .mediaLibrary) + } + ] + } else { + actions.append(UIAction( + title: SiteIconAlertStrings.Actions.changeSiteIcon, + image: UIImage(systemName: "photo.on.rectangle"), + handler: { [weak self] _ in self?.updateSiteIcon(source: .combined) } + )) + } + if FeatureFlag.siteIconCreator.enabled { + actions.append(UIAction( + title: SiteIconAlertStrings.Actions.createWithEmoji, + image: UIImage(systemName: "face.smiling"), + handler: { [weak self] _ in self?.showEmojiPicker() } + )) } + if blog.hasIcon { + actions.append(UIAction( + title: SiteIconAlertStrings.Actions.removeSiteIcon, + image: UIImage(systemName: "trash"), + attributes: [.destructive], + handler: { [weak self] _ in self?.removeSiteIcon() } + )) + } + return actions + } - present(alert, animated: true) + enum SiteIconSource { + case photosLibrary + case camera + case mediaLibrary + case combined // legacy option } - func updateSiteIcon() { + func updateSiteIcon(source: SiteIconSource = .combined) { siteIconPickerPresenter = SiteIconPickerPresenter(blog: blog) siteIconPickerPresenter?.onCompletion = { [ weak self] media, error in if error != nil { @@ -88,7 +101,16 @@ extension SitePickerViewController { self?.dismiss(animated: true) } - siteIconPickerPresenter?.presentPickerFrom(self) + switch source { + case .photosLibrary: + siteIconPickerPresenter?.presentPhotosPicker(from: self) + case .camera: + siteIconPickerPresenter?.presentCamera(from: self) + case .mediaLibrary: + siteIconPickerPresenter?.presentMediaLibraryPicker(from: self) + case .combined: + siteIconPickerPresenter?.presentPickerFrom(self) + } } func showEmojiPicker() { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift index cc1fd9eb0ebf..d53b82a8878a 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController.swift @@ -22,7 +22,7 @@ final class SitePickerViewController: UIViewController { let mediaService: MediaService private(set) lazy var blogDetailHeaderView: BlogDetailHeaderView = { - let headerView = BlogDetailHeaderView(items: []) + let headerView = BlogDetailHeaderView(items: [], delegate: self) headerView.translatesAutoresizingMaskIntoConstraints = false return headerView }() @@ -51,7 +51,6 @@ final class SitePickerViewController: UIViewController { private func setupHeaderView() { blogDetailHeaderView.blog = blog - blogDetailHeaderView.delegate = self view.addSubview(blogDetailHeaderView) view.pinSubviewToAllEdges(blogDetailHeaderView) } @@ -71,25 +70,6 @@ final class SitePickerViewController: UIViewController { extension SitePickerViewController: BlogDetailHeaderViewDelegate { - func siteIconTapped() { - guard siteIconShouldAllowDroppedImages() else { - // Gracefully ignore the tap for users that can not upload files or - // blogs that do not have capabilities since those will not support the REST API icon update - return - } - - WPAnalytics.track(.siteSettingsSiteIconTapped) - - NoticesDispatch.lock() - - guard FeatureFlag.siteIconCreator.enabled else { - showUpdateSiteIconAlert() - return - } - - showSiteIconSelectionAlert() - } - func siteIconReceivedDroppedImage(_ image: UIImage?) { if !siteIconShouldAllowDroppedImages() { // Gracefully ignore the drop for users that can not upload files or diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift index 925c8570a52c..abca55a3d7c0 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift @@ -49,6 +49,9 @@ final class SiteIconPickerPresenter: NSObject { return pickerViewController }() + private var dataSource: AnyObject? + private var mediaCapturePresenter: AnyObject? + // MARK: - Public methods /// Designated Initializer @@ -66,20 +69,56 @@ final class SiteIconPickerPresenter: NSObject { unregisterChangeObserver() } + func presentPhotosPicker(from presentingViewController: UIViewController) { + var configuration = PHPickerConfiguration() + configuration.filter = .images + + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = self + presentingViewController.present(picker, animated: true) + } + + func presentCamera(from presentingViewController: UIViewController) { + let picker = WPMediaCapturePresenter(presenting: presentingViewController) + picker.completionBlock = { [weak self] info in + if let image = info?[UIImagePickerController.InfoKey.originalImage] as? UIImage { + self?.showImageCropViewController(image, presentingViewController: presentingViewController) + } + } + picker.mediaType = .image + picker.presentCapture() + mediaCapturePresenter = picker // Retain + } + + func presentMediaLibraryPicker(from presentingViewController: UIViewController) { + let options = WPMediaPickerOptions() + options.showMostRecentFirst = true + options.filter = [.image] + options.allowMultipleSelection = false + options.showSearchBar = true + options.badgedUTTypes = [UTType.gif.identifier] + options.preferredStatusBarStyle = WPStyleGuide.preferredStatusBarStyle + options.allowCaptureOfMedia = false + + let pickerViewController = WPNavigationMediaPickerViewController(options: options) + + let dataSource = MediaLibraryPickerDataSource(blog: blog) + dataSource.ignoreSyncErrors = true + self.dataSource = dataSource + + pickerViewController.showGroupSelector = false + pickerViewController.dataSource = dataSource + pickerViewController.delegate = self + pickerViewController.modalPresentationStyle = .formSheet + + presentingViewController.present(pickerViewController, animated: true) + } + /// Presents a new WPMediaPickerViewController instance. /// @objc func presentPickerFrom(_ viewController: UIViewController) { - if FeatureFlag.nativePhotoPicker.enabled { - var configuration = PHPickerConfiguration() - configuration.filter = .images - - let picker = PHPickerViewController(configuration: configuration) - picker.delegate = self - viewController.present(picker, animated: true) - } else { - viewController.present(mediaPickerViewController, animated: true) - registerChangeObserver(forPicker: mediaPickerViewController.mediaPicker) - } + viewController.present(mediaPickerViewController, animated: true) + registerChangeObserver(forPicker: mediaPickerViewController.mediaPicker) } // MARK: - Private Methods @@ -97,7 +136,7 @@ final class SiteIconPickerPresenter: NSObject { /// Shows a new ImageCropViewController for the given image. /// - fileprivate func showImageCropViewController(_ image: UIImage, picker: PHPickerViewController? = nil) { + fileprivate func showImageCropViewController(_ image: UIImage, presentingViewController: UIViewController? = nil) { DispatchQueue.main.async { SVProgressHUD.dismiss() let imageCropViewController = ImageCropViewController(image: image) @@ -141,14 +180,14 @@ final class SiteIconPickerPresenter: NSObject { } } } - if let picker { + if let presentingViewController { imageCropViewController.shouldShowCancelButton = true - imageCropViewController.onCancel = { [weak picker] in + imageCropViewController.onCancel = { [weak presentingViewController] in // Dismiss the crop controller but not the picker - picker?.dismiss(animated: true) + presentingViewController?.dismiss(animated: true) } let navigationController = UINavigationController(rootViewController: imageCropViewController) - picker.present(navigationController, animated: true) + presentingViewController.present(navigationController, animated: true) } else { self.mediaPickerViewController.show(after: imageCropViewController) } @@ -204,7 +243,7 @@ extension SiteIconPickerPresenter: PHPickerViewControllerDelegate { self.originalMedia = nil result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in if let image = image as? UIImage { - self?.showImageCropViewController(image, picker: picker) + self?.showImageCropViewController(image, presentingViewController: picker) } else { self?.showErrorLoadingImageMessage() } @@ -212,6 +251,8 @@ extension SiteIconPickerPresenter: PHPickerViewControllerDelegate { } } +extension SiteIconPickerPresenter: UINavigationControllerDelegate {} + extension SiteIconPickerPresenter: WPMediaPickerViewControllerDelegate { func mediaPickerControllerWillBeginLoadingData(_ picker: WPMediaPickerViewController) { @@ -245,6 +286,7 @@ extension SiteIconPickerPresenter: WPMediaPickerViewControllerDelegate { /// Retrieves the chosen image and triggers the ImageCropViewController display. /// func mediaPickerController(_ picker: WPMediaPickerViewController, didFinishPicking assets: [WPMediaAsset]) { + dataSource = nil mediaLibraryDataSource.searchCancelled() if assets.isEmpty { return @@ -279,7 +321,11 @@ extension SiteIconPickerPresenter: WPMediaPickerViewControllerDelegate { self?.showErrorLoadingImageMessage() return } - self?.showImageCropViewController(image) + if FeatureFlag.nativePhotoPicker.enabled { + self?.showImageCropViewController(image, presentingViewController: picker) + } else { + self?.showImageCropViewController(image) + } }) default: break diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker.swift new file mode 100644 index 000000000000..9d9302abcf1f --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker.swift @@ -0,0 +1,37 @@ +import UIKit +import Gridicons + +struct MediaPickerMenu { + static func makePickFromPhotosAction(_ handler: @escaping () -> Void) -> UIAction { + UIAction( + title: Strings.pickFromPhotosLibrary, + image: UIImage(systemName: "photo.on.rectangle.angled"), + attributes: [], + handler: { _ in handler() } + ) + } + + static func makeTakePhotoAction(_ handler: @escaping () -> Void) -> UIAction { + UIAction( + title: Strings.takePhoto, + image: UIImage(systemName: "camera"), + attributes: [], + handler: { _ in handler() } + ) + } + + static func makePickFromMediaAction(_ handler: @escaping () -> Void) -> UIAction { + UIAction( + title: Strings.pickFromMedia, + image: UIImage(systemName: "photo.stack"), + attributes: [], + handler: { _ in handler() } + ) + } +} + +private enum Strings { + static let pickFromPhotosLibrary = NSLocalizedString("mediaPicker.pickFromPhotosLibrary", value: "Choose from Device", comment: "The name of the action in the context menu") + static let takePhoto = NSLocalizedString("mediaPicker.takePhoto", value: "Take Photo", comment: "The name of the action in the context menu") + static let pickFromMedia = NSLocalizedString("mediaPicker.pickFromMediaLibrary", value: "Choose from Media", comment: "The name of the action in the context menu (user's WordPress Media Library") +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index fab0a449ec36..e314592830fe 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -386,6 +386,8 @@ 0A9610F928B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9610FA28B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9687BC28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */; }; + 0C0AE7592A8FAD6A007D9D6C /* MediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */; }; + 0C0AE75A2A8FAD6A007D9D6C /* MediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */; }; 0C0D3B0D2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; 0C0D3B0E2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; 0C2C83FA2A6EABF300A3ACD9 /* StatsPeriodCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2C83F92A6EABF300A3ACD9 /* StatsPeriodCache.swift */; }; @@ -6082,6 +6084,7 @@ 0A69300A28B5AA5E00E98DE1 /* FullScreenCommentReplyViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelTests.swift; sourceTree = ""; }; 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+Comparable.swift"; sourceTree = ""; }; 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelMock.swift; sourceTree = ""; }; + 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPicker.swift; sourceTree = ""; }; 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStream.swift; sourceTree = ""; }; 0C2C83F92A6EABF300A3ACD9 /* StatsPeriodCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPeriodCache.swift; sourceTree = ""; }; 0C2C83FC2A6EBD3F00A3ACD9 /* StatsInsightsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsInsightsCache.swift; sourceTree = ""; }; @@ -12398,6 +12401,7 @@ D80BC7A3207487F200614A59 /* MediaLibraryPicker.swift */, 0C8FC9A02A8BC8630059DCE4 /* PHPickerController+Extensions.swift */, 0C8FC9A62A8BFAAD0059DCE4 /* NSItemProvider+Exportable.swift */, + 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */, 7E14635620B3BEAB00B95F41 /* WPStyleGuide+Loader.swift */, 7ECD5B8020C4D823001AEBC5 /* MediaPreviewHelper.swift */, ); @@ -21366,6 +21370,7 @@ 74729CAE205722E300D1394D /* AbstractPost+Searchable.swift in Sources */, 2906F812110CDA8900169D56 /* EditCommentViewController.m in Sources */, 98F93182239AF64800E4E96E /* ThisWeekWidgetStats.swift in Sources */, + 0C0AE7592A8FAD6A007D9D6C /* MediaPicker.swift in Sources */, F16C35DA23F3F76C00C81331 /* PostAutoUploadMessageProvider.swift in Sources */, 91D8364121946EFB008340B2 /* GutenbergMediaPickerHelper.swift in Sources */, F1D690161F82913F00200E30 /* FeatureFlag.swift in Sources */, @@ -24727,6 +24732,7 @@ FABB23AC2602FC2C00C8785C /* ReachabilityUtils.m in Sources */, FABB23AD2602FC2C00C8785C /* TenorDataSource.swift in Sources */, FABB23AE2602FC2C00C8785C /* ReaderTopicService+Subscriptions.swift in Sources */, + 0C0AE75A2A8FAD6A007D9D6C /* MediaPicker.swift in Sources */, 98517E5A28220411001FFD45 /* BloggingPromptTableViewCell.swift in Sources */, FAC1B81F29B0C2AC00E0C542 /* BlazeOverlayViewModel.swift in Sources */, FABB23B02602FC2C00C8785C /* Data.swift in Sources */, From 1805a9fc1b061cba20621d1eb93991178348bad6 Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 21 Aug 2023 07:46:50 -0400 Subject: [PATCH 3/4] Add MediaPickerMenu --- .../SitePickerViewController+SiteIcon.swift | 49 ++-- .../SiteIconPickerPresenter.swift | 65 ++--- .../ViewRelated/Media/MediaPicker.swift | 37 --- .../ViewRelated/Media/MediaPickerMenu.swift | 272 ++++++++++++++++++ WordPress/WordPress.xcodeproj/project.pbxproj | 12 +- 5 files changed, 310 insertions(+), 125 deletions(-) delete mode 100644 WordPress/Classes/ViewRelated/Media/MediaPicker.swift create mode 100644 WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift diff --git a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift index b4794d76a662..1f56968bf257 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Picker/SitePickerViewController+SiteIcon.swift @@ -4,6 +4,7 @@ import WordPressShared import SwiftUI import SVProgressHUD import Gridicons +import PhotosUI extension SitePickerViewController { @@ -35,24 +36,23 @@ extension SitePickerViewController { } private func makeUpdateSiteIconActions() -> [UIAction] { + let presenter = makeSiteIconPresenter() + let mediaMenu = MediaPickerMenu(viewConroller: self, filter: .images) var actions: [UIAction] = [] if FeatureFlag.nativePhotoPicker.enabled { actions += [ - MediaPickerMenu.makePickFromPhotosAction { [weak self] in - self?.updateSiteIcon(source: .photosLibrary) - }, - MediaPickerMenu.makeTakePhotoAction { [weak self] in - self?.updateSiteIcon(source: .camera) - }, - MediaPickerMenu.makePickFromMediaAction { [weak self] in - self?.updateSiteIcon(source: .mediaLibrary) - } + mediaMenu.makePhotosAction(delegate: presenter), + mediaMenu.makeCameraAction(delegate: presenter), + mediaMenu.makeMediaAction(blog: blog, delegate: presenter) ] } else { actions.append(UIAction( title: SiteIconAlertStrings.Actions.changeSiteIcon, image: UIImage(systemName: "photo.on.rectangle"), - handler: { [weak self] _ in self?.updateSiteIcon(source: .combined) } + handler: { [weak self] _ in + guard let self else { return } + presenter.presentPickerFrom(self) + } )) } if FeatureFlag.siteIconCreator.enabled { @@ -73,16 +73,9 @@ extension SitePickerViewController { return actions } - enum SiteIconSource { - case photosLibrary - case camera - case mediaLibrary - case combined // legacy option - } - - func updateSiteIcon(source: SiteIconSource = .combined) { - siteIconPickerPresenter = SiteIconPickerPresenter(blog: blog) - siteIconPickerPresenter?.onCompletion = { [ weak self] media, error in + private func makeSiteIconPresenter() -> SiteIconPickerPresenter { + let presenter = SiteIconPickerPresenter(blog: blog) + presenter.onCompletion = { [ weak self] media, error in if error != nil { self?.showErrorForSiteIconUpdate() } else if let media = media { @@ -95,22 +88,12 @@ extension SitePickerViewController { self?.siteIconPickerPresenter = nil self?.startAlertTimer() } - - siteIconPickerPresenter?.onIconSelection = { [weak self] in + presenter.onIconSelection = { [weak self] in self?.blogDetailHeaderView.updatingIcon = true self?.dismiss(animated: true) } - - switch source { - case .photosLibrary: - siteIconPickerPresenter?.presentPhotosPicker(from: self) - case .camera: - siteIconPickerPresenter?.presentCamera(from: self) - case .mediaLibrary: - siteIconPickerPresenter?.presentMediaLibraryPicker(from: self) - case .combined: - siteIconPickerPresenter?.presentPickerFrom(self) - } + self.siteIconPickerPresenter = presenter + return presenter } func showEmojiPicker() { diff --git a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift index abca55a3d7c0..f8efb848f484 100644 --- a/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift +++ b/WordPress/Classes/ViewRelated/Blog/Site Settings/SiteIconPickerPresenter.swift @@ -69,51 +69,6 @@ final class SiteIconPickerPresenter: NSObject { unregisterChangeObserver() } - func presentPhotosPicker(from presentingViewController: UIViewController) { - var configuration = PHPickerConfiguration() - configuration.filter = .images - - let picker = PHPickerViewController(configuration: configuration) - picker.delegate = self - presentingViewController.present(picker, animated: true) - } - - func presentCamera(from presentingViewController: UIViewController) { - let picker = WPMediaCapturePresenter(presenting: presentingViewController) - picker.completionBlock = { [weak self] info in - if let image = info?[UIImagePickerController.InfoKey.originalImage] as? UIImage { - self?.showImageCropViewController(image, presentingViewController: presentingViewController) - } - } - picker.mediaType = .image - picker.presentCapture() - mediaCapturePresenter = picker // Retain - } - - func presentMediaLibraryPicker(from presentingViewController: UIViewController) { - let options = WPMediaPickerOptions() - options.showMostRecentFirst = true - options.filter = [.image] - options.allowMultipleSelection = false - options.showSearchBar = true - options.badgedUTTypes = [UTType.gif.identifier] - options.preferredStatusBarStyle = WPStyleGuide.preferredStatusBarStyle - options.allowCaptureOfMedia = false - - let pickerViewController = WPNavigationMediaPickerViewController(options: options) - - let dataSource = MediaLibraryPickerDataSource(blog: blog) - dataSource.ignoreSyncErrors = true - self.dataSource = dataSource - - pickerViewController.showGroupSelector = false - pickerViewController.dataSource = dataSource - pickerViewController.delegate = self - pickerViewController.modalPresentationStyle = .formSheet - - presentingViewController.present(pickerViewController, animated: true) - } - /// Presents a new WPMediaPickerViewController instance. /// @objc func presentPickerFrom(_ viewController: UIViewController) { @@ -136,7 +91,7 @@ final class SiteIconPickerPresenter: NSObject { /// Shows a new ImageCropViewController for the given image. /// - fileprivate func showImageCropViewController(_ image: UIImage, presentingViewController: UIViewController? = nil) { + func showImageCropViewController(_ image: UIImage, presentingViewController: UIViewController? = nil) { DispatchQueue.main.async { SVProgressHUD.dismiss() let imageCropViewController = ImageCropViewController(image: image) @@ -241,17 +196,29 @@ extension SiteIconPickerPresenter: PHPickerViewControllerDelegate { WPAnalytics.track(.siteSettingsSiteIconGalleryPicked) self.showLoadingMessage() self.originalMedia = nil - result.itemProvider.loadObject(ofClass: UIImage.self) { [weak self] image, _ in - if let image = image as? UIImage { + MediaPickerMenu.loadImage(for: result) { [weak self] image, error in + if let image { self?.showImageCropViewController(image, presentingViewController: picker) } else { + DDLogError("Failed to load image: \(String(describing: error))") self?.showErrorLoadingImageMessage() } } } } -extension SiteIconPickerPresenter: UINavigationControllerDelegate {} +extension SiteIconPickerPresenter: ImagePickerControllerDelegate { + func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + guard let presentingViewController = picker.presentingViewController else { + return + } + presentingViewController.dismiss(animated: true) { + if let image = info[.originalImage] as? UIImage { + self.showImageCropViewController(image, presentingViewController: presentingViewController) + } + } + } +} extension SiteIconPickerPresenter: WPMediaPickerViewControllerDelegate { diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker.swift deleted file mode 100644 index 9d9302abcf1f..000000000000 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker.swift +++ /dev/null @@ -1,37 +0,0 @@ -import UIKit -import Gridicons - -struct MediaPickerMenu { - static func makePickFromPhotosAction(_ handler: @escaping () -> Void) -> UIAction { - UIAction( - title: Strings.pickFromPhotosLibrary, - image: UIImage(systemName: "photo.on.rectangle.angled"), - attributes: [], - handler: { _ in handler() } - ) - } - - static func makeTakePhotoAction(_ handler: @escaping () -> Void) -> UIAction { - UIAction( - title: Strings.takePhoto, - image: UIImage(systemName: "camera"), - attributes: [], - handler: { _ in handler() } - ) - } - - static func makePickFromMediaAction(_ handler: @escaping () -> Void) -> UIAction { - UIAction( - title: Strings.pickFromMedia, - image: UIImage(systemName: "photo.stack"), - attributes: [], - handler: { _ in handler() } - ) - } -} - -private enum Strings { - static let pickFromPhotosLibrary = NSLocalizedString("mediaPicker.pickFromPhotosLibrary", value: "Choose from Device", comment: "The name of the action in the context menu") - static let takePhoto = NSLocalizedString("mediaPicker.takePhoto", value: "Take Photo", comment: "The name of the action in the context menu") - static let pickFromMedia = NSLocalizedString("mediaPicker.pickFromMediaLibrary", value: "Choose from Media", comment: "The name of the action in the context menu (user's WordPress Media Library") -} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift b/WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift new file mode 100644 index 000000000000..17cdaca713b2 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift @@ -0,0 +1,272 @@ +import UIKit +import PhotosUI +import WPMediaPicker +import UniformTypeIdentifiers +import AVFoundation +import CocoaLumberjack + +/// A convenience API for creating actions for picking media from different +/// source supported by the app: Photos library, Camera, Media library. +struct MediaPickerMenu { + weak var presentingViewController: UIViewController? + var filter: MediaFilter? + var isMultipleSelectionEnabled: Bool + + enum MediaFilter { + case images + case videos + } + + /// Initializes the options. + /// + /// - parameters: + /// - presentingViewController: The view controller to use for presentation. + /// - filter: By default, `nil` – allow all content types. + /// - isMultipleSelectionEnabled: By default, `false`. + init(viewConroller: UIViewController, + filter: MediaFilter? = nil, + isMultipleSelectionEnabled: Bool = false) { + self.presentingViewController = viewConroller + self.filter = filter + self.isMultipleSelectionEnabled = isMultipleSelectionEnabled + } +} + +// MARK: - MediaPickerMenu (Photos) + +extension MediaPickerMenu { + /// Returns an action for picking photos from the device's Photos library. + /// + /// - note: Use `MediaPicker.loadImage(for:)` to retrieve an image from the result. + func makePhotosAction(delegate: PHPickerViewControllerDelegate) -> UIAction { + UIAction( + title: Strings.pickFromPhotosLibrary, + image: UIImage(systemName: "photo.on.rectangle.angled"), + attributes: [], + handler: { _ in showPhotosPicker(delegate: delegate) } + ) + } + + func showPhotosPicker(delegate: PHPickerViewControllerDelegate) { + var configuration = PHPickerConfiguration() + if let filter { + switch filter { + case .images: + configuration.filter = .images + case .videos: + configuration.filter = .videos + } + } + if isMultipleSelectionEnabled { + configuration.selectionLimit = 0 + configuration.selection = .ordered + } + let picker = PHPickerViewController(configuration: configuration) + picker.delegate = delegate + presentingViewController?.present(picker, animated: true) + } + + /// Retrieves an image for the given picker result. + /// + /// - parameter completion: The completion closure that gets called on the main thread. + static func loadImage(for result: PHPickerResult, _ completion: @escaping (UIImage?, Error?) -> Void) { + let provider = result.itemProvider + if provider.canLoadObject(ofClass: UIImage.self) { + provider.loadObject(ofClass: UIImage.self) { value, error in + DispatchQueue.main.async { + if let image = value as? UIImage { + completion(image, nil) + } else { + DDLogError("Failed to load image for provider with registered types \(provider.registeredTypeIdentifiers) with error \(String(describing: error))") + + completion(nil, error) + } + } + } + } else if provider.hasItemConformingToTypeIdentifier(UTType.image.identifier) { + // This is required for certain image formats, such as WebP, for which + // NSItemProvider doesn't automatically provide the `UIImage` representation. + provider.loadDataRepresentation(forTypeIdentifier: UTType.image.identifier) { data, error in + let image = data.flatMap(UIImage.init) + DispatchQueue.main.async { + if let image { + completion(image, nil) + } else { + DDLogError("Failed to load image for provider with registered types \(provider.registeredTypeIdentifiers) with error \(String(describing: error))") + completion(nil, error) + } + } + } + } else { + DDLogError("No image representation available for provider with registered types: \(provider.registeredTypeIdentifiers)") + DispatchQueue.main.async { + completion(nil, nil) + } + } + } +} + +// MARK: - MediaPickerMenu (Camera) + +protocol ImagePickerControllerDelegate: AnyObject { + // Hides `NSObject` and `UINavigationControllerDelegate` conformances that + // the original `UIImagePickerControllerDelegate` has. + + /// - parameter info: If the info is empty, nothing was selected. + func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) +} + +extension MediaPickerMenu { + /// Returns an action from capturing media using the device's camera. + /// + /// - parameters: + /// - camera: The camera to use. By default, `.rear`. + /// - delegate: The delegate. + func makeCameraAction( + camera: UIImagePickerController.CameraDevice = .rear, + delegate: ImagePickerControllerDelegate + ) -> UIAction { + UIAction( + title: cameraActionTitle, + image: UIImage(systemName: "camera"), + attributes: [], + handler: { _ in showCamera(camera: camera, delegate: delegate) } + ) + } + + private var cameraActionTitle: String { + guard let filter else { + return Strings.takePhotoOrVideo + } + switch filter { + case .images: return Strings.takePhoto + case .videos: return Strings.takeVideo + } + } + + func showCamera(camera: UIImagePickerController.CameraDevice, delegate: ImagePickerControllerDelegate) { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .authorized, .notDetermined: + actuallyShowCamera(camera: camera, delegate: delegate) + case .restricted, .denied: + showAccessRestrictedAlert() + @unknown default: + showAccessRestrictedAlert() + } + } + + private func actuallyShowCamera(camera: UIImagePickerController.CameraDevice, delegate: ImagePickerControllerDelegate) { + let picker = UIImagePickerController() + picker.sourceType = .camera + picker.cameraDevice = camera + picker.videoQuality = .typeHigh + if let filter { + switch filter { + case .images: picker.mediaTypes = [UTType.image.identifier] + case .videos: picker.mediaTypes = [UTType.movie.identifier] + } + } else { + picker.mediaTypes = [UTType.image.identifier, UTType.movie.identifier] + } + + let delegate = ImagePickerDelegate(delegate: delegate) + picker.delegate = delegate + objc_setAssociatedObject(picker, &MediaPickerMenu.strongDelegateKey, delegate, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + + presentingViewController?.present(picker, animated: true) + } + + private func showAccessRestrictedAlert() { + let alert = UIAlertController(title: Strings.noCameraAccessTitle, message: Strings.noCameraAccessMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: Strings.buttonOK, style: .cancel)) + alert.addAction(UIAlertAction(title: Strings.noCameraOpenSettings, style: .default) { _ in + guard let url = URL(string: UIApplication.openSettingsURLString) else { + return assertionFailure("Failed to create Open Settigns URL") + } + UIApplication.shared.open(url) + }) + presentingViewController?.present(alert, animated: true) + } + + + private final class ImagePickerDelegate: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate { + weak var delegate: ImagePickerControllerDelegate? + + init(delegate: ImagePickerControllerDelegate) { + self.delegate = delegate + } + + func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) { + delegate?.imagePicker(picker, didFinishPickingMediaWithInfo: info) + } + + func imagePickerControllerDidCancel(_ picker: UIImagePickerController) { + delegate?.imagePicker(picker, didFinishPickingMediaWithInfo: [:]) + } + } + + private static var strongDelegateKey: UInt8 = 0 +} + +// MARK: - MediaPickerMenu (WordPress Media) + +extension MediaPickerMenu { + func makeMediaAction(blog: Blog, delegate: WPMediaPickerViewControllerDelegate) -> UIAction { + UIAction( + title: Strings.pickFromMedia, + image: UIImage(systemName: "photo.stack"), + attributes: [], + handler: { _ in showMediaPicker(blog: blog, delegate: delegate) } + ) + } + + func showMediaPicker(blog: Blog, delegate: WPMediaPickerViewControllerDelegate) { + let options = WPMediaPickerOptions() + options.showMostRecentFirst = true + if let filter { + switch filter { + case .images: + options.filter = [.image] + case .videos: + options.filter = [.video] + } + } + options.allowMultipleSelection = isMultipleSelectionEnabled + options.showSearchBar = true + options.badgedUTTypes = [UTType.gif.identifier] + options.preferredStatusBarStyle = WPStyleGuide.preferredStatusBarStyle + options.allowCaptureOfMedia = false + + let dataSource = MediaLibraryPickerDataSource(blog: blog) + dataSource.ignoreSyncErrors = true + + let picker = WPNavigationMediaPickerViewController(options: options) + picker.showGroupSelector = false + picker.dataSource = dataSource + picker.delegate = delegate + picker.modalPresentationStyle = .formSheet + + objc_setAssociatedObject(picker, &MediaPickerMenu.dataSourceAssociatedKey, dataSource, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + + presentingViewController?.present(picker, animated: true) + } + + private static var dataSourceAssociatedKey: UInt8 = 0 +} + +private enum Strings { + // MARK: Actions + + static let pickFromPhotosLibrary = NSLocalizedString("mediaPicker.pickFromPhotosLibrary", value: "Choose from Device", comment: "The name of the action in the context menu") + static let takePhoto = NSLocalizedString("mediaPicker.takePhoto", value: "Take Photo", comment: "The name of the action in the context menu") + static let takeVideo = NSLocalizedString("mediaPicker.takeVideo", value: "Take Video", comment: "The name of the action in the context menu") + static let takePhotoOrVideo = NSLocalizedString("mediaPicker.takePhotoOrVideo", value: "Take Photo or Video", comment: "The name of the action in the context menu") + static let pickFromMedia = NSLocalizedString("mediaPicker.pickFromMediaLibrary", value: "Choose from Media", comment: "The name of the action in the context menu (user's WordPress Media Library") + + // MARK: Misc + + static let noCameraAccessTitle = NSLocalizedString("mediaPicker.noCameraAccessTitle", value: "Media Capture", comment: "Title for alert when access to camera is not granted") + static let noCameraAccessMessage = NSLocalizedString("mediaPicker.noCameraAccessMessage", value: "This app needs permission to access the Camera to capture new media, please change the privacy settings if you wish to allow this.", comment: "Message for alert when access to camera is not granted") + static let noCameraOpenSettings = NSLocalizedString("mediaPicker.openSettings", value: "Open Settings", comment: "Button that opens the Settings app") + static let buttonOK = NSLocalizedString("OK", value: "OK", comment: "OK") +} diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index e314592830fe..57da551d223d 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -386,8 +386,8 @@ 0A9610F928B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9610FA28B2E56300076EBA /* UserSuggestion+Comparable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */; }; 0A9687BC28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */; }; - 0C0AE7592A8FAD6A007D9D6C /* MediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */; }; - 0C0AE75A2A8FAD6A007D9D6C /* MediaPicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */; }; + 0C0AE7592A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; + 0C0AE75A2A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */; }; 0C0D3B0D2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; 0C0D3B0E2A4C79DE0050A00D /* BlazeCampaignsStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */; }; 0C2C83FA2A6EABF300A3ACD9 /* StatsPeriodCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C2C83F92A6EABF300A3ACD9 /* StatsPeriodCache.swift */; }; @@ -6084,7 +6084,7 @@ 0A69300A28B5AA5E00E98DE1 /* FullScreenCommentReplyViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelTests.swift; sourceTree = ""; }; 0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+Comparable.swift"; sourceTree = ""; }; 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelMock.swift; sourceTree = ""; }; - 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPicker.swift; sourceTree = ""; }; + 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaPickerMenu.swift; sourceTree = ""; }; 0C0D3B0C2A4C79DE0050A00D /* BlazeCampaignsStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignsStream.swift; sourceTree = ""; }; 0C2C83F92A6EABF300A3ACD9 /* StatsPeriodCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsPeriodCache.swift; sourceTree = ""; }; 0C2C83FC2A6EBD3F00A3ACD9 /* StatsInsightsCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatsInsightsCache.swift; sourceTree = ""; }; @@ -12401,7 +12401,7 @@ D80BC7A3207487F200614A59 /* MediaLibraryPicker.swift */, 0C8FC9A02A8BC8630059DCE4 /* PHPickerController+Extensions.swift */, 0C8FC9A62A8BFAAD0059DCE4 /* NSItemProvider+Exportable.swift */, - 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */, + 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */, 7E14635620B3BEAB00B95F41 /* WPStyleGuide+Loader.swift */, 7ECD5B8020C4D823001AEBC5 /* MediaPreviewHelper.swift */, ); @@ -21370,7 +21370,7 @@ 74729CAE205722E300D1394D /* AbstractPost+Searchable.swift in Sources */, 2906F812110CDA8900169D56 /* EditCommentViewController.m in Sources */, 98F93182239AF64800E4E96E /* ThisWeekWidgetStats.swift in Sources */, - 0C0AE7592A8FAD6A007D9D6C /* MediaPicker.swift in Sources */, + 0C0AE7592A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */, F16C35DA23F3F76C00C81331 /* PostAutoUploadMessageProvider.swift in Sources */, 91D8364121946EFB008340B2 /* GutenbergMediaPickerHelper.swift in Sources */, F1D690161F82913F00200E30 /* FeatureFlag.swift in Sources */, @@ -24732,7 +24732,7 @@ FABB23AC2602FC2C00C8785C /* ReachabilityUtils.m in Sources */, FABB23AD2602FC2C00C8785C /* TenorDataSource.swift in Sources */, FABB23AE2602FC2C00C8785C /* ReaderTopicService+Subscriptions.swift in Sources */, - 0C0AE75A2A8FAD6A007D9D6C /* MediaPicker.swift in Sources */, + 0C0AE75A2A8FAD6A007D9D6C /* MediaPickerMenu.swift in Sources */, 98517E5A28220411001FFD45 /* BloggingPromptTableViewCell.swift in Sources */, FAC1B81F29B0C2AC00E0C542 /* BlazeOverlayViewModel.swift in Sources */, FABB23B02602FC2C00C8785C /* Data.swift in Sources */, From 5eccbcd1abd3b36e8fdaa601546fd2deece883b2 Mon Sep 17 00:00:00 2001 From: Alex Grebenyuk Date: Mon, 21 Aug 2023 15:47:44 -0400 Subject: [PATCH 4/4] Update WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift Co-authored-by: Momo Ozawa --- .../Blog/Blog Details/Detail Header/SiteIconView.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift index de0d79922397..27654b660df2 100644 --- a/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift +++ b/WordPress/Classes/ViewRelated/Blog/Blog Details/Detail Header/SiteIconView.swift @@ -42,10 +42,10 @@ class SiteIconView: UIView { /// Set the menu to be displayed when the button is tapped. The menu replaces /// teh default on tap action. - func setMenu(_ menu: UIMenu, onMenuTriggerd: @escaping () -> Void) { + func setMenu(_ menu: UIMenu, onMenuTriggered: @escaping () -> Void) { button.menu = menu button.showsMenuAsPrimaryAction = true - button.addAction(UIAction { _ in onMenuTriggerd() }, for: .menuActionTriggered) + button.addAction(UIAction { _ in onMenuTriggered() }, for: .menuActionTriggered) } private let button: UIButton = {