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/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/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