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

Adds Duplicate/Copy post functionality #15460

Merged
merged 15 commits into from
Dec 14, 2020
2 changes: 1 addition & 1 deletion Podfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ workspace 'WordPress.xcworkspace'
## ===================================
##
def wordpress_shared
pod 'WordPressShared', '~> 1.13.0'
pod 'WordPressShared', '~> 1.14.0-beta.1'
#pod 'WordPressShared', :git => 'https://github.com/wordpress-mobile/WordPress-iOS-Shared.git', :tag => ''
#pod 'WordPressShared', :git => 'https://github.com/wordpress-mobile/WordPress-iOS-Shared.git', :branch => ''
#pod 'WordPressShared', :git => 'https://github.com/wordpress-mobile/WordPress-iOS-Shared.git', :commit => ''
Expand Down
8 changes: 4 additions & 4 deletions Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -414,7 +414,7 @@ PODS:
- WordPressShared (~> 1.12)
- wpxmlrpc (~> 0.9.0)
- WordPressMocks (0.0.9)
- WordPressShared (1.13.0):
- WordPressShared (1.14.0-beta.1):
- CocoaLumberjack (~> 3.4)
- FormatterKit/TimeIntervalFormatter (= 1.8.2)
- WordPressUI (1.7.4)
Expand Down Expand Up @@ -507,7 +507,7 @@ DEPENDENCIES:
- WordPressAuthenticator (~> 1.31.0-beta.4)
- WordPressKit (~> 4.23-beta)
- WordPressMocks (~> 0.0.9)
- WordPressShared (~> 1.13.0)
- WordPressShared (~> 1.14.0-beta.1)
- WordPressUI (~> 1.7.4)
- WPMediaPicker (~> 1.7.2)
- Yoga (from `https://raw.githubusercontent.com/wordpress-mobile/gutenberg-mobile/v1.43.0/third-party-podspecs/Yoga.podspec.json`)
Expand Down Expand Up @@ -755,7 +755,7 @@ SPEC CHECKSUMS:
WordPressAuthenticator: 3bc0d09d1617e540c6475cceac62ed6c3755cf22
WordPressKit: 95fe61b3e482fb80e8608186fff5e86aa1e2fd3e
WordPressMocks: 903d2410f41a09fb2e0a1b44ad36ad80310570fb
WordPressShared: 532ad68f954d37ea901e8c7e3ca62913c43ff787
WordPressShared: db04af4eed52585c02ced2a81a4651968c35d642
WordPressUI: 4226e616dd8e7aab5a8d2172b7c378454e72182e
WPMediaPicker: d5ae9a83cd5cc0e4de46bfc1c59120aa86658bc3
wpxmlrpc: bf55a43a7e710bd2a4fb8c02dfe83b1246f14f13
Expand All @@ -769,6 +769,6 @@ SPEC CHECKSUMS:
ZendeskSupportSDK: dcb2596ad05a63d662e8c7924357babbf327b421
ZIPFoundation: 249fa8890597086cd536bb2df5c9804d84e122b0

PODFILE CHECKSUM: 1e28a318613a1fa0b2751fece56519190bd1f2d0
PODFILE CHECKSUM: 5e0d7588702057ace90645c9819f05d584c3c3e2

