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 de73363a3591..0fa35a8a435f 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -382,6 +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 */; }; 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 */; }; @@ -6070,6 +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 = ""; }; 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 = ""; }; @@ -12375,6 +12378,7 @@ D80BC79F2074722000614A59 /* CameraCaptureCoordinator.swift */, D80BC7A12074739300614A59 /* MediaLibraryStrings.swift */, D80BC7A3207487F200614A59 /* MediaLibraryPicker.swift */, + 0C0AE7582A8FAD6A007D9D6C /* MediaPicker.swift */, 0C8FC9A02A8BC8630059DCE4 /* PHPickerContoller+Extensions.swift */, 0C8FC9A62A8BFAAD0059DCE4 /* NSIterProvider+Exportable.swift */, 7E14635620B3BEAB00B95F41 /* WPStyleGuide+Loader.swift */, @@ -21338,6 +21342,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 */, @@ -24694,6 +24699,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 */,