From 0b1ef40c73fd14ab7f3096323d9c1ce831a864af Mon Sep 17 00:00:00 2001 From: kean Date: Mon, 21 Aug 2023 07:46:50 -0400 Subject: [PATCH] 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 0fa35a8a435f..b2e43b9f8077 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -382,8 +382,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 */; }; @@ -6072,7 +6072,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 = ""; }; @@ -12378,7 +12378,7 @@ D80BC79F2074722000614A59 /* CameraCaptureCoordinator.swift */, D80BC7A12074739300614A59 /* MediaLibraryStrings.swift */, D80BC7A3207487F200614A59 /* MediaLibraryPicker.swift */, - 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */, + 0C0AE7582A8FAD6A007D9D6C /* MediaPickerMenu.swift */, 0C8FC9A02A8BC8630059DCE4 /* PHPickerContoller+Extensions.swift */, 0C8FC9A62A8BFAAD0059DCE4 /* NSIterProvider+Exportable.swift */, 7E14635620B3BEAB00B95F41 /* WPStyleGuide+Loader.swift */, @@ -21342,7 +21342,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 */, @@ -24699,7 +24699,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 */,