COCOAPODS: 1.10.0
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
* [**] Reader: Added 'P2s' stream. [#15442]
* [*] Add a new P2 default site icon to replace the generic default site icon. [#15430]
* [*] Block Editor: Fix Gallery block uploads when the editor is closed. [#15457]
* [**] Posts List: Adds duplicate post functionality [#15460]
Copy link
Author

Choose a reason for hiding this comment

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

NOTE: I need to move this to the next release if this is not finished for 16.4

* [***] Block Editor: New Block: File [https://github.com/wordpress-mobile/gutenberg-mobile/pull/2835]
* [*] Reader: Removes gray tint from site icons that contain transparency (located in Reader > Settings > Followed sites).

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,10 @@ + (TracksEventPair *)eventPairForStat:(WPAnalyticsStat)stat
eventName = @"post_list_button_pressed";
eventProperties = @{ TracksEventPropertyButtonKey : @"edit" };
break;
case WPAnalyticsStatPostListDuplicateAction:
eventName = @"post_list_button_pressed";
eventProperties = @{ TracksEventPropertyButtonKey : @"copy" }; // Property aligned with Android
break;
mkevins marked this conversation as resolved.
Show resolved Hide resolved
case WPAnalyticsStatPostListExcessiveLoadMoreDetected:
eventName = @"post_list_excessive_load_more_detected";
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Foundation
func edit(_ post: AbstractPost)
func view(_ post: AbstractPost)
func stats(for post: AbstractPost)
func duplicate(_ post: AbstractPost)
func publish(_ post: AbstractPost)
func trash(_ post: AbstractPost)
func restore(_ post: AbstractPost)
Expand Down
5 changes: 5 additions & 0 deletions WordPress/Classes/ViewRelated/Post/PostActionSheet.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ class PostActionSheet {
actionSheetController.addDefaultActionWithTitle(Titles.stats) { [weak self] _ in
self?.interactivePostViewDelegate?.stats(for: post)
}
case .duplicate:
actionSheetController.addDefaultActionWithTitle(Titles.duplicate) { [weak self] _ in
self?.interactivePostViewDelegate?.duplicate(post)
}
case .publish:
actionSheetController.addDefaultActionWithTitle(Titles.publish) { [weak self] _ in
self?.interactivePostViewDelegate?.publish(post)
Expand Down Expand Up @@ -90,6 +94,7 @@ class PostActionSheet {
static let cancel = NSLocalizedString("Cancel", comment: "Dismiss the post action sheet")
static let cancelAutoUpload = NSLocalizedString("Cancel Upload", comment: "Label for the Post List option that cancels automatic uploading of a post.")
static let stats = NSLocalizedString("Stats", comment: "Label for post stats option. Tapping displays statistics for a post.")
static let duplicate = NSLocalizedString("Duplicate", comment: "Label for post duplicate option. Tapping displays statistics for a post.")
static let publish = NSLocalizedString("Publish Now", comment: "Label for an option that moves a publishes a post immediately")
static let draft = NSLocalizedString("Move to Draft", comment: "Label for an option that moves a post to the draft folder")
static let delete = NSLocalizedString("Delete Permanently", comment: "Label for the delete post option. Tapping permanently deletes a post.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class PostCardStatusViewModel: NSObject {
case more
case publish
case stats
case duplicate
case moveToDraft
case trash
case cancelAutoUpload
Expand Down Expand Up @@ -181,6 +182,10 @@ class PostCardStatusViewModel: NSObject {
buttons.append(.share)
}

if post.status == .publish || post.status == .draft {
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if it's already been discussed, but do we want to consider other statuses here (e.g. scheduled, pending, etc.)?

Copy link
Author

Choose a reason for hiding this comment

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

I thought of it but I couldn't find any other condition that should prevent the user from copying the post. I used the same approach on Android

Copy link
Contributor

Choose a reason for hiding this comment

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

For this, I mean the cases that allow the user to duplicate the post. I don't think it's a blocker, especially since we already have matching behavior on Android, and maybe we can consider other cases for a future iteration.

Copy link
Author

Choose a reason for hiding this comment

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

For this, I mean the cases that allow the user to duplicate the post. I don't think it's a blocker, especially since we already have matching behavior on Android, and maybe we can consider other cases for a future iteration.

I agree @mkevins 👍 I'll merge this and reconsider in a next iteration.

buttons.append(.duplicate)
}

if post.status != .draft {
buttons.append(.moveToDraft)
}
Expand Down
63 changes: 63 additions & 0 deletions WordPress/Classes/ViewRelated/Post/PostListEditorPresenter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,49 @@ struct PostListEditorPresenter {
}
}

static func handleCopy(post: Post, in postListViewController: PostListViewController) {
// Autosaves are ignored for posts with local changes.
if !post.hasLocalChanges(), post.hasAutosaveRevision {
let conflictsResolutionViewController = copyConflictsResolutionViewController(didTapOption: { copyLocal, cancel in
if cancel {
return
}
if copyLocal {
openEditorWithCopy(with: post, in: postListViewController)
} else {
handle(post: post, in: postListViewController)
}
})
postListViewController.present(conflictsResolutionViewController, animated: true)
} else {
openEditorWithCopy(with: post, in: postListViewController)
}
}

private static func openEditor(with post: Post, loadAutosaveRevision: Bool, in postListViewController: PostListViewController) {
let editor = EditPostViewController(post: post, loadAutosaveRevision: loadAutosaveRevision)
editor.modalPresentationStyle = .fullScreen
postListViewController.present(editor, animated: false)
WPAppAnalytics.track(.postListEditAction, withProperties: postListViewController.propertiesForAnalytics(), with: post)
}

private static func openEditorWithCopy(with post: Post, in postListViewController: PostListViewController) {
// Copy Post
let context = ContextManager.sharedInstance().mainContext
let postService = PostService(managedObjectContext: context)
let newPost = postService.createDraftPost(for: post.blog)
newPost.postTitle = post.postTitle
newPost.content = post.content
newPost.categories = post.categories
newPost.postFormat = post.postFormat
// Open Editor
let editor = EditPostViewController(post: newPost, loadAutosaveRevision: false)
editor.modalPresentationStyle = .fullScreen
postListViewController.present(editor, animated: false)
// Track Analytics event
WPAppAnalytics.track(.postListDuplicateAction, withProperties: postListViewController.propertiesForAnalytics(), with: post)
}

private static let dateFormatter: DateFormatter = {
let formatter = DateFormatter()
formatter.dateStyle = .medium
Expand Down Expand Up @@ -68,4 +104,31 @@ struct PostListEditorPresenter {

return alertController
}

/// A dialog giving the user the choice between copying the current version of the post or resolving conflicts with edit.
private static func copyConflictsResolutionViewController(didTapOption: @escaping (_ copyLocal: Bool, _ cancel: Bool) -> Void) -> UIAlertController {

let title = NSLocalizedString("Post sync conflict", comment: "Title displayed in popup when user tries to copy a post with unsaved changes")

let message = NSLocalizedString("The post you are trying to copy has two versions that are in conflict or you recently made changes but didn\'t save them.\nEdit the post first to resolve any conflict or proceed with copying the version from this app.", comment: "Message displayed in popup when user tries to copy a post with conflicts")

let editFirstButtonTitle = NSLocalizedString("Edit the post first", comment: "Button title displayed in popup indicating that the user edits the post first")
let copyLocalButtonTitle = NSLocalizedString("Copy the version from this app", comment: "Button title displayed in popup indicating the user copied the local copy")
let cancelButtonTitle = NSLocalizedString("Cancel", comment: "Cancel button.")

let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
alertController.addAction(UIAlertAction(title: editFirstButtonTitle, style: .default) { _ in
didTapOption(false, false)
})
alertController.addAction(UIAlertAction(title: copyLocalButtonTitle, style: .default) { _ in
didTapOption(true, false)
})
alertController.addAction(UIAlertAction(title: cancelButtonTitle, style: .cancel) { _ in
didTapOption(false, true)
})

alertController.view.accessibilityIdentifier = "copy-version-conflict-alert"

return alertController
}
}
12 changes: 12 additions & 0 deletions WordPress/Classes/ViewRelated/Post/PostListViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,14 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe
PostListEditorPresenter.handle(post: post, in: self)
}

private func editDuplicatePost(apost: AbstractPost) {
guard let post = apost as? Post else {
return
}

PostListEditorPresenter.handleCopy(post: post, in: self)
}

func presentAlertForPostBeingUploaded() {
let message = NSLocalizedString("This post is currently uploading. It won't take long – try again soon and you'll be able to edit it.", comment: "Prompts the user that the post is being uploaded and cannot be edited while that process is ongoing.")

Expand Down Expand Up @@ -651,6 +659,10 @@ class PostListViewController: AbstractPostListViewController, UIViewControllerRe
}
}

func duplicate(_ post: AbstractPost) {
editDuplicatePost(apost: post)
}

func publish(_ post: AbstractPost) {
publishPost(post)
}
Expand Down
22 changes: 18 additions & 4 deletions WordPress/WordPressTest/PostActionSheetTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ class PostActionSheetTests: XCTestCase {
postActionSheet.show(for: viewModel, from: view)

let options = viewControllerMock.viewControllerPresented?.actions.compactMap { $0.title }
XCTAssertEqual(["Cancel", "Stats", "Share", "Move to Draft", "Move to Trash"], options)
XCTAssertEqual(["Cancel", "Stats", "Share", "Duplicate", "Move to Draft", "Move to Trash"], options)
}

func testLocallyPublishedPostShowsCancelAutoUploadOption() {
Expand All @@ -34,7 +34,7 @@ class PostActionSheetTests: XCTestCase {
postActionSheet.show(for: viewModel, from: view, isCompactOrSearching: true)

let options = viewControllerMock.viewControllerPresented?.actions.compactMap { $0.title }
XCTAssertEqual([Titles.cancel, Titles.cancelAutoUpload, Titles.draft, Titles.trash], options)
XCTAssertEqual([Titles.cancel, Titles.cancelAutoUpload, Titles.duplicate, Titles.draft, Titles.trash], options)
}

func testDraftedPostOptions() {
Expand All @@ -43,7 +43,7 @@ class PostActionSheetTests: XCTestCase {
postActionSheet.show(for: viewModel, from: view)

let options = viewControllerMock.viewControllerPresented?.actions.compactMap { $0.title }
XCTAssertEqual(["Cancel", "Publish Now", "Move to Trash"], options)
XCTAssertEqual(["Cancel", "Publish Now", "Duplicate", "Move to Trash"], options)
}

func testScheduledPostOptions() {
Expand All @@ -70,7 +70,7 @@ class PostActionSheetTests: XCTestCase {
postActionSheet.show(for: viewModel, from: view, isCompactOrSearching: true)

let options = viewControllerMock.viewControllerPresented?.actions.compactMap { $0.title }
XCTAssertEqual(["Cancel", "View", "Stats", "Share", "Move to Draft", "Move to Trash"], options)
XCTAssertEqual(["Cancel", "View", "Stats", "Share", "Duplicate", "Move to Draft", "Move to Trash"], options)
}

func testCallDelegateWhenStatsTapped() {
Expand All @@ -82,6 +82,15 @@ class PostActionSheetTests: XCTestCase {
XCTAssertTrue(interactivePostViewDelegateMock.didCallHandleStats)
}

func testCallDelegateWhenDuplicateTapped() {
let viewModel = PostCardStatusViewModel(post: PostBuilder().published().withRemote().build())

postActionSheet.show(for: viewModel, from: view)
tap("Duplicate", in: viewControllerMock.viewControllerPresented)

XCTAssertTrue(interactivePostViewDelegateMock.didCallHandleDuplicate)
}

func testCallDelegateWhenShareTapped() {
let viewModel = PostCardStatusViewModel(post: PostBuilder().published().withRemote().build())

Expand Down Expand Up @@ -171,6 +180,7 @@ private class UIViewControllerMock: UIViewController {

class InteractivePostViewDelegateMock: InteractivePostViewDelegate {
private(set) var didCallHandleStats = false
private(set) var didCallHandleDuplicate = false
private(set) var didCallHandleDraft = false
private(set) var didCallHandleTrashPost = false
private(set) var didCallEdit = false
Expand All @@ -183,6 +193,10 @@ class InteractivePostViewDelegateMock: InteractivePostViewDelegate {
didCallHandleStats = true
}

func duplicate(_ post: AbstractPost) {
didCallHandleDuplicate = true
}

func draft(_ post: AbstractPost) {
didCallHandleDraft = true
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,48 +27,48 @@ class PostCardStatusViewModelTests: XCTestCase {
(
"Draft with remote",
PostBuilder(context).drafted().withRemote().build(),
ButtonGroups(primary: [.edit, .view, .more], secondary: [.publish, .trash])
ButtonGroups(primary: [.edit, .view, .more], secondary: [.publish, .duplicate, .trash])
),
(
"Draft that was not uploaded to the server",
PostBuilder(context).drafted().with(remoteStatus: .failed).build(),
ButtonGroups(primary: [.edit, .publish, .trash], secondary: [])
ButtonGroups(primary: [.edit, .publish, .more], secondary: [.duplicate, .trash])
),
(
"Draft with remote and confirmed local changes",
PostBuilder(context).drafted().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().build(),
ButtonGroups(primary: [.edit, .cancelAutoUpload, .more], secondary: [.publish, .trash])
ButtonGroups(primary: [.edit, .cancelAutoUpload, .more], secondary: [.publish, .duplicate, .trash])
),
(
"Draft with remote and canceled local changes",
PostBuilder(context).drafted().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().cancelledAutoUpload().build(),
ButtonGroups(primary: [.edit, .publish, .trash], secondary: [])
ButtonGroups(primary: [.edit, .publish, .more], secondary: [.duplicate, .trash])
),
(
"Local published draft with confirmed auto-upload",
PostBuilder(context).published().with(remoteStatus: .failed).confirmedAutoUpload().build(),
ButtonGroups(primary: [.edit, .cancelAutoUpload, .more], secondary: [.moveToDraft, .trash])
ButtonGroups(primary: [.edit, .cancelAutoUpload, .more], secondary: [.duplicate, .moveToDraft, .trash])
),
(
"Local published draft with canceled auto-upload",
PostBuilder(context).published().with(remoteStatus: .failed).build(),
ButtonGroups(primary: [.edit, .publish, .more], secondary: [.moveToDraft, .trash])
ButtonGroups(primary: [.edit, .publish, .more], secondary: [.duplicate, .moveToDraft, .trash])
),
(
"Published post",
PostBuilder(context).published().withRemote().build(),
ButtonGroups(primary: [.edit, .view, .more], secondary: [.stats, .share, .moveToDraft, .trash])
ButtonGroups(primary: [.edit, .view, .more], secondary: [.stats, .share, .duplicate, .moveToDraft, .trash])
),
(
"Published post with local confirmed changes",
PostBuilder(context).published().withRemote().with(remoteStatus: .failed).confirmedAutoUpload().build(),
ButtonGroups(primary: [.edit, .cancelAutoUpload, .more], secondary: [.stats, .share, .moveToDraft, .trash])
ButtonGroups(primary: [.edit, .cancelAutoUpload, .more], secondary: [.stats, .share, .duplicate, .moveToDraft, .trash])
),
(
"Post with the max number of auto uploades retry reached",
PostBuilder(context).with(remoteStatus: .failed)
.with(autoUploadAttemptsCount: 3).confirmedAutoUpload().build(),
ButtonGroups(primary: [.edit, .retry, .more], secondary: [.publish, .moveToDraft, .trash])
ButtonGroups(primary: [.edit, .retry, .more], secondary: [.publish, .duplicate, .moveToDraft, .trash])
),
]

Expand Down