Skip to content
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
5 changes: 5 additions & 0 deletions WordPress/Classes/Models/AbstractPost.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ extension AbstractPost {

// MARK: - Status

/// Returns `true` is the post has one of the given statuses.
func isStatus(in statuses: Set<Status>) -> Bool {
statuses.contains(status ?? .draft)
}

/// - note: deprecated (kahu-offline-mode)
@objc
var statusTitle: String? {
Expand Down
18 changes: 10 additions & 8 deletions WordPress/Classes/Services/PostCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ protocol PostCoordinatorDelegate: AnyObject {

class PostCoordinator: NSObject {

enum SavingError: Error {
enum SavingError: Error, LocalizedError {
case mediaFailure(AbstractPost)
case unknown

var errorDescription: String? {
Strings.genericErrorTitle
}
}

@objc static let shared = PostCoordinator()
Expand Down Expand Up @@ -153,7 +157,7 @@ class PostCoordinator: NSObject {
@MainActor
func _publish(_ post: AbstractPost, options: PublishingOptions) async throws {
assert(post.isOriginal())
assert(post.status == .draft)
assert(post.isStatus(in: [.draft, .pending]))

await pauseSyncing(for: post)
defer { resumeSyncing(for: post) }
Expand Down Expand Up @@ -217,13 +221,10 @@ class PostCoordinator: NSObject {

/// 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 {
Expand All @@ -238,7 +239,7 @@ class PostCoordinator: NSObject {
guard let topViewController = UIApplication.shared.mainWindow?.topmostPresentedViewController else {
return
}
let alert = UIAlertController(title: Strings.errorTitle, message: error.localizedDescription, preferredStyle: .alert)
let alert = UIAlertController(title: Strings.genericErrorTitle, message: error.localizedDescription, preferredStyle: .alert)
if let error = error as? PostRepository.PostSaveError {
switch error {
case .conflict:
Expand Down Expand Up @@ -298,7 +299,7 @@ class PostCoordinator: NSObject {

/// Returns `true` if the post is eligible for syncing.
func isSyncAllowed(for post: AbstractPost) -> Bool {
post.status == .draft
post.status == .draft || post.status == .pending
}

/// Returns `true` if post has any revisions that need to be synced.
Expand Down Expand Up @@ -1215,6 +1216,7 @@ private enum Strings {
static let deletePost = NSLocalizedString("postsList.deletePost.message", value: "Post deleted permanently", comment: "A short message explaining that a post was deleted permanently.")
static let movePageToTrash = NSLocalizedString("postsList.movePageToTrash.message", value: "Page moved to trash", comment: "A short message explaining that a page was moved to the trash bin.")
static let deletePage = NSLocalizedString("postsList.deletePage.message", value: "Page deleted permanently", comment: "A short message explaining that a page was deleted permanently.")
static let errorTitle = NSLocalizedString("postNotice.errorTitle", value: "An error occured", comment: "A generic error message title")
static let genericErrorTitle = NSLocalizedString("postNotice.errorTitle", value: "An error occured", comment: "A generic error message title")
static let buttonOK = NSLocalizedString("postNotice.ok", value: "OK", comment: "Button OK")
static let errorUnsyncedChangesMessage = NSLocalizedString("postNotice.errorUnsyncedChangesMessage", value: "The app is uploading previously made changes to the server. Please try again later.", comment: "An error message")
}
5 changes: 4 additions & 1 deletion WordPress/Classes/Services/PostRepository+Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ extension RemotePostCreateParameters {
status: (post.status ?? .draft).rawValue
)
date = post.dateCreated
authorID = post.authorID?.intValue
// - warning: the currnet Core Data model defaults to `0`
if let authorID = post.authorID?.intValue, authorID > 0 {
self.authorID = authorID
}
title = post.postTitle
content = post.content
password = post.password
Expand Down
32 changes: 26 additions & 6 deletions WordPress/Classes/Services/PostRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ import WordPressKit

final class PostRepository {

enum Error: Swift.Error {
enum Error: Swift.Error, LocalizedError {
case remoteAPIUnavailable
case hasUnsyncedChanges
case patchingUnsyncedPost // Should never happen

var errorDescription: String? {
switch self {
case .remoteAPIUnavailable: return Strings.genericErrorMessage
case .hasUnsyncedChanges: return Strings.errorUnsyncedChangesMessage
case .patchingUnsyncedPost: return Strings.genericErrorMessage
}
}
}

private let coreDataStack: CoreDataStackSwift
Expand Down Expand Up @@ -170,15 +180,19 @@ final class PostRepository {
/// revisions are used only for content.
@MainActor
func _update(_ post: AbstractPost, changes: RemotePostUpdateParameters) async throws {
let original = post.original ?? post
guard let postID = original.postID, postID.intValue > 0 else {
assert(post.isOriginal())

guard post.revision == nil else {
throw PostRepository.Error.hasUnsyncedChanges
}
guard let postID = post.postID, postID.intValue > 0 else {
assertionFailure("Trying to patch a non-existent post")
return
throw PostRepository.Error.patchingUnsyncedPost
}
let uploadedPost = try await _patch(post, postID: postID, changes: changes, overwrite: true)

let context = coreDataStack.mainContext
PostHelper.update(original, with: uploadedPost, in: context, overwrite: true)
PostHelper.update(post, with: uploadedPost, in: context, overwrite: true)
ContextManager.shared.saveContextAndWait(context)
}

Expand Down Expand Up @@ -213,7 +227,8 @@ final class PostRepository {
case .conflict:
// Fetch the latest post to consolidate the changes
let remotePost = try await service.post(withID: postID)
if changes.content != nil && remotePost.content != original.content {
// Check for false positives
if changes.content != nil && remotePost.content != changes.content && remotePost.content != original.content {
// The conflict in content can be resolved only manually
throw PostSaveError.conflict(latest: remotePost)
}
Expand Down Expand Up @@ -670,3 +685,8 @@ extension PostRepository {
}

}

private enum Strings {
static let genericErrorMessage = NSLocalizedString("postList.genericErrorMessage", value: "Something went wrong", comment: "A generic error message title")
static let errorUnsyncedChangesMessage = NSLocalizedString("postList.errorUnsyncedChangesMessage", value: "The app is uploading previously made changes to the server. Please try again later.", comment: "An error message")
}
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ final class PageMenuViewModelTests: CoreDataTestCase {
.map { $0.buttons }
let expectedButtons: [[AbstractPostButton]] = [
[.view],
[.duplicate, .publish],
[.publish, .duplicate],
[.setParent],
[.trash]
]
Expand All @@ -174,7 +174,7 @@ final class PageMenuViewModelTests: CoreDataTestCase {
.map { $0.buttons }
let expectedButtons: [[AbstractPostButton]] = [
[.view],
[.moveToDraft, .publish],
[.moveToDraft],
[.setParent],
[.trash]
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation

enum AbstractPostHelper {
/// - warning: deprecated (kahu-offline-mode)
static func editorPublishAction(for post: AbstractPost) -> PostEditorAction {
post.blog.isPublishingPostsAllowed() ? .publish : .submitForReview
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,11 @@ extension AbstractPostButton: AbstractPostMenuAction {
switch self {
case .retry: return Strings.retry
case .view: return post.status == .publish ? Strings.view : Strings.preview
case .publish: return AbstractPostHelper.editorPublishAction(for: post).publishActionLabel
case .publish:
guard RemoteFeatureFlag.syncPublishing.enabled() else {
return AbstractPostHelper.editorPublishAction(for: post).publishActionLabel
}
return Strings.publish
case .stats: return Strings.stats
case .duplicate: return Strings.duplicate
case .moveToDraft: return Strings.draft
Expand Down Expand Up @@ -194,6 +198,7 @@ extension AbstractPostButton: AbstractPostMenuAction {
static let trash = NSLocalizedString("posts.trash.actionTitle", value: "Move to trash", comment: "Label for a option that moves a post to the trash folder")
static let view = NSLocalizedString("posts.view.actionTitle", value: "View", comment: "Label for the view post button. Tapping displays the post as it appears on the web.")
static let preview = NSLocalizedString("posts.preview.actionTitle", value: "Preview", comment: "Label for the preview post button. Tapping displays the post as it appears on the web.")
static let publish = NSLocalizedString("posts.publish.actionTitle", value: "Publish", comment: "Label for the publish post button.")
static let retry = NSLocalizedString("posts.retry.actionTitle", value: "Retry", comment: "Retry uploading the post.")
static let share = NSLocalizedString("posts.share.actionTitle", value: "Share", comment: "Share the post.")
static let blaze = NSLocalizedString("posts.blaze.actionTitle", value: "Promote with Blaze", comment: "Promote the post with Blaze.")
Expand Down
27 changes: 20 additions & 7 deletions WordPress/Classes/ViewRelated/Post/PageMenuViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ final class PageMenuViewModel: AbstractPostMenuViewModel {
private let isSitePostsPage: Bool
private let isJetpackFeaturesEnabled: Bool
private let isBlazeFlagEnabled: Bool
private let isSyncPublishingEnabled: Bool

var buttonSections: [AbstractPostButtonSection] {
[
Expand All @@ -27,13 +28,15 @@ final class PageMenuViewModel: AbstractPostMenuViewModel {
isSiteHomepage: Bool,
isSitePostsPage: Bool,
isJetpackFeaturesEnabled: Bool = JetpackFeaturesRemovalCoordinator.jetpackFeaturesEnabled(),
isBlazeFlagEnabled: Bool = BlazeHelper.isBlazeFlagEnabled()
isBlazeFlagEnabled: Bool = BlazeHelper.isBlazeFlagEnabled(),
isSyncPublishingEnabled: Bool = RemoteFeatureFlag.syncPublishing.enabled()
) {
self.page = page
self.isSiteHomepage = isSiteHomepage
self.isSitePostsPage = isSitePostsPage
self.isJetpackFeaturesEnabled = isJetpackFeaturesEnabled
self.isBlazeFlagEnabled = isBlazeFlagEnabled
self.isSyncPublishingEnabled = isSyncPublishingEnabled
}

private func createPrimarySection() -> AbstractPostButtonSection {
Expand All @@ -49,6 +52,10 @@ final class PageMenuViewModel: AbstractPostMenuViewModel {
private func createSecondarySection() -> AbstractPostButtonSection {
var buttons = [AbstractPostButton]()

if canPublish {
buttons.append(.publish)
}

if page.status != .draft && !isSiteHomepage {
buttons.append(.moveToDraft)
}
Expand All @@ -61,17 +68,23 @@ final class PageMenuViewModel: AbstractPostMenuViewModel {
buttons.append(.share)
}

if page.status != .trash && page.isFailed {
buttons.append(.retry)
}

if !page.isFailed, page.status != .publish && page.status != .trash {
buttons.append(.publish)
if !isSyncPublishingEnabled {
if page.status != .trash && page.isFailed {
buttons.append(.retry)
}
}

return AbstractPostButtonSection(buttons: buttons)
}

private var canPublish: Bool {
guard isSyncPublishingEnabled else {
return !page.isFailed && page.status != .publish && page.status != .trash
}
let userCanPublish = page.blog.capabilities != nil ? page.blog.isPublishingPostsAllowed() : true
return page.isStatus(in: [.draft, .pending]) && userCanPublish
}

private func createBlazeSection() -> AbstractPostButtonSection {
var buttons = [AbstractPostButton]()

Expand Down
46 changes: 35 additions & 11 deletions WordPress/Classes/ViewRelated/Post/PostCardStatusViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel {
private let isBlazeFlagEnabled: Bool
private let isSyncPublishingEnabled: Bool

/// - warning: deprecated (kahu-offline-mode)
var progressBlock: ((Float) -> Void)? = nil {
didSet {
if let _ = oldValue, let uuid = progressObserverUUID {
Expand Down Expand Up @@ -86,6 +87,23 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel {
}

var statusColor: UIColor {
guard isSyncPublishingEnabled else {
return _statusColor
}
switch post.status ?? .draft {
case .pending:
return .success
case .scheduled:
return .primary(.shade40)
case .trash:
return .error
default:
return .neutral(.shade70)
}
}

/// - warning: deprecated (kahu-offline-mode)
var _statusColor: UIColor {
guard let status = postStatus else {
return .neutral(.shade70)
}
Expand Down Expand Up @@ -166,6 +184,10 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel {
private func createSecondarySection() -> AbstractPostButtonSection {
var buttons = [AbstractPostButton]()

if canPublish {
buttons.append(.publish)
}

if post.status != .draft {
buttons.append(.moveToDraft)
}
Expand All @@ -190,10 +212,6 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel {
}
}

if canPublish {
buttons.append(.publish)
}

return AbstractPostButtonSection(buttons: buttons)
}

Expand Down Expand Up @@ -230,21 +248,26 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel {
return autoUploadInteractor.canCancelAutoUpload(of: post)
}

/// Returns true if any of the following conditions are true:
///
/// * The post is a draft.
/// * The post failed to upload and has local changes but the user canceled auto-uploading
/// * The upload failed and the user cannot Cancel it anymore. This happens when we reached the maximum number of retries.
private var canPublish: Bool {
guard isSyncPublishingEnabled else {
return _canPublish
}
let userCanPublish = post.blog.capabilities != nil ? post.blog.isPublishingPostsAllowed() : true
return (post.status == .draft || post.status == .pending) && userCanPublish
}

/// - warning: deprecated (kahu-offline-mode)
private var _canPublish: Bool {
let isNotCancelableWithFailedToUploadChanges: Bool = post.isFailed && post.hasLocalChanges() && !autoUploadInteractor.canCancelAutoUpload(of: post)
return post.isDraft() || isNotCancelableWithFailedToUploadChanges
}

func statusAndBadges(separatedBy separator: String) -> String {
let sticky = post.isStickyPost && !isUploadingOrFailed ? Constants.stickyLabel : ""
let sticky = post.isStickyPost ? Constants.stickyLabel : ""
let pending = (post.status == .pending && isSyncPublishingEnabled) ? Constants.pendingReview : ""
let status = self.status ?? ""

return [status, sticky].filter { !$0.isEmpty }.joined(separator: separator)
return [status, pending, sticky].filter { !$0.isEmpty }.joined(separator: separator)
}

/// Determine what the failed status message should be and return it.
Expand All @@ -266,6 +289,7 @@ class PostCardStatusViewModel: NSObject, AbstractPostMenuViewModel {

private enum Constants {
static let stickyLabel = NSLocalizedString("Sticky", comment: "Label text that defines a post marked as sticky")
static let pendingReview = NSLocalizedString("postList.badgePendingReview", value: "Pending review", comment: "Badge for post cells")
}

enum StatusMessages {
Expand Down
Loading