From 73bb0fb8789f31336d1152a85032e85f536c31b3 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:15:38 +1300 Subject: [PATCH 01/31] Create a new `PostGBKEditorViewController` super class --- .../NewGutenberg/NewGutenbergViewController.swift | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index f6ba67ae4348..e7056864c319 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -13,7 +13,12 @@ import Photos import Pulse import Support -class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor { +// To support editing `AbstractPost` from Core Data and `AnyPostWithEditContext` from the Rust library +class PostGBKEditorViewController: UIViewController { + +} + +class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { let errorDomain: String = "GutenbergViewController.errorDomain" From a6fc3d780f73d1e7e555dcabea0dc4752ad0d74b Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:17:33 +1300 Subject: [PATCH 02/31] Remove an unused initialiser --- .../NewGutenbergViewController.swift | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index e7056864c319..9763736c123e 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -101,24 +101,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi func getHTML() -> String { post.content ?? "" } // MARK: - Initializers - required convenience init( - post: AbstractPost, - replaceEditor: @escaping ReplaceEditorCallback, - editorSession: PostEditorAnalyticsSession? - ) { - self.init( - post: post, - replaceEditor: replaceEditor, - editorSession: editorSession, - // Notice this parameter. - // The value is the default set in the required init but we need to set it explicitly, - // otherwise we'd trigger and infinite loop on this init. - // - // The reason we need this init at all even though the other one does the same job is - // to conform to the PostEditor protocol. - navigationBarManager: nil - ) - } required init( post: AbstractPost, From ff5ecba33dfe03ff7e51ee01287150932cf9f2c2 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:18:16 +1300 Subject: [PATCH 03/31] Remove unused parameters --- .../ViewRelated/NewGutenberg/NewGutenbergViewController.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 9763736c123e..8ce0465bca02 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -105,15 +105,13 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi required init( post: AbstractPost, replaceEditor: @escaping ReplaceEditorCallback, - editorSession: PostEditorAnalyticsSession? = nil, - navigationBarManager: PostEditorNavigationBarManager? = nil ) { self.post = post self.replaceEditor = replaceEditor self.editorSession = PostEditorAnalyticsSession(editor: .gutenbergKit, post: post) - self.navigationBarManager = navigationBarManager ?? PostEditorNavigationBarManager() + self.navigationBarManager = PostEditorNavigationBarManager() EditorLocalization.localize = getLocalizedString From 94efa5cc4d1861309783b131ef6b90cbd1f1ebb0 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:35:13 +1300 Subject: [PATCH 04/31] Move initialiser to the super class --- .../NewGutenbergViewController.swift | 81 ++++++++++++------- 1 file changed, 53 insertions(+), 28 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 8ce0465bca02..17fe391aa815 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -16,6 +16,52 @@ import Support // To support editing `AbstractPost` from Core Data and `AnyPostWithEditContext` from the Rust library class PostGBKEditorViewController: UIViewController { + let navigationBarManager: PostEditorNavigationBarManager + + private let editorViewController: GutenbergKit.EditorViewController + + init( + postId: Int?, + postType: String, + title: String?, + content: String?, + status: String?, + blog: Blog + ) { + self.navigationBarManager = PostEditorNavigationBarManager() + + EditorLocalization.localize = getLocalizedString + + // Create configuration with post content + let editorConfiguration = EditorConfiguration(blog: blog, postType: postType) + .toBuilder() + .setTitle(title ?? "") + .setContent(content ?? "") + .setPostID(postId) + .setPostStatus(status ?? "draft") + .setNativeInserterEnabled(FeatureFlag.nativeBlockInserter.enabled) + .build() + + // Use prefetched dependencies if available (fast path with spinner), + // otherwise pass nil and GutenbergKit will fetch them (shows progress bar) + let cachedDependencies = EditorDependencyManager.shared.dependencies(for: blog) + + self.editorViewController = GutenbergKit.EditorViewController( + configuration: editorConfiguration, + dependencies: cachedDependencies, + mediaPicker: MediaPickerController(blog: blog) + ) + + super.init(nibName: nil, bundle: nil) + + self.editorViewController.delegate = self + self.navigationBarManager.delegate = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } + } class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { @@ -61,8 +107,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi } } - let navigationBarManager: PostEditorNavigationBarManager - // MARK: - Private variables // TODO: reimplemet @@ -70,7 +114,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi // MARK: - GutenbergKit - private var editorViewController: GutenbergKit.EditorViewController private var isModalDialogOpen = false lazy var autosaver = Autosaver() { [weak self] in @@ -106,41 +149,23 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi post: AbstractPost, replaceEditor: @escaping ReplaceEditorCallback, ) { - self.post = post self.replaceEditor = replaceEditor self.editorSession = PostEditorAnalyticsSession(editor: .gutenbergKit, post: post) - self.navigationBarManager = PostEditorNavigationBarManager() - - EditorLocalization.localize = getLocalizedString // Create configuration with post content let postType = post is Page ? "page" : "post" let postStatus = post.status?.rawValue ?? "draft" - let editorConfiguration = EditorConfiguration(blog: post.blog, postType: postType) - .toBuilder() - .setTitle(post.postTitle ?? "") - .setContent(post.content ?? "") - .setPostID(post.postID?.intValue) - .setPostStatus(postStatus) - .setNativeInserterEnabled(FeatureFlag.nativeBlockInserter.enabled) - .build() - - // Use prefetched dependencies if available (fast path with spinner), - // otherwise pass nil and GutenbergKit will fetch them (shows progress bar) - let cachedDependencies = EditorDependencyManager.shared.dependencies(for: post.blog) - self.editorViewController = GutenbergKit.EditorViewController( - configuration: editorConfiguration, - dependencies: cachedDependencies, - mediaPicker: MediaPickerController(blog: post.blog) + super.init( + postId: post.postID?.intValue, + postType: postType, + title: post.postTitle ?? "", + content: post.content ?? "", + status: postStatus, + blog: post.blog ) - - super.init(nibName: nil, bundle: nil) - - self.editorViewController.delegate = self - self.navigationBarManager.delegate = self } required init?(coder aDecoder: NSCoder) { From 65199d00bf61321eafeade22e890d6b314b901a3 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:30:52 +1300 Subject: [PATCH 05/31] Add empty `EditorViewControllerDelegate` implementation --- .../NewGutenbergViewController.swift | 53 ++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 17fe391aa815..4a5ac558c711 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -14,7 +14,7 @@ import Pulse import Support // To support editing `AbstractPost` from Core Data and `AnyPostWithEditContext` from the Rust library -class PostGBKEditorViewController: UIViewController { +class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewControllerDelegate { let navigationBarManager: PostEditorNavigationBarManager @@ -62,6 +62,57 @@ class PostGBKEditorViewController: UIViewController { fatalError() } + // MARK: - GutenbergKit.EditorViewControllerDelegate + + func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { + // Do nothing + } + + func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) { + // Do nothing + } + + func editor(_ viewContoller: GutenbergKit.EditorViewController, didEncounterCriticalError error: any Error) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateContentWithState state: GutenbergKit.EditorState) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: GutenbergKit.EditorState) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateFeaturedImage mediaID: Int) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didLogException error: GutenbergKit.GutenbergJSException) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didTriggerAutocompleter type: String) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didOpenModalDialog dialogType: String) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didCloseModalDialog dialogType: String) { + // Do nothing + } + + func editorDidRequestLatestContent(_ controller: GutenbergKit.EditorViewController) -> (title: String, content: String)? { + // Do nothing + return nil + } + } class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { From 54aca0fcfd4cf0d2e8950e6ed9fea588506c5cfe Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:33:49 +1300 Subject: [PATCH 06/31] Add empty `PostEditorNavigationBarManagerDelegate` implementation --- .../NewGutenbergViewController.swift | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 4a5ac558c711..f817ffbce528 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -14,7 +14,7 @@ import Pulse import Support // To support editing `AbstractPost` from Core Data and `AnyPostWithEditContext` from the Rust library -class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewControllerDelegate { +class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewControllerDelegate, PostEditorNavigationBarManagerDelegate { let navigationBarManager: PostEditorNavigationBarManager @@ -113,6 +113,46 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont return nil } + // MARK: - PostEditorNavigationBarManagerDelegate + + var publishButtonText: String { + wpAssertionFailure("To be implemented by subclasses") + return "" + } + + var isPublishButtonEnabled: Bool { + wpAssertionFailure("To be implemented by subclasses") + return false + } + + var uploadingButtonSize: CGSize { + wpAssertionFailure("To be implemented by subclasses") + return .zero + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { + // Do nothing + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { + // Do nothing + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, redoWasPressed sender: UIButton) { + // Do nothing + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { + // Do nothing + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { + // Do nothing + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, displayCancelMediaUploads sender: UIButton) { + // Do nothing + } } class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { From c4a3f8dc650eec2e861b1b35573677fa94561571 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:43:02 +1300 Subject: [PATCH 07/31] Add override keywords --- .../NewGutenbergViewController.swift | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index f817ffbce528..29cbc6aaec53 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -452,11 +452,17 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi navigationBarManager.undoButton.isEnabled = enabled navigationBarManager.redoButton.isEnabled = enabled } + +/* + Fix issue: Non-'@objc' instance method 'editorDidLoad' declared in 'PostGBKEditorViewController' cannot be overridden from extension + + Add the extension back if needed. } extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate { + */ - func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { + override func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { 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 @@ -466,25 +472,25 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate } } - func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) { + override func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) { // Do nothing } - func editor(_ viewContoller: GutenbergKit.EditorViewController, didEncounterCriticalError error: any Error) { + override func editor(_ viewContoller: GutenbergKit.EditorViewController, didEncounterCriticalError error: any Error) { onClose?() } - func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateContentWithState state: GutenbergKit.EditorState) { + override func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateContentWithState state: GutenbergKit.EditorState) { editorContentWasUpdated() autosaver.contentDidChange() } - func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: GutenbergKit.EditorState) { + override func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: GutenbergKit.EditorState) { gutenbergDidRequestToggleRedoButton(!state.hasRedo) gutenbergDidRequestToggleUndoButton(!state.hasUndo) } - func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateFeaturedImage mediaID: Int) { + override func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateFeaturedImage mediaID: Int) { let featuredImageID = post.featuredImage?.mediaID?.intValue guard featuredImageID != mediaID else { @@ -495,7 +501,7 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate self.featuredImageHelper.setFeaturedImage(mediaID: mediaID) } - func editor(_ viewController: GutenbergKit.EditorViewController, didLogException error: GutenbergKit.GutenbergJSException) { + override func editor(_ viewController: GutenbergKit.EditorViewController, didLogException error: GutenbergKit.GutenbergJSException) { logException(error) { // Do nothing } @@ -503,7 +509,7 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate // MARK: - Media Picker Helpers - func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) { + override func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) { let flags = mediaFilterFlags(using: config.allowedTypes ?? []) let initialSelectionArray: [Int] @@ -535,7 +541,7 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate } } - func editor(_ viewController: GutenbergKit.EditorViewController, didTriggerAutocompleter type: String) { + override func editor(_ viewController: GutenbergKit.EditorViewController, didTriggerAutocompleter type: String) { switch type { case "at-symbol": showSuggestions(type: .mention) { [weak self] result in @@ -562,17 +568,17 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate } } - func editor(_ viewController: GutenbergKit.EditorViewController, didOpenModalDialog dialogType: String) { + override func editor(_ viewController: GutenbergKit.EditorViewController, didOpenModalDialog dialogType: String) { isModalDialogOpen = true setNavigationItemsEnabled(false) } - func editor(_ viewController: GutenbergKit.EditorViewController, didCloseModalDialog dialogType: String) { + override func editor(_ viewController: GutenbergKit.EditorViewController, didCloseModalDialog dialogType: String) { isModalDialogOpen = false setNavigationItemsEnabled(true) } - func editorDidRequestLatestContent(_ controller: GutenbergKit.EditorViewController) -> (title: String, content: String)? { + override func editorDidRequestLatestContent(_ controller: GutenbergKit.EditorViewController) -> (title: String, content: String)? { // Return the current post title and content from Core Data. // This is the authoritative source, updated via autosave. return (post.postTitle ?? "", post.content ?? "") From 9b8f6f0195fdfb05e272b20803ac687cb0cd437b Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:50:26 +1300 Subject: [PATCH 08/31] Move `PostEditorNavigationBarManagerDelegate` implementation --- .../NewGutenbergViewController.swift | 107 +++++++++--------- 1 file changed, 55 insertions(+), 52 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 29cbc6aaec53..83953be672a3 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -617,6 +617,61 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate return WPMediaType(rawValue: mediaType) } + + // MARK: - PostEditorNavigationBarManagerDelegate + + var publishButtonText: String { + return postEditorStateContext.publishButtonText + } + + var isPublishButtonEnabled: Bool { + return postEditorStateContext.isPublishButtonEnabled + } + + var uploadingButtonSize: CGSize { + return AztecPostViewController.Constants.uploadingButtonSize + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { + performAfterUpdatingContent { [self] in + cancelEditing() + } + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { + editorViewController.undo() + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, redoWasPressed sender: UIButton) { + editorViewController.redo() + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { + // Currently unsupported, do nothing. + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, displayCancelMediaUploads sender: UIButton) { + // Currently unsupported, do nothing. + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { + performAfterUpdatingContent { [self] in + if editorHasContent { + handlePrimaryActionButtonTap() + } else { + showAlertForEmptyPostPublish() + } + } + } + + private func performAfterUpdatingContent(_ closure: @MainActor @escaping () -> Void) { + navigationController?.view.isUserInteractionEnabled = false + Task { @MainActor in + await getLatestContent() + navigationController?.view.isUserInteractionEnabled = true + closure() + } + } } extension GutenbergKit.EditorViewControllerDelegate { @@ -840,18 +895,6 @@ extension NewGutenbergViewController: PostEditorStateContextDelegate { extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { - var publishButtonText: String { - return postEditorStateContext.publishButtonText - } - - var isPublishButtonEnabled: Bool { - return postEditorStateContext.isPublishButtonEnabled - } - - var uploadingButtonSize: CGSize { - return AztecPostViewController.Constants.uploadingButtonSize - } - func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) { DispatchQueue.main.async { UIView.animate(withDuration: 0.2) { @@ -870,46 +913,6 @@ extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { } } - func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { - performAfterUpdatingContent { [self] in - cancelEditing() - } - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { - editorViewController.undo() - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, redoWasPressed sender: UIButton) { - editorViewController.redo() - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { - // Currently unsupported, do nothing. - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, displayCancelMediaUploads sender: UIButton) { - // Currently unsupported, do nothing. - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { - performAfterUpdatingContent { [self] in - if editorHasContent { - handlePrimaryActionButtonTap() - } else { - showAlertForEmptyPostPublish() - } - } - } - - private func performAfterUpdatingContent(_ closure: @MainActor @escaping () -> Void) { - navigationController?.view.isUserInteractionEnabled = false - Task { @MainActor in - await getLatestContent() - navigationController?.view.isUserInteractionEnabled = true - closure() - } - } } /// This extension handles the "more" actions triggered by the top right From 715a8a4cf6ce1c9b83507e7f79703cdabbd5da88 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:52:45 +1300 Subject: [PATCH 09/31] Add override keywords --- .../NewGutenbergViewController.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 83953be672a3..14780023593b 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -620,41 +620,41 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate // MARK: - PostEditorNavigationBarManagerDelegate - var publishButtonText: String { + override var publishButtonText: String { return postEditorStateContext.publishButtonText } - var isPublishButtonEnabled: Bool { + override var isPublishButtonEnabled: Bool { return postEditorStateContext.isPublishButtonEnabled } - var uploadingButtonSize: CGSize { + override var uploadingButtonSize: CGSize { return AztecPostViewController.Constants.uploadingButtonSize } - func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { + override func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { performAfterUpdatingContent { [self] in cancelEditing() } } - func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { + override func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { editorViewController.undo() } - func navigationBarManager(_ manager: PostEditorNavigationBarManager, redoWasPressed sender: UIButton) { + override func navigationBarManager(_ manager: PostEditorNavigationBarManager, redoWasPressed sender: UIButton) { editorViewController.redo() } - func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { + override func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { // Currently unsupported, do nothing. } - func navigationBarManager(_ manager: PostEditorNavigationBarManager, displayCancelMediaUploads sender: UIButton) { + override func navigationBarManager(_ manager: PostEditorNavigationBarManager, displayCancelMediaUploads sender: UIButton) { // Currently unsupported, do nothing. } - func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { + override func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { performAfterUpdatingContent { [self] in if editorHasContent { handlePrimaryActionButtonTap() @@ -893,7 +893,7 @@ extension NewGutenbergViewController: PostEditorStateContextDelegate { // MARK: - PostEditorNavigationBarManagerDelegate -extension NewGutenbergViewController: PostEditorNavigationBarManagerDelegate { +extension NewGutenbergViewController { func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) { DispatchQueue.main.async { From f6b7b3e123038878d5e583ebc0c603f34fd2183f Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:53:11 +1300 Subject: [PATCH 10/31] Remove private keyword --- .../ViewRelated/NewGutenberg/NewGutenbergViewController.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 14780023593b..30f5e0b167e3 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -18,7 +18,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont let navigationBarManager: PostEditorNavigationBarManager - private let editorViewController: GutenbergKit.EditorViewController + /* private */ let editorViewController: GutenbergKit.EditorViewController init( postId: Int?, From c6b6826353acae4304b659dbbcb4c4ba9c79d3e8 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:55:19 +1300 Subject: [PATCH 11/31] Remove empty override functions --- .../NewGutenberg/NewGutenbergViewController.swift | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 30f5e0b167e3..7a0d549b71d2 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -472,10 +472,6 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate } } - override func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) { - // Do nothing - } - override func editor(_ viewContoller: GutenbergKit.EditorViewController, didEncounterCriticalError error: any Error) { onClose?() } @@ -646,14 +642,6 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate editorViewController.redo() } - override func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { - // Currently unsupported, do nothing. - } - - override func navigationBarManager(_ manager: PostEditorNavigationBarManager, displayCancelMediaUploads sender: UIButton) { - // Currently unsupported, do nothing. - } - override func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { performAfterUpdatingContent { [self] in if editorHasContent { From 1e6144ba5a84b38f131bc6428cd22f0b095a1a5f Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 21:59:30 +1300 Subject: [PATCH 12/31] Move `didUpdateHistoryState` delegate function to the super class --- .../NewGutenbergViewController.swift | 52 ++++++++----------- 1 file changed, 22 insertions(+), 30 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 7a0d549b71d2..e11f2bd5d769 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -81,7 +81,8 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont } func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: GutenbergKit.EditorState) { - // Do nothing + gutenbergDidRequestToggleRedoButton(!state.hasRedo) + gutenbergDidRequestToggleUndoButton(!state.hasUndo) } func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateFeaturedImage mediaID: Int) { @@ -155,6 +156,26 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont } } +private extension PostGBKEditorViewController { + func gutenbergDidRequestToggleRedoButton(_ isDisabled: Bool) { + DispatchQueue.main.async { + UIView.animate(withDuration: 0.2) { + self.navigationBarManager.redoButton.isUserInteractionEnabled = isDisabled ? false : true + self.navigationBarManager.redoButton.alpha = isDisabled ? 0.3 : 1.0 + } + } + } + + func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) { + DispatchQueue.main.async { + UIView.animate(withDuration: 0.2) { + self.navigationBarManager.undoButton.isUserInteractionEnabled = isDisabled ? false : true + self.navigationBarManager.undoButton.alpha = isDisabled ? 0.3 : 1.0 + } + } + } +} + class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { let errorDomain: String = "GutenbergViewController.errorDomain" @@ -481,11 +502,6 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate autosaver.contentDidChange() } - override func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: GutenbergKit.EditorState) { - gutenbergDidRequestToggleRedoButton(!state.hasRedo) - gutenbergDidRequestToggleUndoButton(!state.hasUndo) - } - override func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateFeaturedImage mediaID: Int) { let featuredImageID = post.featuredImage?.mediaID?.intValue @@ -879,30 +895,6 @@ extension NewGutenbergViewController: PostEditorStateContextDelegate { } } -// MARK: - PostEditorNavigationBarManagerDelegate - -extension NewGutenbergViewController { - - func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) { - DispatchQueue.main.async { - UIView.animate(withDuration: 0.2) { - self.navigationBarManager.undoButton.isUserInteractionEnabled = isDisabled ? false : true - self.navigationBarManager.undoButton.alpha = isDisabled ? 0.3 : 1.0 - } - } - } - - func gutenbergDidRequestToggleRedoButton(_ isDisabled: Bool) { - DispatchQueue.main.async { - UIView.animate(withDuration: 0.2) { - self.navigationBarManager.redoButton.isUserInteractionEnabled = isDisabled ? false : true - self.navigationBarManager.redoButton.alpha = isDisabled ? 0.3 : 1.0 - } - } - } - -} - /// This extension handles the "more" actions triggered by the top right /// navigation bar button of Gutenberg editor. extension NewGutenbergViewController { From a165d71b48e40db1c8e52272fbe5e4f83f90cf91 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:02:15 +1300 Subject: [PATCH 13/31] Move `didLogException` delegate function to the super class --- .../NewGutenbergViewController.swift | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index e11f2bd5d769..02df3c1aa57e 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -89,8 +89,12 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont // Do nothing } - func editor(_ viewController: GutenbergKit.EditorViewController, didLogException error: GutenbergKit.GutenbergJSException) { - // Do nothing + func editor(_ viewController: GutenbergKit.EditorViewController, didLogException exception: GutenbergKit.GutenbergJSException) { + DispatchQueue.main.async { + WordPressAppDelegate.crashLogging?.logJavaScriptException(exception) { + // Do nothing + } + } } func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) { @@ -395,12 +399,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi self.present(SubmitFeedbackViewController(source: "gutenberg_kit", feedbackPrefix: "Editor"), animated: true) } - func logException(_ exception: GutenbergJSException, with callback: @escaping () -> Void) { - DispatchQueue.main.async { - WordPressAppDelegate.crashLogging?.logJavaScriptException(exception, callback: callback) - } - } - // MARK: - Keyboard Observers private func setupKeyboardObservers() { @@ -513,12 +511,6 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate self.featuredImageHelper.setFeaturedImage(mediaID: mediaID) } - override func editor(_ viewController: GutenbergKit.EditorViewController, didLogException error: GutenbergKit.GutenbergJSException) { - logException(error) { - // Do nothing - } - } - // MARK: - Media Picker Helpers override func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) { From 20a16e33e5d026fa5aa7d9cae3812b0963fa05c4 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:05:55 +1300 Subject: [PATCH 14/31] Move modal dialog delegate functions to the super class --- .../NewGutenbergViewController.swift | 36 ++++++++----------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 02df3c1aa57e..e4ff74e32612 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -20,6 +20,8 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont /* private */ let editorViewController: GutenbergKit.EditorViewController + private var isModalDialogOpen = false + init( postId: Int?, postType: String, @@ -106,11 +108,13 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont } func editor(_ viewController: GutenbergKit.EditorViewController, didOpenModalDialog dialogType: String) { - // Do nothing + isModalDialogOpen = true + setNavigationItemsEnabled(false) } func editor(_ viewController: GutenbergKit.EditorViewController, didCloseModalDialog dialogType: String) { - // Do nothing + isModalDialogOpen = false + setNavigationItemsEnabled(true) } func editorDidRequestLatestContent(_ controller: GutenbergKit.EditorViewController) -> (title: String, content: String)? { @@ -178,6 +182,14 @@ private extension PostGBKEditorViewController { } } } + + func setNavigationItemsEnabled(_ enabled: Bool) { + navigationBarManager.closeButton.isEnabled = enabled + navigationBarManager.moreButton.isEnabled = enabled + navigationBarManager.publishButton.isEnabled = enabled + navigationBarManager.undoButton.isEnabled = enabled + navigationBarManager.redoButton.isEnabled = enabled + } } class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { @@ -230,8 +242,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi // MARK: - GutenbergKit - private var isModalDialogOpen = false - lazy var autosaver = Autosaver() { [weak self] in self?.performAutoSave() } @@ -464,14 +474,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi } } - private func setNavigationItemsEnabled(_ enabled: Bool) { - navigationBarManager.closeButton.isEnabled = enabled - navigationBarManager.moreButton.isEnabled = enabled - navigationBarManager.publishButton.isEnabled = enabled - navigationBarManager.undoButton.isEnabled = enabled - navigationBarManager.redoButton.isEnabled = enabled - } - /* Fix issue: Non-'@objc' instance method 'editorDidLoad' declared in 'PostGBKEditorViewController' cannot be overridden from extension @@ -572,16 +574,6 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate } } - override func editor(_ viewController: GutenbergKit.EditorViewController, didOpenModalDialog dialogType: String) { - isModalDialogOpen = true - setNavigationItemsEnabled(false) - } - - override func editor(_ viewController: GutenbergKit.EditorViewController, didCloseModalDialog dialogType: String) { - isModalDialogOpen = false - setNavigationItemsEnabled(true) - } - override func editorDidRequestLatestContent(_ controller: GutenbergKit.EditorViewController) -> (title: String, content: String)? { // Return the current post title and content from Core Data. // This is the authoritative source, updated via autosave. From 3d0b4730f3b663ce6a3e80c8b1689d83593159b8 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:08:29 +1300 Subject: [PATCH 15/31] Move undo redo delegate functions to the super class --- .../NewGutenberg/NewGutenbergViewController.swift | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index e4ff74e32612..6b03317f8921 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -144,11 +144,11 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont } func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { - // Do nothing + editorViewController.undo() } func navigationBarManager(_ manager: PostEditorNavigationBarManager, redoWasPressed sender: UIButton) { - // Do nothing + editorViewController.redo() } func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { @@ -634,14 +634,6 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate } } - override func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { - editorViewController.undo() - } - - override func navigationBarManager(_ manager: PostEditorNavigationBarManager, redoWasPressed sender: UIButton) { - editorViewController.redo() - } - override func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { performAfterUpdatingContent { [self] in if editorHasContent { From f0cf75c8cbcbde6c0ad84e416f33ff7f680d20cc Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:11:33 +1300 Subject: [PATCH 16/31] Move keyboard observers to the super class --- .../NewGutenbergViewController.swift | 93 ++++++++++--------- 1 file changed, 47 insertions(+), 46 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 6b03317f8921..527dd2e81e3e 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -22,6 +22,10 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont private var isModalDialogOpen = false + private var keyboardShowObserver: Any? + private var keyboardHideObserver: Any? + private var keyboardFrame = CGRect.zero + init( postId: Int?, postType: String, @@ -190,6 +194,49 @@ private extension PostGBKEditorViewController { navigationBarManager.undoButton.isEnabled = enabled navigationBarManager.redoButton.isEnabled = enabled } + + // MARK: - Keyboard Observers + + func setupKeyboardObservers() { + keyboardShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: .main) { [weak self] (notification) in + if let self, let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { + self.keyboardFrame = keyboardRect + self.updateConstraintsToAvoidKeyboard(frame: keyboardRect) + } + } + keyboardHideObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidHideNotification, object: nil, queue: .main) { [weak self] (notification) in + if let self, let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { + self.keyboardFrame = keyboardRect + self.updateConstraintsToAvoidKeyboard(frame: keyboardRect) + } + } + } + + func tearDownKeyboardObservers() { + if let keyboardShowObserver { + NotificationCenter.default.removeObserver(keyboardShowObserver) + } + if let keyboardHideObserver { + NotificationCenter.default.removeObserver(keyboardHideObserver) + } + } + + func updateConstraintsToAvoidKeyboard(frame: CGRect) { + keyboardFrame = frame + let minimumKeyboardHeight = CGFloat(50) + guard let suggestionViewBottomConstraint else { + return + } + + // There are cases where the keyboard is not visible, but the system instead of returning zero, returns a low number, for example: 0, 3, 69. + // So in those scenarios, we just need to take in account the safe area and ignore the keyboard all together. + if keyboardFrame.height < minimumKeyboardHeight { + suggestionViewBottomConstraint.constant = -self.view.safeAreaInsets.bottom + } + else { + suggestionViewBottomConstraint.constant = -self.keyboardFrame.height + } + } } class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { @@ -248,9 +295,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi // MARK: - Private Properties - private var keyboardShowObserver: Any? - private var keyboardHideObserver: Any? - private var keyboardFrame = CGRect.zero private var suggestionViewBottomConstraint: NSLayoutConstraint? private var currentSuggestionsController: GutenbergSuggestionsViewController? @@ -409,49 +453,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi self.present(SubmitFeedbackViewController(source: "gutenberg_kit", feedbackPrefix: "Editor"), animated: true) } - // MARK: - Keyboard Observers - - private func setupKeyboardObservers() { - keyboardShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: .main) { [weak self] (notification) in - if let self, let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { - self.keyboardFrame = keyboardRect - self.updateConstraintsToAvoidKeyboard(frame: keyboardRect) - } - } - keyboardHideObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidHideNotification, object: nil, queue: .main) { [weak self] (notification) in - if let self, let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { - self.keyboardFrame = keyboardRect - self.updateConstraintsToAvoidKeyboard(frame: keyboardRect) - } - } - } - - private func tearDownKeyboardObservers() { - if let keyboardShowObserver { - NotificationCenter.default.removeObserver(keyboardShowObserver) - } - if let keyboardHideObserver { - NotificationCenter.default.removeObserver(keyboardHideObserver) - } - } - - private func updateConstraintsToAvoidKeyboard(frame: CGRect) { - keyboardFrame = frame - let minimumKeyboardHeight = CGFloat(50) - guard let suggestionViewBottomConstraint else { - return - } - - // There are cases where the keyboard is not visible, but the system instead of returning zero, returns a low number, for example: 0, 3, 69. - // So in those scenarios, we just need to take in account the safe area and ignore the keyboard all together. - if keyboardFrame.height < minimumKeyboardHeight { - suggestionViewBottomConstraint.constant = -self.view.safeAreaInsets.bottom - } - else { - suggestionViewBottomConstraint.constant = -self.keyboardFrame.height - } - } - private func loadAuthenticationCookiesAsync() async -> Bool { guard post.blog.isPrivate() else { return true From 890ab3c443d686553e026e275b5eb07565017930 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:12:51 +1300 Subject: [PATCH 17/31] Call setupKeyboardObservers in the super class --- .../NewGutenberg/NewGutenbergViewController.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 527dd2e81e3e..4c952add2652 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -68,6 +68,11 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont fatalError() } + override func viewDidLoad() { + super.viewDidLoad() + setupKeyboardObservers() + } + // MARK: - GutenbergKit.EditorViewControllerDelegate func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { @@ -350,7 +355,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi override func viewDidLoad() { super.viewDidLoad() - setupKeyboardObservers() view.backgroundColor = .systemBackground From 9147c8585177677e73ec4005e8f6a9f29d264fb7 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:16:10 +1300 Subject: [PATCH 18/31] Move showSuggestions function to the super class --- .../NewGutenbergViewController.swift | 155 +++++++++--------- 1 file changed, 76 insertions(+), 79 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 4c952add2652..8e2975b79a7b 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -16,6 +16,7 @@ import Support // To support editing `AbstractPost` from Core Data and `AnyPostWithEditContext` from the Rust library class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewControllerDelegate, PostEditorNavigationBarManagerDelegate { + let blog: Blog let navigationBarManager: PostEditorNavigationBarManager /* private */ let editorViewController: GutenbergKit.EditorViewController @@ -26,6 +27,9 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont private var keyboardHideObserver: Any? private var keyboardFrame = CGRect.zero + private var suggestionViewBottomConstraint: NSLayoutConstraint? + private var currentSuggestionsController: GutenbergSuggestionsViewController? + init( postId: Int?, postType: String, @@ -34,6 +38,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont status: String?, blog: Blog ) { + self.blog = blog self.navigationBarManager = PostEditorNavigationBarManager() EditorLocalization.localize = getLocalizedString @@ -242,6 +247,77 @@ private extension PostGBKEditorViewController { suggestionViewBottomConstraint.constant = -self.keyboardFrame.height } } + + // MARK: - Suggestions implementation + + func showSuggestions(type: SuggestionType, callback: @escaping (Swift.Result) -> Void) { + // Prevent multiple suggestions UI instances - simply ignore if already showing + guard currentSuggestionsController == nil else { + return + } + guard let siteID = blog.dotComID else { + callback(.failure(GutenbergSuggestionsViewController.SuggestionError.notAvailable as NSError)) + return + } + + switch type { + case .mention: + guard SuggestionService.shared.shouldShowSuggestions(for: blog) else { return } + case .xpost: + guard SiteSuggestionService.shared.shouldShowSuggestions(for: blog) else { return } + } + + let previousFirstResponder = view.findFirstResponder() + let suggestionsController = GutenbergSuggestionsViewController(siteID: siteID, suggestionType: type) + currentSuggestionsController = suggestionsController + suggestionsController.onCompletion = { [weak self] (result) in + callback(result) + + if let self { + // Clear the current controller reference + self.currentSuggestionsController = nil + self.suggestionViewBottomConstraint = nil + + // Clean up the UI (should only happen if parent still exists) + suggestionsController.view.removeFromSuperview() + suggestionsController.removeFromParent() + + previousFirstResponder?.becomeFirstResponder() + } + + var analyticsName: String + switch type { + case .mention: + analyticsName = "user" + case .xpost: + analyticsName = "xpost" + } + + var didSelectSuggestion = false + if case let .success(text) = result, !text.isEmpty { + didSelectSuggestion = true + } + + let analyticsProperties: [String: Any] = [ + "suggestion_type": analyticsName, + "did_select_suggestion": didSelectSuggestion + ] + + WPAnalytics.track(.gutenbergSuggestionSessionFinished, properties: analyticsProperties) + } + addChild(suggestionsController) + view.addSubview(suggestionsController.view) + let suggestionsBottomConstraint = suggestionsController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) + NSLayoutConstraint.activate([ + suggestionsController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0), + suggestionsController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0), + suggestionsBottomConstraint, + suggestionsController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) + ]) + self.suggestionViewBottomConstraint = suggestionsBottomConstraint + updateConstraintsToAvoidKeyboard(frame: keyboardFrame) + suggestionsController.didMove(toParent: self) + } } class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { @@ -298,11 +374,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi self?.performAutoSave() } - // MARK: - Private Properties - - private var suggestionViewBottomConstraint: NSLayoutConstraint? - private var currentSuggestionsController: GutenbergSuggestionsViewController? - // TODO: remove (none of these APIs are needed for the new editor) func prepopulateMediaItems(_ media: [Media]) {} var debouncer = WordPressShared.Debouncer(delay: 10) @@ -715,80 +786,6 @@ extension NewGutenbergViewController { } -// MARK: - Suggestions implementation - -extension NewGutenbergViewController { - - private func showSuggestions(type: SuggestionType, callback: @escaping (Swift.Result) -> Void) { - // Prevent multiple suggestions UI instances - simply ignore if already showing - guard currentSuggestionsController == nil else { - return - } - guard let siteID = post.blog.dotComID else { - callback(.failure(GutenbergSuggestionsViewController.SuggestionError.notAvailable as NSError)) - return - } - - switch type { - case .mention: - guard SuggestionService.shared.shouldShowSuggestions(for: post.blog) else { return } - case .xpost: - guard SiteSuggestionService.shared.shouldShowSuggestions(for: post.blog) else { return } - } - - let previousFirstResponder = view.findFirstResponder() - let suggestionsController = GutenbergSuggestionsViewController(siteID: siteID, suggestionType: type) - currentSuggestionsController = suggestionsController - suggestionsController.onCompletion = { [weak self] (result) in - callback(result) - - if let self { - // Clear the current controller reference - self.currentSuggestionsController = nil - self.suggestionViewBottomConstraint = nil - - // Clean up the UI (should only happen if parent still exists) - suggestionsController.view.removeFromSuperview() - suggestionsController.removeFromParent() - - previousFirstResponder?.becomeFirstResponder() - } - - var analyticsName: String - switch type { - case .mention: - analyticsName = "user" - case .xpost: - analyticsName = "xpost" - } - - var didSelectSuggestion = false - if case let .success(text) = result, !text.isEmpty { - didSelectSuggestion = true - } - - let analyticsProperties: [String: Any] = [ - "suggestion_type": analyticsName, - "did_select_suggestion": didSelectSuggestion - ] - - WPAnalytics.track(.gutenbergSuggestionSessionFinished, properties: analyticsProperties) - } - addChild(suggestionsController) - view.addSubview(suggestionsController.view) - let suggestionsBottomConstraint = suggestionsController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) - NSLayoutConstraint.activate([ - suggestionsController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0), - suggestionsController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0), - suggestionsBottomConstraint, - suggestionsController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) - ]) - self.suggestionViewBottomConstraint = suggestionsBottomConstraint - updateConstraintsToAvoidKeyboard(frame: keyboardFrame) - suggestionsController.didMove(toParent: self) - } -} - // MARK: - GutenbergBridgeDataSource extension NewGutenbergViewController/*: GutenbergBridgeDataSource*/ { From b35e2588be0e4d92fd72bf24fe4f2dbbddcfb680 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:17:11 +1300 Subject: [PATCH 19/31] Move `didTriggerAutocompleter` to the super class --- .../NewGutenbergViewController.swift | 52 +++++++++---------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 8e2975b79a7b..45835f8b04fc 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -118,7 +118,30 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont } func editor(_ viewController: GutenbergKit.EditorViewController, didTriggerAutocompleter type: String) { - // Do nothing + switch type { + case "at-symbol": + showSuggestions(type: .mention) { [weak self] result in + switch result { + case .success(let suggestion): + // Appended space completes the autocomplete session + self?.editorViewController.appendTextAtCursor(suggestion + " ") + case .failure(let error): + DDLogError("Mention selection cancelled or failed: \(error)") + } + } + case "plus-symbol": + showSuggestions(type: .xpost) { [weak self] result in + switch result { + case .success(let suggestion): + // Appended space completes the autocomplete session + self?.editorViewController.appendTextAtCursor(suggestion + " ") + case .failure(let error): + DDLogError("Xpost selection cancelled or failed: \(error)") + } + } + default: + DDLogError("Unknown autocompleter type: \(type)") + } } func editor(_ viewController: GutenbergKit.EditorViewController, didOpenModalDialog dialogType: String) { @@ -623,33 +646,6 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate } } - override func editor(_ viewController: GutenbergKit.EditorViewController, didTriggerAutocompleter type: String) { - switch type { - case "at-symbol": - showSuggestions(type: .mention) { [weak self] result in - switch result { - case .success(let suggestion): - // Appended space completes the autocomplete session - self?.editorViewController.appendTextAtCursor(suggestion + " ") - case .failure(let error): - DDLogError("Mention selection cancelled or failed: \(error)") - } - } - case "plus-symbol": - showSuggestions(type: .xpost) { [weak self] result in - switch result { - case .success(let suggestion): - // Appended space completes the autocomplete session - self?.editorViewController.appendTextAtCursor(suggestion + " ") - case .failure(let error): - DDLogError("Xpost selection cancelled or failed: \(error)") - } - } - default: - DDLogError("Unknown autocompleter type: \(type)") - } - } - override func editorDidRequestLatestContent(_ controller: GutenbergKit.EditorViewController) -> (title: String, content: String)? { // Return the current post title and content from Core Data. // This is the authoritative source, updated via autosave. From b66faedf8b2275cb6dc40472f31d899b97e74d4f Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:18:13 +1300 Subject: [PATCH 20/31] Move setting background --- .../ViewRelated/NewGutenberg/NewGutenbergViewController.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 45835f8b04fc..ac377eda4c06 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -75,6 +75,8 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont override func viewDidLoad() { super.viewDidLoad() + view.backgroundColor = .systemBackground + setupKeyboardObservers() } @@ -450,8 +452,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi override func viewDidLoad() { super.viewDidLoad() - view.backgroundColor = .systemBackground - createRevisionOfPost(loadAutosaveRevision: false) setupEditorView() configureNavigationBar() From ab3a0e1139247dec7ebe818dfc2984acb9491e88 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:18:45 +1300 Subject: [PATCH 21/31] Move deinit to the super class --- .../NewGutenberg/NewGutenbergViewController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index ac377eda4c06..71903e2dfeca 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -73,6 +73,10 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont fatalError() } + deinit { + tearDownKeyboardObservers() + } + override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground @@ -443,10 +447,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi fatalError() } - deinit { - tearDownKeyboardObservers() - } - // MARK: - Lifecycle methods override func viewDidLoad() { From 7d05cc4980334db6e23eaca95ba05bf71abde8b5 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:20:56 +1300 Subject: [PATCH 22/31] Move `setupEditorView()` to the super class --- .../NewGutenbergViewController.swift | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 71903e2dfeca..b636dd3c943d 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -82,6 +82,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont view.backgroundColor = .systemBackground setupKeyboardObservers() + setupEditorView() } // MARK: - GutenbergKit.EditorViewControllerDelegate @@ -234,6 +235,22 @@ private extension PostGBKEditorViewController { navigationBarManager.redoButton.isEnabled = enabled } + func setupEditorView() { + view.tintColor = UIAppColor.editorPrimary + + addChild(editorViewController) + view.addSubview(editorViewController.view) + view.pinSubviewToAllEdges(editorViewController.view) + editorViewController.didMove(toParent: self) + +#if DEBUG + editorViewController.webView.isInspectable = true +#endif + + // Doesn't seem to do anything + setContentScrollView(editorViewController.webView.scrollView) + } + // MARK: - Keyboard Observers func setupKeyboardObservers() { @@ -453,7 +470,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi super.viewDidLoad() createRevisionOfPost(loadAutosaveRevision: false) - setupEditorView() configureNavigationBar() refreshInterface() @@ -476,22 +492,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi onViewDidLoad() } - private func setupEditorView() { - view.tintColor = UIAppColor.editorPrimary - - addChild(editorViewController) - view.addSubview(editorViewController.view) - view.pinSubviewToAllEdges(editorViewController.view) - editorViewController.didMove(toParent: self) - -#if DEBUG - editorViewController.webView.isInspectable = true -#endif - - // Doesn't seem to do anything - setContentScrollView(editorViewController.webView.scrollView) - } - // MARK: - Functions private func configureNavigationBar() { From 32e4acb9d6455efc86e32e7d221fdcdf7ec6f335 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:24:06 +1300 Subject: [PATCH 23/31] Move configureNavigationBar to the super class --- .../NewGutenbergViewController.swift | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index b636dd3c943d..05c68c3d22fc 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -83,6 +83,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont setupKeyboardObservers() setupEditorView() + configureNavigationBar() } // MARK: - GutenbergKit.EditorViewControllerDelegate @@ -251,6 +252,18 @@ private extension PostGBKEditorViewController { setContentScrollView(editorViewController.webView.scrollView) } + func configureNavigationBar() { + navigationController?.navigationBar.accessibilityIdentifier = "Gutenberg Editor Navigation Bar" + navigationItem.leftBarButtonItems = navigationBarManager.leftBarButtonItems + + edgesForExtendedLayout = [] + // TODO: make it work +// configureDefaultNavigationBarAppearance() + + navigationBarManager.moreButton.menu = makeMoreMenu() + navigationBarManager.moreButton.showsMenuAsPrimaryAction = true + } + // MARK: - Keyboard Observers func setupKeyboardObservers() { @@ -470,7 +483,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi super.viewDidLoad() createRevisionOfPost(loadAutosaveRevision: false) - configureNavigationBar() refreshInterface() // Load auth cookies if needed (for private sites) @@ -494,18 +506,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi // MARK: - Functions - private func configureNavigationBar() { - navigationController?.navigationBar.accessibilityIdentifier = "Gutenberg Editor Navigation Bar" - navigationItem.leftBarButtonItems = navigationBarManager.leftBarButtonItems - - edgesForExtendedLayout = [] - // TODO: make it work -// configureDefaultNavigationBarAppearance() - - navigationBarManager.moreButton.menu = makeMoreMenu() - navigationBarManager.moreButton.showsMenuAsPrimaryAction = true - } - private func refreshInterface() { reloadPublishButton() navigationItem.rightBarButtonItems = post.status == .trash ? [] : navigationBarManager.rightBarButtonItems From d0c92a7ba3b16165a3f1f7f91f20807efea9e45b Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:24:31 +1300 Subject: [PATCH 24/31] Allow overriding `makeMoreMenu` --- .../NewGutenbergViewController.swift | 40 ++++++++++--------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 05c68c3d22fc..2b8693722531 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -86,6 +86,10 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont configureNavigationBar() } + func makeMoreMenu() -> UIMenu { + fatalError("To be implemented by subclasses") + } + // MARK: - GutenbergKit.EditorViewControllerDelegate func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { @@ -724,6 +728,24 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate closure() } } + + override func makeMoreMenu() -> UIMenu { + UIMenu(title: "", image: nil, identifier: nil, options: [], children: [ + UIDeferredMenuElement.uncached { [weak self] callback in + // Common actions at the top so they are always in the same + // relative place. + callback(self?.makeMoreMenuMainSections() ?? []) + }, + UIDeferredMenuElement.uncached { [weak self] callback in + // Dynamic actions at the bottom. The actions are loaded asynchronously + // because they need the latest post content from the editor + // to display the correct state. + self?.performAfterUpdatingContent { + callback(self?.makeMoreMenuAsyncSections() ?? []) + } + } + ]) + } } extension GutenbergKit.EditorViewControllerDelegate { @@ -877,24 +899,6 @@ extension NewGutenbergViewController { case managedObjectContextMissing = 2 } - func makeMoreMenu() -> UIMenu { - UIMenu(title: "", image: nil, identifier: nil, options: [], children: [ - UIDeferredMenuElement.uncached { [weak self] callback in - // Common actions at the top so they are always in the same - // relative place. - callback(self?.makeMoreMenuMainSections() ?? []) - }, - UIDeferredMenuElement.uncached { [weak self] callback in - // Dynamic actions at the bottom. The actions are loaded asynchronously - // because they need the latest post content from the editor - // to display the correct state. - self?.performAfterUpdatingContent { - callback(self?.makeMoreMenuAsyncSections() ?? []) - } - } - ]) - } - private func makeMoreMenuMainSections() -> [UIMenuElement] { return [ UIMenu(title: "", subtitle: "", options: .displayInline, children: makeMoreMenuActions()), From acaada5c4970ec4fcf16f057a809bf3ef5a1a963 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:28:03 +1300 Subject: [PATCH 25/31] Move `loadAuthenticationCookiesAsync` to the super class --- .../NewGutenbergViewController.swift | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 2b8693722531..4569bcf50abd 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -84,6 +84,11 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont setupKeyboardObservers() setupEditorView() configureNavigationBar() + + // Load auth cookies if needed (for private sites) + Task { + await loadAuthenticationCookiesAsync() + } } func makeMoreMenu() -> UIMenu { @@ -268,6 +273,28 @@ private extension PostGBKEditorViewController { navigationBarManager.moreButton.showsMenuAsPrimaryAction = true } + func loadAuthenticationCookiesAsync() async -> Bool { + guard blog.isPrivate() else { + return true + } + + guard let authenticator = RequestAuthenticator(blog: blog), + let blogURL = blog.url, + let authURL = URL(string: blogURL) else { + return false + } + + let cookieJar = WKWebsiteDataStore.default().httpCookieStore + + return await withCheckedContinuation { continuation in + // Always call authenticator.request() to ensure cookies are properly loaded into WKWebView + authenticator.request(url: authURL, cookieJar: cookieJar) { _ in + DDLogInfo("Authentication cookies loaded into shared cookie store for GutenbergKit") + continuation.resume(returning: true) + } + } + } + // MARK: - Keyboard Observers func setupKeyboardObservers() { @@ -489,11 +516,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi createRevisionOfPost(loadAutosaveRevision: false) refreshInterface() - // Load auth cookies if needed (for private sites) - Task { - await loadAuthenticationCookiesAsync() - } - SiteSuggestionService.shared.prefetchSuggestionsIfNeeded(for: post.blog) { // Do nothing } @@ -555,28 +577,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi self.present(SubmitFeedbackViewController(source: "gutenberg_kit", feedbackPrefix: "Editor"), animated: true) } - private func loadAuthenticationCookiesAsync() async -> Bool { - guard post.blog.isPrivate() else { - return true - } - - guard let authenticator = RequestAuthenticator(blog: post.blog), - let blogURL = post.blog.url, - let authURL = URL(string: blogURL) else { - return false - } - - let cookieJar = WKWebsiteDataStore.default().httpCookieStore - - return await withCheckedContinuation { continuation in - // Always call authenticator.request() to ensure cookies are properly loaded into WKWebView - authenticator.request(url: authURL, cookieJar: cookieJar) { _ in - DDLogInfo("Authentication cookies loaded into shared cookie store for GutenbergKit") - continuation.resume(returning: true) - } - } - } - /* Fix issue: Non-'@objc' instance method 'editorDidLoad' declared in 'PostGBKEditorViewController' cannot be overridden from extension From 22f0796d27fbc094816d7fbc7ef06557292fa68a Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:28:54 +1300 Subject: [PATCH 26/31] Move `prefetchSuggestionsIfNeeded` to the super class --- .../NewGutenberg/NewGutenbergViewController.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 4569bcf50abd..749625ba03d3 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -89,6 +89,10 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont Task { await loadAuthenticationCookiesAsync() } + + SiteSuggestionService.shared.prefetchSuggestionsIfNeeded(for: blog) { + // Do nothing + } } func makeMoreMenu() -> UIMenu { @@ -516,10 +520,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi createRevisionOfPost(loadAutosaveRevision: false) refreshInterface() - SiteSuggestionService.shared.prefetchSuggestionsIfNeeded(for: post.blog) { - // Do nothing - } - // TODO: reimplement // service?.syncJetpackSettingsForBlog(post.blog, success: { [weak self] in //// self?.gutenberg.updateCapabilities() From 1a2c8c487e66e93f6b0e83ebd767cda15f283d0f Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:48:37 +1300 Subject: [PATCH 27/31] Move `refreshInterface` to the super class --- .../NewGutenbergViewController.swift | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 749625ba03d3..ea1765fb0db0 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -20,6 +20,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont let navigationBarManager: PostEditorNavigationBarManager /* private */ let editorViewController: GutenbergKit.EditorViewController + private let status: String // TODO: Can be deleted? private var isModalDialogOpen = false @@ -38,6 +39,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont status: String?, blog: Blog ) { + self.status = status ?? "draft" self.blog = blog self.navigationBarManager = PostEditorNavigationBarManager() @@ -49,7 +51,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont .setTitle(title ?? "") .setContent(content ?? "") .setPostID(postId) - .setPostStatus(status ?? "draft") + .setPostStatus(self.status) .setNativeInserterEnabled(FeatureFlag.nativeBlockInserter.enabled) .build() @@ -84,6 +86,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont setupKeyboardObservers() setupEditorView() configureNavigationBar() + refreshInterface() // Load auth cookies if needed (for private sites) Task { @@ -95,6 +98,11 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont } } + func refreshInterface() { + navigationBarManager.reloadPublishButton() + navigationItem.rightBarButtonItems = self.status == "trash" ? [] : navigationBarManager.rightBarButtonItems + } + func makeMoreMenu() -> UIMenu { fatalError("To be implemented by subclasses") } @@ -518,7 +526,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi super.viewDidLoad() createRevisionOfPost(loadAutosaveRevision: false) - refreshInterface() // TODO: reimplement // service?.syncJetpackSettingsForBlog(post.blog, success: { [weak self] in @@ -532,11 +539,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi // MARK: - Functions - private func refreshInterface() { - reloadPublishButton() - navigationItem.rightBarButtonItems = post.status == .trash ? [] : navigationBarManager.rightBarButtonItems - } - func toggleEditingMode() { editorViewController.isCodeEditorEnabled.toggle() } From 8cead7652c9915a80e455a0da586ac453d354436 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:56:20 +1300 Subject: [PATCH 28/31] Move the super class to its own file --- .../NewGutenbergViewController.swift | 409 ----------------- .../PostGBKEditorViewController.swift | 410 ++++++++++++++++++ 2 files changed, 410 insertions(+), 409 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index ea1765fb0db0..f6a04328e18e 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -13,415 +13,6 @@ import Photos import Pulse import Support -// To support editing `AbstractPost` from Core Data and `AnyPostWithEditContext` from the Rust library -class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewControllerDelegate, PostEditorNavigationBarManagerDelegate { - - let blog: Blog - let navigationBarManager: PostEditorNavigationBarManager - - /* private */ let editorViewController: GutenbergKit.EditorViewController - private let status: String // TODO: Can be deleted? - - private var isModalDialogOpen = false - - private var keyboardShowObserver: Any? - private var keyboardHideObserver: Any? - private var keyboardFrame = CGRect.zero - - private var suggestionViewBottomConstraint: NSLayoutConstraint? - private var currentSuggestionsController: GutenbergSuggestionsViewController? - - init( - postId: Int?, - postType: String, - title: String?, - content: String?, - status: String?, - blog: Blog - ) { - self.status = status ?? "draft" - self.blog = blog - self.navigationBarManager = PostEditorNavigationBarManager() - - EditorLocalization.localize = getLocalizedString - - // Create configuration with post content - let editorConfiguration = EditorConfiguration(blog: blog, postType: postType) - .toBuilder() - .setTitle(title ?? "") - .setContent(content ?? "") - .setPostID(postId) - .setPostStatus(self.status) - .setNativeInserterEnabled(FeatureFlag.nativeBlockInserter.enabled) - .build() - - // Use prefetched dependencies if available (fast path with spinner), - // otherwise pass nil and GutenbergKit will fetch them (shows progress bar) - let cachedDependencies = EditorDependencyManager.shared.dependencies(for: blog) - - self.editorViewController = GutenbergKit.EditorViewController( - configuration: editorConfiguration, - dependencies: cachedDependencies, - mediaPicker: MediaPickerController(blog: blog) - ) - - super.init(nibName: nil, bundle: nil) - - self.editorViewController.delegate = self - self.navigationBarManager.delegate = self - } - - required init?(coder aDecoder: NSCoder) { - fatalError() - } - - deinit { - tearDownKeyboardObservers() - } - - override func viewDidLoad() { - super.viewDidLoad() - view.backgroundColor = .systemBackground - - setupKeyboardObservers() - setupEditorView() - configureNavigationBar() - refreshInterface() - - // Load auth cookies if needed (for private sites) - Task { - await loadAuthenticationCookiesAsync() - } - - SiteSuggestionService.shared.prefetchSuggestionsIfNeeded(for: blog) { - // Do nothing - } - } - - func refreshInterface() { - navigationBarManager.reloadPublishButton() - navigationItem.rightBarButtonItems = self.status == "trash" ? [] : navigationBarManager.rightBarButtonItems - } - - func makeMoreMenu() -> UIMenu { - fatalError("To be implemented by subclasses") - } - - // MARK: - GutenbergKit.EditorViewControllerDelegate - - func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { - // Do nothing - } - - func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) { - // Do nothing - } - - func editor(_ viewContoller: GutenbergKit.EditorViewController, didEncounterCriticalError error: any Error) { - // Do nothing - } - - func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateContentWithState state: GutenbergKit.EditorState) { - // Do nothing - } - - func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: GutenbergKit.EditorState) { - gutenbergDidRequestToggleRedoButton(!state.hasRedo) - gutenbergDidRequestToggleUndoButton(!state.hasUndo) - } - - func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateFeaturedImage mediaID: Int) { - // Do nothing - } - - func editor(_ viewController: GutenbergKit.EditorViewController, didLogException exception: GutenbergKit.GutenbergJSException) { - DispatchQueue.main.async { - WordPressAppDelegate.crashLogging?.logJavaScriptException(exception) { - // Do nothing - } - } - } - - func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) { - // Do nothing - } - - func editor(_ viewController: GutenbergKit.EditorViewController, didTriggerAutocompleter type: String) { - switch type { - case "at-symbol": - showSuggestions(type: .mention) { [weak self] result in - switch result { - case .success(let suggestion): - // Appended space completes the autocomplete session - self?.editorViewController.appendTextAtCursor(suggestion + " ") - case .failure(let error): - DDLogError("Mention selection cancelled or failed: \(error)") - } - } - case "plus-symbol": - showSuggestions(type: .xpost) { [weak self] result in - switch result { - case .success(let suggestion): - // Appended space completes the autocomplete session - self?.editorViewController.appendTextAtCursor(suggestion + " ") - case .failure(let error): - DDLogError("Xpost selection cancelled or failed: \(error)") - } - } - default: - DDLogError("Unknown autocompleter type: \(type)") - } - } - - func editor(_ viewController: GutenbergKit.EditorViewController, didOpenModalDialog dialogType: String) { - isModalDialogOpen = true - setNavigationItemsEnabled(false) - } - - func editor(_ viewController: GutenbergKit.EditorViewController, didCloseModalDialog dialogType: String) { - isModalDialogOpen = false - setNavigationItemsEnabled(true) - } - - func editorDidRequestLatestContent(_ controller: GutenbergKit.EditorViewController) -> (title: String, content: String)? { - // Do nothing - return nil - } - - // MARK: - PostEditorNavigationBarManagerDelegate - - var publishButtonText: String { - wpAssertionFailure("To be implemented by subclasses") - return "" - } - - var isPublishButtonEnabled: Bool { - wpAssertionFailure("To be implemented by subclasses") - return false - } - - var uploadingButtonSize: CGSize { - wpAssertionFailure("To be implemented by subclasses") - return .zero - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { - // Do nothing - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { - editorViewController.undo() - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, redoWasPressed sender: UIButton) { - editorViewController.redo() - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { - // Do nothing - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { - // Do nothing - } - - func navigationBarManager(_ manager: PostEditorNavigationBarManager, displayCancelMediaUploads sender: UIButton) { - // Do nothing - } -} - -private extension PostGBKEditorViewController { - func gutenbergDidRequestToggleRedoButton(_ isDisabled: Bool) { - DispatchQueue.main.async { - UIView.animate(withDuration: 0.2) { - self.navigationBarManager.redoButton.isUserInteractionEnabled = isDisabled ? false : true - self.navigationBarManager.redoButton.alpha = isDisabled ? 0.3 : 1.0 - } - } - } - - func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) { - DispatchQueue.main.async { - UIView.animate(withDuration: 0.2) { - self.navigationBarManager.undoButton.isUserInteractionEnabled = isDisabled ? false : true - self.navigationBarManager.undoButton.alpha = isDisabled ? 0.3 : 1.0 - } - } - } - - func setNavigationItemsEnabled(_ enabled: Bool) { - navigationBarManager.closeButton.isEnabled = enabled - navigationBarManager.moreButton.isEnabled = enabled - navigationBarManager.publishButton.isEnabled = enabled - navigationBarManager.undoButton.isEnabled = enabled - navigationBarManager.redoButton.isEnabled = enabled - } - - func setupEditorView() { - view.tintColor = UIAppColor.editorPrimary - - addChild(editorViewController) - view.addSubview(editorViewController.view) - view.pinSubviewToAllEdges(editorViewController.view) - editorViewController.didMove(toParent: self) - -#if DEBUG - editorViewController.webView.isInspectable = true -#endif - - // Doesn't seem to do anything - setContentScrollView(editorViewController.webView.scrollView) - } - - func configureNavigationBar() { - navigationController?.navigationBar.accessibilityIdentifier = "Gutenberg Editor Navigation Bar" - navigationItem.leftBarButtonItems = navigationBarManager.leftBarButtonItems - - edgesForExtendedLayout = [] - // TODO: make it work -// configureDefaultNavigationBarAppearance() - - navigationBarManager.moreButton.menu = makeMoreMenu() - navigationBarManager.moreButton.showsMenuAsPrimaryAction = true - } - - func loadAuthenticationCookiesAsync() async -> Bool { - guard blog.isPrivate() else { - return true - } - - guard let authenticator = RequestAuthenticator(blog: blog), - let blogURL = blog.url, - let authURL = URL(string: blogURL) else { - return false - } - - let cookieJar = WKWebsiteDataStore.default().httpCookieStore - - return await withCheckedContinuation { continuation in - // Always call authenticator.request() to ensure cookies are properly loaded into WKWebView - authenticator.request(url: authURL, cookieJar: cookieJar) { _ in - DDLogInfo("Authentication cookies loaded into shared cookie store for GutenbergKit") - continuation.resume(returning: true) - } - } - } - - // MARK: - Keyboard Observers - - func setupKeyboardObservers() { - keyboardShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: .main) { [weak self] (notification) in - if let self, let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { - self.keyboardFrame = keyboardRect - self.updateConstraintsToAvoidKeyboard(frame: keyboardRect) - } - } - keyboardHideObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidHideNotification, object: nil, queue: .main) { [weak self] (notification) in - if let self, let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { - self.keyboardFrame = keyboardRect - self.updateConstraintsToAvoidKeyboard(frame: keyboardRect) - } - } - } - - func tearDownKeyboardObservers() { - if let keyboardShowObserver { - NotificationCenter.default.removeObserver(keyboardShowObserver) - } - if let keyboardHideObserver { - NotificationCenter.default.removeObserver(keyboardHideObserver) - } - } - - func updateConstraintsToAvoidKeyboard(frame: CGRect) { - keyboardFrame = frame - let minimumKeyboardHeight = CGFloat(50) - guard let suggestionViewBottomConstraint else { - return - } - - // There are cases where the keyboard is not visible, but the system instead of returning zero, returns a low number, for example: 0, 3, 69. - // So in those scenarios, we just need to take in account the safe area and ignore the keyboard all together. - if keyboardFrame.height < minimumKeyboardHeight { - suggestionViewBottomConstraint.constant = -self.view.safeAreaInsets.bottom - } - else { - suggestionViewBottomConstraint.constant = -self.keyboardFrame.height - } - } - - // MARK: - Suggestions implementation - - func showSuggestions(type: SuggestionType, callback: @escaping (Swift.Result) -> Void) { - // Prevent multiple suggestions UI instances - simply ignore if already showing - guard currentSuggestionsController == nil else { - return - } - guard let siteID = blog.dotComID else { - callback(.failure(GutenbergSuggestionsViewController.SuggestionError.notAvailable as NSError)) - return - } - - switch type { - case .mention: - guard SuggestionService.shared.shouldShowSuggestions(for: blog) else { return } - case .xpost: - guard SiteSuggestionService.shared.shouldShowSuggestions(for: blog) else { return } - } - - let previousFirstResponder = view.findFirstResponder() - let suggestionsController = GutenbergSuggestionsViewController(siteID: siteID, suggestionType: type) - currentSuggestionsController = suggestionsController - suggestionsController.onCompletion = { [weak self] (result) in - callback(result) - - if let self { - // Clear the current controller reference - self.currentSuggestionsController = nil - self.suggestionViewBottomConstraint = nil - - // Clean up the UI (should only happen if parent still exists) - suggestionsController.view.removeFromSuperview() - suggestionsController.removeFromParent() - - previousFirstResponder?.becomeFirstResponder() - } - - var analyticsName: String - switch type { - case .mention: - analyticsName = "user" - case .xpost: - analyticsName = "xpost" - } - - var didSelectSuggestion = false - if case let .success(text) = result, !text.isEmpty { - didSelectSuggestion = true - } - - let analyticsProperties: [String: Any] = [ - "suggestion_type": analyticsName, - "did_select_suggestion": didSelectSuggestion - ] - - WPAnalytics.track(.gutenbergSuggestionSessionFinished, properties: analyticsProperties) - } - addChild(suggestionsController) - view.addSubview(suggestionsController.view) - let suggestionsBottomConstraint = suggestionsController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) - NSLayoutConstraint.activate([ - suggestionsController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0), - suggestionsController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0), - suggestionsBottomConstraint, - suggestionsController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) - ]) - self.suggestionViewBottomConstraint = suggestionsBottomConstraint - updateConstraintsToAvoidKeyboard(frame: keyboardFrame) - suggestionsController.didMove(toParent: self) - } -} - class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { let errorDomain: String = "GutenbergViewController.errorDomain" diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift new file mode 100644 index 000000000000..182b19a3c481 --- /dev/null +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -0,0 +1,410 @@ +import Foundation +import UIKit + +class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewControllerDelegate, PostEditorNavigationBarManagerDelegate { + + let blog: Blog + let navigationBarManager: PostEditorNavigationBarManager + + /* private */ let editorViewController: GutenbergKit.EditorViewController + private let status: String // TODO: Can be deleted? + + private var isModalDialogOpen = false + + private var keyboardShowObserver: Any? + private var keyboardHideObserver: Any? + private var keyboardFrame = CGRect.zero + + private var suggestionViewBottomConstraint: NSLayoutConstraint? + private var currentSuggestionsController: GutenbergSuggestionsViewController? + + init( + postId: Int?, + postType: String, + title: String?, + content: String?, + status: String?, + blog: Blog + ) { + self.status = status ?? "draft" + self.blog = blog + self.navigationBarManager = PostEditorNavigationBarManager() + + EditorLocalization.localize = getLocalizedString + + // Create configuration with post content + let editorConfiguration = EditorConfiguration(blog: blog, postType: postType) + .toBuilder() + .setTitle(title ?? "") + .setContent(content ?? "") + .setPostID(postId) + .setPostStatus(self.status) + .setNativeInserterEnabled(FeatureFlag.nativeBlockInserter.enabled) + .build() + + // Use prefetched dependencies if available (fast path with spinner), + // otherwise pass nil and GutenbergKit will fetch them (shows progress bar) + let cachedDependencies = EditorDependencyManager.shared.dependencies(for: blog) + + self.editorViewController = GutenbergKit.EditorViewController( + configuration: editorConfiguration, + dependencies: cachedDependencies, + mediaPicker: MediaPickerController(blog: blog) + ) + + super.init(nibName: nil, bundle: nil) + + self.editorViewController.delegate = self + self.navigationBarManager.delegate = self + } + + required init?(coder aDecoder: NSCoder) { + fatalError() + } + + deinit { + tearDownKeyboardObservers() + } + + override func viewDidLoad() { + super.viewDidLoad() + view.backgroundColor = .systemBackground + + setupKeyboardObservers() + setupEditorView() + configureNavigationBar() + refreshInterface() + + // Load auth cookies if needed (for private sites) + Task { + await loadAuthenticationCookiesAsync() + } + + SiteSuggestionService.shared.prefetchSuggestionsIfNeeded(for: blog) { + // Do nothing + } + } + + func refreshInterface() { + navigationBarManager.reloadPublishButton() + navigationItem.rightBarButtonItems = self.status == "trash" ? [] : navigationBarManager.rightBarButtonItems + } + + func makeMoreMenu() -> UIMenu { + fatalError("To be implemented by subclasses") + } + + // MARK: - GutenbergKit.EditorViewControllerDelegate + + func editorDidLoad(_ viewContoller: GutenbergKit.EditorViewController) { + // Do nothing + } + + func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) { + // Do nothing + } + + func editor(_ viewContoller: GutenbergKit.EditorViewController, didEncounterCriticalError error: any Error) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateContentWithState state: GutenbergKit.EditorState) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateHistoryState state: GutenbergKit.EditorState) { + gutenbergDidRequestToggleRedoButton(!state.hasRedo) + gutenbergDidRequestToggleUndoButton(!state.hasUndo) + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didUpdateFeaturedImage mediaID: Int) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didLogException exception: GutenbergKit.GutenbergJSException) { + DispatchQueue.main.async { + WordPressAppDelegate.crashLogging?.logJavaScriptException(exception) { + // Do nothing + } + } + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) { + // Do nothing + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didTriggerAutocompleter type: String) { + switch type { + case "at-symbol": + showSuggestions(type: .mention) { [weak self] result in + switch result { + case .success(let suggestion): + // Appended space completes the autocomplete session + self?.editorViewController.appendTextAtCursor(suggestion + " ") + case .failure(let error): + DDLogError("Mention selection cancelled or failed: \(error)") + } + } + case "plus-symbol": + showSuggestions(type: .xpost) { [weak self] result in + switch result { + case .success(let suggestion): + // Appended space completes the autocomplete session + self?.editorViewController.appendTextAtCursor(suggestion + " ") + case .failure(let error): + DDLogError("Xpost selection cancelled or failed: \(error)") + } + } + default: + DDLogError("Unknown autocompleter type: \(type)") + } + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didOpenModalDialog dialogType: String) { + isModalDialogOpen = true + setNavigationItemsEnabled(false) + } + + func editor(_ viewController: GutenbergKit.EditorViewController, didCloseModalDialog dialogType: String) { + isModalDialogOpen = false + setNavigationItemsEnabled(true) + } + + func editorDidRequestLatestContent(_ controller: GutenbergKit.EditorViewController) -> (title: String, content: String)? { + // Do nothing + return nil + } + + // MARK: - PostEditorNavigationBarManagerDelegate + + var publishButtonText: String { + wpAssertionFailure("To be implemented by subclasses") + return "" + } + + var isPublishButtonEnabled: Bool { + wpAssertionFailure("To be implemented by subclasses") + return false + } + + var uploadingButtonSize: CGSize { + wpAssertionFailure("To be implemented by subclasses") + return .zero + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, closeWasPressed sender: UIButton) { + // Do nothing + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, undoWasPressed sender: UIButton) { + editorViewController.undo() + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, redoWasPressed sender: UIButton) { + editorViewController.redo() + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, moreWasPressed sender: UIButton) { + // Do nothing + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, publishButtonWasPressed sender: UIButton) { + // Do nothing + } + + func navigationBarManager(_ manager: PostEditorNavigationBarManager, displayCancelMediaUploads sender: UIButton) { + // Do nothing + } +} + +private extension PostGBKEditorViewController { + func gutenbergDidRequestToggleRedoButton(_ isDisabled: Bool) { + DispatchQueue.main.async { + UIView.animate(withDuration: 0.2) { + self.navigationBarManager.redoButton.isUserInteractionEnabled = isDisabled ? false : true + self.navigationBarManager.redoButton.alpha = isDisabled ? 0.3 : 1.0 + } + } + } + + func gutenbergDidRequestToggleUndoButton(_ isDisabled: Bool) { + DispatchQueue.main.async { + UIView.animate(withDuration: 0.2) { + self.navigationBarManager.undoButton.isUserInteractionEnabled = isDisabled ? false : true + self.navigationBarManager.undoButton.alpha = isDisabled ? 0.3 : 1.0 + } + } + } + + func setNavigationItemsEnabled(_ enabled: Bool) { + navigationBarManager.closeButton.isEnabled = enabled + navigationBarManager.moreButton.isEnabled = enabled + navigationBarManager.publishButton.isEnabled = enabled + navigationBarManager.undoButton.isEnabled = enabled + navigationBarManager.redoButton.isEnabled = enabled + } + + func setupEditorView() { + view.tintColor = UIAppColor.editorPrimary + + addChild(editorViewController) + view.addSubview(editorViewController.view) + view.pinSubviewToAllEdges(editorViewController.view) + editorViewController.didMove(toParent: self) + +#if DEBUG + editorViewController.webView.isInspectable = true +#endif + + // Doesn't seem to do anything + setContentScrollView(editorViewController.webView.scrollView) + } + + func configureNavigationBar() { + navigationController?.navigationBar.accessibilityIdentifier = "Gutenberg Editor Navigation Bar" + navigationItem.leftBarButtonItems = navigationBarManager.leftBarButtonItems + + edgesForExtendedLayout = [] + // TODO: make it work +// configureDefaultNavigationBarAppearance() + + navigationBarManager.moreButton.menu = makeMoreMenu() + navigationBarManager.moreButton.showsMenuAsPrimaryAction = true + } + + func loadAuthenticationCookiesAsync() async -> Bool { + guard blog.isPrivate() else { + return true + } + + guard let authenticator = RequestAuthenticator(blog: blog), + let blogURL = blog.url, + let authURL = URL(string: blogURL) else { + return false + } + + let cookieJar = WKWebsiteDataStore.default().httpCookieStore + + return await withCheckedContinuation { continuation in + // Always call authenticator.request() to ensure cookies are properly loaded into WKWebView + authenticator.request(url: authURL, cookieJar: cookieJar) { _ in + DDLogInfo("Authentication cookies loaded into shared cookie store for GutenbergKit") + continuation.resume(returning: true) + } + } + } + + // MARK: - Keyboard Observers + + func setupKeyboardObservers() { + keyboardShowObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidShowNotification, object: nil, queue: .main) { [weak self] (notification) in + if let self, let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { + self.keyboardFrame = keyboardRect + self.updateConstraintsToAvoidKeyboard(frame: keyboardRect) + } + } + keyboardHideObserver = NotificationCenter.default.addObserver(forName: UIResponder.keyboardDidHideNotification, object: nil, queue: .main) { [weak self] (notification) in + if let self, let keyboardRect = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect { + self.keyboardFrame = keyboardRect + self.updateConstraintsToAvoidKeyboard(frame: keyboardRect) + } + } + } + + func tearDownKeyboardObservers() { + if let keyboardShowObserver { + NotificationCenter.default.removeObserver(keyboardShowObserver) + } + if let keyboardHideObserver { + NotificationCenter.default.removeObserver(keyboardHideObserver) + } + } + + func updateConstraintsToAvoidKeyboard(frame: CGRect) { + keyboardFrame = frame + let minimumKeyboardHeight = CGFloat(50) + guard let suggestionViewBottomConstraint else { + return + } + + // There are cases where the keyboard is not visible, but the system instead of returning zero, returns a low number, for example: 0, 3, 69. + // So in those scenarios, we just need to take in account the safe area and ignore the keyboard all together. + if keyboardFrame.height < minimumKeyboardHeight { + suggestionViewBottomConstraint.constant = -self.view.safeAreaInsets.bottom + } + else { + suggestionViewBottomConstraint.constant = -self.keyboardFrame.height + } + } + + // MARK: - Suggestions implementation + + func showSuggestions(type: SuggestionType, callback: @escaping (Swift.Result) -> Void) { + // Prevent multiple suggestions UI instances - simply ignore if already showing + guard currentSuggestionsController == nil else { + return + } + guard let siteID = blog.dotComID else { + callback(.failure(GutenbergSuggestionsViewController.SuggestionError.notAvailable as NSError)) + return + } + + switch type { + case .mention: + guard SuggestionService.shared.shouldShowSuggestions(for: blog) else { return } + case .xpost: + guard SiteSuggestionService.shared.shouldShowSuggestions(for: blog) else { return } + } + + let previousFirstResponder = view.findFirstResponder() + let suggestionsController = GutenbergSuggestionsViewController(siteID: siteID, suggestionType: type) + currentSuggestionsController = suggestionsController + suggestionsController.onCompletion = { [weak self] (result) in + callback(result) + + if let self { + // Clear the current controller reference + self.currentSuggestionsController = nil + self.suggestionViewBottomConstraint = nil + + // Clean up the UI (should only happen if parent still exists) + suggestionsController.view.removeFromSuperview() + suggestionsController.removeFromParent() + + previousFirstResponder?.becomeFirstResponder() + } + + var analyticsName: String + switch type { + case .mention: + analyticsName = "user" + case .xpost: + analyticsName = "xpost" + } + + var didSelectSuggestion = false + if case let .success(text) = result, !text.isEmpty { + didSelectSuggestion = true + } + + let analyticsProperties: [String: Any] = [ + "suggestion_type": analyticsName, + "did_select_suggestion": didSelectSuggestion + ] + + WPAnalytics.track(.gutenbergSuggestionSessionFinished, properties: analyticsProperties) + } + addChild(suggestionsController) + view.addSubview(suggestionsController.view) + let suggestionsBottomConstraint = suggestionsController.view.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0) + NSLayoutConstraint.activate([ + suggestionsController.view.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 0), + suggestionsController.view.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: 0), + suggestionsBottomConstraint, + suggestionsController.view.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) + ]) + self.suggestionViewBottomConstraint = suggestionsBottomConstraint + updateConstraintsToAvoidKeyboard(frame: keyboardFrame) + suggestionsController.didMove(toParent: self) + } +} From 7e2cea64b317d2a596f1f16e4a8bbbc676190875 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 22:57:33 +1300 Subject: [PATCH 29/31] Move extension functions out to its own file --- .../NewGutenberg/GBKExtensions.swift | 48 +++++++++++++++++++ .../NewGutenbergViewController.swift | 44 ----------------- 2 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift new file mode 100644 index 000000000000..13e9de23512f --- /dev/null +++ b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift @@ -0,0 +1,48 @@ +import Foundation +import GutenbergKit +import Pulse +import Support + +extension GutenbergKit.EditorViewControllerDelegate { + func editor(_ viewController: GutenbergKit.EditorViewController, didLogNetworkRequest request: GutenbergKit.RecordedNetworkRequest) { + guard ExtensiveLogging.enabled, let url = URL(string: request.url) else { + return + } + + var urlRequest = URLRequest(url: url) + urlRequest.httpMethod = request.method + urlRequest.allHTTPHeaderFields = request.requestHeaders + urlRequest.httpBody = request.requestBody?.data(using: .utf8) + + let httpResponse = HTTPURLResponse( + url: url, + statusCode: request.status, + httpVersion: nil, + headerFields: request.responseHeaders + ) + + LoggerStore.shared.storeRequest( + urlRequest, + response: httpResponse, + error: nil, + data: request.responseBody?.data(using: .utf8) + ) + } +} + +private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) -> String { + switch value { + case .showMore: NSLocalizedString("editor.blockInserter.showMore", value: "Show More", comment: "Button title to expand and show more blocks") + case .showLess: NSLocalizedString("editor.blockInserter.showLess", value: "Show Less", comment: "Button title to collapse and show fewer blocks") + case .search: NSLocalizedString("editor.blockInserter.search", value: "Search", comment: "Placeholder text for block search field") + case .insertBlock: NSLocalizedString("editor.blockInserter.insertBlock", value: "Insert Block", comment: "Context menu action to insert a block") + case .failedToInsertMedia: NSLocalizedString("editor.media.failedToInsert", value: "Failed to insert media", comment: "Error message when media insertion fails") + case .patterns: NSLocalizedString("editor.patterns.title", value: "Patterns", comment: "Navigation title for patterns view") + case .noPatternsFound: NSLocalizedString("editor.patterns.noPatternsFound", value: "No Patterns Found", comment: "Title shown when no patterns match the search") + case .insertPattern: NSLocalizedString("editor.patterns.insertPattern", value: "Insert Pattern", comment: "Context menu action to insert a pattern") + case .patternsCategoryUncategorized: NSLocalizedString("editor.patterns.uncategorized", value: "Uncategorized", comment: "Category name for patterns without a category") + case .patternsCategoryAll: NSLocalizedString("editor.patterns.all", value: "All", comment: "Category name for section showing all patterns") + case .loadingEditor: NSLocalizedString("editor.loading.title", value: "Loading Editor", comment: "Text shown while the editor is loading") + case .editorError: NSLocalizedString("editor.error.title", value: "Editor Error", comment: "Title shown when the editor encounters an error") + } +} diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index f6a04328e18e..86a8a7bcb137 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -341,33 +341,6 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate } } -extension GutenbergKit.EditorViewControllerDelegate { - func editor(_ viewController: GutenbergKit.EditorViewController, didLogNetworkRequest request: GutenbergKit.RecordedNetworkRequest) { - guard ExtensiveLogging.enabled, let url = URL(string: request.url) else { - return - } - - var urlRequest = URLRequest(url: url) - urlRequest.httpMethod = request.method - urlRequest.allHTTPHeaderFields = request.requestHeaders - urlRequest.httpBody = request.requestBody?.data(using: .utf8) - - let httpResponse = HTTPURLResponse( - url: url, - statusCode: request.status, - httpVersion: nil, - headerFields: request.responseHeaders - ) - - LoggerStore.shared.storeRequest( - urlRequest, - response: httpResponse, - error: nil, - data: request.responseBody?.data(using: .utf8) - ) - } -} - // MARK: - GutenbergBridgeDelegate extension NewGutenbergViewController { @@ -626,20 +599,3 @@ private extension NewGutenbergViewController { // Extend Gutenberg JavaScript exception struct to conform the protocol defined in the Crash Logging service extension GutenbergJSException.StacktraceLine: @retroactive AutomatticTracks.JSStacktraceLine {} extension GutenbergJSException: @retroactive AutomatticTracks.JSException {} - -private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) -> String { - switch value { - case .showMore: NSLocalizedString("editor.blockInserter.showMore", value: "Show More", comment: "Button title to expand and show more blocks") - case .showLess: NSLocalizedString("editor.blockInserter.showLess", value: "Show Less", comment: "Button title to collapse and show fewer blocks") - case .search: NSLocalizedString("editor.blockInserter.search", value: "Search", comment: "Placeholder text for block search field") - case .insertBlock: NSLocalizedString("editor.blockInserter.insertBlock", value: "Insert Block", comment: "Context menu action to insert a block") - case .failedToInsertMedia: NSLocalizedString("editor.media.failedToInsert", value: "Failed to insert media", comment: "Error message when media insertion fails") - case .patterns: NSLocalizedString("editor.patterns.title", value: "Patterns", comment: "Navigation title for patterns view") - case .noPatternsFound: NSLocalizedString("editor.patterns.noPatternsFound", value: "No Patterns Found", comment: "Title shown when no patterns match the search") - case .insertPattern: NSLocalizedString("editor.patterns.insertPattern", value: "Insert Pattern", comment: "Context menu action to insert a pattern") - case .patternsCategoryUncategorized: NSLocalizedString("editor.patterns.uncategorized", value: "Uncategorized", comment: "Category name for patterns without a category") - case .patternsCategoryAll: NSLocalizedString("editor.patterns.all", value: "All", comment: "Category name for section showing all patterns") - case .loadingEditor: NSLocalizedString("editor.loading.title", value: "Loading Editor", comment: "Text shown while the editor is loading") - case .editorError: NSLocalizedString("editor.error.title", value: "Editor Error", comment: "Title shown when the editor encounters an error") - } -} From a1409939c8bc3c89c67d228d83b548a130f5267d Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 23:02:39 +1300 Subject: [PATCH 30/31] Add missing imports --- .../ViewRelated/NewGutenberg/NewGutenbergViewController.swift | 2 -- .../NewGutenberg/PostGBKEditorViewController.swift | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 86a8a7bcb137..d0610e1a9335 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -10,8 +10,6 @@ import WordPressShared import WebKit import CocoaLumberjackSwift import Photos -import Pulse -import Support class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, PublishingEditor { diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index 182b19a3c481..051bf8c50343 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -1,5 +1,9 @@ import Foundation import UIKit +import WebKit +import GutenbergKit +import WordPressShared +import WordPressUI class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewControllerDelegate, PostEditorNavigationBarManagerDelegate { From f452c121794ccb56614681b7c04684e1d14a3bc6 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Sat, 24 Jan 2026 23:02:48 +1300 Subject: [PATCH 31/31] Add an extension: `EditorLocalizableString.localized` --- .../Classes/ViewRelated/NewGutenberg/GBKExtensions.swift | 6 ++++++ .../NewGutenberg/PostGBKEditorViewController.swift | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift index 13e9de23512f..1c18c01476d7 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/GBKExtensions.swift @@ -46,3 +46,9 @@ private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) case .editorError: NSLocalizedString("editor.error.title", value: "Editor Error", comment: "Title shown when the editor encounters an error") } } + +extension EditorLocalizableString { + var localized: String { + getLocalizedString(for: self) + } +} diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index 051bf8c50343..d53c60d0f665 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -34,7 +34,7 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont self.blog = blog self.navigationBarManager = PostEditorNavigationBarManager() - EditorLocalization.localize = getLocalizedString + EditorLocalization.localize = { $0.localized } // Create configuration with post content let editorConfiguration = EditorConfiguration(blog: blog, postType: postType)