diff --git a/WordPress/Classes/Services/MediaCoordinator.swift b/WordPress/Classes/Services/MediaCoordinator.swift index 11f6a3fb41f7..f6655df570bd 100644 --- a/WordPress/Classes/Services/MediaCoordinator.swift +++ b/WordPress/Classes/Services/MediaCoordinator.swift @@ -60,6 +60,7 @@ class MediaCoordinator: NSObject { /// /// - Returns: `true` if all media in the post is uploading or was uploaded, `false` otherwise. /// + @discardableResult func uploadMedia(for post: AbstractPost, automatedRetry: Bool = false) -> Bool { let failedMedia: [Media] = post.media.filter({ $0.remoteStatus == .failed }) let mediasToUpload: [Media] @@ -230,6 +231,7 @@ class MediaCoordinator: NSObject { trackUploadOf(media, analyticsInfo: analyticsInfo) let uploadProgress = uploadMedia(media) + totalProgress.setUserInfoObject(uploadProgress, forKey: .uploadProgress) totalProgress.addChild(uploadProgress, withPendingUnitCount: MediaExportProgressUnits.uploadDone) } diff --git a/WordPress/Classes/ViewRelated/Aztec/Media/MediaProgressCoordinator.swift b/WordPress/Classes/ViewRelated/Aztec/Media/MediaProgressCoordinator.swift index 6712c7f47498..d4bc9b85aafe 100644 --- a/WordPress/Classes/ViewRelated/Aztec/Media/MediaProgressCoordinator.swift +++ b/WordPress/Classes/ViewRelated/Aztec/Media/MediaProgressCoordinator.swift @@ -13,6 +13,7 @@ extension ProgressUserInfoKey { static let mediaID = ProgressUserInfoKey("mediaID") static let mediaError = ProgressUserInfoKey("mediaError") static let mediaObject = ProgressUserInfoKey("mediaObject") + static let uploadProgress = ProgressUserInfoKey("uploadProgress") } /// Media Progress Coordinator allow the tracking of progress on multiple media objects. diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift index e7d19341b279..9266edbc8597 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PrepublishingViewController.swift @@ -2,6 +2,7 @@ import UIKit import WordPressAuthenticator import Combine import WordPressUI +import SwiftUI enum PrepublishingIdentifier { case title @@ -57,21 +58,20 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource let tableView = UITableView(frame: .zero, style: .plain) private let footerSeparator = UIView() - let publishButton: NUXButton = { - let nuxButton = NUXButton() - nuxButton.isPrimary = true - nuxButton.accessibilityIdentifier = "publish" - return nuxButton - }() - private weak var titleField: UITextField? + private lazy var publishButtonViewModel = PublishButtonViewModel(title: "Publish") { [weak self] in + self?.buttonPublishTapped() + } + /// Determines whether the text has been first responder already. If it has, don't force it back on the user unless it's been selected by them. private var hasSelectedText: Bool = false private var cancellables = Set() @Published private var keyboardShown: Bool = false + private weak var mediaPollingTimer: Timer? + init(post: Post, identifiers: [PrepublishingIdentifier], completion: @escaping (CompletionResult) -> (), @@ -151,8 +151,6 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource view.pinSubviewToSafeArea(stackView) view.backgroundColor = .systemBackground - - announcePublishButton() } private func configureHeader() { @@ -170,14 +168,23 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource private func setupPublishButton() -> UIView { let footerView = UIView() - footerView.addSubview(publishButton) - publishButton.translatesAutoresizingMaskIntoConstraints = false - footerView.pinSubviewToSafeArea(publishButton, insets: Constants.nuxButtonInsets) - publishButton.addTarget(self, action: #selector(publish), for: .touchUpInside) + let hostingViewController = UIHostingController(rootView: PublishButton(viewModel: publishButtonViewModel).tint(Color(uiColor: .primary))) + addChild(hostingViewController) + + footerView.addSubview(hostingViewController.view) + hostingViewController.view.translatesAutoresizingMaskIntoConstraints = false + footerView.pinSubviewToSafeArea(hostingViewController.view, insets: Constants.nuxButtonInsets) updatePublishButtonLabel() + if FeatureFlag.offlineMode.enabled { + updatePublishButtonState() + mediaPollingTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { [weak self] _ in + self?.updatePublishButtonState() + } + } + return footerView } @@ -422,11 +429,23 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource // MARK: - Publish Button + private func updatePublishButtonState() { + if let state = PublishButtonState.uploadingState(for: post) { + publishButtonViewModel.state = state + } else { + if case .loading = publishButtonViewModel.state { + // Do nothing + } else { + publishButtonViewModel.state = .default + } + } + } + private func updatePublishButtonLabel() { - publishButton.setTitle(post.isScheduled() ? Strings.schedule : Strings.publish, for: .normal) + publishButtonViewModel.title = post.isScheduled() ? Strings.schedule : Strings.publish } - @objc func publish(_ sender: UIButton) { + private func buttonPublishTapped() { didTapPublish = true navigationController?.dismiss(animated: true) { WPAnalytics.track(.editorPostPublishNowTapped) @@ -460,12 +479,6 @@ final class PrepublishingViewController: UIViewController, UITableViewDataSource // MARK: - Accessibility - private func announcePublishButton() { - DispatchQueue.main.asyncAfter(deadline: .now()) { - UIAccessibility.post(notification: .screenChanged, argument: self.publishButton) - } - } - fileprivate enum Constants { static let reuseIdentifier = "wpTableViewCell" static let textFieldReuseIdentifier = "wpTextFieldCell" diff --git a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift index 89094bd1225d..767066c41d89 100644 --- a/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift +++ b/WordPress/Classes/ViewRelated/Post/Prepublishing/PublishButton.swift @@ -15,6 +15,7 @@ struct PublishButton: View { .controlSize(.large) .disabled(isDisabled) .buttonBorderShape(.roundedRectangle(radius: 8)) + .accessibilityIdentifier("publish") switch viewModel.state { case .default: @@ -80,11 +81,11 @@ struct PublishButton: View { } final class PublishButtonViewModel: ObservableObject { - let title: String - let onSubmitTapped: () -> Void + @Published var title: String @Published var state: PublishButtonState = .default + let onSubmitTapped: () -> Void - init(title: String, onSubmitTapped: @escaping () -> Void, state: PublishButtonState = .default) { + init(title: String, state: PublishButtonState = .default, onSubmitTapped: @escaping () -> Void) { self.title = title self.onSubmitTapped = onSubmitTapped self.state = state @@ -98,8 +99,40 @@ enum PublishButtonState { case failed(title: String, details: String? = nil, onRetryTapped: (() -> Void)? = nil) struct Progress { - let completed: Int64 - let total: Int64 + var completed: Int64 + var total: Int64 + } + + /// Returns the state of the button based on the current upload progress + /// for the given post. + static func uploadingState(for post: AbstractPost, coordinator: MediaCoordinator = .shared) -> PublishButtonState? { + if post.hasFailedMedia { + return .failed(title: Strings.mediaUploadFailed, onRetryTapped: { + coordinator.uploadMedia(for: post) + }) + } + if coordinator.isUploadingMedia(for: post) { + var totalUploadProgress = Progress(completed: 0, total: 0) + var completedUploadCount = 0 + var totalUploadCount = 0 + + for media in post.media { + if let progress = coordinator.progress(for: media) { + if let uploadProgress = progress.userInfo[.uploadProgress] as? Foundation.Progress, + let filesize = media.filesize?.int64Value { + totalUploadProgress.completed += Int64(Double(filesize) * uploadProgress.fractionCompleted) + totalUploadProgress.total += filesize + } + + if progress.fractionCompleted >= 1.0 { + completedUploadCount += 1 + } + totalUploadCount += 1 + } + } + return .uploading(title: Strings.uploadingMedia + ": \(completedUploadCount) / \(totalUploadCount)", progress: totalUploadProgress) + } + return nil } } @@ -110,15 +143,17 @@ private enum Strings { } static let retry = NSLocalizedString("publishButton.retry", value: "Retry", comment: "Retry button title") + static let mediaUploadFailed = NSLocalizedString("prepublishing.mediaUploadFailed", value: "Failed to upload media", comment: "Title for an error messaage in the pre-publishing sheet") + static let uploadingMedia = NSLocalizedString("prepublishing.uploadingMedia", value: "Uploading media", comment: "Title for a publish button state in the pre-publishing sheet") } #Preview { VStack(spacing: 16) { - PublishButton(viewModel: .init(title: "Publish", onSubmitTapped: {}, state: .default)) - PublishButton(viewModel: .init(title: "Publish", onSubmitTapped: {}, state: .loading)) - PublishButton(viewModel: .init(title: "Publish", onSubmitTapped: {}, state: .uploading(title: "Uploading media...", progress: .init(completed: 100, total: 2000)))) - PublishButton(viewModel: .init(title: "Publish", onSubmitTapped: {}, state: .failed(title: "Failed to upload media"))) - PublishButton(viewModel: .init(title: "Publish", onSubmitTapped: {}, state: .failed(title: "Failed to upload media", details: "Not connected to Internet", onRetryTapped: {}))) + PublishButton(viewModel: .init(title: "Publish", state: .default) {}) + PublishButton(viewModel: .init(title: "Publish", state: .loading) {}) + PublishButton(viewModel: .init(title: "Publish", state: .uploading(title: "Uploading media...", progress: .init(completed: 100, total: 2000))) {}) + PublishButton(viewModel: .init(title: "Publish", state: .failed(title: "Failed to upload media")) {}) + PublishButton(viewModel: .init(title: "Publish", state: .failed(title: "Failed to upload media", details: "Not connected to Internet", onRetryTapped: {})) {}) } .padding() }