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

Offline Mode: Trash, Delete, Restore #22947

Merged
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
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ end

def wordpress_kit
# pod 'WordPressKit', '~> 15.0'
pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: '8b61680b934ca1bebdbe7f2abb091ff22d08fedf'
pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', commit: 'ebaf7d97503a47bde2917d464ac9914c626b1c50'
# pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', branch: ''
# pod 'WordPressKit', git: 'https://github.com/wordpress-mobile/WordPressKit-iOS.git', tag: ''
# pod 'WordPressKit', path: '../WordPressKit-iOS'
Expand Down
8 changes: 4 additions & 4 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ DEPENDENCIES:
- SwiftLint (= 0.54.0)
- WordPress-Editor-iOS (~> 1.19.11)
- WordPressAuthenticator (>= 9.0.2, ~> 9.0)
- WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `8b61680b934ca1bebdbe7f2abb091ff22d08fedf`)
- WordPressKit (from `https://github.com/wordpress-mobile/WordPressKit-iOS.git`, commit `ebaf7d97503a47bde2917d464ac9914c626b1c50`)
- WordPressShared (>= 2.3.1, ~> 2.3)
- WordPressUI (~> 1.15)
- ZendeskSupportSDK (= 5.3.0)
Expand Down Expand Up @@ -175,15 +175,15 @@ EXTERNAL SOURCES:
Gutenberg:
:podspec: https://cdn.a8c-ci.services/gutenberg-mobile/Gutenberg-v1.116.0.podspec
WordPressKit:
:commit: 8b61680b934ca1bebdbe7f2abb091ff22d08fedf
:commit: ebaf7d97503a47bde2917d464ac9914c626b1c50
:git: https://github.com/wordpress-mobile/WordPressKit-iOS.git

CHECKOUT OPTIONS:
FSInteractiveMap:
:git: https://github.com/wordpress-mobile/FSInteractiveMap.git
:tag: 0.2.0
WordPressKit:
:commit: 8b61680b934ca1bebdbe7f2abb091ff22d08fedf
:commit: ebaf7d97503a47bde2917d464ac9914c626b1c50
:git: https://github.com/wordpress-mobile/WordPressKit-iOS.git

SPEC CHECKSUMS:
Expand Down Expand Up @@ -230,6 +230,6 @@ SPEC CHECKSUMS:
ZendeskSupportSDK: 3a8e508ab1d9dd22dc038df6c694466414e037ba
ZIPFoundation: fa9ae5af13b7cf168245f24d1c672a4fb972e37f

PODFILE CHECKSUM: c86dcd99525fbd017b651da9402b873c51429a52
PODFILE CHECKSUM: e408b4e6076ddd1bb2f1455312d73afad1eebf11

COCOAPODS: 1.15.2
8 changes: 7 additions & 1 deletion WordPress/Classes/Models/AbstractPost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,6 @@ extension AbstractPost {
current = next
}
return revisions

}

// TODO: Replace with a new flag
Expand Down Expand Up @@ -225,4 +224,11 @@ extension AbstractPost {
didChangeValue(forKey: "revision")
}
}

