Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for viewing trashed posts #23142

Merged
merged 5 commits into from
May 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* [*] Add "Parent Page" to "Page Settings" [#23136]
* [*] Add "Slug" and "Excerpt" fields to "Page Settings" [#23135]
* [*] 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]

24.8
-----
Expand Down
9 changes: 9 additions & 0 deletions WordPress/Classes/Models/AbstractPost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@ extension AbstractPost {
}
}

var localizedPostType: String {
switch self {
case is Page:
return NSLocalizedString("postType.page", value: "page", comment: "Localized post type: `Page`")
default:
return NSLocalizedString("postType.post", value: "post", comment: "Localized post type: `Post`")
}
}

// MARK: - Misc

/// Represent the supported properties used to sort posts.
Expand Down
12 changes: 11 additions & 1 deletion WordPress/Classes/Services/PostCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ class PostCoordinator: NSObject {
}
}

private func handleError(_ error: Error, for post: AbstractPost) {
func handleError(_ error: Error, for post: AbstractPost) {
guard let topViewController = UIApplication.shared.mainWindow?.topmostPresentedViewController else {
wpAssertionFailure("Failed to show an error alert")
return
Expand Down Expand Up @@ -332,6 +332,16 @@ class PostCoordinator: NSObject {
save(post)
}

/// Restores a trashed post by moving it to draft.
@MainActor
func restore(_ post: AbstractPost) async throws {
wpAssert(post.isOriginal())

var changes = RemotePostUpdateParameters()
changes.status = Post.Status.draft.rawValue
try await _update(post, changes: changes)
}

/// Sets the post state to "updating" and performs the given changes.
private func _performChanges(_ changes: RemotePostUpdateParameters, for post: AbstractPost) {
Task { @MainActor in
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -740,7 +740,6 @@ class AztecPostViewController: UIViewController, PostEditor {
navigationController?.navigationBar.isTranslucent = false
navigationController?.navigationBar.accessibilityIdentifier = "Azctec Editor Navigation Bar"
navigationItem.leftBarButtonItems = navigationBarManager.leftBarButtonItems
navigationItem.rightBarButtonItems = navigationBarManager.rightBarButtonItemsAztec
navigationItem.titleView = navigationBarManager.blogTitleViewLabel
}

Expand Down Expand Up @@ -823,6 +822,7 @@ class AztecPostViewController: UIViewController, PostEditor {
reloadEditorContents()
reloadPublishButton()
refreshTitleViewForMediaUploadIfNeeded()
navigationItem.rightBarButtonItems = post.status == .trash ? [] : navigationBarManager.rightBarButtonItemsAztec
}

func refreshTitleViewForMediaUploadIfNeeded() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -438,7 +438,6 @@ class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelega
navigationController?.navigationBar.isTranslucent = false
navigationController?.navigationBar.accessibilityIdentifier = "Gutenberg Editor Navigation Bar"
navigationItem.leftBarButtonItems = navigationBarManager.leftBarButtonItems
navigationItem.rightBarButtonItems = navigationBarManager.rightBarButtonItems

// Add bottom border line
let screenScale = UIScreen.main.scale
Expand Down Expand Up @@ -481,6 +480,7 @@ class GutenbergViewController: UIViewController, PostEditor, FeaturedImageDelega
reloadBlogIconView()
reloadEditorContents()
reloadPublishButton()
navigationItem.rightBarButtonItems = post.status == .trash ? [] : navigationBarManager.rightBarButtonItems
}

func toggleEditingMode() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ struct PageEditorPresenter {
return false
}

guard page.status != .trash else {
if page.status == .trash && !FeatureFlag.syncPublishing.enabled {
// No editing posts that are trashed.
return false
}

Expand Down
6 changes: 5 additions & 1 deletion WordPress/Classes/ViewRelated/Post/PostEditor+Publish.swift
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,10 @@ extension PublishingEditor {
return _discardChanges()
}

guard post.status != .trash else {
return true // No revision is created for trashed posts
}

guard let context = post.managedObjectContext else {
wpAssertionFailure("Missing managedObjectContext")
return true
Expand Down Expand Up @@ -724,7 +728,7 @@ extension PublishingEditor {

wpAssert(post.latest() == post, "Must be opened with the latest verison of the post")

if !post.isUnsavedRevision {
if !post.isUnsavedRevision && post.status != .trash {
DDLogDebug("Creating new revision")
post = post._createRevision()
}
Expand Down
69 changes: 66 additions & 3 deletions WordPress/Classes/ViewRelated/Post/PostEditor.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import UIKit
import Combine
import WordPressFlux

enum EditMode {
case richText
Expand Down Expand Up @@ -141,7 +142,12 @@ extension PostEditor where Self: UIViewController {
guard FeatureFlag.syncPublishing.enabled else {
return
}
showAutosaveAvailableAlertIfNeeded()

if post.original().status == .trash {
showPostTrashedOverlay()
} else {
showAutosaveAvailableAlertIfNeeded()
}

var cancellables: [AnyCancellable] = []

Expand All @@ -160,6 +166,8 @@ extension PostEditor where Self: UIViewController {
objc_setAssociatedObject(self, &cancellablesKey, cancellables, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
}

// MARK: - Autosave

private func showAutosaveAvailableAlertIfNeeded() {
// The revision has unsaved local changes, takes precedence over autosave
guard post.changes.isEmpty else {
Expand All @@ -182,6 +190,8 @@ extension PostEditor where Self: UIViewController {
present(alert, animated: true)
}

// MARK: - App Termination

private func appWillTerminate() {
guard let context = post.managedObjectContext else {
return
Expand All @@ -206,15 +216,62 @@ extension PostEditor where Self: UIViewController {
}
}

// MARK: - Conflict Resolution

private func postConflictResolved(_ notification: Foundation.Notification) {
guard
let userInfo = notification.userInfo,
let post = userInfo[PostCoordinator.NotificationKey.postConflictResolved] as? AbstractPost
else {
return
}
self.post = post
createRevisionOfPost()
self.configureWithUpdatedPost(post)
}

// MARK: - Restore Trashed Post

private func showPostTrashedOverlay() {
let overlay = PostTrashedOverlayView()
view.addSubview(overlay)
overlay.translatesAutoresizingMaskIntoConstraints = false
overlay.onOverlayTapped = { [weak self] in self?.showRestorePostAlert(with: $0) }
view.pinSubviewToAllEdges(overlay)
}

private func showRestorePostAlert(with overlay: PostTrashedOverlayView) {
overlay.isUserInteractionEnabled = false

let postType = post.localizedPostType.lowercased()
let alert = UIAlertController(title: String(format: Strings.trashedPostSheetTitle, postType), message: String(format: Strings.trashedPostSheetMessage, postType), preferredStyle: .alert)
alert.addAction(UIAlertAction(title: Strings.trashedPostSheetCancel, style: .cancel) { _ in
overlay.isUserInteractionEnabled = true
})
alert.addAction(UIAlertAction(title: Strings.trashedPostSheetRecover, style: .default) { [weak self] _ in
guard let self else { return }
Task {
await self.restorePostFromTrash()
}
})
present(alert, animated: true)
}

@MainActor
private func restorePostFromTrash() async {
SVProgressHUD.show()
defer { SVProgressHUD.dismiss() }
let coordinator = PostCoordinator.shared
do {
try await coordinator.restore(post)
ActionDispatcher.dispatch(NoticeAction.post(Notice(title: Strings.trashedPostRestored)))
self.configureWithUpdatedPost(post)
} catch {
coordinator.handleError(error, for: post)
}
}

private func configureWithUpdatedPost(_ post: AbstractPost) {
self.post = post // Even if it's the same instance, it's how you currently refresh the editor
self.createRevisionOfPost()
}
}

Expand Down Expand Up @@ -242,4 +299,10 @@ private enum Strings {

static let autosaveAlertContinue = NSLocalizedString("autosaveAlert.viewChanges", value: "View Changes", comment: "An alert suggesting to load autosaved revision for a published post")
static let autosaveAlertCancel = NSLocalizedString("autosaveAlert.cancel", value: "Cancel", comment: "An alert suggesting to load autosaved revision for a published post")

static let trashedPostSheetTitle = NSLocalizedString("postEditor.recoverTrashedPostAlert.title", value: "Trashed %@", comment: "Editor, alert for recovering a trashed post")
static let trashedPostSheetMessage = NSLocalizedString("postEditor.recoverTrashedPostAlert.message", value: "A trashed %1$@ can't be edited. To edit this %1$@, you'll need to restore it by moving it back to a draft.", comment: "Editor, alert for recovering a trashed post")
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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP

let post = postAtIndexPath(indexPath)

guard post.status != .trash else {
if post.status == .trash && !FeatureFlag.syncPublishing.enabled {
// No editing posts that are trashed.
return
}
Expand Down
18 changes: 18 additions & 0 deletions WordPress/Classes/ViewRelated/Post/PostTrashedOverlayView.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import UIKit

final class PostTrashedOverlayView: UIView {
var onOverlayTapped: ((PostTrashedOverlayView) -> Void)?

override init(frame: CGRect) {
super.init(frame: frame)
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}

override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
onOverlayTapped?(self)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ final class PostSearchViewController: UIViewController, UITableViewDelegate, UIS

switch viewModel.posts[indexPath.row].latest() {
case let post as Post:
guard post.status != .trash else { return }
if post.status == .trash && !FeatureFlag.syncPublishing.enabled {
// No editing posts that are trashed.
return
}
delegate?.edit(post)
case let page as Page:
guard page.status != .trash else { return }
Expand Down
7 changes: 4 additions & 3 deletions WordPress/UITests/Tests/PagesTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ class PageTests: XCTestCase {
.enterTextInTitle(text: postTitle, postType: .page)
.post(action: .publish, postType: .page)

try MySiteScreen()
.scrollToPagesCard()
.verifyPagePublished(title: postTitle)
// TODO: reimplement this part of the test (flaky)
// try MySiteScreen()
// .scrollToPagesCard()
// .verifyPagePublished(title: postTitle)
}
}
6 changes: 6 additions & 0 deletions WordPress/WordPress.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,8 @@
0A9687BC28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */; };
0C01A6EA2AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift */; };
0C01A6EB2AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift */; };
0C02E6C52BE3B6E30055F0F6 /* PostTrashedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C02E6C42BE3B6E30055F0F6 /* PostTrashedOverlayView.swift */; };
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] rename to TrashedPostOverlayView.swift to match class name

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I renamed the class itself to PostTrashedOverlayView – using Post as a namescape.

0C02E6C62BE3B6E30055F0F6 /* PostTrashedOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C02E6C42BE3B6E30055F0F6 /* PostTrashedOverlayView.swift */; };
0C03AECA2B7D995F00B64A25 /* PublishButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03AEC92B7D995F00B64A25 /* PublishButton.swift */; };
0C03AECB2B7D995F00B64A25 /* PublishButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C03AEC92B7D995F00B64A25 /* PublishButton.swift */; };
0C0453282AC73343003079C8 /* SiteMediaVideoDurationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */; };
Expand Down Expand Up @@ -6290,6 +6292,7 @@
0A9610F828B2E56300076EBA /* UserSuggestion+Comparable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UserSuggestion+Comparable.swift"; sourceTree = "<group>"; };
0A9687BB28B40771009DCD2F /* FullScreenCommentReplyViewModelMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullScreenCommentReplyViewModelMock.swift; sourceTree = "<group>"; };
0C01A6E92AB37F0F009F7145 /* SiteMediaCollectionCellSelectionOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaCollectionCellSelectionOverlayView.swift; sourceTree = "<group>"; };
0C02E6C42BE3B6E30055F0F6 /* PostTrashedOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostTrashedOverlayView.swift; sourceTree = "<group>"; };
0C03AEC92B7D995F00B64A25 /* PublishButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublishButton.swift; sourceTree = "<group>"; };
0C0453272AC73343003079C8 /* SiteMediaVideoDurationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaVideoDurationView.swift; sourceTree = "<group>"; };
0C04532A2AC77245003079C8 /* SiteMediaDocumentInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteMediaDocumentInfoView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -15270,6 +15273,7 @@
8B260D7D2444FC9D0010F756 /* PostVisibilitySelectorViewController.swift */,
593F26601CAB00CA00F14073 /* PostSharingController.swift */,
E155EC711E9B7DCE009D7F63 /* PostTagPickerViewController.swift */,
0C02E6C42BE3B6E30055F0F6 /* PostTrashedOverlayView.swift */,
);
path = Post;
sourceTree = "<group>";
Expand Down Expand Up @@ -22865,6 +22869,7 @@
FAB8004925AEDC2300D5D54A /* JetpackBackupCompleteViewController.swift in Sources */,
9A8ECE0C2254A3260043C8DA /* JetpackLoginViewController.swift in Sources */,
46F583A92624CE790010A723 /* BlockEditorSettings+CoreDataClass.swift in Sources */,
0C02E6C52BE3B6E30055F0F6 /* PostTrashedOverlayView.swift in Sources */,
B50C0C641EF42B3A00372C65 /* Header+WordPress.swift in Sources */,
FAA5C90F2B7FC74C002E073B /* PostSyncStateViewModel.swift in Sources */,
F504D2B125D60C5900A2764C /* StoryMediaLoader.swift in Sources */,
Expand Down Expand Up @@ -25047,6 +25052,7 @@
FABB22A72602FC2C00C8785C /* BlogListViewController+Activity.swift in Sources */,
3F810A5B2616870C00ADDCC2 /* UnifiedPrologueIntroContentView.swift in Sources */,
FABB22A82602FC2C00C8785C /* TableDataCoordinator.swift in Sources */,
0C02E6C62BE3B6E30055F0F6 /* PostTrashedOverlayView.swift in Sources */,
FABB22A92602FC2C00C8785C /* SettingsMultiTextViewController.m in Sources */,
FABB22AA2602FC2C00C8785C /* FilterSheetViewController.swift in Sources */,
FABB22AB2602FC2C00C8785C /* Plan.swift in Sources */,
Expand Down