From aa81131bcf44acda0b08a2314e86e4f940537543 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 28 Oct 2026 08:11:01 -0400 Subject: [PATCH 01/17] build: update GutenbergKit to trunk Pins the pr-build/839 snapshot, which has trunk's Swift sources and a prebuilt web bundle that trunk revisions lack, until a GutenbergKit prerelease is published. --- Modules/Package.resolved | 5 ++--- Modules/Package.swift | 5 ++++- Package.resolved | 5 ++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/Modules/Package.resolved b/Modules/Package.resolved index 1255cefde198..8420df04c615 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "dd3f23948b260a1c9986a7d17d46643d207bd0bde18b8d1c2538688bd9b2e5f9", + "originHash" : "3733f442fc358e7e3c0c3066a60379b592682c23d6ba1a450c7c8f3e95613a79", "pins" : [ { "identity" : "automattic-tracks-ios", @@ -122,8 +122,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "b6604e26792725b2e8125b3b715e1d4a298441b6", - "version" : "0.19.0" + "revision" : "79edefae17ac610bfbd36863adc220c3b15a72d7" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index a5f1f4703685..b7913dfb40b5 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -63,7 +63,10 @@ let package = Package( revision: "b34794c9a3f32312e1593d4a3d120572afa0d010" ), .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), - .package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.19.0"), + .package( + url: "https://github.com/wordpress-mobile/GutenbergKit", + revision: "79edefae17ac610bfbd36863adc220c3b15a72d7" + ), .package( url: "https://github.com/automattic/wordpress-rs", exact: "0.9.1" diff --git a/Package.resolved b/Package.resolved index 0767464fe359..eefb1a630616 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "f5cdff960717c84869b888911edb7d85c24e067a5afc87dab1896518df70b864", + "originHash" : "495f02d8954dc227ee3df7512b9269bb4b230e15fb7c96ba5bfd6c54b7df4a8c", "pins" : [ { "identity" : "automattic-tracks-ios", @@ -122,8 +122,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "b6604e26792725b2e8125b3b715e1d4a298441b6", - "version" : "0.19.0" + "revision" : "79edefae17ac610bfbd36863adc220c3b15a72d7" } }, { From 420d9b56e11f27b81aa32d64a7d967e83f947f48 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 30 Sep 2026 12:53:51 -0400 Subject: [PATCH 02/17] fix(editor): implement GutenbergKit's editor-unavailable callback GutenbergKit now requires delegates to implement editorDidBecomeUnavailable(_:), with no default, so a subclass override is never silently skipped. --- .../Editor/CommentGutenbergEditorViewController.swift | 4 ++++ .../NewGutenberg/PostGBKEditorViewController.swift | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift index f5bc79d02ab8..4229573ba013 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift @@ -83,6 +83,10 @@ extension CommentGutenbergEditorViewController: GutenbergKit.EditorViewControlle // Do nothing } + func editorDidBecomeUnavailable(_ viewController: GutenbergKit.EditorViewController) { + // Do nothing + } + func editorDidRequestLatestContent(_ controller: GutenbergKit.EditorViewController) -> (title: String, content: String)? { return nil } diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index 0974e201d94a..b6f796b656f2 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -112,6 +112,10 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont // Do nothing } + func editorDidBecomeUnavailable(_ viewController: GutenbergKit.EditorViewController) { + // Do nothing + } + func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) { // Do nothing } From c2d7962ec24bb60456f76dfc9d2be27d2a2ccfcb Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 30 Sep 2026 12:53:54 -0400 Subject: [PATCH 03/17] feat(editor): localize GutenbergKit's crash and media error strings GutenbergKit added strings for its editor crash notice and block inserter media errors, which otherwise appear in English. --- .../NewGutenberg/GBKExtensions.swift | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift index 2f705879e639..fee91dd11408 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift @@ -65,6 +65,21 @@ private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) value: "Failed to insert media", comment: "Error message when media insertion fails" ) + case .failedToLoadSelectedMedia: + NSLocalizedString( + "editor.media.failedToLoadSelected", + value: "The selected media could not be loaded. It may not be fully downloaded to this device.", + comment: "Error message when media picked in the block inserter cannot be loaded, e.g. an iCloud photo" + ) + case .failedToProcessCapturedMedia: + NSLocalizedString( + "editor.media.failedToProcessCaptured", + value: "The captured media could not be processed.", + comment: + "Error message when a photo or video taken with the camera from the block inserter cannot be processed" + ) + case .ok: + SharedStrings.Button.ok case .patterns: NSLocalizedString("editor.patterns.title", value: "Patterns", comment: "Navigation title for patterns view") case .noPatternsFound: @@ -121,6 +136,24 @@ private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) value: "Editor Error", comment: "Title shown when the editor encounters an error" ) + case .editorCrashedTitle: + NSLocalizedString( + "editor.crashed.title", + value: "The editor stopped working", + comment: "Title shown over the editor after it crashes" + ) + case .editorCrashedDescription: + NSLocalizedString( + "editor.crashed.description", + value: "Reload the editor to continue editing.", + comment: "Message shown over the editor after it crashes, above a button to reload it" + ) + case .editorCrashedReload: + NSLocalizedString( + "editor.crashed.reload", + value: "Reload Editor", + comment: "Button title to reload the editor after it crashes" + ) case .lockdownModeTitle: NSLocalizedString( "editor.lockdownMode.title", From 26320fa5e61b1dd12b99bba0d7d08073a2919ed0 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 30 Sep 2026 12:54:13 -0400 Subject: [PATCH 04/17] fix(editor): disable editor controls while the editor is unavailable After a crash, GutenbergKit refuses calls into the editor until it reloads, so undo, redo, the editor mode toggle, and custom post saving would silently do nothing. --- .../CustomPostEditorViewController.swift | 18 +++++++++++++++++- .../NewGutenbergViewController.swift | 10 ++++++++++ .../PostGBKEditorViewController.swift | 15 ++++++++++++--- 3 files changed, 39 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift index 824eb8d0ca74..ec30b4d6a7e2 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift @@ -100,11 +100,23 @@ class CustomPostEditorViewController: PostGBKEditorViewController { } override func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { + super.editorDidLoad(viewContoller) + + primarySaveButton.isEnabled = true if editorSession?.started == false { editorSession?.start() } } + override func editorDidBecomeUnavailable(_ viewController: GutenbergKit.EditorViewController) { + super.editorDidBecomeUnavailable(viewController) + + // Saving reads the content from the editor, so it has to wait for the reload. + primarySaveButton.isEnabled = false + redoButton.isEnabled = false + undoButton.isEnabled = false + } + override func editor( _ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: EditorState @@ -178,7 +190,11 @@ private extension CustomPostEditorViewController { } func rightBarButtonItems() -> [UIBarButtonItem] { - var children: [UIMenuElement] = [editorModeToggle(), settingsAction(), helpAction(), feedbackAction()] + // Deferred so the toggle reflects the current editor mode and availability. + let editorModeToggle = UIDeferredMenuElement.uncached { [weak self] resolve in + resolve(self.map { [$0.editorModeToggle()] } ?? []) + } + var children: [UIMenuElement] = [editorModeToggle, settingsAction(), helpAction(), feedbackAction()] if post?.status ?? .draft == .draft { let menu = UIDeferredMenuElement.uncached { [weak self] resolve in diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 0109635afcc2..0084494cc934 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -228,6 +228,8 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi */ override func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { + super.editorDidLoad(viewContoller) + if !editorSession.started { // Note that this method is also used to track startup performance // It assumes this is being called when the editor has finished loading @@ -237,6 +239,14 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi } } + override func editorDidBecomeUnavailable(_ viewController: GutenbergKit.EditorViewController) { + super.editorDidBecomeUnavailable(viewController) + + // A reloaded editor starts with empty history and re-enables these as the user edits. + gutenbergDidRequestToggleRedoButton(true) + gutenbergDidRequestToggleUndoButton(true) + } + override func editor( _ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: GutenbergKit.EditorState diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index b6f796b656f2..345e2400db32 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -18,6 +18,9 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont private var currentSuggestionsController: GutenbergSuggestionsViewController? + /// Whether the editor crashed and has not reloaded yet, so calls into it are refused. + private(set) var isEditorUnavailable = false + init( postId: Int?, postType: PostTypeDetails, @@ -81,7 +84,11 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont let title = editorViewController.isCodeEditorEnabled ? PostEditorStrings.visualEditor : PostEditorStrings.codeEditor let icon = editorViewController.isCodeEditorEnabled ? "doc.richtext" : "curlybraces" - return UIAction(title: title, image: UIImage(systemName: icon)) { [weak editorViewController] _ in + return UIAction( + title: title, + image: UIImage(systemName: icon), + attributes: isEditorUnavailable ? [.disabled] : [] + ) { [weak editorViewController] _ in editorViewController?.isCodeEditorEnabled.toggle() } } @@ -108,12 +115,14 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont // MARK: - GutenbergKit.EditorViewControllerDelegate + /// Subclasses that override this must call `super`. func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { - // Do nothing + isEditorUnavailable = false } + /// Subclasses that override this must call `super`. func editorDidBecomeUnavailable(_ viewController: GutenbergKit.EditorViewController) { - // Do nothing + isEditorUnavailable = true } func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) { From c883e45657804a91dc31fc8905fd82b826c87468 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 30 Sep 2026 12:54:28 -0400 Subject: [PATCH 05/17] fix(editor): reload custom posts from their last saved content GutenbergKit restarts a reloaded editor from the host's latest content, or from the content it opened with when there is none, which would discard drafts saved earlier in the session. --- .../NewGutenberg/CustomPostEditorViewController.swift | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift index ec30b4d6a7e2..205c6150d183 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift @@ -117,6 +117,13 @@ class CustomPostEditorViewController: PostGBKEditorViewController { undoButton.isEnabled = false } + override func editorDidRequestLatestContent( + _ controller: GutenbergKit.EditorViewController + ) -> (title: String, content: String)? { + // After a save, the post is newer than the content the editor opened with. + post.map { ($0.title?.raw ?? "", $0.content.raw ?? "") } + } + override func editor( _ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: EditorState From 731a3b58f43dac0dc778471fe34ff5e5638e37f7 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 30 Sep 2026 12:54:35 -0400 Subject: [PATCH 06/17] fix(editor): let users close the custom post editor without its content Closing read the content from the editor and gave up when it couldn't, so an editor that was still loading, failed to load, or crashed couldn't be closed. --- .../CustomPostEditorViewController.swift | 15 +++++++++++++-- .../PostGBKEditorViewController.swift | 4 ++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift index 205c6150d183..8e23a24c6b7f 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift @@ -158,12 +158,23 @@ private extension CustomPostEditorViewController { navigationController?.view.isUserInteractionEnabled = true } + // The editor can't report its content before it loads, but nothing could have been edited yet either. + guard hasEditorLoaded || editorService.hasSettingsChanges else { + endEditorSession(outcome: .cancel) + navigationController?.dismiss(animated: true) + return + } + let changed: Bool + var canSave = true do { changed = try await hasUnsavedChanges() } catch { + // Without the editor's content (e.g. after a crash), let the user discard + // their changes rather than trapping them in the editor. DDLogError("Failed to get editor content: \(error)") - return + changed = true + canSave = false } if changed { @@ -174,7 +185,7 @@ private extension CustomPostEditorViewController { self.endEditorSession(outcome: .discard) self.navigationController?.dismiss(animated: true) } - if post?.status ?? .draft == .draft { + if canSave, post?.status ?? .draft == .draft { alert.addAction( UIAlertAction( title: PostEditorStrings.saveDraft, diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index 345e2400db32..c11a22d42e59 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -18,6 +18,9 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont private var currentSuggestionsController: GutenbergSuggestionsViewController? + /// Whether the editor has finished loading at least once, so the user could have edited it. + private(set) var hasEditorLoaded = false + /// Whether the editor crashed and has not reloaded yet, so calls into it are refused. private(set) var isEditorUnavailable = false @@ -117,6 +120,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont /// Subclasses that override this must call `super`. func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { + hasEditorLoaded = true isEditorUnavailable = false } From aaf416926322871728afee53ddef4829b905186b Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 28 Oct 2026 09:30:20 -0400 Subject: [PATCH 07/17] fix(editor): don't save posts to the server while GutenbergKit locks saving GutenbergKit now reports when saving is locked, e.g. during its media uploads, whose temporary URLs would otherwise be saved. Saves show the existing media-uploading alert, and closing keeps the changes in the local revision. --- .../PostGBKEditorViewControllerTests.swift | 45 +++++++++++++++++++ .../NewGutenbergViewController.swift | 14 +++++- .../PostGBKEditorViewController.swift | 14 ++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift index 9b9125c082f2..1c2f2257263b 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift @@ -89,6 +89,51 @@ struct PostGBKEditorViewControllerTests { #expect(reported?.isEmpty == true) } + @Test("tracks whether the editor locked saving") + func tracksSavingLock() { + let viewController = makeViewController() + + viewController.editor( + viewController.editorViewController, + didUpdateSaveAvailability: EditorSaveAvailability(isSaveable: true, isSavingLocked: true) + ) + #expect(viewController.isSavingLocked) + + viewController.editor( + viewController.editorViewController, + didUpdateSaveAvailability: EditorSaveAvailability(isSaveable: true, isSavingLocked: false) + ) + #expect(!viewController.isSavingLocked) + } + + @Test("clears the saving lock when the editor becomes unavailable") + func clearsSavingLockWhenUnavailable() { + let viewController = makeViewController() + viewController.editor( + viewController.editorViewController, + didUpdateSaveAvailability: EditorSaveAvailability(isSaveable: true, isSavingLocked: true) + ) + + // The reloaded editor reports its availability again, so a lock from + // before the crash must not keep blocking saves. + viewController.editorDidBecomeUnavailable(viewController.editorViewController) + + #expect(!viewController.isSavingLocked) + } + + private func makeViewController() -> PostGBKEditorViewController { + let context = ContextManager.forTesting().mainContext + let blog = BlogBuilder(context).build() + return PostGBKEditorViewController( + postId: nil, + postType: .post, + title: "", + content: "", + status: "draft", + blog: blog + ) + } + /// Returns the media the picker has actually preselected. /// /// Read from the embedded collection view controller, which the picker adds as diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 0084494cc934..05c034e2f0fa 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -76,7 +76,8 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi func prepopulateMediaItems(_ media: [Media]) {} var debouncer = WordPressShared.Debouncer(delay: 10) var replaceEditor: (EditorViewController, EditorViewController) -> () - var isUploadingMedia: Bool { false } + // The editor uploads media itself and locks saving until uploads finish. + var isUploadingMedia: Bool { isSavingLocked } var wordCount: UInt { 0 } var postIsReblogged: Bool = false var entryPoint: PostEditorEntryPoint = .unknown @@ -318,6 +319,12 @@ extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { performAfterUpdatingContent { [self] in + // The editor's uploads stop when it closes, so keep the changes in the local + // revision rather than syncing their temporary media URLs. + guard !isUploadingMedia else { + editorSession.end(outcome: .cancel) + return dismissOrPopView() + } cancelEditing() } } @@ -335,6 +342,11 @@ extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { } func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { + // Unlike the media uploads of other editors, `PostCoordinator` can't wait for + // the editor's before publishing. + guard !isUploadingMedia else { + return displayMediaIsUploadingAlert() + } performAfterUpdatingContent { [self] in if editorHasContent { handlePrimaryActionButtonTap() diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index c11a22d42e59..9ade859616ae 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -24,6 +24,10 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont /// Whether the editor crashed and has not reloaded yet, so calls into it are refused. private(set) var isEditorUnavailable = false + /// Whether the editor locked saving, e.g. during a media upload, so its content + /// may reference temporary media URLs that must not reach the server. + private(set) var isSavingLocked = false + init( postId: Int?, postType: PostTypeDetails, @@ -127,6 +131,8 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont /// Subclasses that override this must call `super`. func editorDidBecomeUnavailable(_ viewController: GutenbergKit.EditorViewController) { isEditorUnavailable = true + // The reloaded editor reports its availability again. + isSavingLocked = false } func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) { @@ -188,6 +194,14 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont // Do nothing } + /// Subclasses that override this must call `super`. + func editor( + _ viewController: GutenbergKit.EditorViewController, + didUpdateSaveAvailability availability: GutenbergKit.EditorSaveAvailability + ) { + isSavingLocked = availability.isSavingLocked + } + func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateFeaturedImage mediaID: Int) { // Do nothing } From e173fad7f7557144824bb70ca6bbcc1b4c885948 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 28 Oct 2026 09:30:28 -0400 Subject: [PATCH 08/17] fix(editor): disable custom post saving while GutenbergKit locks saving Custom posts save straight to the server, so saving during GutenbergKit's media uploads would persist their temporary URLs. --- .../CustomPostEditorViewController.swift | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift index 8e23a24c6b7f..951dc13b9844 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift @@ -102,7 +102,7 @@ class CustomPostEditorViewController: PostGBKEditorViewController { override func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { super.editorDidLoad(viewContoller) - primarySaveButton.isEnabled = true + updatePrimarySaveButton() if editorSession?.started == false { editorSession?.start() } @@ -112,11 +112,24 @@ class CustomPostEditorViewController: PostGBKEditorViewController { super.editorDidBecomeUnavailable(viewController) // Saving reads the content from the editor, so it has to wait for the reload. - primarySaveButton.isEnabled = false + updatePrimarySaveButton() redoButton.isEnabled = false undoButton.isEnabled = false } + override func editor( + _ viewController: GutenbergKit.EditorViewController, + didUpdateSaveAvailability availability: GutenbergKit.EditorSaveAvailability + ) { + super.editor(viewController, didUpdateSaveAvailability: availability) + + updatePrimarySaveButton() + } + + private func updatePrimarySaveButton() { + primarySaveButton.isEnabled = !isEditorUnavailable && !isSavingLocked + } + override func editorDidRequestLatestContent( _ controller: GutenbergKit.EditorViewController ) -> (title: String, content: String)? { @@ -166,7 +179,8 @@ private extension CustomPostEditorViewController { } let changed: Bool - var canSave = true + // A locked editor's content can reference temporary media URLs. + var canSave = !isSavingLocked do { changed = try await hasUnsavedChanges() } catch { @@ -217,7 +231,8 @@ private extension CustomPostEditorViewController { if post?.status ?? .draft == .draft { let menu = UIDeferredMenuElement.uncached { [weak self] resolve in Task { - let enabled = (try? await self?.hasUnsavedChanges()) == true + let hasUnsavedChanges = (try? await self?.hasUnsavedChanges()) == true + let enabled = hasUnsavedChanges && self?.isSavingLocked == false let saveDraft = UIAction( title: PostEditorStrings.saveDraft, image: UIImage(systemName: "doc"), From f58d0c0f308c73229c2f67aaf2c9d67f204bca41 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 28 Oct 2026 11:05:34 -0400 Subject: [PATCH 09/17] style: format PostEditorStrings with swift-format Separates the formatter's changes from the strings added next. --- .../NewGutenberg/PostEditorStrings.swift | 66 +++++++++++++++---- 1 file changed, 55 insertions(+), 11 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift index aea1577dbdae..10ff18e4b921 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift @@ -2,19 +2,63 @@ import Foundation enum PostEditorStrings { // More menu - static let codeEditor = NSLocalizedString("postEditor.moreMenu.codeEditor", value: "Code Editor", comment: "Post Editor / Button in the 'More' menu") - static let visualEditor = NSLocalizedString("postEditor.moreMenu.visualEditor", value: "Visual Editor", comment: "Post Editor / Button in the 'More' menu") - static let helpAndSupport = NSLocalizedString("postEditor.moreMenu.helpAndSupport", value: "Help & Support", comment: "Post Editor / Button in the 'More' menu") - static let help = NSLocalizedString("postEditor.moreMenu.help", value: "Help", comment: "Post Editor / Button in the 'More' menu") - static let postSettings = NSLocalizedString("postEditor.moreMenu.postSettings", value: "Post Settings", comment: "Post Editor / Button in the 'More' menu") - static let sendFeedback = NSLocalizedString("postEditor.moreMenu.sendFeedback", value: "Send Feedback", comment: "Post Editor / Button in the 'More' menu") - static let saveDraft = NSLocalizedString("postEditor.moreMenu.saveDraft", value: "Save Draft", comment: "Post Editor / Button in the 'More' menu") + static let codeEditor = NSLocalizedString( + "postEditor.moreMenu.codeEditor", + value: "Code Editor", + comment: "Post Editor / Button in the 'More' menu" + ) + static let visualEditor = NSLocalizedString( + "postEditor.moreMenu.visualEditor", + value: "Visual Editor", + comment: "Post Editor / Button in the 'More' menu" + ) + static let helpAndSupport = NSLocalizedString( + "postEditor.moreMenu.helpAndSupport", + value: "Help & Support", + comment: "Post Editor / Button in the 'More' menu" + ) + static let help = NSLocalizedString( + "postEditor.moreMenu.help", + value: "Help", + comment: "Post Editor / Button in the 'More' menu" + ) + static let postSettings = NSLocalizedString( + "postEditor.moreMenu.postSettings", + value: "Post Settings", + comment: "Post Editor / Button in the 'More' menu" + ) + static let sendFeedback = NSLocalizedString( + "postEditor.moreMenu.sendFeedback", + value: "Send Feedback", + comment: "Post Editor / Button in the 'More' menu" + ) + static let saveDraft = NSLocalizedString( + "postEditor.moreMenu.saveDraft", + value: "Save Draft", + comment: "Post Editor / Button in the 'More' menu" + ) // Close confirmation - static let keepEditing = NSLocalizedString("postEditor.closeConfirmationAlert.keepEditing", value: "Keep Editing", comment: "Button to keep the changes in an alert confirming discarding changes") - static let discardChanges = NSLocalizedString("postEditor.closeConfirmationAlert.discardChanges", value: "Discard Changes", comment: "Button in an alert confirming discarding changes") + static let keepEditing = NSLocalizedString( + "postEditor.closeConfirmationAlert.keepEditing", + value: "Keep Editing", + comment: "Button to keep the changes in an alert confirming discarding changes" + ) + static let discardChanges = NSLocalizedString( + "postEditor.closeConfirmationAlert.discardChanges", + value: "Discard Changes", + comment: "Button in an alert confirming discarding changes" + ) // Primary actions - static let publish = NSLocalizedString("postEditor.publishButton.title", value: "Publish", comment: "Title for the publish button in the post editor navigation bar") - static let update = NSLocalizedString("postEditor.updateButton.title", value: "Update", comment: "Title for the update button in the post editor navigation bar") + static let publish = NSLocalizedString( + "postEditor.publishButton.title", + value: "Publish", + comment: "Title for the publish button in the post editor navigation bar" + ) + static let update = NSLocalizedString( + "postEditor.updateButton.title", + value: "Update", + comment: "Title for the update button in the post editor navigation bar" + ) } From f797688f5e770e469615c9ca6c9a35d5ce9ea7f5 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 28 Oct 2026 11:23:03 -0400 Subject: [PATCH 10/17] fix(editor): confirm closing the editor while GutenbergKit uploads media GutenbergKit's uploads run in the editor's web view and stop when it closes, so closing mid-upload loses the media. Unlike the app's own uploads, they can't finish in the background. --- .../NewGutenbergViewController.swift | 27 ++++++++++++++++--- .../NewGutenberg/PostEditorStrings.swift | 15 +++++++++++ 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 05c034e2f0fa..93d0e0de26f7 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -319,16 +319,35 @@ extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { performAfterUpdatingContent { [self] in - // The editor's uploads stop when it closes, so keep the changes in the local - // revision rather than syncing their temporary media URLs. + // Closing ends the editor's uploads, so confirm before the media is lost. guard !isUploadingMedia else { - editorSession.end(outcome: .cancel) - return dismissOrPopView() + return showLeaveWhileUploadingAlert() } cancelEditing() } } + private func showLeaveWhileUploadingAlert() { + let alert = UIAlertController( + title: PostEditorStrings.leaveWhileUploadingTitle, + message: PostEditorStrings.leaveWhileUploadingMessage, + preferredStyle: .alert + ) + alert.addCancelActionWithTitle(PostEditorStrings.keepEditing) + alert.addDestructiveActionWithTitle(PostEditorStrings.leave) { [weak self] _ in + guard let self else { return } + // The upload may have finished while the alert was shown. + guard isUploadingMedia else { + return cancelEditing() + } + // Keep the changes in the local revision rather than syncing their + // temporary media URLs. + editorSession.end(outcome: .cancel) + dismissOrPopView() + } + present(alert, animated: true) + } + func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { editorViewController.undo() } diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift index 10ff18e4b921..7a3224b2e141 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift @@ -49,6 +49,21 @@ enum PostEditorStrings { value: "Discard Changes", comment: "Button in an alert confirming discarding changes" ) + static let leaveWhileUploadingTitle = NSLocalizedString( + "postEditor.leaveWhileUploadingAlert.title", + value: "Media is still uploading", + comment: "Title of an alert shown when closing the editor while media is uploading" + ) + static let leaveWhileUploadingMessage = NSLocalizedString( + "postEditor.leaveWhileUploadingAlert.message", + value: "If you leave now, the upload stops and the media won't be added to your post.", + comment: "Message of an alert shown when closing the editor while media is uploading" + ) + static let leave = NSLocalizedString( + "postEditor.leaveWhileUploadingAlert.leave", + value: "Leave", + comment: "Button to close the editor in an alert shown while media is uploading" + ) // Primary actions static let publish = NSLocalizedString( From e7745bb970871fd3fc118f4c53f1959506d21174 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Wed, 28 Oct 2026 13:01:32 -0400 Subject: [PATCH 11/17] build: update GutenbergKit to 0.20.0-alpha.1 Replaces the temporary pr-build snapshot pin with the published prerelease. --- Modules/Package.resolved | 5 +++-- Modules/Package.swift | 5 +---- Package.resolved | 3 ++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/Modules/Package.resolved b/Modules/Package.resolved index 8420df04c615..2993b8deebf4 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3733f442fc358e7e3c0c3066a60379b592682c23d6ba1a450c7c8f3e95613a79", + "originHash" : "592e79b41dba5d3294d9cf4d7d0c1779909fb88094a586af8416e8dce2ab4011", "pins" : [ { "identity" : "automattic-tracks-ios", @@ -122,7 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "79edefae17ac610bfbd36863adc220c3b15a72d7" + "revision" : "0b9e362245ce96948ca4902e0dfdfb4183473969", + "version" : "0.20.0-alpha.1" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index b7913dfb40b5..918d9f933608 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -63,10 +63,7 @@ let package = Package( revision: "b34794c9a3f32312e1593d4a3d120572afa0d010" ), .package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"), - .package( - url: "https://github.com/wordpress-mobile/GutenbergKit", - revision: "79edefae17ac610bfbd36863adc220c3b15a72d7" - ), + .package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.20.0-alpha.1"), .package( url: "https://github.com/automattic/wordpress-rs", exact: "0.9.1" diff --git a/Package.resolved b/Package.resolved index eefb1a630616..4864ec1da187 100644 --- a/Package.resolved +++ b/Package.resolved @@ -122,7 +122,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "79edefae17ac610bfbd36863adc220c3b15a72d7" + "revision" : "0b9e362245ce96948ca4902e0dfdfb4183473969", + "version" : "0.20.0-alpha.1" } }, { From b4aa651c35e00f38ccc40aa94a9a2b857a30504d Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 29 Oct 2026 08:55:22 -0400 Subject: [PATCH 12/17] fix(editor): run the normal close flow after leaving mid-upload Leaving kept the local revision without asking, so a later Update could publish abandoned edits. Refreshing first also covers an upload that finished while the alert was shown. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuHPbc49BP1ZshDSE8 --- .../NewGutenberg/NewGutenbergViewController.swift | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 93d0e0de26f7..d876bef60eef 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -335,15 +335,9 @@ extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { ) alert.addCancelActionWithTitle(PostEditorStrings.keepEditing) alert.addDestructiveActionWithTitle(PostEditorStrings.leave) { [weak self] _ in - guard let self else { return } - // The upload may have finished while the alert was shown. - guard isUploadingMedia else { - return cancelEditing() - } - // Keep the changes in the local revision rather than syncing their - // temporary media URLs. - editorSession.end(outcome: .cancel) - dismissOrPopView() + // Refresh in case the upload finished while the alert was shown, then + // let the user save or discard the remaining changes. + self?.performAfterUpdatingContent { self?.cancelEditing() } } present(alert, animated: true) } From 958b85d5c42b2947900198f6a14a889effeae3e0 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 29 Oct 2026 10:08:13 -0400 Subject: [PATCH 13/17] fix(editor): ask before saving while GutenbergKit locks saving A plugin can hold a save lock for the whole session, e.g. Jetpack's Donations block when its Stripe fetch fails, which blocked updating published posts and custom posts. Asking keeps the user in control. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuHPbc49BP1ZshDSE8 --- .../PostGBKEditorViewControllerTests.swift | 37 ++++++++++++- .../CustomPostEditorViewController.swift | 53 ++++++++++--------- .../NewGutenbergViewController.swift | 34 ++++++------ .../NewGutenberg/PostEditorStrings.swift | 36 +++++++++---- .../PostGBKEditorViewController.swift | 18 ++++++- 5 files changed, 123 insertions(+), 55 deletions(-) diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift index 1c2f2257263b..95ab17ff2daa 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift @@ -115,12 +115,47 @@ struct PostGBKEditorViewControllerTests { ) // The reloaded editor reports its availability again, so a lock from - // before the crash must not keep blocking saves. + // before the crash must not keep asking before saves. viewController.editorDidBecomeUnavailable(viewController.editorViewController) #expect(!viewController.isSavingLocked) } + @Test("saves without asking while saving is unlocked") + func savesWithoutAskingWhenUnlocked() { + let viewController = makeViewController() + let window = UIWindow() + window.rootViewController = viewController + window.makeKeyAndVisible() + + var didSave = false + viewController.performAfterConfirmingSavingLock { didSave = true } + + #expect(didSave) + #expect(viewController.presentedViewController == nil) + } + + @Test("asks before saving while saving is locked") + func asksBeforeSavingWhenLocked() throws { + let viewController = makeViewController() + let window = UIWindow() + window.rootViewController = viewController + window.makeKeyAndVisible() + viewController.editor( + viewController.editorViewController, + didUpdateSaveAvailability: EditorSaveAvailability(isSaveable: true, isSavingLocked: true) + ) + + var didSave = false + viewController.performAfterConfirmingSavingLock { didSave = true } + + // A lock is advisory, so the user can still choose to save. + #expect(!didSave) + let alert = try #require(viewController.presentedViewController as? UIAlertController) + #expect(alert.title == PostEditorStrings.notReadyTitle) + #expect(alert.actions.map(\.title) == [PostEditorStrings.keepEditing, PostEditorStrings.continueAnyway]) + } + private func makeViewController() -> PostGBKEditorViewController { let context = ContextManager.forTesting().mainContext let blog = BlogBuilder(context).build() diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift index 951dc13b9844..9603291bd47f 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift @@ -117,17 +117,8 @@ class CustomPostEditorViewController: PostGBKEditorViewController { undoButton.isEnabled = false } - override func editor( - _ viewController: GutenbergKit.EditorViewController, - didUpdateSaveAvailability availability: GutenbergKit.EditorSaveAvailability - ) { - super.editor(viewController, didUpdateSaveAvailability: availability) - - updatePrimarySaveButton() - } - private func updatePrimarySaveButton() { - primarySaveButton.isEnabled = !isEditorUnavailable && !isSavingLocked + primarySaveButton.isEnabled = !isEditorUnavailable } override func editorDidRequestLatestContent( @@ -179,8 +170,7 @@ private extension CustomPostEditorViewController { } let changed: Bool - // A locked editor's content can reference temporary media URLs. - var canSave = !isSavingLocked + var canSave = true do { changed = try await hasUnsavedChanges() } catch { @@ -205,9 +195,12 @@ private extension CustomPostEditorViewController { title: PostEditorStrings.saveDraft, style: .default, handler: { [weak self] _ in - Task { - await self?.save() - } + self? + .performAfterConfirmingSavingLock { + Task { + await self?.save() + } + } } ) ) @@ -232,15 +225,17 @@ private extension CustomPostEditorViewController { let menu = UIDeferredMenuElement.uncached { [weak self] resolve in Task { let hasUnsavedChanges = (try? await self?.hasUnsavedChanges()) == true - let enabled = hasUnsavedChanges && self?.isSavingLocked == false let saveDraft = UIAction( title: PostEditorStrings.saveDraft, image: UIImage(systemName: "doc"), - attributes: enabled ? [] : [.disabled] + attributes: hasUnsavedChanges ? [] : [.disabled] ) { [weak self] _ in - Task { - await self?.save() - } + self? + .performAfterConfirmingSavingLock { + Task { + await self?.save() + } + } } resolve([saveDraft]) } @@ -259,15 +254,21 @@ private extension CustomPostEditorViewController { private func savePostAction() -> UIAction { if post?.status ?? .draft == .draft { return UIAction(title: PostEditorStrings.publish) { [weak self] _ in - Task { - await self?.showPublishingSheet() - } + self? + .performAfterConfirmingSavingLock { + Task { + await self?.showPublishingSheet() + } + } } } else { return UIAction(title: PostEditorStrings.update) { [weak self] _ in - Task { - await self?.save() - } + self? + .performAfterConfirmingSavingLock { + Task { + await self?.save() + } + } } } } diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index d876bef60eef..1f9696f20a7d 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -76,8 +76,9 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi func prepopulateMediaItems(_ media: [Media]) {} var debouncer = WordPressShared.Debouncer(delay: 10) var replaceEditor: (EditorViewController, EditorViewController) -> () - // The editor uploads media itself and locks saving until uploads finish. - var isUploadingMedia: Bool { isSavingLocked } + // The editor uploads media itself and locks saving until uploads finish. Rather + // than the shared guards blocking saves, `performAfterConfirmingSavingLock` asks. + var isUploadingMedia: Bool { false } var wordCount: UInt { 0 } var postIsReblogged: Bool = false var entryPoint: PostEditorEntryPoint = .unknown @@ -320,17 +321,17 @@ extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { performAfterUpdatingContent { [self] in // Closing ends the editor's uploads, so confirm before the media is lost. - guard !isUploadingMedia else { - return showLeaveWhileUploadingAlert() + guard !isSavingLocked else { + return showLeaveWhileSavingLockedAlert() } cancelEditing() } } - private func showLeaveWhileUploadingAlert() { + private func showLeaveWhileSavingLockedAlert() { let alert = UIAlertController( - title: PostEditorStrings.leaveWhileUploadingTitle, - message: PostEditorStrings.leaveWhileUploadingMessage, + title: PostEditorStrings.notReadyTitle, + message: PostEditorStrings.leaveWhileNotReadyMessage, preferredStyle: .alert ) alert.addCancelActionWithTitle(PostEditorStrings.keepEditing) @@ -357,14 +358,13 @@ extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { // Unlike the media uploads of other editors, `PostCoordinator` can't wait for // the editor's before publishing. - guard !isUploadingMedia else { - return displayMediaIsUploadingAlert() - } - performAfterUpdatingContent { [self] in - if editorHasContent { - handlePrimaryActionButtonTap() - } else { - showAlertForEmptyPostPublish() + performAfterConfirmingSavingLock { [self] in + performAfterUpdatingContent { [self] in + if editorHasContent { + handlePrimaryActionButtonTap() + } else { + showAlertForEmptyPostPublish() + } } } } @@ -556,7 +556,7 @@ extension NewGutenbergViewController { image: UIImage(systemName: "doc"), attributes: (editorHasChanges && editorHasContent) ? [] : [.disabled] ) { [weak self] _ in - self?.buttonSaveDraftTapped() + self?.performAfterConfirmingSavingLock { self?.buttonSaveDraftTapped() } } ) } @@ -570,7 +570,7 @@ extension NewGutenbergViewController { actions.append( UIAction(title: Strings.preview, image: UIImage(systemName: "safari")) { [weak self] _ in - self?.displayPreview() + self?.performAfterConfirmingSavingLock { self?.displayPreview() } } ) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift index 7a3224b2e141..3ac70e374f1c 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift @@ -49,20 +49,36 @@ enum PostEditorStrings { value: "Discard Changes", comment: "Button in an alert confirming discarding changes" ) - static let leaveWhileUploadingTitle = NSLocalizedString( - "postEditor.leaveWhileUploadingAlert.title", - value: "Media is still uploading", - comment: "Title of an alert shown when closing the editor while media is uploading" + static let notReadyTitle = NSLocalizedString( + "postEditor.notReadyAlert.title", + value: "Your post isn't ready yet", + comment: + "Title of an alert shown when saving or closing the editor while something, like a media upload, is in progress" ) - static let leaveWhileUploadingMessage = NSLocalizedString( - "postEditor.leaveWhileUploadingAlert.message", - value: "If you leave now, the upload stops and the media won't be added to your post.", - comment: "Message of an alert shown when closing the editor while media is uploading" + static let leaveWhileNotReadyMessage = NSLocalizedString( + "postEditor.notReadyAlert.leaveMessage", + value: + "If you leave now, anything still in progress, like a media upload, stops and won't be added to your post.", + comment: + "Message of an alert shown when closing the editor while something, like a media upload, is in progress" + ) + static let saveWhileNotReadyMessage = NSLocalizedString( + "postEditor.notReadyAlert.saveMessage", + value: + "Something in the editor, like a media upload, is still in progress. If you continue, it may be missing or broken in your post.", + comment: + "Message of an alert shown when saving, publishing or previewing while something, like a media upload, is in progress" ) static let leave = NSLocalizedString( - "postEditor.leaveWhileUploadingAlert.leave", + "postEditor.notReadyAlert.leave", value: "Leave", - comment: "Button to close the editor in an alert shown while media is uploading" + comment: "Button to close the editor in an alert shown while something, like a media upload, is in progress" + ) + static let continueAnyway = NSLocalizedString( + "postEditor.notReadyAlert.continue", + value: "Continue", + comment: + "Button to save, publish or preview anyway in an alert shown while something, like a media upload, is in progress" ) // Primary actions diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index 9ade859616ae..96a7fd4ff2f9 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -25,7 +25,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont private(set) var isEditorUnavailable = false /// Whether the editor locked saving, e.g. during a media upload, so its content - /// may reference temporary media URLs that must not reach the server. + /// may be incomplete or reference temporary media URLs. private(set) var isSavingLocked = false init( @@ -110,6 +110,22 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont } } + /// Asks before saving while the editor locks saving. A lock is only advisory, so a + /// plugin that never releases one can't keep the user from saving. + func performAfterConfirmingSavingLock(_ action: @escaping () -> Void) { + guard isSavingLocked else { + return action() + } + let alert = UIAlertController( + title: PostEditorStrings.notReadyTitle, + message: PostEditorStrings.saveWhileNotReadyMessage, + preferredStyle: .alert + ) + alert.addCancelActionWithTitle(PostEditorStrings.keepEditing) + alert.addDefaultActionWithTitle(PostEditorStrings.continueAnyway) { _ in action() } + present(alert, animated: true) + } + func feedbackAction() -> UIAction { UIAction(title: PostEditorStrings.sendFeedback, image: UIImage(systemName: "envelope")) { [weak self] _ in self? From 8963c162e2a05bf506ae1172b610d9f2ef9a4d91 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 29 Oct 2026 10:31:51 -0400 Subject: [PATCH 14/17] docs: add a release note for the GutenbergKit 0.20.0-alpha.1 update Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuHPbc49BP1ZshDSE8 --- RELEASE-NOTES.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index eb044d42cb50..e43c7c4bb864 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -9,6 +9,7 @@ * [*] Reader: Fix the spacing around headings in posts [#26170] * [*] Stats: Fix the back button missing on iPhone Duo and on Plus and Pro Max iPhones in landscape [#26207] * [**] iPad and large iPhones: Screens now adapt to the app window's size in split view, Stage Manager and landscape, not the device type [#26201] +* [*] Experimental editor: Recover from editor crashes, and ask before saving while media is still uploading [#26267] 27.4 From 89c7e078390c0dff94391b7996af880d31f5e780 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 29 Oct 2026 12:53:48 -0400 Subject: [PATCH 15/17] fix(editor): ask before closing custom posts while GutenbergKit locks saving Closing ends the editor's uploads, and replacing a block's media doesn't change the saved markup, so the close flow dismissed without a prompt. Posts and pages already ask. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuHPbc49BP1ZshDSE8 --- .../PostGBKEditorViewControllerTests.swift | 16 ++++++++++++ .../CustomPostEditorViewController.swift | 25 ++++++++++++++----- .../NewGutenbergViewController.swift | 21 ++++------------ .../PostGBKEditorViewController.swift | 12 +++++++++ 4 files changed, 52 insertions(+), 22 deletions(-) diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift index 95ab17ff2daa..110dbd9c7c63 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift @@ -156,6 +156,22 @@ struct PostGBKEditorViewControllerTests { #expect(alert.actions.map(\.title) == [PostEditorStrings.keepEditing, PostEditorStrings.continueAnyway]) } + @Test("asks before leaving while saving is locked") + func asksBeforeLeavingWhenLocked() throws { + let viewController = makeViewController() + let window = UIWindow() + window.rootViewController = viewController + window.makeKeyAndVisible() + + var didLeave = false + viewController.showLeaveWhileSavingLockedAlert { didLeave = true } + + #expect(!didLeave) + let alert = try #require(viewController.presentedViewController as? UIAlertController) + #expect(alert.message == PostEditorStrings.leaveWhileNotReadyMessage) + #expect(alert.actions.map(\.title) == [PostEditorStrings.keepEditing, PostEditorStrings.leave]) + } + private func makeViewController() -> PostGBKEditorViewController { let context = ContextManager.forTesting().mainContext let blog = BlogBuilder(context).build() diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift index 9603291bd47f..b91d86bc30c6 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/CustomPostEditorViewController.swift @@ -151,12 +151,20 @@ class CustomPostEditorViewController: PostGBKEditorViewController { private extension CustomPostEditorViewController { @objc func closeButtonAction() { + // Closing ends the editor's uploads, so confirm before the media is lost. + guard !isSavingLocked else { + return showLeaveWhileSavingLockedAlert { [weak self] in + Task { + await self?.dismiss(hasConfirmedSavingLock: true) + } + } + } Task { await dismiss() } } - func dismiss() async { + func dismiss(hasConfirmedSavingLock: Bool = false) async { navigationController?.view.isUserInteractionEnabled = false defer { navigationController?.view.isUserInteractionEnabled = true @@ -195,12 +203,17 @@ private extension CustomPostEditorViewController { title: PostEditorStrings.saveDraft, style: .default, handler: { [weak self] _ in - self? - .performAfterConfirmingSavingLock { - Task { - await self?.save() - } + let save: () -> Void = { + Task { + await self?.save() } + } + // The Leave alert already warned about the upload, so don't ask again. + if hasConfirmedSavingLock { + save() + } else { + self?.performAfterConfirmingSavingLock(save) + } } ) ) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 1f9696f20a7d..0795a1f88a15 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -322,27 +322,16 @@ extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { performAfterUpdatingContent { [self] in // Closing ends the editor's uploads, so confirm before the media is lost. guard !isSavingLocked else { - return showLeaveWhileSavingLockedAlert() + return showLeaveWhileSavingLockedAlert { [weak self] in + // Refresh in case the upload finished while the alert was shown, then + // let the user save or discard the remaining changes. + self?.performAfterUpdatingContent { self?.cancelEditing() } + } } cancelEditing() } } - private func showLeaveWhileSavingLockedAlert() { - let alert = UIAlertController( - title: PostEditorStrings.notReadyTitle, - message: PostEditorStrings.leaveWhileNotReadyMessage, - preferredStyle: .alert - ) - alert.addCancelActionWithTitle(PostEditorStrings.keepEditing) - alert.addDestructiveActionWithTitle(PostEditorStrings.leave) { [weak self] _ in - // Refresh in case the upload finished while the alert was shown, then - // let the user save or discard the remaining changes. - self?.performAfterUpdatingContent { self?.cancelEditing() } - } - present(alert, animated: true) - } - func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { editorViewController.undo() } diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index 96a7fd4ff2f9..b3cbe4c476e8 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -126,6 +126,18 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont present(alert, animated: true) } + /// Asks before closing while the editor locks saving, since closing ends the editor's uploads. + func showLeaveWhileSavingLockedAlert(onLeave: @escaping () -> Void) { + let alert = UIAlertController( + title: PostEditorStrings.notReadyTitle, + message: PostEditorStrings.leaveWhileNotReadyMessage, + preferredStyle: .alert + ) + alert.addCancelActionWithTitle(PostEditorStrings.keepEditing) + alert.addDestructiveActionWithTitle(PostEditorStrings.leave) { _ in onLeave() } + present(alert, animated: true) + } + func feedbackAction() -> UIAction { UIAction(title: PostEditorStrings.sendFeedback, image: UIImage(systemName: "envelope")) { [weak self] _ in self? From e69c547ae6159d4dda38ae69a7214113027d1cf6 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 29 Oct 2026 12:56:21 -0400 Subject: [PATCH 16/17] fix(editor): say "page" in the saving lock alerts for pages The Page editor showed "Your post isn't ready yet", unlike its other page-specific strings. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuHPbc49BP1ZshDSE8 --- .../PostGBKEditorViewControllerTests.swift | 19 ++++++++++++++++--- .../NewGutenberg/PostEditorStrings.swift | 12 +++++++++--- .../PostGBKEditorViewController.swift | 12 ++++++++++-- 3 files changed, 35 insertions(+), 8 deletions(-) diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift index 110dbd9c7c63..002e51712ffa 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift @@ -152,10 +152,23 @@ struct PostGBKEditorViewControllerTests { // A lock is advisory, so the user can still choose to save. #expect(!didSave) let alert = try #require(viewController.presentedViewController as? UIAlertController) - #expect(alert.title == PostEditorStrings.notReadyTitle) + #expect(alert.title == PostEditorStrings.notReadyPostTitle) #expect(alert.actions.map(\.title) == [PostEditorStrings.keepEditing, PostEditorStrings.continueAnyway]) } + @Test("says page in the saving lock alerts for pages") + func saysPageInSavingLockAlerts() throws { + let viewController = makeViewController(postType: .page) + let window = UIWindow() + window.rootViewController = viewController + window.makeKeyAndVisible() + + viewController.showLeaveWhileSavingLockedAlert {} + + let alert = try #require(viewController.presentedViewController as? UIAlertController) + #expect(alert.title == PostEditorStrings.notReadyPageTitle) + } + @Test("asks before leaving while saving is locked") func asksBeforeLeavingWhenLocked() throws { let viewController = makeViewController() @@ -172,12 +185,12 @@ struct PostGBKEditorViewControllerTests { #expect(alert.actions.map(\.title) == [PostEditorStrings.keepEditing, PostEditorStrings.leave]) } - private func makeViewController() -> PostGBKEditorViewController { + private func makeViewController(postType: PostTypeDetails = .post) -> PostGBKEditorViewController { let context = ContextManager.forTesting().mainContext let blog = BlogBuilder(context).build() return PostGBKEditorViewController( postId: nil, - postType: .post, + postType: postType, title: "", content: "", status: "draft", diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift index 3ac70e374f1c..1869fb387398 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift @@ -49,11 +49,17 @@ enum PostEditorStrings { value: "Discard Changes", comment: "Button in an alert confirming discarding changes" ) - static let notReadyTitle = NSLocalizedString( - "postEditor.notReadyAlert.title", + static let notReadyPostTitle = NSLocalizedString( + "postEditor.notReadyAlert.postTitle", value: "Your post isn't ready yet", comment: - "Title of an alert shown when saving or closing the editor while something, like a media upload, is in progress" + "Title of an alert shown when saving or closing the editor for a post while something, like a media upload, is in progress" + ) + static let notReadyPageTitle = NSLocalizedString( + "postEditor.notReadyAlert.pageTitle", + value: "Your page isn't ready yet", + comment: + "Title of an alert shown when saving or closing the editor for a page while something, like a media upload, is in progress" ) static let leaveWhileNotReadyMessage = NSLocalizedString( "postEditor.notReadyAlert.leaveMessage", diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index b3cbe4c476e8..f4950734f066 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -28,6 +28,9 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont /// may be incomplete or reference temporary media URLs. private(set) var isSavingLocked = false + /// Pages say "page" in alerts. Custom post types say "post", as "Post Settings" does. + private let isPage: Bool + init( postId: Int?, postType: PostTypeDetails, @@ -38,6 +41,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont ) { self.status = status ?? "draft" self.blog = blog + self.isPage = postType.postType == PostTypeDetails.page.postType EditorLocalization.localize = { $0.localized } @@ -117,7 +121,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont return action() } let alert = UIAlertController( - title: PostEditorStrings.notReadyTitle, + title: notReadyTitle, message: PostEditorStrings.saveWhileNotReadyMessage, preferredStyle: .alert ) @@ -129,7 +133,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont /// Asks before closing while the editor locks saving, since closing ends the editor's uploads. func showLeaveWhileSavingLockedAlert(onLeave: @escaping () -> Void) { let alert = UIAlertController( - title: PostEditorStrings.notReadyTitle, + title: notReadyTitle, message: PostEditorStrings.leaveWhileNotReadyMessage, preferredStyle: .alert ) @@ -138,6 +142,10 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont present(alert, animated: true) } + private var notReadyTitle: String { + isPage ? PostEditorStrings.notReadyPageTitle : PostEditorStrings.notReadyPostTitle + } + func feedbackAction() -> UIAction { UIAction(title: PostEditorStrings.sendFeedback, image: UIImage(systemName: "envelope")) { [weak self] _ in self? From 8bcbb4ee63efd3ca92672ac8caf6be0737e8bb98 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 29 Oct 2026 14:29:09 -0400 Subject: [PATCH 17/17] fix(editor): say "page" in the saving lock alert messages for pages The titles already said page, but the messages still said post. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017NCdQuHPbc49BP1ZshDSE8 --- .../PostGBKEditorViewControllerTests.swift | 21 ++++++++++++--- .../NewGutenberg/PostEditorStrings.swift | 26 ++++++++++++++----- .../PostGBKEditorViewController.swift | 6 +++-- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift index 002e51712ffa..06549b55e830 100644 --- a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift +++ b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift @@ -163,10 +163,23 @@ struct PostGBKEditorViewControllerTests { window.rootViewController = viewController window.makeKeyAndVisible() - viewController.showLeaveWhileSavingLockedAlert {} + viewController.editor( + viewController.editorViewController, + didUpdateSaveAvailability: EditorSaveAvailability(isSaveable: true, isSavingLocked: true) + ) - let alert = try #require(viewController.presentedViewController as? UIAlertController) - #expect(alert.title == PostEditorStrings.notReadyPageTitle) + viewController.performAfterConfirmingSavingLock {} + let saveAlert = try #require(viewController.presentedViewController as? UIAlertController) + #expect(saveAlert.title == PostEditorStrings.notReadyPageTitle) + #expect(saveAlert.message == PostEditorStrings.savePageWhileNotReadyMessage) + + // A fresh view controller, since the save alert is still presented. + let leaveViewController = makeViewController(postType: .page) + window.rootViewController = leaveViewController + leaveViewController.showLeaveWhileSavingLockedAlert {} + let leaveAlert = try #require(leaveViewController.presentedViewController as? UIAlertController) + #expect(leaveAlert.title == PostEditorStrings.notReadyPageTitle) + #expect(leaveAlert.message == PostEditorStrings.leavePageWhileNotReadyMessage) } @Test("asks before leaving while saving is locked") @@ -181,7 +194,7 @@ struct PostGBKEditorViewControllerTests { #expect(!didLeave) let alert = try #require(viewController.presentedViewController as? UIAlertController) - #expect(alert.message == PostEditorStrings.leaveWhileNotReadyMessage) + #expect(alert.message == PostEditorStrings.leavePostWhileNotReadyMessage) #expect(alert.actions.map(\.title) == [PostEditorStrings.keepEditing, PostEditorStrings.leave]) } diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift index 1869fb387398..a420c4398835 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostEditorStrings.swift @@ -61,19 +61,33 @@ enum PostEditorStrings { comment: "Title of an alert shown when saving or closing the editor for a page while something, like a media upload, is in progress" ) - static let leaveWhileNotReadyMessage = NSLocalizedString( - "postEditor.notReadyAlert.leaveMessage", + static let leavePostWhileNotReadyMessage = NSLocalizedString( + "postEditor.notReadyAlert.leavePostMessage", value: "If you leave now, anything still in progress, like a media upload, stops and won't be added to your post.", comment: - "Message of an alert shown when closing the editor while something, like a media upload, is in progress" + "Message of an alert shown when closing the editor for a post while something, like a media upload, is in progress" ) - static let saveWhileNotReadyMessage = NSLocalizedString( - "postEditor.notReadyAlert.saveMessage", + static let leavePageWhileNotReadyMessage = NSLocalizedString( + "postEditor.notReadyAlert.leavePageMessage", + value: + "If you leave now, anything still in progress, like a media upload, stops and won't be added to your page.", + comment: + "Message of an alert shown when closing the editor for a page while something, like a media upload, is in progress" + ) + static let savePostWhileNotReadyMessage = NSLocalizedString( + "postEditor.notReadyAlert.savePostMessage", value: "Something in the editor, like a media upload, is still in progress. If you continue, it may be missing or broken in your post.", comment: - "Message of an alert shown when saving, publishing or previewing while something, like a media upload, is in progress" + "Message of an alert shown when saving, publishing or previewing a post while something, like a media upload, is in progress" + ) + static let savePageWhileNotReadyMessage = NSLocalizedString( + "postEditor.notReadyAlert.savePageMessage", + value: + "Something in the editor, like a media upload, is still in progress. If you continue, it may be missing or broken in your page.", + comment: + "Message of an alert shown when saving, publishing or previewing a page while something, like a media upload, is in progress" ) static let leave = NSLocalizedString( "postEditor.notReadyAlert.leave", diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index f4950734f066..c72d36cacd2a 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -122,7 +122,8 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont } let alert = UIAlertController( title: notReadyTitle, - message: PostEditorStrings.saveWhileNotReadyMessage, + message: isPage + ? PostEditorStrings.savePageWhileNotReadyMessage : PostEditorStrings.savePostWhileNotReadyMessage, preferredStyle: .alert ) alert.addCancelActionWithTitle(PostEditorStrings.keepEditing) @@ -134,7 +135,8 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont func showLeaveWhileSavingLockedAlert(onLeave: @escaping () -> Void) { let alert = UIAlertController( title: notReadyTitle, - message: PostEditorStrings.leaveWhileNotReadyMessage, + message: isPage + ? PostEditorStrings.leavePageWhileNotReadyMessage : PostEditorStrings.leavePostWhileNotReadyMessage, preferredStyle: .alert ) alert.addCancelActionWithTitle(PostEditorStrings.keepEditing)