func deleteAllRevisions() {
assert(isOriginal())
for revision in allRevisions {
revision.deleteRevision()
}
}
}
2 changes: 2 additions & 0 deletions WordPress/Classes/Models/Post+RefreshStatus.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ extension Post {
/// - Parameters:
/// - onCompletion: block to invoke when status update is finished.
/// - onError: block to invoke if any error occurs while the update is being made.
///
/// - warning: deprecated (kahu-offline-mode)
static func refreshStatus(with coreDataStack: CoreDataStack) {
coreDataStack.performAndSave { context in
let fetch = NSFetchRequest<Post>(entityName: Post.classNameWithoutNamespaces())
Expand Down
62 changes: 60 additions & 2 deletions WordPress/Classes/Services/PostCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -215,11 +215,16 @@ class PostCoordinator: NSObject {
}
}

/// Patches the post but keeps the local revision if any.
/// Patches the post.
///
/// - warning: Can only be used on posts with no unsynced revisions.
///
/// - warning: Work-in-progress (kahu-offline-mode)
@MainActor
func _update(_ post: AbstractPost, changes: RemotePostUpdateParameters) async throws {
assert(post.isOriginal())
assert(post.revision == nil, "Can only be used on posts with no unsynced changes")

let post = post.original()
do {
try await PostRepository(coreDataStack: coreDataStack)._update(post, changes: changes)
Expand Down Expand Up @@ -991,7 +996,60 @@ class PostCoordinator: NSObject {
])
}

// MARK: - Trash/Delete
// MARK: - Trash/Restore/Delete

/// Moves the given post to trash.
@MainActor
func trash(_ post: AbstractPost) async throws {
assert(post.isOriginal())

setUpdating(true, for: post)
defer { setUpdating(false, for: post) }

await pauseSyncing(for: post)
defer { resumeSyncing(for: post) }

let context = coreDataStack.mainContext

guard post.hasRemote() else {
// Delete all the local data
context.deleteObject(post)
ContextManager.shared.saveContextAndWait(context)
return
}

// Delete local revisions
post.deleteAllRevisions()
ContextManager.shared.saveContextAndWait(context)

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

SearchManager.shared.deleteSearchableItem(post)
}

@MainActor
func _delete(_ post: AbstractPost) async throws {
assert(post.isOriginal())

setUpdating(true, for: post)
defer { setUpdating(false, for: post) }

do {
try await PostRepository(coreDataStack: coreDataStack)._delete(post)

MediaCoordinator.shared.cancelUploadOfAllMedia(for: post)
SearchManager.shared.deleteSearchableItem(post)
} catch {
if let error = error as? PostServiceRemoteUpdatePostError, case .notFound = error {
handlePermanentlyDeleted(post)
} else {
handleError(error, for: post)
}
throw error
}
}

func isDeleting(_ post: AbstractPost) -> Bool {
pendingDeletionPostIDs.contains(post.objectID)
Expand Down
7 changes: 6 additions & 1 deletion WordPress/Classes/Services/PostHelper.m
Original file line number Diff line number Diff line change
Expand Up @@ -254,7 +254,12 @@ + (NSArray *)mergePosts:(NSArray <RemotePost *> *)remotePosts

if (purge) {
// Set up predicate for fetching any posts that could be purged for the sync.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(remoteStatusNumber = %@) AND (postID != NULL) AND (original = NULL) AND (revision = NULL) AND (blog = %@)", @(AbstractPostRemoteStatusSync), blog];
NSPredicate *predicate = nil;
if ([RemoteFeature enabled:RemoteFeatureFlagSyncPublishing]) {
predicate = [NSPredicate predicateWithFormat:@"(postID != NULL) AND (original = NULL) AND (revision = NULL) AND (blog = %@)", blog];
} else {
predicate = [NSPredicate predicateWithFormat:@"(remoteStatusNumber = %@) AND (postID != NULL) AND (original = NULL) AND (revision = NULL) AND (blog = %@)", @(AbstractPostRemoteStatusSync), blog];
}
if ([statuses count] > 0) {
NSPredicate *statusPredicate = [NSPredicate predicateWithFormat:@"status IN %@", statuses];
predicate = [NSCompoundPredicate andPredicateWithSubpredicates:@[predicate, statusPredicate]];
Expand Down
81 changes: 23 additions & 58 deletions WordPress/Classes/Services/PostRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,14 @@ final class PostRepository {

private let coreDataStack: CoreDataStackSwift
private let remoteFactory: PostServiceRemoteFactory
private let isSyncPublishingEnabled: Bool

init(coreDataStack: CoreDataStackSwift = ContextManager.shared,
remoteFactory: PostServiceRemoteFactory = PostServiceRemoteFactory()) {
remoteFactory: PostServiceRemoteFactory = PostServiceRemoteFactory(),
isSyncPublishingEnabled: Bool = RemoteFeatureFlag.syncPublishing.enabled()) {
self.coreDataStack = coreDataStack
self.remoteFactory = remoteFactory
self.isSyncPublishingEnabled = isSyncPublishingEnabled
}

/// Sync a specific post from the API
Expand Down Expand Up @@ -237,6 +240,22 @@ final class PostRepository {
ContextManager.shared.saveContextAndWait(context)
}

/// Permanently delete the given post.
@MainActor
func _delete(_ post: AbstractPost) async throws {
assert(post.isOriginal())

guard let postID = post.postID, postID.intValue > 0 else {
assertionFailure("Trying to patch a non-existent post")
return
}
try await getRemoteService(for: post.blog).deletePost(withID: postID.intValue)

let context = coreDataStack.mainContext
context.deleteObject(post)
ContextManager.shared.saveContextAndWait(context)
}

/// Permanently delete the given post from local database and the post's WordPress site.
///
/// - Parameter postID: Object ID of the post
Expand Down Expand Up @@ -283,6 +302,8 @@ final class PostRepository {
/// Move the given post to the trash bin. The post will not be deleted from local database, unless it's delete on its WordPress site.
///
/// - Parameter postID: Object ID of the post
///
/// - warning: deprecated (kahu-offline-mode)
func trash<P: AbstractPost>(_ postID: TaggedManagedObjectID<P>) async throws {
// Trash the original post instead if presents
let original: TaggedManagedObjectID<AbstractPost>? = try await coreDataStack.performQuery { context in
Expand Down Expand Up @@ -347,62 +368,6 @@ final class PostRepository {
}
}
}

/// Move the given post out of the trash bin.
///
/// - Parameters:
/// - postID: Object ID of the given post
/// - status: The post's original status before it's moved to the trash bin.
func restore<P: AbstractPost>(_ postID: TaggedManagedObjectID<P>, to status: BasePost.Status) async throws {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

There is no need for a separate restore. It's "Move to draft" and in terms of the implementation it's "patch" with { "status": "draft" }.

// Restore the original post instead if presents
let original: TaggedManagedObjectID<AbstractPost>? = try await coreDataStack.performQuery { context in
let post = try context.existingObject(with: postID)
if let original = post.original {
return TaggedManagedObjectID(original)
}
return nil
}
if let original {
DDLogInfo("Trash the original post object instead")
try await restore(original, to: status)
return
}

// Update local database
let result: (PostServiceRemote, RemotePost)? = try await coreDataStack.performAndSave { [remoteFactory] context in
let post = try context.existingObject(with: postID)
post.status = status

if let remote = remoteFactory.forBlog(post.blog), !post.isRevision() && (post.postID?.int64Value ?? 0) > 0 {
return (remote, PostHelper.remotePost(with: post))
}
return nil
}

// Call WordPress API if needed
guard let (remote, remotePost) = result else { return }

let updatedRemotePost: RemotePost
do {
updatedRemotePost = try await withCheckedThrowingContinuation { continuation in
remote.restore(remotePost, success: { continuation.resume(returning: $0!) }, failure: { continuation.resume(throwing: $0!)} )
}
} catch {
DDLogError("Failed to restore post: \(error)")

// Put the post back in the trash bin.
try? await coreDataStack.performAndSave { context in
let post = try context.existingObject(with: postID)
post.status = .trash
}
throw error
}

try? await coreDataStack.performAndSave { context in
let post = try context.existingObject(with: postID)
PostHelper.update(post, with: updatedRemotePost, in: context)
}
}
}

private extension PostRepository {
Expand Down Expand Up @@ -625,7 +590,7 @@ extension PostRepository {
// doesn't have local edits
NSPredicate(format: "original = NULL AND revision = NULL"),
// doesn't have local status changes
NSPredicate(format: "remoteStatusNumber = %@", NSNumber(value: AbstractPostRemoteStatus.sync.rawValue)),
self.isSyncPublishingEnabled ? nil : NSPredicate(format: "remoteStatusNumber = %@", NSNumber(value: AbstractPostRemoteStatus.sync.rawValue)),
// is not included in the fetched page lists (i.e. it has been deleted from the site)
NSPredicate(format: "NOT (SELF IN %@)", allPages.map { $0.objectID }),
// we only need to deal with pages that match the filters passed to this function.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Foundation

extension PageListViewController: InteractivePostViewDelegate {

func edit(_ apost: AbstractPost) {
guard let page = apost as? Page else { return }

Expand All @@ -22,8 +21,11 @@ extension PageListViewController: InteractivePostViewDelegate {
}

func trash(_ post: AbstractPost, completion: @escaping () -> Void) {
guard let page = post as? Page else { return }
trashPage(page, completion: completion)
guard RemoteFeatureFlag.syncPublishing.enabled() else {
guard let page = post as? Page else { return }
return trashPage(page, completion: completion)
}
return super._trash(post, completion: completion)
}

func draft(_ apost: AbstractPost) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ final class PageMenuViewModelTests: CoreDataTestCase {
.map { $0.buttons }
let expectedButtons: [[AbstractPostButton]] = [
[.moveToDraft],
[.trash]
[.delete]
]
expect(buttons).to(equal(expectedButtons))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,47 @@ class AbstractPostListViewController: UIViewController,
navigationController?.present(navWrapper, animated: true)
}

func _trash(_ post: AbstractPost, completion: @escaping () -> Void) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The implementation is now shared between posts and pages.
I thought it would also be clearer if there was a separate .delete action, so I added it.

let post = post.original()

func performAction() {
Task {
try? await PostCoordinator.shared.trash(post)
}
}

guard !(post.status == .draft || post.status == .pending) || post.revision != nil else {
return performAction()
}

let alert = UIAlertController(title: Strings.Trash.actionTitle, message: Strings.Trash.message(for: post.latest()), preferredStyle: .alert)
alert.addCancelActionWithTitle(Strings.cancelText) { _ in
completion()
}
alert.addDestructiveActionWithTitle(Strings.Trash.actionTitle) { _ in
performAction()
completion()
}
alert.presentFromRootViewController()
}

func delete(_ post: AbstractPost, completion: @escaping () -> Void) {
let post = post.original()

let alert = UIAlertController(title: Strings.Delete.actionTitle, message: Strings.Delete.message(for: post.latest()), preferredStyle: .alert)
alert.addCancelActionWithTitle(Strings.cancelText) { _ in
completion()
}
alert.addDestructiveActionWithTitle(Strings.Delete.actionTitle) { _ in
completion()
Task {
try? await PostCoordinator.shared._delete(post)
}
}
alert.presentFromRootViewController()
}

/// - warning: deprecated (kahu-offline-mode)
func deletePost(_ post: AbstractPost) {
Task {
await PostCoordinator.shared.delete(post)
Expand Down Expand Up @@ -755,3 +796,25 @@ extension AbstractPostListViewController: NoResultsViewControllerDelegate {
createPost()
}
}

private enum Strings {
static let cancelText = NSLocalizedString("postList.cancel", value: "Cancel", comment: "Cancels an Action")

enum Trash {
static let actionTitle = NSLocalizedString("postList.trash.actionTitle", value: "Move to Trash", comment: "Trash option in the trash post or page confirmation alert.")

static func message(for post: AbstractPost) -> String {
let format = NSLocalizedString("postList.trash.alertMessage", value: "Are you sure you want to trash \"%@\"? Any changes that weren't sent previously to the server will be lost.", comment: "Message of the trash post or page confirmation alert.")
return String(format: format, post.titleForDisplay() ?? "–")
}
}

enum Delete {
static let actionTitle = NSLocalizedString("postList.deletePermanently.actionTitle", value: "Delete Permanently", comment: "Delete option in the confirmation alert when deleting a page from the trash.")

static func message(for post: AbstractPost) -> String {
let format = NSLocalizedString("postList.deletePermanently.alertMessage", value: "Are you sure you want to permanently delete \"%@\"?", comment: "Message of the confirmation alert when deleting a page from the trash.")
return String(format: format, post.titleForDisplay() ?? "–")
}
}
}
Loading