diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index 883ced5602f8..5e128f7bf766 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -5,6 +5,8 @@ * [*] Make it easier to "Share" and "Blaze" a published post with an updated success view [##23128] * [*] Add support for viewing trashed posts and pages and restoring them from the editor [#23142] * [*] Impove the "Post Settings" screen groups/ordering to better align with Gutenberg [#23164] +* [*] Update the "More" menu in the Editor to use modern iOS design and update copy to match Gutenberg [#23145] +* [*] Update the "Revisions" list design and fix an issue with the footer displaying incorrect "Date Created" for drafts [#23145] 24.8 ----- diff --git a/WordPress/Classes/Models/AbstractPost+TitleForVisibility.swift b/WordPress/Classes/Models/AbstractPost+TitleForVisibility.swift index d2308bef2a56..e90449a4563a 100644 --- a/WordPress/Classes/Models/AbstractPost+TitleForVisibility.swift +++ b/WordPress/Classes/Models/AbstractPost+TitleForVisibility.swift @@ -6,9 +6,15 @@ extension AbstractPost { static let publicLabel = NSLocalizedString("Public", comment: "Privacy setting for posts set to 'Public' (default). Should be the same as in core WP.") /// A title describing the status. Ie.: "Public" or "Private" or "Password protected" - /// - /// - warning: deprecated (kahu-offline-mode) (use ``PostVisibility``) @objc var titleForVisibility: String { + guard FeatureFlag.syncPublishing.enabled else { + return _titleForVisibility + } + return PostVisibility(post: self).localizedTitle + } + + /// - warning: deprecated (kahu-offline-mode) (use ``PostVisibility``) + @objc private var _titleForVisibility: String { if password != nil { return AbstractPost.passwordProtectedLabel } else if status == .publishPrivate { diff --git a/WordPress/Classes/Services/PostCoordinator.swift b/WordPress/Classes/Services/PostCoordinator.swift index de08dcd4b403..2ea04cc4724e 100644 --- a/WordPress/Classes/Services/PostCoordinator.swift +++ b/WordPress/Classes/Services/PostCoordinator.swift @@ -452,6 +452,9 @@ class PostCoordinator: NSObject { retryDelay = min(32, retryDelay * 1.5) return retryDelay } + func setLongerDelay() { + retryDelay = max(retryDelay, 20) + } var retryDelay: TimeInterval weak var retryTimer: Timer? @@ -509,10 +512,11 @@ class PostCoordinator: NSObject { } private func startSync(for post: AbstractPost) { - guard let revision = post.getLatestRevisionNeedingSync() else { - let worker = getWorker(for: post) + if let worker = workers[post.objectID], worker.error != nil { worker.error = nil postDidUpdateNotification(for: post) + } + guard let revision = post.getLatestRevisionNeedingSync() else { return DDLogInfo("sync: \(post.objectID.shortDescription) is already up to date") } startSync(for: post, revision: revision) @@ -610,14 +614,16 @@ class PostCoordinator: NSObject { worker.error = error postDidUpdateNotification(for: operation.post) - if !PostCoordinator.isTerminalError(error) { - let delay = worker.nextRetryDelay - worker.retryTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self, weak worker] _ in - guard let self, let worker else { return } - self.didRetryTimerFire(for: worker) - } - worker.log("scheduled retry with delay: \(delay)s.") + if PostCoordinator.isTerminalError(error) { + worker.setLongerDelay() + } + + let delay = worker.nextRetryDelay + worker.retryTimer = Timer.scheduledTimer(withTimeInterval: delay, repeats: false) { [weak self, weak worker] _ in + guard let self, let worker else { return } + self.didRetryTimerFire(for: worker) } + worker.log("scheduled retry with delay: \(delay)s.") if let error = error as? PostRepository.PostSaveError, case .deleted = error { operation.log("post was permanently deleted") diff --git a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift index 091202eaf25f..7b15048bc74a 100644 --- a/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift +++ b/WordPress/Classes/Utility/Analytics/WPAnalyticsEvent.swift @@ -41,6 +41,7 @@ import Foundation case editorPostSlugChanged case editorPostExcerptChanged case editorPostSiteChanged + case editorPostLegacyMoreMenuShown // Resolve post version conflict case resolveConflictScreenShown @@ -666,6 +667,8 @@ import Foundation return "editor_post_excerpt_changed" case .editorPostSiteChanged: return "editor_post_site_changed" + case .editorPostLegacyMoreMenuShown: + return "editor_post_legacy_more_menu_shown" case .resolveConflictScreenShown: return "resolve_conflict_screen_shown" case .resolveConflictSaveTapped: diff --git a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift index 3d57500e61b3..86de50e099df 100644 --- a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift @@ -1197,7 +1197,7 @@ private extension AztecPostViewController { if (post.revisions ?? []).count > 0 { alert.addDefaultActionWithTitle(MoreSheetAlert.historyTitle) { [unowned self] _ in - self.displayHistory() + self.displayRevisionsList() } } diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+MoreActions.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+MoreActions.swift index c9f61cbbbfac..e8cf27fa7d1e 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+MoreActions.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController+MoreActions.swift @@ -11,7 +11,11 @@ extension GutenbergViewController { case managedObjectContextMissing = 2 } + // - warning: deprecated (kahu-offline-mode) + // TODO: Remove when/if confirmed that this is never invoked by Gutenberg. func displayMoreSheet() { + WPAnalytics.track(.editorPostLegacyMoreMenuShown) + // Dismisses and locks the Notices Store from displaying any new notices. ActionDispatcher.dispatch(NoticeAction.lock) let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) @@ -52,7 +56,7 @@ extension GutenbergViewController { if (post.revisions ?? []).count > 0 { alert.addDefaultActionWithTitle(MoreSheetAlert.historyTitle) { [weak self] _ in - self?.displayHistory() + self?.displayRevisionsList() ActionDispatcher.dispatch(NoticeAction.unlock) } } @@ -86,11 +90,68 @@ extension GutenbergViewController { present(alert, animated: true) } + + func makeMoreMenu() -> UIMenu { + UIMenu(title: "", image: nil, identifier: nil, options: [], children: [ + UIDeferredMenuElement.uncached { [weak self] in + $0(self?.makeMoreMenuSections() ?? []) + } + ]) + } + + private func makeMoreMenuSections() -> [UIMenuElement] { + var sections: [UIMenuElement] = [ + UIMenu(title: "", subtitle: "", options: .displayInline, children: makeMoreMenuActions()) + ] + if let string = makeContextStructureString() { + sections.append(UIAction(subtitle: string, attributes: [.disabled], handler: { _ in })) + } + return sections + } + + private func makeMoreMenuActions() -> [UIAction] { + var actions: [UIAction] = [] + + let toggleModeTitle = mode == .richText ? Strings.codeEditor : Strings.visualEditor + let toggleModeIconName = mode == .richText ? "curlybraces" : "doc.richtext" + actions.append(UIAction(title: toggleModeTitle, image: UIImage(systemName: toggleModeIconName)) { [weak self] _ in + self?.toggleEditingMode() + }) + + actions.append(UIAction(title: Strings.preview, image: UIImage(systemName: "safari")) { [weak self] _ in + self?.displayPreview() + }) + + let revisionCount = (post.revisions ?? []).count + if revisionCount > 0 { + actions.append(UIAction(title: Strings.revisions + " (\(revisionCount))", image: UIImage(systemName: "clock.arrow.circlepath")) { [weak self] _ in + self?.displayRevisionsList() + }) + } + + let settingsTitle = self.post is Page ? Strings.pageSettings : Strings.postSettings + actions.append(UIAction(title: settingsTitle, image: UIImage(systemName: "gearshape")) { [weak self] _ in + self?.displayPostSettings() + }) + let helpTitle = JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled() ? Strings.helpAndSupport : Strings.help + actions.append(UIAction(title: helpTitle, image: UIImage(systemName: "questionmark.circle")) { [weak self] _ in + self?.showEditorHelp() + }) + return actions + } + + private func makeContextStructureString() -> String? { + guard mode == .richText, let contentInfo = contentInfo else { + return nil + } + return String(format: Strings.contentStructure, contentInfo.blockCount, contentInfo.wordCount, contentInfo.characterCount) + } } // MARK: - Constants extension GutenbergViewController { + // - warning: deprecated (kahu-offline-mode) struct MoreSheetAlert { static let htmlTitle = NSLocalizedString("Switch to HTML Mode", comment: "Switches the Editor to HTML Mode") static let richTitle = NSLocalizedString("Switch to Visual Mode", comment: "Switches the Editor to Rich Text Mode") @@ -104,3 +165,15 @@ extension GutenbergViewController { static let editorHelpTitle = NSLocalizedString("Help", comment: "Open editor help options") } } + +private enum Strings { + static let codeEditor = NSLocalizedString("postEditor.moreMenu.codeEditor", value: "Code Editor", comment: "Post Editor / Button in the 'More' menu") + static let visualEditor = NSLocalizedString("postEditor.moreMenu.visualEditor", value: "Visual Editor", comment: "Post Editor / Button in the 'More' menu") + static let preview = NSLocalizedString("postEditor.moreMenu.preview", value: "Preview", comment: "Post Editor / Button in the 'More' menu") + static let revisions = NSLocalizedString("postEditor.moreMenu.revisions", value: "Revisions", comment: "Post Editor / Button in the 'More' menu") + static let pageSettings = NSLocalizedString("postEditor.moreMenu.pageSettings", value: "Page Settings", comment: "Post Editor / Button in the 'More' menu") + static let postSettings = NSLocalizedString("postEditor.moreMenu.postSettings", value: "Post Settings", comment: "Post Editor / Button in the 'More' menu") + static let helpAndSupport = NSLocalizedString("postEditor.moreMenu.helpAndSupport", value: "Help & Support", comment: "Post Editor / Button in the 'More' menu") + static let help = NSLocalizedString("postEditor.moreMenu.help", value: "Help", comment: "Post Editor / Button in the 'More' menu") + static let contentStructure = NSLocalizedString("postEditor.moreMenu.contentStructure", value: "Blocks: %li, Words: %li, Characters: %li", comment: "Post Editor / 'More' menu details labels with 'Blocks', 'Words' and 'Characters' counts as parameters (in that order)") +} diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift index da8ac1fb1f7e..16eb9741a50a 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergViewController.swift @@ -449,6 +449,15 @@ class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelega borderBottom.frame = CGRect(x: 0, y: navigationController?.navigationBar.frame.size.height ?? 0 - borderWidth, width: navigationController?.navigationBar.frame.size.width ?? 0, height: borderWidth) borderBottom.autoresizingMask = [.flexibleWidth, .flexibleTopMargin] navigationController?.navigationBar.addSubview(borderBottom) + + if FeatureFlag.syncPublishing.enabled { + navigationBarManager.moreButton.menu = makeMoreMenu() + navigationBarManager.moreButton.showsMenuAsPrimaryAction = true + } + } + + @objc private func buttonMoreTapped() { + displayMoreSheet() } private func reloadBlogIconView() { diff --git a/WordPress/Classes/ViewRelated/Pages/PageListSectionHeaderView.swift b/WordPress/Classes/ViewRelated/Pages/PageListSectionHeaderView.swift deleted file mode 100644 index 688e5a2d43ea..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/PageListSectionHeaderView.swift +++ /dev/null @@ -1,19 +0,0 @@ -import UIKit - -class PageListSectionHeaderView: UIView { - - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var separator: UIView! - - func setTitle(_ title: String) { - titleLabel.text = title.uppercased(with: .current) - } - - override func awakeFromNib() { - super.awakeFromNib() - - backgroundColor = .listBackground - titleLabel.backgroundColor = .listBackground - WPStyleGuide.applyBorderStyle(separator) - } -} diff --git a/WordPress/Classes/ViewRelated/Pages/PageListSectionHeaderView.xib b/WordPress/Classes/ViewRelated/Pages/PageListSectionHeaderView.xib deleted file mode 100644 index 2e5cbbcac673..000000000000 --- a/WordPress/Classes/ViewRelated/Pages/PageListSectionHeaderView.xib +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift b/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift index aaa942a40f23..17dd6717a655 100644 --- a/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/AbstractPostListViewController.swift @@ -758,7 +758,9 @@ class AbstractPostListViewController: UIViewController, SiteStatsInformation.sharedInstance.oauth2Token = blog.authToken SiteStatsInformation.sharedInstance.siteID = blog.dotComID - let postURL = URL(string: post.permaLink! as String) + guard let postURL = post.permaLink.flatMap(URL.init) else { + return wpAssertionFailure("permalink missing or invalid") + } let postStatsTableViewController = PostStatsTableViewController.withJPBannerForBlog(postID: postID, postTitle: post.titleForDisplay(), postURL: postURL) diff --git a/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift b/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift index ab5efecccb9a..08c88342ff85 100644 --- a/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift +++ b/WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift @@ -97,14 +97,10 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel { return .warning } switch post.status ?? .draft { - case .pending: - return .success - case .scheduled: - return .primary(.shade40) case .trash: return .error default: - return .neutral(.shade70) + return .secondaryLabel } } @@ -277,9 +273,18 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel { func statusAndBadges(separatedBy separator: String) -> String { let sticky = post.isStickyPost ? Constants.stickyLabel : "" let pending = (post.status == .pending && isSyncPublishingEnabled) ? Constants.pendingReview : "" + let visibility: String = { + let visibility = PostVisibility(post: post) + switch visibility { + case .public: + return "" + case .private, .protected: + return visibility.localizedTitle + } + }() let status = self.status ?? "" - return [status, pending, sticky].filter { !$0.isEmpty }.joined(separator: separator) + return [status, visibility, pending, sticky].filter { !$0.isEmpty }.joined(separator: separator) } /// Determine what the failed status message should be and return it. diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift b/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift index d4a93768833a..e0e83ebfeaa4 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor+MoreOptions.swift @@ -157,7 +157,7 @@ extension PostEditor { } } - func displayHistory() { + func displayRevisionsList() { guard FeatureFlag.syncPublishing.enabled else { _displayHistory() return @@ -174,7 +174,7 @@ extension PostEditor { self.post.mt_excerpt = revision.postExcerpt // It's important to clear the pending uploads associated with the -     // post. The assumption is that if the revision on the remote, + // post. The assumption is that if the revision on the remote, // its associated media has to be also uploaded. MediaCoordinator.shared.cancelUploadOfAllMedia(for: self.post) self.post.media = [] diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor.swift b/WordPress/Classes/ViewRelated/Post/PostEditor.swift index 0016190ac30e..f86694d5d20d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor.swift @@ -147,6 +147,7 @@ extension PostEditor where Self: UIViewController { showPostTrashedOverlay() } else { showAutosaveAvailableAlertIfNeeded() + showTerminalUploadErrorAlertIfNeeded() } var cancellables: [AnyCancellable] = [] @@ -273,6 +274,39 @@ extension PostEditor where Self: UIViewController { self.post = post // Even if it's the same instance, it's how you currently refresh the editor self.createRevisionOfPost() } + + // MARK: - Failed Media Uploads + + private func showTerminalUploadErrorAlertIfNeeded() { + let hasTerminalError = post.media.contains { + guard let error = $0.error else { return false } + return MediaCoordinator.isTerminalError(error) + } + if hasTerminalError { + let notice = Notice(title: Strings.failingMediaUploadsMessage, feedbackType: .error, actionTitle: Strings.failingMediaUploadsViewAction, actionHandler: { [weak self] _ in + self?.showMediaUploadDetails() + }) + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(700)) { + ActionDispatcherFacade().dispatch(NoticeAction.post(notice)) + } // Delay to let the editor show first + } + } + + private func showMediaUploadDetails() { + let viewController = PostMediaUploadsViewController(post: post) + let nav = UINavigationController(rootViewController: viewController) + nav.navigationBar.isTranslucent = true // Reset to default + viewController.navigationItem.leftBarButtonItem = UIBarButtonItem(systemItem: .close, primaryAction: UIAction { [weak self] _ in + self?.dismiss(animated: true) + }) + if let sheetController = nav.sheetPresentationController { + sheetController.detents = [.medium(), .large()] + sheetController.prefersGrabberVisible = true + sheetController.preferredCornerRadius = 16 + nav.additionalSafeAreaInsets = UIEdgeInsets(top: 8, left: 0, bottom: 0, right: 0) + } + self.present(nav, animated: true) + } } private var cancellablesKey: UInt8 = 0 @@ -305,4 +339,8 @@ private enum Strings { static let trashedPostSheetCancel = NSLocalizedString("postEditor.recoverTrashedPostAlert.cancel", value: "Cancel", comment: "Editor, alert for recovering a trashed post") static let trashedPostSheetRecover = NSLocalizedString("postEditor.recoverTrashedPostAlert.restore", value: "Restore", comment: "Editor, alert for recovering a trashed post") static let trashedPostRestored = NSLocalizedString("postEditor.recoverTrashedPost.postRecoveredNoticeTitle", value: "Post restored as a draft", comment: "Editor, notice for successful recovery a trashed post") + + static let failingMediaUploadsMessage = NSLocalizedString("postEditor.postHasFailingMediaUploadsSnackbar.message", value: "Some media items failed to upload", comment: "A message for a snackbar informing the user that some media files requires their attention") + + static let failingMediaUploadsViewAction = NSLocalizedString("postEditor.postHasFailingMediaUploadsSnackbar.actionView", value: "View", comment: "A 'View' action for a snackbar informing the user that some media files requires their attention") } diff --git a/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift b/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift index db9fe23d2a50..b3be9854585d 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditorNavigationBarManager.swift @@ -117,7 +117,7 @@ class PostEditorNavigationBarManager { return button }() - private lazy var moreButton: UIButton = { + lazy var moreButton: UIButton = { let image = UIImage(named: "editor-more") let button = UIButton(type: .system) button.setImage(image, for: .normal) diff --git a/WordPress/Classes/ViewRelated/Post/PostMediaUploadsView.swift b/WordPress/Classes/ViewRelated/Post/PostMediaUploadsView.swift index a9e4f4a306e3..22818819b471 100644 --- a/WordPress/Classes/ViewRelated/Post/PostMediaUploadsView.swift +++ b/WordPress/Classes/ViewRelated/Post/PostMediaUploadsView.swift @@ -1,6 +1,19 @@ import Foundation import SwiftUI +final class PostMediaUploadsViewController: UIHostingController { + private let viewModel: PostMediaUploadsViewModel + + init(post: AbstractPost) { + self.viewModel = PostMediaUploadsViewModel(post: post) // Manange lifecycle + super.init(rootView: PostMediaUploadsView(viewModel: viewModel)) + } + + required dynamic init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } +} + /// Displays upload progress for the media for the given post. struct PostMediaUploadsView: View { @ObservedObject var viewModel: PostMediaUploadsViewModel diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift index 93af5cf6eb91..336f8481866b 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController+Swift.swift @@ -2,6 +2,7 @@ import UIKit import CoreData import Combine import WordPressKit +import SwiftUI extension PostSettingsViewController { static func make(for post: AbstractPost) -> PostSettingsViewController { @@ -178,6 +179,47 @@ extension PostSettingsViewController: UIAdaptivePresentationControllerDelegate { } } +// MARK: - PostSettingsViewController (Visibility) + +extension PostSettingsViewController { + @objc func showUpdatedPostVisibilityPicker() { + let view = PostVisibilityPicker(selection: .init(post: apost)) { [weak self] selection in + guard let self else { return } + + WPAnalytics.track(.editorPostVisibilityChanged, properties: ["via": "settings"]) + + switch selection.type { + case .public, .protected: + if self.apost.original().status == .scheduled { + // Keep it scheduled + } else { + self.apost.status = .publish + } + case .private: + if self.apost.original().status == .scheduled { + DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(300)) { + self.showWarningPostWillBePublishedAlert() + } + } + self.apost.status = .publishPrivate + } + self.apost.password = selection.password.isEmpty ? nil : selection.password + self.navigationController?.popViewController(animated: true) + self.reloadData() + } + let viewController = UIHostingController(rootView: view) + viewController.title = PostVisibilityPicker.title + viewController.configureDefaultNavigationBarAppearance() + navigationController?.pushViewController(viewController, animated: true) + } + + private func showWarningPostWillBePublishedAlert() { + let alert = UIAlertController(title: nil, message: Strings.warningPostWillBePublishedAlertMessage, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("postSettings.ok", value: "OK", comment: "Button OK"), style: .default)) + present(alert, animated: true) + } +} + // MARK: - PostSettingsViewController (Page Attributes) extension PostSettingsViewController { @@ -239,4 +281,6 @@ extension PostSettingsViewController { private enum Strings { static let errorMessage = NSLocalizedString("postSettings.updateFailedMessage", value: "Failed to update the post settings", comment: "Error message on post/page settings screen") + + static let warningPostWillBePublishedAlertMessage = NSLocalizedString("postSettings.warningPostWillBePublishedAlertMessage", value: "By changing the visibility to 'Private', the post will be published immediately", comment: "An alert message explaning that by changing the visibility to private, the post will be published immediately to your site") } diff --git a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m index 5ae8245cdcb3..bf00ff9681b7 100644 --- a/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m +++ b/WordPress/Classes/ViewRelated/Post/PostSettingsViewController.m @@ -68,7 +68,6 @@ @interface PostSettingsViewController () Void static var title: String { Strings.title } - init(visibility: PostVisibility, onSubmit: @escaping (Selection) -> Void) { - self._selection = State(initialValue: visibility) + init(selection: Selection, onSubmit: @escaping (Selection) -> Void) { + self._selection = State(initialValue: selection) self.onSubmit = onSubmit } @@ -33,11 +37,13 @@ struct PostVisibilityPicker: View { private func makeRow(for visibility: PostVisibility) -> some View { Button(action: { withAnimation { + selection.type = visibility + selection.password = "" + if visibility == .protected { - isEnteringPassword = true + isPasswordFieldFocused = true } else { - selection = visibility - onSubmit(Selection(visibility: visibility, password: nil)) + onSubmit(selection) } } }, label: { @@ -47,52 +53,55 @@ struct PostVisibilityPicker: View { Text(visibility.localizedDetails) .font(.footnote) .foregroundStyle(.secondary) - .opacity(isEnteringPassword ? 0.5 : 1) + .opacity(visibility != .protected && isPasswordFieldFocused ? 0.4 : 1) } Spacer() Image(systemName: "checkmark") .tint(Color(uiColor: .primary)) - .opacity((selection == visibility && !isEnteringPassword) ? 1 : 0) + .opacity((selection.type == visibility && !isPasswordFieldFocused) ? 1 : 0) } }) .tint(.primary) - .disabled(isEnteringPassword && visibility != .protected) + .disabled(isPasswordFieldFocused && visibility != .protected) - if visibility == .protected, isEnteringPassword { + if visibility == .protected, selection.type == .protected { enterPasswordRows } } @ViewBuilder private var enterPasswordRows: some View { - PasswordField(password: $password) - .onSubmit(savePassword) + PasswordField(password: $selection.password, isFocused: isPasswordFieldFocused) + .focused($isPasswordFieldFocused) + .onSubmit(buttonSavePasswordTapped) - HStack { - Button(Strings.cancel) { - withAnimation { - password = "" - isEnteringPassword = false + if isPasswordFieldFocused { + HStack { + Button(Strings.cancel) { + withAnimation { + selection.type = .public + selection.password = "" + } } + .keyboardShortcut(.cancelAction) + Spacer() + Button(Strings.save, action: buttonSavePasswordTapped) + .font(.body.weight(.medium)) + .disabled(selection.password.trimmingCharacters(in: .whitespaces).isEmpty) } - .keyboardShortcut(.cancelAction) - Spacer() - Button(Strings.save, action: savePassword) - .font(.body.weight(.medium)) - .disabled(password.isEmpty) + .buttonStyle(.plain) + .foregroundStyle(Color(uiColor: .brand)) } - .buttonStyle(.plain) - .foregroundStyle(Color(uiColor: .brand)) } - private func savePassword() { + private func buttonSavePasswordTapped() { withAnimation { - selection = .protected - isEnteringPassword = false + isPasswordFieldFocused = false + selection.password = selection.password.trimmingCharacters(in: .whitespaces) isDismissing = true // Let the keyboard dismiss first to avoid janky animation DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(550)) { - onSubmit(Selection(visibility: .protected, password: password)) + onSubmit(selection) } } } @@ -101,13 +110,12 @@ struct PostVisibilityPicker: View { private struct PasswordField: View { @Binding var password: String @State var isSecure = true - @FocusState private var isFocused: Bool + let isFocused: Bool var body: some View { HStack { textField - .focused($isFocused) - if !password.isEmpty { + if isFocused && !password.isEmpty { Button(action: { password = "" }) { Image(systemName: "xmark.circle") .foregroundStyle(.secondary) @@ -119,8 +127,8 @@ private struct PasswordField: View { } } .buttonStyle(.plain) - .onAppear { isFocused = true } } + @ViewBuilder private var textField: some View { if isSecure { @@ -136,8 +144,12 @@ enum PostVisibility: Identifiable, CaseIterable { case `private` case protected + init(post: AbstractPost) { + self.init(status: post.status ?? .draft, password: post.password) + } + init(status: AbstractPost.Status, password: String?) { - if password != nil { + if let password, !password.isEmpty { self = .protected } else if status == .publishPrivate { self = .private @@ -163,7 +175,6 @@ enum PostVisibility: Identifiable, CaseIterable { case .private: NSLocalizedString("postVisibility.private.details", value: "Only visible to site admins and editors", comment: "Details for a 'Private' privacy setting") } } - } private enum Strings { diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index 4f4a1a37a0f9..93269aaebe3f 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -370,18 +370,17 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource // MARK: - Visibility private func configureVisibilityCell(_ cell: WPTableViewCell) { - cell.detailTextLabel?.text = viewModel.visibility.localizedTitle + cell.detailTextLabel?.text = viewModel.visibility.type.localizedTitle } private func didTapVisibilityCell() { - let view = PostVisibilityPicker(visibility: viewModel.visibility) { [weak self] selection in + let view = PostVisibilityPicker(selection: viewModel.visibility) { [weak self] selection in guard let self else { return } - self.viewModel.visibility = selection.visibility - if selection.visibility == .private { + self.viewModel.visibility = selection + if selection.type == .private { self.viewModel.publishDate = nil self.updatePublishButtonLabel() } - self.viewModel.password = selection.password self.reloadData() self.navigationController?.popViewController(animated: true) } @@ -401,7 +400,7 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource } else { cell.detailTextLabel?.text = Strings.immediately } - viewModel.visibility == .private ? cell.disable() : cell.enable() + viewModel.visibility.type == .private ? cell.disable() : cell.enable() } func didTapSchedule(_ indexPath: IndexPath) { @@ -543,8 +542,7 @@ extension PrepublishingOption { private final class PrepublishingViewModel { private let post: AbstractPost - var visibility: PostVisibility - var password: String? + var visibility: PostVisibilityPicker.Selection var publishDate: Date? var publishButtonTitle: String { @@ -557,8 +555,7 @@ private final class PrepublishingViewModel { init(post: AbstractPost) { self.post = post - self.visibility = PostVisibility(status: post.status ?? .draft, password: post.password) - self.password = post.password + self.visibility = .init(post: post) // Ask the user to provide the date every time (ignore the obscure WP dateCreated/dateModified logic) self.publishDate = nil } @@ -568,8 +565,8 @@ private final class PrepublishingViewModel { wpAssert(post.isRevision()) try await coordinator._publish(post.original(), options: .init( - visibility: visibility, - password: password, + visibility: visibility.type, + password: visibility.password, publishDate: publishDate )) } diff --git a/WordPress/Classes/ViewRelated/Post/Revisions/RevisionsTableViewController.swift b/WordPress/Classes/ViewRelated/Post/Revisions/RevisionsTableViewController.swift index c83410bf0356..c5e40da78f4c 100644 --- a/WordPress/Classes/ViewRelated/Post/Revisions/RevisionsTableViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Revisions/RevisionsTableViewController.swift @@ -1,3 +1,5 @@ +import UIKit + class RevisionsTableViewController: UITableViewController { typealias RevisionLoadedBlock = (AbstractPost) -> Void @@ -36,7 +38,7 @@ class RevisionsTableViewController: UITableViewController { required init(post: AbstractPost, onRevisionLoaded: @escaping RevisionLoadedBlock) { self.post = post self.onRevisionLoaded = onRevisionLoaded - super.init(nibName: nil, bundle: nil) + super.init(style: .insetGrouped) } required init?(coder aDecoder: NSCoder) { @@ -65,7 +67,7 @@ class RevisionsTableViewController: UITableViewController { private extension RevisionsTableViewController { private func setupUI() { - navigationItem.title = NSLocalizedString("History", comment: "Title of the post history screen") + navigationItem.title = Strings.title let cellNib = UINib(nibName: RevisionsTableViewCell.classNameWithoutNamespaces(), bundle: Bundle(for: RevisionsTableViewCell.self)) @@ -76,7 +78,9 @@ private extension RevisionsTableViewController { refreshControl.addTarget(self, action: #selector(refreshRevisions), for: .valueChanged) self.refreshControl = refreshControl - tableView.tableFooterView = tableViewFooter + if post?.original().isStatus(in: [.draft, .pending]) == false { + tableView.tableFooterView = tableViewFooter + } tableView.separatorColor = .divider WPStyleGuide.configureColors(view: view, tableView: tableView) @@ -113,7 +117,7 @@ private extension RevisionsTableViewController { @objc private func refreshRevisions() { if sectionCount == 0 { - configureAndDisplayNoResults(title: NoResultsText.loadingTitle, + configureAndDisplayNoResults(title: Strings.loading, accessoryView: NoResultsViewController.loadingAccessoryView()) } @@ -157,7 +161,7 @@ private extension RevisionsTableViewController { return } - SVProgressHUD.show(withStatus: NSLocalizedString("Loading...", comment: "Text displayed in HUD while a revision post is loading.")) + SVProgressHUD.show(withStatus: Strings.loading) let coreDataStack = ContextManager.shared let postRepository = PostRepository(coreDataStack: coreDataStack) @@ -229,16 +233,12 @@ extension RevisionsTableViewController: WPTableViewHandlerDelegate { return Sizes.cellEstimatedRowHeight } - override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { - guard let sectionInfo = tableViewHandler.resultsController?.sections?[section], - let headerView = Bundle.main.loadNibNamed(PageListSectionHeaderView.classNameWithoutNamespaces(), - owner: nil, - options: nil)?.first as? PageListSectionHeaderView else { - return UIView() + override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { + guard let sections = tableViewHandler.resultsController?.sections, + sections.indices.contains(section) else { + return nil } - - headerView.setTitle(sectionInfo.name) - return headerView + return sections[section].name } override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { @@ -287,8 +287,8 @@ extension RevisionsTableViewController: RevisionsView { case (true, let count) where count == 0: // When the API call successed but there are no revisions loaded // This is an edge cas. It shouldn't happen since we open the revisions list only if the post revisions array is not empty. - configureAndDisplayNoResults(title: NoResultsText.noResultsTitle, - subtitle: NoResultsText.noResultsSubtitle) + configureAndDisplayNoResults(title: Strings.noResultsTitle, + subtitle: Strings.noResultsSubtitle) default: hideNoResults() } @@ -311,7 +311,7 @@ private extension Date { private static let shortDateFormatter: DateFormatter = { let formatter = DateFormatter() - formatter.dateStyle = .short + formatter.dateStyle = .medium formatter.timeStyle = .none return formatter }() @@ -326,10 +326,14 @@ private extension Date { } struct NoResultsText { - static let loadingTitle = NSLocalizedString("Loading history...", comment: "Displayed while a call is loading the history.") static let reloadButtonTitle = NSLocalizedString("Try again", comment: "Re-load the history again. It appears if the loading call fails.") - static let noResultsTitle = NSLocalizedString("No history yet", comment: "Displayed when a call is made to load the revisions but there's no result or an error.") - static let noResultsSubtitle = NSLocalizedString("When you make changes in the editor you'll be able to see the history here", comment: "Displayed when a call is made to load the history but there's no result or an error.") static let errorTitle = NSLocalizedString("Oops", comment: "Title for the view when there's an error loading the history") static let errorSubtitle = NSLocalizedString("There was an error loading the history", comment: "Text displayed when there is a failure loading the history.") } + +private enum Strings { + static let title = NSLocalizedString("revisions.title", value: "Revisions", comment: "Post revisions list screen title") + static let loading = NSLocalizedString("revisions.loadingTitle", value: "Loading…", comment: "Post revisions list screen / loading view title") + static let noResultsTitle = NSLocalizedString("revisions.emptyStateTitle", value: "No revisions yet", comment: "Displayed when a call is made to load the revisions but there's no result or an error.") + static let noResultsSubtitle = NSLocalizedString("revisions.emptyStateSubtitle", value: "When you make changes in the editor you'll be able to see the revision history here", comment: "Displayed when a call is made to load the history but there's no result or an error.") +} diff --git a/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreator.swift b/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreator.swift index 677e7ebf0e9c..430916f4ae63 100644 --- a/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreator.swift +++ b/WordPress/Classes/ViewRelated/Site Creation/Wizard/SiteCreator.swift @@ -71,7 +71,7 @@ final class SiteCreator { // MARK: - Helper Extensions -extension String { +private extension String { var subdomain: String { return components(separatedBy: ".").first ?? "" } diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 05b37b6b7eac..586ae2a4ff51 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -1496,7 +1496,6 @@ 59ECF87B1CB7061D00E68F25 /* PostSharingControllerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59ECF87A1CB7061D00E68F25 /* PostSharingControllerTests.swift */; }; 59FBD5621B5684F300734466 /* ThemeServiceTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 59FBD5611B5684F300734466 /* ThemeServiceTests.m */; }; 5D1181E71B4D6DEB003F3084 /* WPStyleGuide+Reader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1181E61B4D6DEB003F3084 /* WPStyleGuide+Reader.swift */; }; - 5D13FA571AF99C2100F06492 /* PageListSectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5D13FA561AF99C2100F06492 /* PageListSectionHeaderView.xib */; }; 5D146EBB189857ED0068FDC6 /* FeaturedImageViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 5D146EBA189857ED0068FDC6 /* FeaturedImageViewController.m */; }; 5D1D04751B7A50B100CDE646 /* Reader.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 5D1D04731B7A50B100CDE646 /* Reader.storyboard */; }; 5D1D04761B7A50B100CDE646 /* ReaderStreamViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D1D04741B7A50B100CDE646 /* ReaderStreamViewController.swift */; }; @@ -2397,7 +2396,6 @@ 8B92D69627CD51FA001F5371 /* DashboardGhostCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B92D69527CD51FA001F5371 /* DashboardGhostCardCell.swift */; }; 8B92D69727CD51FA001F5371 /* DashboardGhostCardCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B92D69527CD51FA001F5371 /* DashboardGhostCardCell.swift */; }; 8B93412F257029F60097D0AC /* FilterChipButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B93412E257029F50097D0AC /* FilterChipButton.swift */; }; - 8B93856E22DC08060010BF02 /* PageListSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B93856D22DC08060010BF02 /* PageListSectionHeaderView.swift */; }; 8BA125EB27D8F5E4008B779F /* UIView+PinSubviewPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA125EA27D8F5E4008B779F /* UIView+PinSubviewPriority.swift */; }; 8BA125EC27D8F5E4008B779F /* UIView+PinSubviewPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8BA125EA27D8F5E4008B779F /* UIView+PinSubviewPriority.swift */; }; 8BA77BCB2482C52A00E1EBBF /* ReaderCardDiscoverAttributionView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8BA77BCA2482C52A00E1EBBF /* ReaderCardDiscoverAttributionView.xib */; }; @@ -4223,7 +4221,6 @@ FABB201D2602FC2C00C8785C /* PostStatsTitleCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98FCFC222231DF43006ECDD4 /* PostStatsTitleCell.xib */; }; FABB20212602FC2C00C8785C /* ReaderDetailToolbar.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8BA77BCC248340CE00E1EBBF /* ReaderDetailToolbar.xib */; }; FABB20222602FC2C00C8785C /* RewindStatusTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4070D75B20E5F55A007CEBDA /* RewindStatusTableViewCell.xib */; }; - FABB20232602FC2C00C8785C /* PageListSectionHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5D13FA561AF99C2100F06492 /* PageListSectionHeaderView.xib */; }; FABB20242602FC2C00C8785C /* JetpackScanThreatDetailsViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = FA7AA4A625BFE0A9005E7200 /* JetpackScanThreatDetailsViewController.xib */; }; FABB20252602FC2C00C8785C /* CollabsableHeaderFilterCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 469EB16424D8B12700C764CB /* CollabsableHeaderFilterCollectionViewCell.xib */; }; FABB20262602FC2C00C8785C /* ReplyTextView.xib in Resources */ = {isa = PBXBuildFile; fileRef = B54E1DEF1A0A7BAA00807537 /* ReplyTextView.xib */; }; @@ -4505,7 +4502,6 @@ FABB21982602FC2C00C8785C /* UIViewController+Notice.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3EFCA2208E308900268758 /* UIViewController+Notice.swift */; }; FABB21992602FC2C00C8785C /* GutenbergFileUploadProcessor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E0462152566938300EB98EF /* GutenbergFileUploadProcessor.swift */; }; FABB219A2602FC2C00C8785C /* EventLoggingDataProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F913BB0F24B3C5CE00C19032 /* EventLoggingDataProvider.swift */; }; - FABB219B2602FC2C00C8785C /* PageListSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B93856D22DC08060010BF02 /* PageListSectionHeaderView.swift */; }; FABB219C2602FC2C00C8785C /* InviteLinks+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = E690F6ED25E05D170015A777 /* InviteLinks+CoreDataClass.swift */; }; FABB219D2602FC2C00C8785C /* Page+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = 59E1D46D1CEF77B500126697 /* Page+CoreDataProperties.swift */; }; FABB219F2602FC2C00C8785C /* RegisterDomainSectionHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 43D74AD520FB5AD5004AD934 /* RegisterDomainSectionHeaderView.swift */; }; @@ -7202,7 +7198,6 @@ 59FBD5611B5684F300734466 /* ThemeServiceTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThemeServiceTests.m; sourceTree = ""; }; 5C1CEB34870A8BA1ED1E502B /* Pods-WordPressUITests.release-alpha.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressUITests.release-alpha.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressUITests/Pods-WordPressUITests.release-alpha.xcconfig"; sourceTree = ""; }; 5D1181E61B4D6DEB003F3084 /* WPStyleGuide+Reader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "WPStyleGuide+Reader.swift"; sourceTree = ""; }; - 5D13FA561AF99C2100F06492 /* PageListSectionHeaderView.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = PageListSectionHeaderView.xib; sourceTree = ""; }; 5D146EB9189857ED0068FDC6 /* FeaturedImageViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = FeaturedImageViewController.h; sourceTree = ""; usesTabs = 0; }; 5D146EBA189857ED0068FDC6 /* FeaturedImageViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FeaturedImageViewController.m; sourceTree = ""; usesTabs = 0; }; 5D1D04731B7A50B100CDE646 /* Reader.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = Reader.storyboard; sourceTree = ""; }; @@ -7801,7 +7796,6 @@ 8B8FE8562343952B00F9AD2E /* PostAutoUploadMessages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostAutoUploadMessages.swift; sourceTree = ""; }; 8B92D69527CD51FA001F5371 /* DashboardGhostCardCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardGhostCardCell.swift; sourceTree = ""; }; 8B93412E257029F50097D0AC /* FilterChipButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilterChipButton.swift; sourceTree = ""; }; - 8B93856D22DC08060010BF02 /* PageListSectionHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PageListSectionHeaderView.swift; sourceTree = ""; }; 8B9E15DAF3E1A369E9BE3407 /* Pods-WordPressUITests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-WordPressUITests.release.xcconfig"; path = "../Pods/Target Support Files/Pods-WordPressUITests/Pods-WordPressUITests.release.xcconfig"; sourceTree = ""; }; 8BA125EA27D8F5E4008B779F /* UIView+PinSubviewPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIView+PinSubviewPriority.swift"; sourceTree = ""; }; 8BA77BCA2482C52A00E1EBBF /* ReaderCardDiscoverAttributionView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ReaderCardDiscoverAttributionView.xib; sourceTree = ""; }; @@ -12987,8 +12981,6 @@ children = ( 0C700B852AE1E1300085C2EE /* PageListCell.swift */, 0C700B882AE1E1940085C2EE /* PageListItemViewModel.swift */, - 8B93856D22DC08060010BF02 /* PageListSectionHeaderView.swift */, - 5D13FA561AF99C2100F06492 /* PageListSectionHeaderView.xib */, 834A49D12A0C23A90042ED3D /* TemplatePageTableViewCell.swift */, ); name = Views; @@ -19724,7 +19716,6 @@ FE015BB12ADA002400F50D7F /* ReaderTopicsNewCardCell.xib in Resources */, 8BA77BCD248340CE00E1EBBF /* ReaderDetailToolbar.xib in Resources */, 4070D75C20E5F55A007CEBDA /* RewindStatusTableViewCell.xib in Resources */, - 5D13FA571AF99C2100F06492 /* PageListSectionHeaderView.xib in Resources */, 1761F18D26209AEE000815EF /* hot-pink-icon-app-76x76.png in Resources */, FA7AA4A725BFE0A9005E7200 /* JetpackScanThreatDetailsViewController.xib in Resources */, 469EB16624D8B12700C764CB /* CollabsableHeaderFilterCollectionViewCell.xib in Resources */, @@ -20261,7 +20252,6 @@ F46597FD28E66A1100D5F49A /* white-on-black-icon-app-60@2x.png in Resources */, FABB20212602FC2C00C8785C /* ReaderDetailToolbar.xib in Resources */, FABB20222602FC2C00C8785C /* RewindStatusTableViewCell.xib in Resources */, - FABB20232602FC2C00C8785C /* PageListSectionHeaderView.xib in Resources */, 8091019629078CFE00FCB4EA /* JetpackFullscreenOverlayViewController.xib in Resources */, 98DCF4A8275945E00008630F /* ReaderDetailNoCommentCell.xib in Resources */, FABB20242602FC2C00C8785C /* JetpackScanThreatDetailsViewController.xib in Resources */, @@ -21779,7 +21769,6 @@ C3B554512965C32A00A04753 /* MenusViewController+JetpackBannerViewController.swift in Sources */, 1E0462162566938300EB98EF /* GutenbergFileUploadProcessor.swift in Sources */, F913BB1024B3C5CE00C19032 /* EventLoggingDataProvider.swift in Sources */, - 8B93856E22DC08060010BF02 /* PageListSectionHeaderView.swift in Sources */, E690F6EF25E05D180015A777 /* InviteLinks+CoreDataClass.swift in Sources */, 59E1D46F1CEF77B500126697 /* Page+CoreDataProperties.swift in Sources */, C373D6E728045281008F8C26 /* SiteIntentData.swift in Sources */, @@ -24688,7 +24677,6 @@ F4CBE3D7292597E3004FFBB6 /* SupportTableViewControllerConfiguration.swift in Sources */, FABB21992602FC2C00C8785C /* GutenbergFileUploadProcessor.swift in Sources */, FABB219A2602FC2C00C8785C /* EventLoggingDataProvider.swift in Sources */, - FABB219B2602FC2C00C8785C /* PageListSectionHeaderView.swift in Sources */, FABB219C2602FC2C00C8785C /* InviteLinks+CoreDataClass.swift in Sources */, 809101992908DE8500FCB4EA /* JetpackFullscreenOverlayViewModel.swift in Sources */, FABB219D2602FC2C00C8785C /* Page+CoreDataProperties.swift in Sources */,