diff --git a/Gutenberg b/Gutenberg new file mode 160000 index 000000000000..06970afa782c --- /dev/null +++ b/Gutenberg @@ -0,0 +1 @@ +Subproject commit 06970afa782cfb58d6d20aacf6014553610601d2 diff --git a/WordPress/Classes/Services/EditorSettings.swift b/WordPress/Classes/Services/EditorSettings.swift index 8638891c5783..3e597f90372f 100644 --- a/WordPress/Classes/Services/EditorSettings.swift +++ b/WordPress/Classes/Services/EditorSettings.swift @@ -84,7 +84,7 @@ class EditorSettings: NSObject { // a configure block as a hack. // In Swift 4, we'll be able to do `instantiateEditor() -> UIViewController & PostEditor`, // and then let the caller configure the editor. - @objc func instantiatePostEditor(post: AbstractPost, configure: (PostEditor, UIViewController) -> Void) -> UIViewController { + func instantiatePostEditor(post: AbstractPost, configure: (PostEditor, UIViewController) -> Void) -> UIViewController { switch current { case .aztec: let vc = AztecPostViewController(post: post) @@ -97,7 +97,7 @@ class EditorSettings: NSObject { } } - @objc func instantiatePageEditor(page post: AbstractPost, configure: (PostEditor, UIViewController) -> Void) -> UIViewController { + func instantiatePageEditor(page post: AbstractPost, configure: (PostEditor, UIViewController) -> Void) -> UIViewController { switch current { case .aztec: let vc = AztecPostViewController(post: post) diff --git a/WordPress/Classes/ViewRelated/Aztec/Helpers/AztecVerificationPromptHelper.swift b/WordPress/Classes/ViewRelated/Aztec/Helpers/AztecVerificationPromptHelper.swift index 0c4262e63771..d53b9e4cb68b 100644 --- a/WordPress/Classes/ViewRelated/Aztec/Helpers/AztecVerificationPromptHelper.swift +++ b/WordPress/Classes/ViewRelated/Aztec/Helpers/AztecVerificationPromptHelper.swift @@ -1,14 +1,25 @@ import UIKit -class AztecVerificationPromptHelper: NSObject { + +typealias VerificationPromptCompletion = (Bool) -> () + +protocol VerificationPromptHelper { + + func updateVerificationStatus() + + func displayVerificationPrompt(from presentingViewController: UIViewController, + then: VerificationPromptCompletion?) + + func needsVerification(before action: PostEditorAction) -> Bool +} + +class AztecVerificationPromptHelper: NSObject, VerificationPromptHelper { private let accountService: AccountService private let wpComAccount: WPAccount private weak var displayedAlert: FancyAlertViewController? - private var completionBlock: AztecVerificationPromptCompletion? - - typealias AztecVerificationPromptCompletion = (Bool) -> () + private var completionBlock: VerificationPromptCompletion? @objc init?(account: WPAccount?) { guard let passedAccount = account, @@ -51,7 +62,7 @@ class AztecVerificationPromptHelper: NSObject { /// - parameter then: Completion callback to be called after the user dismisses the prompt. /// **Note**: The callback fires only when the user tapped "OK" or we silently verified the account in background. It isn't fired when user attempts to resend the verification email. @objc func displayVerificationPrompt(from presentingViewController: UIViewController, - then: AztecVerificationPromptCompletion?) { + then: VerificationPromptCompletion?) { let fancyAlert = FancyAlertViewController.verificationPromptController { [weak self] in let needsVerification = self?.wpComAccount.needsEmailVerification ?? true @@ -87,7 +98,6 @@ class AztecVerificationPromptHelper: NSObject { self?.completionBlock?(!updatedAccount.needsEmailVerification) }, failure: nil) } - } // MARK: - UIViewControllerTransitioningDelegate diff --git a/WordPress/Classes/ViewRelated/Aztec/Helpers/PostEditorUtil.swift b/WordPress/Classes/ViewRelated/Aztec/Helpers/PostEditorUtil.swift new file mode 100644 index 000000000000..955d84716944 --- /dev/null +++ b/WordPress/Classes/ViewRelated/Aztec/Helpers/PostEditorUtil.swift @@ -0,0 +1,430 @@ +import Foundation + +typealias PostEditorViewControllerType = UIViewController & PublishablePostEditor + +class PostEditorUtil: NSObject { + + fileprivate unowned let context: PostEditorViewControllerType + + fileprivate var post: AbstractPost { + return context.post + } + + var postEditorStateContext: PostEditorStateContext { + return context.postEditorStateContext + } + + /// For autosaving - The debouncer will execute local saving every defined number of seconds. + /// In this case every 0.5 second + /// + fileprivate var debouncer = Debouncer(delay: Constants.autoSavingDelay) + + init(context: PostEditorViewControllerType) { + self.context = context + + super.init() + + // The debouncer will perform this callback every 500ms in order to save the post locally with a delay. + debouncer.callback = { [weak self] in + guard let strongSelf = self else { + assertionFailure("self was nil while trying to save a post using Debouncer") + return + } + if strongSelf.post.hasLocalChanges() { + guard let context = strongSelf.post.managedObjectContext else { + return + } + ContextManager.sharedInstance().save(context) + } + } + } + + func handlePublishButtonTap() { + let action = self.postEditorStateContext.action + + publishPost( + action: action, + dismissWhenDone: action.dismissesEditor, + analyticsStat: self.postEditorStateContext.publishActionAnalyticsStat) + } + + func publishPost( + action: PostEditorAction, + dismissWhenDone: Bool, + analyticsStat: WPAnalyticsStat?) { + + // Cancel publishing if media is currently being uploaded + if !action.isAsync && !dismissWhenDone && context.isUploadingMedia { + displayMediaIsUploadingAlert() + return + } + + // If there is any failed media allow it to be removed or cancel publishing + if context.hasFailedMedia { + displayHasFailedMediaAlert(then: { + // Failed media is removed, try again. + // Note: Intentionally not tracking another analytics stat here (no appropriate one exists yet) + self.publishPost(action: action, dismissWhenDone: dismissWhenDone, analyticsStat: analyticsStat) + }) + return + } + + // If the user is trying to publish to WP.com and they haven't verified their account, prompt them to do so. + if let verificationHelper = context.verificationPromptHelper, verificationHelper.needsVerification(before: postEditorStateContext.action) { + verificationHelper.displayVerificationPrompt(from: context) { [unowned self] verifiedInBackground in + // User could've been plausibly silently verified in the background. + // If so, proceed to publishing the post as normal, otherwise save it as a draft. + if !verifiedInBackground { + self.post.status = .draft + } + + self.publishPost(action: action, dismissWhenDone: dismissWhenDone, analyticsStat: analyticsStat) + } + return + } + + let isPage = post is Page + + let publishBlock = { [unowned self] in + if action == .saveAsDraft { + self.post.status = .draft + } else if action == .publish { + if self.post.date_created_gmt == nil { + self.post.date_created_gmt = Date() + } + + if self.post.status != .publishPrivate { + self.post.status = .publish + } + } else if action == .publishNow { + self.post.date_created_gmt = Date() + + if self.post.status != .publishPrivate { + self.post.status = .publish + } + } + + if let analyticsStat = analyticsStat { + self.trackPostSave(stat: analyticsStat) + } + + if action.isAsync || dismissWhenDone { + self.asyncUploadPost(action: action) + } else { + self.uploadPost(action: action, dismissWhenDone: dismissWhenDone) + } + } + + let promoBlock = { [unowned self] in + UserDefaults.standard.asyncPromoWasDisplayed = true + + let controller = FancyAlertViewController.makeAsyncPostingAlertController(action: action, isPage: isPage, onConfirm: publishBlock) + controller.modalPresentationStyle = .custom + controller.transitioningDelegate = self + self.context.present(controller, animated: true, completion: nil) + } + + if action.isAsync { + if !UserDefaults.standard.asyncPromoWasDisplayed { + promoBlock() + } else { + displayPublishConfirmationAlert(for: action, onPublish: publishBlock) + } + } else { + publishBlock() + } + } + + fileprivate func displayMediaIsUploadingAlert() { + let alertController = UIAlertController(title: MediaUploadingAlert.title, message: MediaUploadingAlert.message, preferredStyle: .alert) + alertController.addDefaultActionWithTitle(MediaUploadingAlert.acceptTitle) + context.present(alertController, animated: true, completion: nil) + } + + fileprivate func displayHasFailedMediaAlert(then: @escaping () -> ()) { + let alertController = UIAlertController(title: FailedMediaRemovalAlert.title, message: FailedMediaRemovalAlert.message, preferredStyle: .alert) + alertController.addDefaultActionWithTitle(FailedMediaRemovalAlert.acceptTitle) { [weak self] alertAction in + self?.context.removeFailedMedia() + then() + } + + alertController.addCancelActionWithTitle(FailedMediaRemovalAlert.cancelTitle) + context.present(alertController, animated: true, completion: nil) + } + + /// Displays a publish confirmation alert with two options: "Keep Editing" and String for Action. + /// + /// - Parameters: + /// - action: Publishing action being performed + /// - dismissWhenDone: if `true`, the VC will be dismissed if the user picks "Publish". + /// + fileprivate func displayPublishConfirmationAlert(for action: PostEditorAction, onPublish publishAction: @escaping () -> ()) { + let title = action.publishingActionQuestionLabel + let keepEditingTitle = NSLocalizedString("Keep Editing", comment: "Button shown when the author is asked for publishing confirmation.") + let publishTitle = action.publishActionLabel + let style: UIAlertController.Style = UIDevice.isPad() ? .alert : .actionSheet + let alertController = UIAlertController(title: title, message: nil, preferredStyle: style) + + alertController.addCancelActionWithTitle(keepEditingTitle) + alertController.addDefaultActionWithTitle(publishTitle) { _ in + publishAction() + } + context.present(alertController, animated: true, completion: nil) + } + + private func trackPostSave(stat: WPAnalyticsStat) { + guard stat != .editorSavedDraft && stat != .editorQuickSavedDraft else { + WPAppAnalytics.track(stat, withProperties: [WPAppAnalyticsKeyEditorSource: context.analyticsEditorSource], with: post.blog) + return + } + + let originalWordCount = post.original?.content?.wordCount() ?? 0 + let wordCount = post.content?.wordCount() ?? 0 + var properties: [String: Any] = ["word_count": wordCount, WPAppAnalyticsKeyEditorSource: context.analyticsEditorSource] + if post.hasRemote() { + properties["word_diff_count"] = originalWordCount + } + + if stat == .editorPublishedPost { + properties[WPAnalyticsStatEditorPublishedPostPropertyCategory] = post.hasCategories() + properties[WPAnalyticsStatEditorPublishedPostPropertyPhoto] = post.hasPhoto() + properties[WPAnalyticsStatEditorPublishedPostPropertyTag] = post.hasTags() + properties[WPAnalyticsStatEditorPublishedPostPropertyVideo] = post.hasVideo() + } + + WPAppAnalytics.track(stat, withProperties: properties, with: post) + } + + // MARK: - Close button handling + + func cancelEditing() { + stopEditing() + + if post.canSave() && post.hasUnsavedChanges() { + showPostHasChangesAlert() + } else { + discardChangesAndUpdateGUI() + } + } + + func discardChangesAndUpdateGUI() { + discardChanges() + + dismissOrPopView(didSave: false) + } + + func discardChanges() { + guard let managedObjectContext = post.managedObjectContext, let originalPost = post.original else { + return + } + + WPAppAnalytics.track(.editorDiscardedChanges, withProperties: [WPAppAnalyticsKeyEditorSource: context.analyticsEditorSource], with: post) + + context.post = originalPost + context.post.deleteRevision() + + if context.shouldRemovePostOnDismiss { + post.remove() + } + + context.cancelUploadOfAllMedia(for: post) + ContextManager.sharedInstance().save(managedObjectContext) + } + + func showPostHasChangesAlert() { + let title = NSLocalizedString("You have unsaved changes.", comment: "Title of message with options that shown when there are unsaved changes and the author is trying to move away from the post.") + let cancelTitle = NSLocalizedString("Keep Editing", comment: "Button shown if there are unsaved changes and the author is trying to move away from the post.") + let saveTitle = NSLocalizedString("Save Draft", comment: "Button shown if there are unsaved changes and the author is trying to move away from the post.") + let updateTitle = NSLocalizedString("Update Draft", comment: "Button shown if there are unsaved changes and the author is trying to move away from an already saved draft.") + let updatePostTitle = NSLocalizedString("Update Post", comment: "Button shown if there are unsaved changes and the author is trying to move away from an already published post.") + let updatePageTitle = NSLocalizedString("Update Page", comment: "Button shown if there are unsaved changes and the author is trying to move away from an already published page.") + let discardTitle = NSLocalizedString("Discard", comment: "Button shown if there are unsaved changes and the author is trying to move away from the post.") + + let alertController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) + + // Button: Keep editing + alertController.addCancelActionWithTitle(cancelTitle) + + // Button: Save Draft/Update Draft + if post.hasLocalChanges() { + let title: String = { + if post.status == .draft { + if !post.hasRemote() { + return saveTitle + } else { + return updateTitle + } + } else if post is Page { + return updatePageTitle + } else { + return updatePostTitle + } + }() + + // The post is a local or remote draft + alertController.addDefaultActionWithTitle(title) { _ in + let action: PostEditorAction = (self.post.status == .draft) ? .saveAsDraft : .publish + self.publishPost(action: action, dismissWhenDone: true, analyticsStat: self.postEditorStateContext.publishActionAnalyticsStat) + } + } + + // Button: Discard + alertController.addDestructiveActionWithTitle(discardTitle) { _ in + self.discardChangesAndUpdateGUI() + } + + alertController.popoverPresentationController?.barButtonItem = context.navigationBarManager.closeBarButtonItem + context.present(alertController, animated: true, completion: nil) + } + +} + +extension PostEditorUtil: UIViewControllerTransitioningDelegate { + func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { + guard presented is FancyAlertViewController else { + return nil + } + + return FancyAlertPresentationController(presentedViewController: presented, presenting: presenting) + } +} + +// MARK: - Publishing + +extension PostEditorUtil { + + /// Shows the publishing overlay and starts the publishing process. + /// + fileprivate func uploadPost(action: PostEditorAction, dismissWhenDone: Bool) { + SVProgressHUD.setDefaultMaskType(.clear) + SVProgressHUD.show(withStatus: action.publishingActionLabel) + postEditorStateContext.updated(isBeingPublished: true) + + uploadPost() { [weak self] uploadedPost, error in + guard let strongSelf = self else { + return + } + strongSelf.postEditorStateContext.updated(isBeingPublished: false) + SVProgressHUD.dismiss() + + let generator = UINotificationFeedbackGenerator() + generator.prepare() + + if let error = error { + DDLogError("Error publishing post: \(error.localizedDescription)") + + SVProgressHUD.showDismissibleError(withStatus: action.publishingErrorLabel) + generator.notificationOccurred(.error) + } else if let uploadedPost = uploadedPost { + strongSelf.context.post = uploadedPost + + generator.notificationOccurred(.success) + } + + if dismissWhenDone { + strongSelf.dismissOrPopView(didSave: true) + } else { + strongSelf.createRevisionOfPost() + } + } + } + + /// Starts the publishing process. + /// + fileprivate func asyncUploadPost(action: PostEditorAction) { + postEditorStateContext.updated(isBeingPublished: true) + + mapUIContentToPostAndSave() + + post.updatePathForDisplayImageBasedOnContent() + + PostCoordinator.shared.save(post: post) + + dismissOrPopView(didSave: true, shouldShowPostEpilogue: false) + + self.postEditorStateContext.updated(isBeingPublished: false) + } + + /// Uploads the post + /// + /// - Parameters: + /// - completion: the closure to execute when the publish operation completes. + /// + private func uploadPost(completion: ((_ post: AbstractPost?, _ error: Error?) -> Void)?) { + mapUIContentToPostAndSave() + + let managedObjectContext = ContextManager.sharedInstance().mainContext + let postService = PostService(managedObjectContext: managedObjectContext) + postService.uploadPost(post, success: { uploadedPost in + completion?(uploadedPost, nil) + }) { error in + completion?(nil, error) + } + } + + func dismissOrPopView(didSave: Bool, shouldShowPostEpilogue: Bool = true) { + stopEditing() + + WPAppAnalytics.track(.editorClosed, withProperties: [WPAppAnalyticsKeyEditorSource: context.analyticsEditorSource], with: post) + + if let onClose = context.onClose { + onClose(didSave, shouldShowPostEpilogue) + } else if context.isModal() { + context.presentingViewController?.dismiss(animated: true, completion: nil) + } else { + _ = context.navigationController?.popViewController(animated: true) + } + } + + func stopEditing() { + context.view.endEditing(true) + } + + func mapUIContentToPostAndSave() { + post.postTitle = context.postTitle + post.content = context.getHTML() + debouncer.call() + } + + // TODO: Rip this out and put it into the PostService + func createRevisionOfPost() { + guard let managedObjectContext = post.managedObjectContext else { + return + } + + // Using performBlock: with the AbstractPost on the main context: + // Prevents a hang on opening this view on slow and fast devices + // by deferring the cloning and UI update. + // Slower devices have the effect of the content appearing after + // a short delay + + managedObjectContext.performAndWait { + context.post = self.post.createRevision() + ContextManager.sharedInstance().save(managedObjectContext) + } + } +} + +extension PostEditorUtil { + + struct Constants { + static let autoSavingDelay = Double(0.5) + } + + struct Analytics { + static let headerStyleValues = ["none", "h1", "h2", "h3", "h4", "h5", "h6"] + } + + struct MediaUploadingAlert { + static let title = NSLocalizedString("Uploading media", comment: "Title for alert when trying to save/exit a post before media upload process is complete.") + static let message = NSLocalizedString("You are currently uploading media. Please wait until this completes.", comment: "This is a notification the user receives if they are trying to save a post (or exit) before the media upload process is complete.") + static let acceptTitle = NSLocalizedString("OK", comment: "Accept Action") + } + + struct FailedMediaRemovalAlert { + static let title = NSLocalizedString("Uploads failed", comment: "Title for alert when trying to save post with failed media items") + static let message = NSLocalizedString("Some media uploads failed. This action will remove all failed media from the post.\nSave anyway?", comment: "Confirms with the user if they save the post all media that failed to upload will be removed from it.") + static let acceptTitle = NSLocalizedString("Yes", comment: "Accept Action") + static let cancelTitle = NSLocalizedString("Not Now", comment: "Nicer dialog answer for \"No\".") + } +} diff --git a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift index 5b621759d108..799815a369ac 100644 --- a/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift +++ b/WordPress/Classes/ViewRelated/Aztec/ViewControllers/AztecPostViewController.swift @@ -15,7 +15,9 @@ import MobileCoreServices // MARK: - Aztec's Native Editor! // -class AztecPostViewController: UIViewController, PostEditor { +class AztecPostViewController: UIViewController, PublishablePostEditor { + + // MARK: - PublishablePostEditor conformance /// Closure to be executed when the editor gets closed. /// Pass `false` for `showPostEpilogue` to prevent the post epilogue @@ -23,12 +25,49 @@ class AztecPostViewController: UIViewController, PostEditor { /// var onClose: ((_ changesSaved: Bool, _ showPostEpilogue: Bool) -> ())? + /// Verification Prompt Helper + /// + /// - Returns: `nil` when there's no need for showing the verification prompt. + var verificationPromptHelper: VerificationPromptHelper? { + return aztecVerificationPromptHelper + } + + fileprivate lazy var aztecVerificationPromptHelper: AztecVerificationPromptHelper? = { + return AztecVerificationPromptHelper(account: self.post.blog.account) + }() + + var postTitle: String { + get { + return titleTextField.text + } + set { + titleTextField.text = newValue + } + } + + var isUploadingMedia: Bool { + return mediaCoordinator.isUploadingMedia(for: post) + } + + var analyticsEditorSource: String { + return Analytics.editorSource + } /// Indicates if Aztec was launched for Photo Posting /// var isOpenedDirectlyForPhotoPost = false - private let navigationBarManager = PostEditorNavigationBarManager() + let navigationBarManager = PostEditorNavigationBarManager() + + func cancelUploadOfAllMedia(for post: AbstractPost) { + mediaCoordinator.cancelUploadOfAllMedia(for: post) + } + + // MARK: - fileprivate & private variables + + fileprivate lazy var postEditorUtil = { + return PostEditorUtil(context: self) + }() /// Format Bar /// @@ -231,7 +270,7 @@ class AztecPostViewController: UIViewController, PostEditor { /// Post being currently edited /// - fileprivate(set) var post: AbstractPost { + var post: AbstractPost { didSet { removeObservers(fromPost: oldValue) addObservers(toPost: post) @@ -250,7 +289,7 @@ class AztecPostViewController: UIViewController, PostEditor { /// Boolean indicating whether the post should be removed whenever the changes are discarded, or not. /// - fileprivate var shouldRemovePostOnDismiss = false + fileprivate(set) var shouldRemovePostOnDismiss = false /// Media Library Data Source @@ -291,7 +330,7 @@ class AztecPostViewController: UIViewController, PostEditor { /// Maintainer of state for editor - like for post button /// - fileprivate lazy var postEditorStateContext: PostEditorStateContext = { + fileprivate(set) lazy var postEditorStateContext: PostEditorStateContext = { return self.createEditorStateContext(for: self.post) }() @@ -326,13 +365,6 @@ class AztecPostViewController: UIViewController, PostEditor { fileprivate var originalTrailingBarButtonGroup = [UIBarButtonItemGroup]() - /// Verification Prompt Helper - /// - /// - Returns: `nil` when there's no need for showing the verification prompt. - fileprivate lazy var verificationPromptHelper: AztecVerificationPromptHelper? = { - return AztecVerificationPromptHelper(account: self.post.blog.account) - }() - /// The view to show when media picker has no assets to show. /// fileprivate let noResultsView = NoResultsViewController.controller() @@ -351,11 +383,6 @@ class AztecPostViewController: UIViewController, PostEditor { /// private var mediaPreviewHelper: MediaPreviewHelper? = nil - /// For autosaving - The debouncer will execute local saving every defined number of seconds. - /// In this case every 0.5 second - /// - var debouncer = Debouncer(delay: Constants.autoSavingDelay) - // MARK: - Initializers /// Initializer @@ -377,20 +404,6 @@ class AztecPostViewController: UIViewController, PostEditor { PostCoordinator.shared.cancelAnyPendingSaveOf(post: post) addObservers(toPost: post) - - // The debouncer will perform this callback every 500ms in order to save the post locally with a delay. - debouncer.callback = { [weak self] in - guard let strongSelf = self else { - assertionFailure("self was nil while trying to save a post using Debouncer") - return - } - if strongSelf.post.hasLocalChanges() { - guard let context = strongSelf.post.managedObjectContext else { - return - } - ContextManager.sharedInstance().save(context) - } - } } required init?(coder aDecoder: NSCoder) { @@ -421,7 +434,7 @@ class AztecPostViewController: UIViewController, PostEditor { WPFontManager.loadNotoFontFamily() registerAttachmentImageProviders() - createRevisionOfPost() + postEditorUtil.createRevisionOfPost() // Setup configureNavigationBar() @@ -551,24 +564,9 @@ class AztecPostViewController: UIViewController, PostEditor { /// Returns a new Editor Context for a given Post instance. /// private func createEditorStateContext(for post: AbstractPost) -> PostEditorStateContext { - var originalPostStatus: BasePost.Status? = nil - - if let originalPost = post.original, let postStatus = originalPost.status, originalPost.hasRemote() { - originalPostStatus = postStatus - } - - // Self-hosted non-Jetpack blogs have no capabilities, so we'll default - // to showing Publish Now instead of Submit for Review. - // - let userCanPublish = post.blog.capabilities != nil ? post.blog.isPublishingPostsAllowed() : true - - return PostEditorStateContext(originalPostStatus: originalPostStatus, - userCanPublish: userCanPublish, - publishDate: post.dateCreated, - delegate: self) + return PostEditorStateContext(post: post, delegate: self) } - // MARK: - Configuration Methods override func updateViewConstraints() { @@ -1004,12 +1002,7 @@ extension AztecPostViewController: AztecNavigationControllerDelegate { // extension AztecPostViewController { @IBAction func publishButtonTapped(sender: UIButton) { - let action = self.postEditorStateContext.action - - publishPost( - action: action, - dismissWhenDone: action.dismissesEditor, - analyticsStat: self.postEditorStateContext.publishActionAnalyticsStat) + postEditorUtil.handlePublishButtonTap() } @IBAction func secondaryPublishButtonTapped() { @@ -1023,7 +1016,7 @@ extension AztecPostViewController { let secondaryStat = self.postEditorStateContext.secondaryPublishActionAnalyticsStat let publishPostClosure = { [unowned self] in - self.publishPost( + self.postEditorUtil.publishPost( action: action, dismissWhenDone: action.dismissesEditor, analyticsStat: secondaryStat) @@ -1036,148 +1029,8 @@ extension AztecPostViewController { } } - func showPostHasChangesAlert() { - let title = NSLocalizedString("You have unsaved changes.", comment: "Title of message with options that shown when there are unsaved changes and the author is trying to move away from the post.") - let cancelTitle = NSLocalizedString("Keep Editing", comment: "Button shown if there are unsaved changes and the author is trying to move away from the post.") - let saveTitle = NSLocalizedString("Save Draft", comment: "Button shown if there are unsaved changes and the author is trying to move away from the post.") - let updateTitle = NSLocalizedString("Update Draft", comment: "Button shown if there are unsaved changes and the author is trying to move away from an already saved draft.") - let updatePostTitle = NSLocalizedString("Update Post", comment: "Button shown if there are unsaved changes and the author is trying to move away from an already published post.") - let updatePageTitle = NSLocalizedString("Update Page", comment: "Button shown if there are unsaved changes and the author is trying to move away from an already published page.") - let discardTitle = NSLocalizedString("Discard", comment: "Button shown if there are unsaved changes and the author is trying to move away from the post.") - - let alertController = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) - - // Button: Keep editing - alertController.addCancelActionWithTitle(cancelTitle) - - // Button: Save Draft/Update Draft - if post.hasLocalChanges() { - let title: String = { - if post.status == .draft { - if !post.hasRemote() { - return saveTitle - } else { - return updateTitle - } - } else if post is Page { - return updatePageTitle - } else { - return updatePostTitle - } - }() - - // The post is a local or remote draft - alertController.addDefaultActionWithTitle(title) { _ in - let action: PostEditorAction = (self.post.status == .draft) ? .saveAsDraft : .publish - self.publishPost(action: action, dismissWhenDone: true, analyticsStat: self.postEditorStateContext.publishActionAnalyticsStat) - } - } - - // Button: Discard - alertController.addDestructiveActionWithTitle(discardTitle) { _ in - self.discardChangesAndUpdateGUI() - } - - alertController.popoverPresentationController?.barButtonItem = navigationBarManager.closeBarButtonItem - present(alertController, animated: true, completion: nil) - } - - private func publishPost( - action: PostEditorAction, - dismissWhenDone: Bool, - analyticsStat: WPAnalyticsStat?) { - - // Cancel publishing if media is currently being uploaded - if !action.isAsync && !dismissWhenDone && mediaCoordinator.isUploadingMedia(for: post) { - displayMediaIsUploadingAlert() - return - } - - // If there is any failed media allow it to be removed or cancel publishing - if hasFailedMedia { - displayHasFailedMediaAlert(then: { - // Failed media is removed, try again. - // Note: Intentionally not tracking another analytics stat here (no appropriate one exists yet) - self.publishPost(action: action, dismissWhenDone: dismissWhenDone, analyticsStat: analyticsStat) - }) - return - } - - // If the user is trying to publish to WP.com and they haven't verified their account, prompt them to do so. - if let verificationHelper = verificationPromptHelper, verificationHelper.needsVerification(before: postEditorStateContext.action) { - verificationHelper.displayVerificationPrompt(from: self) { [unowned self] verifiedInBackground in - // User could've been plausibly silently verified in the background. - // If so, proceed to publishing the post as normal, otherwise save it as a draft. - if !verifiedInBackground { - self.post.status = .draft - } - - self.publishPost(action: action, dismissWhenDone: dismissWhenDone, analyticsStat: analyticsStat) - } - return - } - - let isPage = post is Page - - let publishBlock = { [unowned self] in - if action == .saveAsDraft { - self.post.status = .draft - } else if action == .publish { - if self.post.date_created_gmt == nil { - self.post.date_created_gmt = Date() - } - - if self.post.status != .publishPrivate { - self.post.status = .publish - } - } else if action == .publishNow { - self.post.date_created_gmt = Date() - - if self.post.status != .publishPrivate { - self.post.status = .publish - } - } - - - if let analyticsStat = analyticsStat { - self.trackPostSave(stat: analyticsStat) - } - - if action.isAsync || dismissWhenDone { - self.asyncUploadPost(action: action) - } else { - self.uploadPost(action: action, dismissWhenDone: dismissWhenDone) - } - } - - let promoBlock = { [unowned self] in - UserDefaults.standard.asyncPromoWasDisplayed = true - - let controller = FancyAlertViewController.makeAsyncPostingAlertController(action: action, isPage: isPage, onConfirm: publishBlock) - controller.modalPresentationStyle = .custom - controller.transitioningDelegate = self - self.present(controller, animated: true, completion: nil) - } - - if action.isAsync { - if !UserDefaults.standard.asyncPromoWasDisplayed { - promoBlock() - } else { - displayPublishConfirmationAlert(for: action, onPublish: publishBlock) - } - } else { - publishBlock() - } - } - - func displayMediaIsUploadingAlert() { - let alertController = UIAlertController(title: MediaUploadingAlert.title, message: MediaUploadingAlert.message, preferredStyle: .alert) - alertController.addDefaultActionWithTitle(MediaUploadingAlert.acceptTitle) - present(alertController, animated: true, completion: nil) - } - @IBAction func closeWasPressed() { - cancelEditing() + postEditorUtil.cancelEditing() } @IBAction func blogPickerWasPressed() { @@ -1194,28 +1047,6 @@ extension AztecPostViewController { displayMoreSheet() } - private func trackPostSave(stat: WPAnalyticsStat) { - guard stat != .editorSavedDraft && stat != .editorQuickSavedDraft else { - WPAppAnalytics.track(stat, withProperties: [WPAppAnalyticsKeyEditorSource: Analytics.editorSource], with: post.blog) - return - } - - let originalWordCount = post.original?.content?.wordCount() ?? 0 - let wordCount = post.content?.wordCount() ?? 0 - var properties: [String: Any] = ["word_count": wordCount, WPAppAnalyticsKeyEditorSource: Analytics.editorSource] - if post.hasRemote() { - properties["word_diff_count"] = originalWordCount - } - - if stat == .editorPublishedPost { - properties[WPAnalyticsStatEditorPublishedPostPropertyCategory] = post.hasCategories() - properties[WPAnalyticsStatEditorPublishedPostPropertyPhoto] = post.hasPhoto() - properties[WPAnalyticsStatEditorPublishedPostPropertyTag] = post.hasTags() - properties[WPAnalyticsStatEditorPublishedPostPropertyVideo] = post.hasVideo() - } - - WPAppAnalytics.track(stat, withProperties: properties, with: post) - } } @@ -1405,16 +1236,7 @@ private extension AztecPostViewController { navigationController?.pushViewController(previewController, animated: true) } - func displayHasFailedMediaAlert(then: @escaping () -> ()) { - let alertController = UIAlertController(title: FailedMediaRemovalAlert.title, message: FailedMediaRemovalAlert.message, preferredStyle: .alert) - alertController.addDefaultActionWithTitle(FailedMediaRemovalAlert.acceptTitle) { alertAction in - self.removeFailedMedia() - then() - } - alertController.addCancelActionWithTitle(FailedMediaRemovalAlert.cancelTitle) - present(alertController, animated: true, completion: nil) - } @IBAction func displayCancelMediaUploads() { let alertController = UIAlertController(title: MediaUploadingCancelAlert.title, message: MediaUploadingCancelAlert.message, preferredStyle: .alert) @@ -1425,26 +1247,6 @@ private extension AztecPostViewController { present(alertController, animated: true, completion: nil) return } - - /// Displays a publish confirmation alert with two options: "Keep Editing" and String for Action. - /// - /// - Parameters: - /// - action: Publishing action being performed - /// - dismissWhenDone: if `true`, the VC will be dismissed if the user picks "Publish". - /// - func displayPublishConfirmationAlert(for action: PostEditorAction, onPublish publishAction: @escaping () -> ()) { - let title = action.publishingActionQuestionLabel - let keepEditingTitle = NSLocalizedString("Keep Editing", comment: "Button shown when the author is asked for publishing confirmation.") - let publishTitle = action.publishActionLabel - let style: UIAlertController.Style = UIDevice.isPad() ? .alert : .actionSheet - let alertController = UIAlertController(title: title, message: nil, preferredStyle: style) - - alertController.addCancelActionWithTitle(keepEditingTitle) - alertController.addDefaultActionWithTitle(publishTitle) { _ in - publishAction() - } - present(alertController, animated: true, completion: nil) - } } @@ -1530,7 +1332,7 @@ extension AztecPostViewController: UITextViewDelegate { } func textViewDidChange(_ textView: UITextView) { - mapUIContentToPostAndSave() + postEditorUtil.mapUIContentToPostAndSave() refreshPlaceholderVisibility() switch textView { @@ -1632,7 +1434,7 @@ extension AztecPostViewController: UITextViewDelegate { // extension AztecPostViewController { func titleTextFieldDidChange(_ textField: UITextField) { - mapUIContentToPostAndSave() + postEditorUtil.mapUIContentToPostAndSave() editorContentWasUpdated() } } @@ -2510,24 +2312,6 @@ private extension AztecPostViewController { // private extension AztecPostViewController { - // TODO: Rip this out and put it into the PostService - func createRevisionOfPost() { - guard let context = post.managedObjectContext else { - return - } - - // Using performBlock: with the AbstractPost on the main context: - // Prevents a hang on opening this view on slow and fast devices - // by deferring the cloning and UI update. - // Slower devices have the effect of the content appearing after - // a short delay - - context.performAndWait { - self.post = self.post.createRevision() - ContextManager.sharedInstance().save(context) - } - } - // TODO: Rip this and put it into PostService, as well func recreatePostRevision(in blog: Blog) { let shouldCreatePage = post is Page @@ -2545,67 +2329,19 @@ private extension AztecPostViewController { target.tags = source.tags } - discardChanges() + postEditorUtil.discardChanges() post = newPost - createRevisionOfPost() + postEditorUtil.createRevisionOfPost() RecentSitesService().touch(blog: blog) // TODO: Add this snippet, if needed, once we've relocated this helper to PostService //[self syncOptionsIfNecessaryForBlog:blog afterBlogChanged:YES]; } - func cancelEditing() { - stopEditing() - - if post.canSave() && post.hasUnsavedChanges() { - showPostHasChangesAlert() - } else { - discardChangesAndUpdateGUI() - } - } - func stopEditing() { view.endEditing(true) } - func discardChanges() { - guard let context = post.managedObjectContext, let originalPost = post.original else { - return - } - - WPAppAnalytics.track(.editorDiscardedChanges, withProperties: [WPAppAnalyticsKeyEditorSource: Analytics.editorSource], with: post) - - post = originalPost - post.deleteRevision() - - if shouldRemovePostOnDismiss { - post.remove() - } - - mediaCoordinator.cancelUploadOfAllMedia(for: post) - ContextManager.sharedInstance().save(context) - } - - func discardChangesAndUpdateGUI() { - discardChanges() - - dismissOrPopView(didSave: false) - } - - func dismissOrPopView(didSave: Bool, shouldShowPostEpilogue: Bool = true) { - stopEditing() - - WPAppAnalytics.track(.editorClosed, withProperties: [WPAppAnalyticsKeyEditorSource: Analytics.editorSource], with: post) - - if let onClose = onClose { - onClose(didSave, shouldShowPostEpilogue) - } else if isModal() { - presentingViewController?.dismiss(animated: true, completion: nil) - } else { - _ = navigationController?.popViewController(animated: true) - } - } - func contentByStrippingMediaAttachments() -> String { if mode == .html { setHTML(htmlTextView.text) @@ -2620,83 +2356,6 @@ private extension AztecPostViewController { return strippedHTML } - - func mapUIContentToPostAndSave() { - post.postTitle = titleTextField.text - post.content = getHTML() - debouncer.call() - } -} - -// MARK: - Publishing - -private extension AztecPostViewController { - - /// Shows the publishing overlay and starts the publishing process. - /// - func uploadPost(action: PostEditorAction, dismissWhenDone: Bool) { - SVProgressHUD.setDefaultMaskType(.clear) - SVProgressHUD.show(withStatus: action.publishingActionLabel) - postEditorStateContext.updated(isBeingPublished: true) - - uploadPost() { uploadedPost, error in - self.postEditorStateContext.updated(isBeingPublished: false) - SVProgressHUD.dismiss() - - let generator = UINotificationFeedbackGenerator() - generator.prepare() - - if let error = error { - DDLogError("Error publishing post: \(error.localizedDescription)") - - SVProgressHUD.showDismissibleError(withStatus: action.publishingErrorLabel) - generator.notificationOccurred(.error) - } else if let uploadedPost = uploadedPost { - self.post = uploadedPost - - generator.notificationOccurred(.success) - } - - if dismissWhenDone { - self.dismissOrPopView(didSave: true) - } else { - self.createRevisionOfPost() - } - } - } - - /// Starts the publishing process. - /// - func asyncUploadPost(action: PostEditorAction) { - postEditorStateContext.updated(isBeingPublished: true) - - mapUIContentToPostAndSave() - - post.updatePathForDisplayImageBasedOnContent() - - PostCoordinator.shared.save(post: post) - - dismissOrPopView(didSave: true, shouldShowPostEpilogue: false) - - self.postEditorStateContext.updated(isBeingPublished: false) - } - - /// Uploads the post - /// - /// - Parameters: - /// - completion: the closure to execute when the publish operation completes. - /// - private func uploadPost(completion: ((_ post: AbstractPost?, _ error: Error?) -> Void)?) { - mapUIContentToPostAndSave() - - let managedObjectContext = ContextManager.sharedInstance().mainContext - let postService = PostService(managedObjectContext: managedObjectContext) - postService.uploadPost(post, success: { uploadedPost in - completion?(uploadedPost, nil) - }) { error in - completion?(nil, error) - } - } } // MARK: - Computed Properties @@ -3085,11 +2744,11 @@ extension AztecPostViewController { return failedIDs } - fileprivate var hasFailedMedia: Bool { + var hasFailedMedia: Bool { return !failedMediaIDs.isEmpty } - fileprivate func removeFailedMedia() { + func removeFailedMedia() { for mediaID in failedMediaIDs { if let attachment = self.findAttachment(withUploadID: mediaID) { richTextView.remove(attachmentID: attachment.identifier) @@ -3801,8 +3460,6 @@ extension AztecPostViewController { static let formatBarMediaButtonRotationDuration: TimeInterval = 0.3 static let formatBarMediaButtonRotationAngle: CGFloat = .pi / 4.0 } - - static let autoSavingDelay = Double(0.5) } struct MoreSheetAlert { @@ -3865,19 +3522,6 @@ extension AztecPostViewController { static let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel Action") } - struct MediaUploadingAlert { - static let title = NSLocalizedString("Uploading media", comment: "Title for alert when trying to save/exit a post before media upload process is complete.") - static let message = NSLocalizedString("You are currently uploading media. Please wait until this completes.", comment: "This is a notification the user receives if they are trying to save a post (or exit) before the media upload process is complete.") - static let acceptTitle = NSLocalizedString("OK", comment: "Accept Action") - } - - struct FailedMediaRemovalAlert { - static let title = NSLocalizedString("Uploads failed", comment: "Title for alert when trying to save post with failed media items") - static let message = NSLocalizedString("Some media uploads failed. This action will remove all failed media from the post.\nSave anyway?", comment: "Confirms with the user if they save the post all media that failed to upload will be removed from it.") - static let acceptTitle = NSLocalizedString("Yes", comment: "Accept Action") - static let cancelTitle = NSLocalizedString("Not Now", comment: "Nicer dialog answer for \"No\".") - } - struct MediaUploadingCancelAlert { static let title = NSLocalizedString("Cancel media uploads", comment: "Dialog box title for when the user is cancelling an upload.") static let message = NSLocalizedString("You are currently uploading media. This action will cancel uploads in progress.\n\nAre you sure?", comment: "This prompt is displayed when the user attempts to stop media uploads in the post editor.") @@ -3892,16 +3536,6 @@ extension AztecPostViewController { } -extension AztecPostViewController: UIViewControllerTransitioningDelegate { - func presentationController(forPresented presented: UIViewController, presenting: UIViewController?, source: UIViewController) -> UIPresentationController? { - guard presented is FancyAlertViewController else { - return nil - } - - return FancyAlertPresentationController(presentedViewController: presented, presenting: presenting) - } -} - extension AztecPostViewController: PostEditorNavigationBarManagerDelegate { var publishButtonText: String { return self.postEditorStateContext.publishButtonText diff --git a/WordPress/Classes/ViewRelated/Post/PostEditor.swift b/WordPress/Classes/ViewRelated/Post/PostEditor.swift index cf41f558958a..f68cae4598c5 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditor.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditor.swift @@ -4,14 +4,14 @@ import Gridicons /// Common interface to all editors /// -@objc protocol PostEditor: class { +protocol PostEditor: class { /// Initialize editor with a post. /// init(post: AbstractPost) /// The post being edited. /// - var post: AbstractPost { get } + var post: AbstractPost { get set } /// Closure to be executed when the editor gets closed. /// @@ -22,6 +22,49 @@ import Gridicons var isOpenedDirectlyForPhotoPost: Bool { get set } } +protocol PublishablePostEditor: PostEditor { + /// Boolean indicating whether the post should be removed whenever the changes are discarded, or not. + /// + var shouldRemovePostOnDismiss: Bool { get } + + /// Cancels all ongoing uploads + /// + ///TODO: We won't need this once media uploading is extracted to PostEditorUtil + func cancelUploadOfAllMedia(for post: AbstractPost) + + /// Whether the editor has failed media or not + /// + //TODO: We won't need this once media uploading is extracted to PostEditorUtil + var hasFailedMedia: Bool { get } + + //TODO: We won't need this once media uploading is extracted to PostEditorUtil + var isUploadingMedia: Bool { get } + + //TODO: We won't need this once media uploading is extracted to PostEditorUtil + //TODO: Otherwise the signature needs refactoring, it is too ambiguous for a protocol method + func removeFailedMedia() + + /// Verification prompt helper + var verificationPromptHelper: VerificationPromptHelper? { get } + + /// Post editor state context + var postEditorStateContext: PostEditorStateContext { get } + + /// Update editor UI with given html + func setHTML(_ html: String) + + /// Return the current html in the editor + func getHTML() -> String + + /// Title of the post + var postTitle: String { get set } + + /// Describes the editor type to be used in analytics reporting + var analyticsEditorSource: String { get } + + var navigationBarManager: PostEditorNavigationBarManager { get } +} + protocol PostEditorNavigationBarManagerDelegate: class { var publishButtonText: String { get } var isPublishButtonEnabled: Bool { get } diff --git a/WordPress/Classes/ViewRelated/Post/PostEditorState.swift b/WordPress/Classes/ViewRelated/Post/PostEditorState.swift index 44bb80fc0cab..ccdc63ca72fc 100644 --- a/WordPress/Classes/ViewRelated/Post/PostEditorState.swift +++ b/WordPress/Classes/ViewRelated/Post/PostEditorState.swift @@ -187,6 +187,25 @@ public class PostEditorStateContext { } } + convenience init(post: AbstractPost, + delegate: PostEditorStateContextDelegate) { + var originalPostStatus: BasePost.Status? = nil + + if let originalPost = post.original, let postStatus = originalPost.status, originalPost.hasRemote() { + originalPostStatus = postStatus + } + + // Self-hosted non-Jetpack blogs have no capabilities, so we'll default + // to showing Publish Now instead of Submit for Review. + // + let userCanPublish = post.blog.capabilities != nil ? post.blog.isPublishingPostsAllowed() : true + + self.init(originalPostStatus: originalPostStatus, + userCanPublish: userCanPublish, + publishDate: post.dateCreated, + delegate: delegate) + } + /// The default initializer /// /// - Parameters: @@ -195,7 +214,7 @@ public class PostEditorStateContext { /// - publishDate: The post publish date /// - delegate: Delegate for listening to change in state for the editor /// - init(originalPostStatus: BasePost.Status? = nil, userCanPublish: Bool = true, publishDate: Date? = nil, delegate: PostEditorStateContextDelegate) { + required init(originalPostStatus: BasePost.Status? = nil, userCanPublish: Bool = true, publishDate: Date? = nil, delegate: PostEditorStateContextDelegate) { self.originalPostStatus = originalPostStatus self.currentPostStatus = originalPostStatus self.userCanPublish = userCanPublish diff --git a/WordPress/RN/Gutenberg/GutenbergController.swift b/WordPress/RN/Gutenberg/GutenbergController.swift index 8655034d5847..64e4736e1b68 100644 --- a/WordPress/RN/Gutenberg/GutenbergController.swift +++ b/WordPress/RN/Gutenberg/GutenbergController.swift @@ -2,21 +2,83 @@ import UIKit import React import WPMediaPicker -class GutenbergController: UIViewController, PostEditor { +class GutenbergController: UIViewController, PublishablePostEditor { + + private struct Analytics { + static let editorSource = "gutenberg" + } + + enum RequestHTMLReason { + case publish + case close + } + + var html: String { + set { + post.content = newValue + } + get { + return post.content ?? "" + } + } + + var postTitle: String + + /// Maintainer of state for editor - like for post button + /// + private(set) lazy var postEditorStateContext: PostEditorStateContext = { + return PostEditorStateContext(post: post, delegate: self) + }() + + var verificationPromptHelper: VerificationPromptHelper? + + var analyticsEditorSource: String { + return Analytics.editorSource + } var onClose: ((Bool, Bool) -> Void)? var isOpenedDirectlyForPhotoPost: Bool = false - let post: AbstractPost - let gutenberg: Gutenberg + var isUploadingMedia: Bool { + return false + } - let navBarManager = PostEditorNavigationBarManager() + func removeFailedMedia() { + // TODO + } + + var shouldRemovePostOnDismiss: Bool = false + + func cancelUploadOfAllMedia(for post: AbstractPost) { + //TODO + } + + func setHTML(_ html: String) { + self.html = html + //TODO: Update Gutenberg UI + } + + func getHTML() -> String { + return html + } + + var post: AbstractPost { + didSet { + postEditorStateContext = PostEditorStateContext(post: post, delegate: self) + } + } + + let navigationBarManager = PostEditorNavigationBarManager() lazy var mediaPickerHelper: GutenbergMediaPickerHelper = { return GutenbergMediaPickerHelper(context: self, post: post) }() + var hasFailedMedia: Bool { + return false + } + var mainContext: NSManagedObjectContext { return ContextManager.sharedInstance().mainContext } @@ -30,15 +92,28 @@ class GutenbergController: UIViewController, PostEditor { return currentBlogCount <= 1 || post.hasRemote() } + private var requestHTMLReason: RequestHTMLReason? + + private lazy var postEditorUtil = { + return PostEditorUtil(context: self) + }() + + private let gutenberg: Gutenberg + required init(post: AbstractPost) { guard let post = post as? Post else { fatalError() } self.post = post - self.gutenberg = Gutenberg(props: ["initialData": post.content ?? ""]) + self.postTitle = post.postTitle ?? "" + self.gutenberg = Gutenberg(props: ["initialData": self.post.content ?? ""]) + self.verificationPromptHelper = AztecVerificationPromptHelper(account: self.post.blog.account) + self.shouldRemovePostOnDismiss = post.hasNeverAttemptedToUpload() + super.init(nibName: nil, bundle: nil) - navBarManager.delegate = self + PostCoordinator.shared.cancelAnyPendingSaveOf(post: post) + navigationBarManager.delegate = self } required init?(coder aDecoder: NSCoder) { @@ -55,16 +130,22 @@ class GutenbergController: UIViewController, PostEditor { override func viewDidLoad() { super.viewDidLoad() + postEditorUtil.createRevisionOfPost() configureNavigationBar() reloadBlogPickerButton() gutenberg.delegate = self } + override func viewWillAppear(_ animated: Bool) { + super.viewWillAppear(animated) + verificationPromptHelper?.updateVerificationStatus() + } + func configureNavigationBar() { navigationController?.navigationBar.isTranslucent = false navigationController?.navigationBar.accessibilityIdentifier = "Gutenberg Editor Navigation Bar" - navigationItem.leftBarButtonItems = navBarManager.leftBarButtonItems - navigationItem.rightBarButtonItems = navBarManager.rightBarButtonItems + navigationItem.leftBarButtonItems = navigationBarManager.leftBarButtonItems + navigationItem.rightBarButtonItems = navigationBarManager.rightBarButtonItems } func reloadBlogPickerButton() { @@ -73,32 +154,7 @@ class GutenbergController: UIViewController, PostEditor { pickerTitle = blogName } - navBarManager.reloadBlogPickerButton(with: pickerTitle, enabled: !isSingleSiteMode) - } - - @objc private func close(sender: UIBarButtonItem) { - close(didSave: false) - } - - private func close(didSave: Bool) { - onClose?(didSave, false) - } -} - -extension GutenbergController { - func closeButtonPressed() { - close(didSave: false) - } - - func saveButtonPressed(with content: String) { - guard let post = post as? Post else { - return - } - post.content = content - PostCoordinator.shared.save(post: post) - DispatchQueue.main.async { [weak self] in - self?.close(didSave: true) - } + navigationBarManager.reloadBlogPickerButton(with: pickerTitle, enabled: !isSingleSiteMode) } } @@ -111,17 +167,47 @@ extension GutenbergController: GutenbergBridgeDelegate { } func gutenbergDidProvideHTML(_ html: String, changed: Bool) { + self.html = html + + // TODO: currently we don't need to set this because Update button is always active + // but in the future we might need this + // postEditorStateContext.updated(hasChanges: changed) + + if let reason = requestHTMLReason { + requestHTMLReason = nil // clear the reason + switch reason { + case .publish: + postEditorUtil.handlePublishButtonTap() + case .close: + postEditorUtil.cancelEditing() + } + } + } +} +extension GutenbergController: PostEditorStateContextDelegate { + + func context(_ context: PostEditorStateContext, didChangeAction: PostEditorAction) { + reloadPublishButton() + } + + func context(_ context: PostEditorStateContext, didChangeActionAllowed: Bool) { + reloadPublishButton() } + + func reloadPublishButton() { + navigationBarManager.reloadPublishButton() + } + } extension GutenbergController: PostEditorNavigationBarManagerDelegate { var publishButtonText: String { - return "Publish" + return postEditorStateContext.publishButtonText } var isPublishButtonEnabled: Bool { - return true + return postEditorStateContext.isPublishButtonEnabled } var uploadingButtonSize: CGSize { @@ -129,7 +215,8 @@ extension GutenbergController: PostEditorNavigationBarManagerDelegate { } func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { - close(didSave: false) + requestHTMLReason = .close + gutenberg.requestHTML() } func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { @@ -141,6 +228,7 @@ extension GutenbergController: PostEditorNavigationBarManagerDelegate { } func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { + requestHTMLReason = .publish gutenberg.requestHTML() } diff --git a/WordPress/WordPress.xcodeproj/project.pbxproj b/WordPress/WordPress.xcodeproj/project.pbxproj index 16a39a955aa5..ca997e8510ec 100644 --- a/WordPress/WordPress.xcodeproj/project.pbxproj +++ b/WordPress/WordPress.xcodeproj/project.pbxproj @@ -690,6 +690,7 @@ 85ED988817DFA00000090D0B /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 85ED988717DFA00000090D0B /* Images.xcassets */; }; 85F8E19D1B018698000859BB /* PushAuthenticationServiceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85F8E19C1B018698000859BB /* PushAuthenticationServiceTests.swift */; }; 91D8364121946EFB008340B2 /* GutenbergMediaPickerHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91D8364021946EFB008340B2 /* GutenbergMediaPickerHelper.swift */; }; + 91EB11BC219B19700057F17D /* PostEditorUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91EB11BB219B19700057F17D /* PostEditorUtil.swift */; }; 930D5C8B1ED3298F00D9A771 /* WordPressComStatsiOS.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 930D5C8A1ED3298F00D9A771 /* WordPressComStatsiOS.framework */; }; 930F09171C7D110E00995926 /* ShareExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 930F09161C7D110E00995926 /* ShareExtensionService.swift */; }; 930F09191C7E1C1E00995926 /* ShareExtensionService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 930F09161C7D110E00995926 /* ShareExtensionService.swift */; }; @@ -2365,6 +2366,7 @@ 8CE5BBD00FF1470AC4B88247 /* Pods_WordPressTodayWidget.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_WordPressTodayWidget.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 8D1107310486CEB800E47090 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 91D8364021946EFB008340B2 /* GutenbergMediaPickerHelper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GutenbergMediaPickerHelper.swift; sourceTree = ""; }; + 91EB11BB219B19700057F17D /* PostEditorUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostEditorUtil.swift; sourceTree = ""; }; 930284B618EAF7B600CB0BF4 /* LocalCoreDataService.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = LocalCoreDataService.h; sourceTree = ""; }; 93069F54176237A4000C966D /* ActivityLogViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ActivityLogViewController.h; sourceTree = ""; }; 93069F55176237A4000C966D /* ActivityLogViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ActivityLogViewController.m; sourceTree = ""; }; @@ -3841,7 +3843,7 @@ name = Products; sourceTree = ""; }; - 29B97314FDCFA39411CA2CEA = { + 29B97314FDCFA39411CA2CEA /* CustomTemplate */ = { isa = PBXGroup; children = ( 7E3E9B5F2177A44400FD5797 /* RN */, @@ -4125,6 +4127,7 @@ isa = PBXGroup; children = ( 40F88F631F86C26600AE3FAF /* AztecVerificationPromptHelper.swift */, + 91EB11BB219B19700057F17D /* PostEditorUtil.swift */, ); path = Helpers; sourceTree = ""; @@ -7490,7 +7493,7 @@ bg, sk, ); - mainGroup = 29B97314FDCFA39411CA2CEA; + mainGroup = 29B97314FDCFA39411CA2CEA /* CustomTemplate */; productRefGroup = 19C28FACFE9D520D11CA2CBB /* Products */; projectDirPath = ""; projectRoot = ""; @@ -8496,6 +8499,7 @@ D817799420ABFDB300330998 /* ReaderPostCellActions.swift in Sources */, FF7E510A1FBDE0C500AD2918 /* MediaAttachment+WordPress.swift in Sources */, 402B2A7920ACD7690027C1DC /* ActivityStore.swift in Sources */, + 91EB11BC219B19700057F17D /* PostEditorUtil.swift in Sources */, E62AFB6A1DC8E593007484FC /* NSAttributedString+WPRichText.swift in Sources */, B54866CA1A0D7042004AC79D /* NSAttributedString+Helpers.swift in Sources */, E105205B1F2B1CF400A948F6 /* BlogToBlogMigration_61_62.swift in Sources */,