diff --git a/Modules/Sources/WordPressUI/Deprecated/NoResultsViewController.swift b/Modules/Sources/WordPressUI/Deprecated/NoResultsViewController.swift index e7822776f9fc..429e95543422 100644 --- a/Modules/Sources/WordPressUI/Deprecated/NoResultsViewController.swift +++ b/Modules/Sources/WordPressUI/Deprecated/NoResultsViewController.swift @@ -174,6 +174,20 @@ import WordPressShared displayTitleViewOnly = false } + /// Configures the view for an in-progress operation without replacing it with the no-connection presentation. + public func configureForLoading(title: String) { + titleText = title + subtitleText = nil + attributedSubtitleText = nil + configureAttributedSubtitle = nil + attributedTitleText = nil + buttonText = nil + imageName = nil + subtitleImageName = nil + accessorySubview = Self.loadingAccessoryView() + displayTitleViewOnly = false + } + /// Public method to show the title specifically formatted for no search results. /// When the view is configured, it will display just a label with specific constraints. /// diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 4cb6fb035a49..56ed3116fcb0 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -5,6 +5,7 @@ * [*] [build tooling] Add a String Catalog localization pipeline (plurals + build-free catalog generation) with a CI coverage gate [#25688] * [*] [internal] Stats widgets: migrate the site picker from SiriKit to App Intents [#25753] * [*] Fix Blogging Reminders text color in dark mode [#25780] +* [*] Share extensions: Fix the site picker showing an error while sites are still loading [#25798] 27.0 ----- diff --git a/WordPress/WordPressShareExtension/Sources/UI/ShareModularViewController.swift b/WordPress/WordPressShareExtension/Sources/UI/ShareModularViewController.swift index e191e8c616a6..3ca3a1b3eb46 100644 --- a/WordPress/WordPressShareExtension/Sources/UI/ShareModularViewController.swift +++ b/WordPress/WordPressShareExtension/Sources/UI/ShareModularViewController.swift @@ -11,6 +11,8 @@ class ShareModularViewController: ShareExtensionAbstractViewController { fileprivate var isPublishingPost: Bool = false fileprivate var isFetchingCategories: Bool = false + fileprivate var isFetchingSites: Bool = false + fileprivate var sitesState: SitesState = .loading /// StackView container for the tables /// @@ -31,7 +33,11 @@ class ShareModularViewController: ShareExtensionAbstractViewController { /// Back Bar Button /// fileprivate lazy var backButton: UIBarButtonItem = { - let backTitle = AppLocalizedString("Back", comment: "Back action on share extension site picker screen. Takes the user to the share extension editor screen.") + let backTitle = AppLocalizedString( + "Back", + comment: + "Back action on share extension site picker screen. Takes the user to the share extension editor screen." + ) let button = UIBarButtonItem(title: backTitle, style: .plain, target: self, action: #selector(backWasPressed)) button.accessibilityIdentifier = "Back Button" return button @@ -41,7 +47,12 @@ class ShareModularViewController: ShareExtensionAbstractViewController { /// fileprivate lazy var cancelButton: UIBarButtonItem = { let cancelTitle = AppLocalizedString("Cancel", comment: "Cancel action on the app extension modules screen.") - let button = UIBarButtonItem(title: cancelTitle, style: .plain, target: self, action: #selector(cancelWasPressed)) + let button = UIBarButtonItem( + title: cancelTitle, + style: .plain, + target: self, + action: #selector(cancelWasPressed) + ) button.accessibilityIdentifier = "Cancel Button" return button }() @@ -51,12 +62,23 @@ class ShareModularViewController: ShareExtensionAbstractViewController { fileprivate lazy var publishButton: UIBarButtonItem = { let publishTitle: String if self.originatingExtension == .share { - publishTitle = AppLocalizedString("Publish", comment: "Publish post action on share extension site picker screen.") + publishTitle = AppLocalizedString( + "Publish", + comment: "Publish post action on share extension site picker screen." + ) } else { - publishTitle = AppLocalizedString("Save", comment: "Save draft post action on share extension site picker screen.") + publishTitle = AppLocalizedString( + "Save", + comment: "Save draft post action on share extension site picker screen." + ) } - let button = UIBarButtonItem(title: publishTitle, style: .plain, target: self, action: #selector(publishWasPressed)) + let button = UIBarButtonItem( + title: publishTitle, + style: .plain, + target: self, + action: #selector(publishWasPressed) + ) if self.originatingExtension == .share { button.accessibilityIdentifier = "Publish Button" } else { @@ -132,14 +154,17 @@ class ShareModularViewController: ShareExtensionAbstractViewController { // Use the filename as the uploadID here. if extractedImage.insertionState == .requiresInsertion { - self.shareData.contentBody = self.shareData.contentBody.stringByAppendingMediaURL(mediaURL: imageURL.absoluteString, uploadID: imageURL.lastPathComponent) + self.shareData.contentBody = self.shareData.contentBody.stringByAppendingMediaURL( + mediaURL: imageURL.absoluteString, + uploadID: imageURL.lastPathComponent + ) } }) // Clear out the extension context after loading it once. We don't need it anymore. self.context = nil self.refreshModulesTable() - } + } } fileprivate func setupPrimarySiteIfNeeded() { @@ -189,6 +214,7 @@ class ShareModularViewController: ShareExtensionAbstractViewController { fileprivate func setupSitesTableView() { // Register the cells sitesTableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.sitesReuseIdentifier) + sitesTableView.register(UITableViewCell.self, forCellReuseIdentifier: Constants.sitesStatusReuseIdentifier) sitesTableView.estimatedRowHeight = Constants.siteRowHeight // Hide the separators, whenever the table is empty @@ -242,11 +268,14 @@ extension ShareModularViewController { } @objc func pullToRefresh(sender: UIRefreshControl) { + guard !isFetchingSites else { + sender.endRefreshing() + return + } ShareExtensionAbstractViewController.clearCache() isFetchingCategories = true clearCategoriesAndRefreshModulesTable() - clearSiteDataAndRefreshSitesTable() - reloadSitesIfNeeded() + fetchSites() } func showPostTypePicker() { @@ -291,11 +320,17 @@ extension ShareModularViewController { guard let siteID = shareData.selectedSiteID, let allSiteCategories = shareData.allCategoriesForSelectedSite, isFetchingCategories == false, - isPublishingPost == false else { + isPublishingPost == false + else { return } - let categoryInfo = SiteCategories(siteID: siteID, allCategories: allSiteCategories, selectedCategories: shareData.userSelectedCategories, defaultCategoryID: shareData.defaultCategoryID) + let categoryInfo = SiteCategories( + siteID: siteID, + allCategories: allSiteCategories, + selectedCategories: shareData.userSelectedCategories, + defaultCategoryID: shareData.defaultCategoryID + ) let categoriesPicker = ShareCategoriesPickerViewController(categoryInfo: categoryInfo) categoriesPicker.title = Strings.categoryCellTitle categoriesPicker.onValueChanged = { [weak self] categoryInfo in @@ -335,19 +370,23 @@ extension ShareModularViewController: UITableViewDataSource { let cell = tableView.dequeueReusableCell(withIdentifier: Constants.modulesReuseIdentifier)! configureModulesCell(cell, indexPath: indexPath) return cell - } else { + } else if sitesState == .loaded { let cell = tableView.dequeueReusableCell(withIdentifier: Constants.sitesReuseIdentifier)! configureSiteCell(cell, indexPath: indexPath) return cell + } else { + let cell = tableView.dequeueReusableCell(withIdentifier: Constants.sitesStatusReuseIdentifier)! + configureSitesStatusCell(cell) + return cell } } func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return UITableView.automaticDimension + UITableView.automaticDimension } func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { - return tableView.estimatedRowHeight + tableView.estimatedRowHeight } func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { @@ -386,6 +425,18 @@ extension ShareModularViewController: UITableViewDataSource { // MARK: - UITableView Delegate Conformance extension ShareModularViewController: UITableViewDelegate { + func tableView(_ tableView: UITableView, willSelectRowAt indexPath: IndexPath) -> IndexPath? { + guard tableView == sitesTableView else { + return indexPath + } + switch sitesState { + case .loaded where !isFetchingSites, .failed: + return indexPath + case .loading, .loaded, .empty: + return nil + } + } + func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { if tableView == sitesTableView { selectedSitesTableRowAt(indexPath) @@ -425,8 +476,13 @@ fileprivate extension ShareModularViewController { if isFetchingCategories { cell.isUserInteractionEnabled = false cell.accessoryType = .none - cell.accessoryView = categoryActivityIndicator - categoryActivityIndicator.startAnimating() + if isFetchingSites { + categoryActivityIndicator.stopAnimating() + cell.accessoryView = nil + } else { + cell.accessoryView = categoryActivityIndicator + categoryActivityIndicator.startAnimating() + } } else { switch shareData.categoryCountForSelectedSite { case 0, 1: @@ -444,7 +500,8 @@ fileprivate extension ShareModularViewController { cell.detailTextLabel?.text = shareData.selectedCategoriesNameString if (shareData.userSelectedCategories == nil || shareData.userSelectedCategories?.isEmpty == true) - && shareData.defaultCategoryID == Constants.unknownDefaultCategoryID { + && shareData.defaultCategoryID == Constants.unknownDefaultCategoryID + { cell.detailTextLabel?.textColor = UIAppColor.neutral(.shade30) } else { cell.detailTextLabel?.textColor = UIAppColor.neutral(.shade70) @@ -459,7 +516,10 @@ fileprivate extension ShareModularViewController { cell.detailTextLabel?.text = tags cell.detailTextLabel?.textColor = UIAppColor.neutral(.shade70) } else { - cell.detailTextLabel?.text = AppLocalizedString("Add tags", comment: "Placeholder text for tags module in share extension.") + cell.detailTextLabel?.text = AppLocalizedString( + "Add tags", + comment: "Placeholder text for tags module in share extension." + ) cell.detailTextLabel?.textColor = UIAppColor.neutral(.shade30) } case .summary: @@ -488,26 +548,32 @@ fileprivate extension ShareModularViewController { func selectedModulesTableRowAt(_ indexPath: IndexPath) { switch ModulesSection(rawValue: indexPath.section)! { case .type: - modulesTableView.flashRowAtIndexPath(indexPath, - scrollPosition: .none, - flashLength: Constants.flashAnimationLength, - completion: nil) + modulesTableView.flashRowAtIndexPath( + indexPath, + scrollPosition: .none, + flashLength: Constants.flashAnimationLength, + completion: nil + ) showPostTypePicker() return case .categories: if shareData.categoryCountForSelectedSite > 1 { - modulesTableView.flashRowAtIndexPath(indexPath, - scrollPosition: .none, - flashLength: Constants.flashAnimationLength, - completion: nil) + modulesTableView.flashRowAtIndexPath( + indexPath, + scrollPosition: .none, + flashLength: Constants.flashAnimationLength, + completion: nil + ) showCategoriesPicker() } return case .tags: - modulesTableView.flashRowAtIndexPath(indexPath, - scrollPosition: .none, - flashLength: Constants.flashAnimationLength, - completion: nil) + modulesTableView.flashRowAtIndexPath( + indexPath, + scrollPosition: .none, + flashLength: Constants.flashAnimationLength, + completion: nil + ) showTagsPicker() return case .summary: @@ -523,9 +589,11 @@ fileprivate extension ShareModularViewController { } else if originatingExtension == .saveToDraft && shareData.sharedImageDict.isEmpty { return SummaryText.summaryDraftPostDefault } else if originatingExtension == .saveToDraft && !shareData.sharedImageDict.isEmpty { - return ShareNoticeText.pluralize(shareData.sharedImageDict.count, - singular: SummaryText.summaryDraftPostSingular, - plural: SummaryText.summaryDraftPostPlural) + return ShareNoticeText.pluralize( + shareData.sharedImageDict.count, + singular: SummaryText.summaryDraftPostSingular, + plural: SummaryText.summaryDraftPostPlural + ) } else { return String() } @@ -535,9 +603,11 @@ fileprivate extension ShareModularViewController { } else if originatingExtension == .saveToDraft && shareData.sharedImageDict.isEmpty { return SummaryText.summaryDraftPageDefault } else if originatingExtension == .saveToDraft && !shareData.sharedImageDict.isEmpty { - return ShareNoticeText.pluralize(shareData.sharedImageDict.count, - singular: SummaryText.summaryDraftPageSingular, - plural: SummaryText.summaryDraftPagePlural) + return ShareNoticeText.pluralize( + shareData.sharedImageDict.count, + singular: SummaryText.summaryDraftPageSingular, + plural: SummaryText.summaryDraftPagePlural + ) } else { return String() } @@ -562,6 +632,48 @@ fileprivate extension ShareModularViewController { // MARK: - Sites UITableView Helpers fileprivate extension ShareModularViewController { + func configureSitesStatusCell(_ cell: UITableViewCell) { + let title: String + let isLoading: Bool + let isFailure: Bool + switch sitesState { + case .loading: + title = StatusText.loadingTitle + isLoading = true + isFailure = false + cell.selectionStyle = .none + case .empty: + title = StatusText.noSitesTitle + isLoading = false + isFailure = false + cell.selectionStyle = .none + case .failed: + title = StatusText.sitesFailureTitle + isLoading = false + isFailure = true + cell.selectionStyle = .default + case .loaded: + assertionFailure("A status cell should not be shown when sites are loaded") + return + } + + cell.accessoryType = .none + cell.contentConfiguration = UIHostingConfiguration { + HStack(spacing: 12) { + if isLoading { + ProgressView() + } + Text(title) + .font(.callout) + .foregroundStyle(.secondary) + } + .accessibilityElement(children: .ignore) + .accessibilityLabel(title) + .accessibilityAddTraits(isFailure ? .isButton : .isStaticText) + } + .margins(.vertical, 12) + } + func configureSiteCell(_ cell: UITableViewCell, indexPath: IndexPath) { guard let site = siteForRowAtIndexPath(indexPath) else { return @@ -571,7 +683,8 @@ fileprivate extension ShareModularViewController { cell.contentConfiguration = UIHostingConfiguration { ShareSiteCellView(site: site) - }.margins(.vertical, 12) + } + .margins(.vertical, 12) if site.blogID.intValue == shareData.selectedSiteID { cell.accessoryType = .checkmark @@ -581,15 +694,33 @@ fileprivate extension ShareModularViewController { } var rowCountForSites: Int { - return sites?.count ?? 0 + switch sitesState { + case .loading, .empty, .failed: + return 1 + case .loaded: + return sites?.count ?? 0 + } } func selectedSitesTableRowAt(_ indexPath: IndexPath) { - sitesTableView.flashRowAtIndexPath(indexPath, scrollPosition: .none, flashLength: Constants.flashAnimationLength, completion: nil) + if sitesState == .failed { + retryLoadingSites() + return + } + guard sitesState == .loaded, !isFetchingSites else { + return + } + sitesTableView.flashRowAtIndexPath( + indexPath, + scrollPosition: .none, + flashLength: Constants.flashAnimationLength, + completion: nil + ) guard let cell = sitesTableView.cellForRow(at: indexPath), let site = siteForRowAtIndexPath(indexPath), - site.blogID.intValue != shareData.selectedSiteID else { + site.blogID.intValue != shareData.selectedSiteID + else { return } @@ -610,7 +741,7 @@ fileprivate extension ShareModularViewController { } func clearAllSelectedSiteRows() { - for row in 0 ..< rowCountForSites { + for row in 0.. (), onFailure: @escaping () -> ()) { + func fetchDefaultCategoryForSelectedSite(onSuccess: @escaping (NSNumber) -> (), onFailure: @escaping () -> ()) { guard let _ = oauth2Token, let siteID = shareData.selectedSiteID else { return } - if let cachedDefaultCategoryID = ShareExtensionAbstractViewController.cachedDefaultCategoryIDForSite(NSNumber(value: siteID)) { + if let cachedDefaultCategoryID = ShareExtensionAbstractViewController.cachedDefaultCategoryIDForSite( + NSNumber(value: siteID) + ) { onSuccess(cachedDefaultCategoryID) } else { let networkService = AppExtensionsService() - networkService.fetchSettingsForSite(siteID, onSuccess: { settings in - guard let settings, let defaultCategoryID = settings.defaultCategoryID else { - onFailure() - return + networkService.fetchSettingsForSite( + siteID, + onSuccess: { settings in + guard let settings, let defaultCategoryID = settings.defaultCategoryID else { + onFailure() + return + } + ShareExtensionAbstractViewController.storeDefaultCategoryID( + defaultCategoryID, + for: NSNumber(value: siteID) + ) + onSuccess(defaultCategoryID) } - ShareExtensionAbstractViewController.storeDefaultCategoryID(defaultCategoryID, for: NSNumber(value: siteID)) - onSuccess(defaultCategoryID) - }) { error in + ) { error in // The current user probably does not have permissions to access site settings OR needs to be VPNed. - let error = self.createErrorWithDescription("Could not successfully fetch the settings for site: \(siteID). Error: \(String(describing: error))") + let error = self.createErrorWithDescription( + "Could not successfully fetch the settings for site: \(siteID). Error: \(String(describing: error))" + ) self.tracks.trackExtensionError(error) onFailure() } @@ -746,7 +908,10 @@ fileprivate extension ShareModularViewController { func loadDefaultCategory(_ defaultCategoryID: NSNumber, from categories: [RemotePostCategory]) { if defaultCategoryID == Constants.unknownDefaultCategoryID { - self.shareData.setDefaultCategory(categoryID: defaultCategoryID, categoryName: Constants.unknownDefaultCategoryName) + self.shareData.setDefaultCategory( + categoryID: defaultCategoryID, + categoryName: Constants.unknownDefaultCategoryName + ) } else { let defaultCategoryArray = categories.filter { $0.categoryID == defaultCategoryID } if !defaultCategoryArray.isEmpty, let defaultCategory = defaultCategoryArray.first { @@ -758,28 +923,44 @@ fileprivate extension ShareModularViewController { func reloadSitesIfNeeded() { guard !hasSites else { + sitesState = .loaded sitesTableView.reloadData() - showEmptySitesIfNeeded() + updatePublishButtonStatus() + return + } + fetchSites() + } + + func fetchSites() { + guard !isFetchingSites else { return } + isFetchingSites = true + showSitesLoadingState() let networkService = AppExtensionsService() networkService.fetchSites(onSuccess: { blogs in DispatchQueue.main.async { + self.isFetchingSites = false self.sites = (blogs) ?? [RemoteBlog]() + self.sitesState = self.hasSites ? .loaded : .empty + self.refreshControl.endRefreshing() self.sitesTableView.reloadData() - self.showEmptySitesIfNeeded() - self.fetchCategoriesForSelectedSite() + if self.hasSites { + self.fetchCategoriesForSelectedSite() + } else { + self.refreshModulesTable(categoriesLoaded: true) + } } }) { DispatchQueue.main.async { + self.isFetchingSites = false self.sites = [RemoteBlog]() + self.sitesState = .failed + self.refreshControl.endRefreshing() self.sitesTableView.reloadData() - self.showEmptySitesIfNeeded() self.refreshModulesTable(categoriesLoaded: true) } } - - showLoadingView() } func savePostToRemoteSite() { @@ -829,15 +1010,18 @@ fileprivate extension ShareModularViewController { let service = AppExtensionsService() prepareForPublishing() - service.saveAndUploadPost(shareData: shareData, - siteID: siteID, - onComplete: { - self.dismiss() - }, onFailure: { - let error = self.createErrorWithDescription("Failed to save and upload post with no media.") - self.tracks.trackExtensionError(error) - self.showRetryAlert() - }) + service.saveAndUploadPost( + shareData: shareData, + siteID: siteID, + onComplete: { + self.dismiss() + }, + onFailure: { + let error = self.createErrorWithDescription("Failed to save and upload post with no media.") + self.tracks.trackExtensionError(error) + self.showRetryAlert() + } + ) } func uploadPostAndMedia(siteID: Int, localImageURLs: [URL]) { @@ -856,16 +1040,19 @@ fileprivate extension ShareModularViewController { } prepareForPublishing() - service.saveAndUploadPostWithMedia(shareData: shareData, - siteID: siteID, - localMediaFileURLs: localImageURLs, - requestEnqueued: { - self.dismiss() - }, onFailure: { - let error = self.createErrorWithDescription("Failed to save and upload post with media.") - self.tracks.trackExtensionError(error) - self.showRetryAlert() - }) + service.saveAndUploadPostWithMedia( + shareData: shareData, + siteID: siteID, + localMediaFileURLs: localImageURLs, + requestEnqueued: { + self.dismiss() + }, + onFailure: { + let error = self.createErrorWithDescription("Failed to save and upload post with media.") + self.tracks.trackExtensionError(error) + self.showRetryAlert() + } + ) } func showRetryAlert() { @@ -909,7 +1096,10 @@ fileprivate extension ShareModularViewController { func showPermissionsAlert() { let title = AppLocalizedString("Sharing Error", comment: "Share extension error dialog title.") - let message = AppLocalizedString("Your account does not have permission to upload media to this site. The Site Administrator can change these permissions.", comment: "Share extension error dialog text.") + let message = AppLocalizedString( + "Your account does not have permission to upload media to this site. The Site Administrator can change these permissions.", + comment: "Share extension error dialog text." + ) let dismiss = AppLocalizedString("Return to post", comment: "Share extension error dialog cancel button text") let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) @@ -965,36 +1155,105 @@ fileprivate extension ShareModularViewController { fileprivate extension ShareModularViewController { struct Constants { static let sitesReuseIdentifier = "sitesReuseIdentifier" + static let sitesStatusReuseIdentifier = "sitesStatusReuseIdentifier" static let modulesReuseIdentifier = String(describing: ShareModularViewController.self) static let siteRowHeight = CGFloat(74.0) static let defaultRowHeight = CGFloat(44.0) static let flashAnimationLength = 0.2 static let unknownDefaultCategoryID = NSNumber(value: -1) - static let unknownDefaultCategoryName = AppLocalizedString("Default", comment: "Placeholder text displayed in the share extension's summary view. It lets the user know the default category will be used on their post.") + static let unknownDefaultCategoryName = AppLocalizedString( + "Default", + comment: + "Placeholder text displayed in the share extension's summary view. It lets the user know the default category will be used on their post." + ) } struct SummaryText { - static let summaryPostPublishing = AppLocalizedString("Publish post on:", comment: "Text displayed in the share extension's summary view. It describes the publish post action.") - static let summaryDraftPostDefault = AppLocalizedString("Save draft post on:", comment: "Text displayed in the share extension's summary view that describes the save draft post action.") - static let summaryDraftPostSingular = AppLocalizedString("Save 1 photo as a draft post on:", comment: "Text displayed in the share extension's summary view that describes the action of saving a single photo in a draft post.") - static let summaryDraftPostPlural = AppLocalizedString("Save %ld photos as a draft post on:", comment: "Text displayed in the share extension's summary view that describes the action of saving multiple photos in a draft post.") - static let summaryPagePublishing = AppLocalizedString("Publish page on:", comment: "Text displayed in the share extension's summary view. It describes the publish page action.") - static let summaryDraftPageDefault = AppLocalizedString("Save draft page on:", comment: "Text displayed in the share extension's summary view that describes the save draft page action.") - static let summaryDraftPageSingular = AppLocalizedString("Save 1 photo as a draft page on:", comment: "Text displayed in the share extension's summary view that describes the action of saving a single photo in a draft page.") - static let summaryDraftPagePlural = AppLocalizedString("Save %ld photos as a draft page on:", comment: "Text displayed in the share extension's summary view that describes the action of saving multiple photos in a draft page.") + static let summaryPostPublishing = AppLocalizedString( + "Publish post on:", + comment: "Text displayed in the share extension's summary view. It describes the publish post action." + ) + static let summaryDraftPostDefault = AppLocalizedString( + "Save draft post on:", + comment: "Text displayed in the share extension's summary view that describes the save draft post action." + ) + static let summaryDraftPostSingular = AppLocalizedString( + "Save 1 photo as a draft post on:", + comment: + "Text displayed in the share extension's summary view that describes the action of saving a single photo in a draft post." + ) + static let summaryDraftPostPlural = AppLocalizedString( + "Save %ld photos as a draft post on:", + comment: + "Text displayed in the share extension's summary view that describes the action of saving multiple photos in a draft post." + ) + static let summaryPagePublishing = AppLocalizedString( + "Publish page on:", + comment: "Text displayed in the share extension's summary view. It describes the publish page action." + ) + static let summaryDraftPageDefault = AppLocalizedString( + "Save draft page on:", + comment: "Text displayed in the share extension's summary view that describes the save draft page action." + ) + static let summaryDraftPageSingular = AppLocalizedString( + "Save 1 photo as a draft page on:", + comment: + "Text displayed in the share extension's summary view that describes the action of saving a single photo in a draft page." + ) + static let summaryDraftPagePlural = AppLocalizedString( + "Save %ld photos as a draft page on:", + comment: + "Text displayed in the share extension's summary view that describes the action of saving multiple photos in a draft page." + ) } struct StatusText { - static let loadingTitle = AppLocalizedString("Fetching sites...", comment: "A short message to inform the user data for their sites are being fetched.") - static let publishingPostTitle = AppLocalizedString("Publishing post...", comment: "A short message that informs the user a post is being published to the server from the share extension.") - static let savingPostTitle = AppLocalizedString("Saving post…", comment: "A short message that informs the user a draft post is being saved to the server from the share extension.") - static let publishingPageTitle = AppLocalizedString("Publishing page...", comment: "A short message that informs the user a page is being published to the server from the share extension.") - static let savingPageTitle = AppLocalizedString("Saving page…", comment: "A short message that informs the user a draft page is being saved to the server from the share extension.") - static let cancellingTitle = AppLocalizedString("Canceling...", comment: "A short message that informs the user the share extension is being canceled.") - static let noSitesTitle = AppLocalizedString("No available sites", comment: "A short message that informs the user no sites could be loaded in the share extension.") + static let loadingTitle = AppLocalizedString( + "Fetching sites...", + comment: "A short message to inform the user data for their sites are being fetched." + ) + static let publishingPostTitle = AppLocalizedString( + "Publishing post...", + comment: + "A short message that informs the user a post is being published to the server from the share extension." + ) + static let savingPostTitle = AppLocalizedString( + "Saving post…", + comment: + "A short message that informs the user a draft post is being saved to the server from the share extension." + ) + static let publishingPageTitle = AppLocalizedString( + "Publishing page...", + comment: + "A short message that informs the user a page is being published to the server from the share extension." + ) + static let savingPageTitle = AppLocalizedString( + "Saving page…", + comment: + "A short message that informs the user a draft page is being saved to the server from the share extension." + ) + static let cancellingTitle = AppLocalizedString( + "Canceling...", + comment: "A short message that informs the user the share extension is being canceled." + ) + static let noSitesTitle = AppLocalizedString( + "No available sites", + comment: "A short message that informs the user their account has no available sites." + ) + static let sitesFailureTitle = AppLocalizedString( + "Couldn't load sites. Tap to retry.", + comment: "Error message shown when loading sites failed in the share extension." + ) } } +fileprivate enum SitesState { + case loading + case loaded + case empty + case failed +} + private enum Strings { static let typeCellTitle = AppLocalizedString("Type", comment: "Type menu item in share extension.") static let categoryCellTitle = AppLocalizedString("Category", comment: "Category menu item in share extension.")