Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3179db1
hoist settings
dcalhoun Apr 3, 2025
dfce8e6
refactor: Update settings via JavaScript bridge
dcalhoun Apr 4, 2025
1952cc9
fix: Fetch the entirety of the editor settings
dcalhoun Apr 4, 2025
8ebe71e
Revert "refactor: Update settings via JavaScript bridge"
dcalhoun Apr 4, 2025
b3b2129
feat: Postpone editor initialization
dcalhoun Apr 4, 2025
f9231fa
Revert "feat: Postpone editor initialization"
dcalhoun Apr 4, 2025
17d5798
refactor: Defer editor start
dcalhoun Apr 4, 2025
de5e535
refactor: Simplify RawBlockEditorSettingsService
dcalhoun Apr 5, 2025
e5222b4
feat: Cache editor settings fetch
dcalhoun Apr 5, 2025
45c644c
feat: Editor settings use stale-while-revalidate cache
dcalhoun Apr 5, 2025
4ce5fd9
style: Use shorthand
dcalhoun Apr 5, 2025
0e4ba97
feat: Cache editor settings across editor sessions
dcalhoun Apr 5, 2025
c379b1a
refactor: Avoid duplicative settings requests
dcalhoun Apr 5, 2025
e19b669
feat: Remove time-based cache
dcalhoun Apr 5, 2025
09ffd57
build: Update GutenbergKit ref
dcalhoun Apr 9, 2025
676f0ff
build: Update GutenbergKit ref
dcalhoun Apr 11, 2025
bf7fcff
build: Update GutenbergKit ref
dcalhoun Apr 11, 2025
fe5ff15
build: Update GutenbergKit ref
dcalhoun Apr 17, 2025
bfa177e
perf: Preload editor settings within My Site
dcalhoun Apr 17, 2025
8f12133
fix: Enable GutenbergKit warmup via remote feature flag
dcalhoun Apr 17, 2025
acd2567
feat: Start editor without settings after three seconds
dcalhoun Apr 23, 2025
e8a892c
refactor: DRY up fetching editor settings
dcalhoun Apr 23, 2025
487ede4
build: Update GutenbergKit ref
dcalhoun Apr 24, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ let package = Package(
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
// We can't use wordpress-rs branches nor commits here. Only tags work.
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20250411"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "fc369073730384c8946bee15ec8ff7c763cf69c9"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "fa72e630203e7472d55f4abedfd5c462d2333584"),
.package(url: "https://github.com/Automattic/color-studio", branch: "trunk"),
.package(url: "https://github.com/wordpress-mobile/AztecEditor-iOS", from: "1.20.0"),
],
Expand Down
4 changes: 2 additions & 2 deletions WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

16 changes: 16 additions & 0 deletions WordPress/Classes/Models/Blog/Blog+RawBlockEditorSettings.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import Foundation
import CoreData

extension Blog {
private static let rawBlockEditorSettingsKey = "rawBlockEditorSettings"

/// Stores the raw block editor settings dictionary
var rawBlockEditorSettings: [String: Any]? {
get {
return getOptionValue(Self.rawBlockEditorSettingsKey) as? [String: Any]
}
set {
setValue(newValue, forOption: Self.rawBlockEditorSettingsKey)
}
}
}
71 changes: 71 additions & 0 deletions WordPress/Classes/Services/RawBlockEditorSettingsService.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import Foundation
import WordPressKit
import WordPressShared

class RawBlockEditorSettingsService {
private let blog: Blog
private let remoteAPI: WordPressOrgRestApi
private var isRefreshing: Bool = false

init?(blog: Blog) {
guard let remoteAPI = WordPressOrgRestApi(blog: blog) else {
return nil
}

self.blog = blog
self.remoteAPI = remoteAPI
}

@MainActor
private func fetchSettingsFromAPI() async throws -> [String: Any] {
let result = await self.remoteAPI.get(path: "/wp-block-editor/v1/settings")
switch result {
case .success(let response):
guard let dictionary = response as? [String: Any] else {
throw NSError(domain: "RawBlockEditorSettingsService", code: 1, userInfo: [NSLocalizedDescriptionKey: "Invalid response format"])
}
blog.rawBlockEditorSettings = dictionary
return dictionary
case .failure(let error):
throw error
}
}

@MainActor
func fetchSettings() async throws -> [String: Any] {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if we've never fetched the settings and the network connection fails?

In my testing it seems like it'll just show a spinner forever?

If we don't have locally cached settings and the network request fails we should probably:

  1. Try again some number of times (maybe 2?)
  2. Show an error message

I could see an argument for showing some kind of editor with no remote settings, but I don't know how feasible that is?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your experience is surprising. These changes were built to accomplish what you describe. When I block the network request (via Charles Proxy) for the editor settings, the editor continues to load with default editor settings after a moment.

How are you causing the network connection failure in your testing?

The screen recording below showcases both the default settings and site-specific settings.

Screen.Recording.2025-04-16.at.16.44.12.mov

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact, the situation you describe is a part of the testing instructions in the PR description. 😄 I'm perplexed as to why you may be experiencing a different outcome.

// Start a background refresh if not already refreshing
if !isRefreshing {
isRefreshing = true
Task {
do {
_ = try await fetchSettingsFromAPI()
} catch {
DDLogError("Error refreshing block editor settings: \(error)")
}
isRefreshing = false
}
}

// Return cached settings if available
if let cachedSettings = blog.rawBlockEditorSettings {
return cachedSettings
}

// If no cache and no background refresh in progress, fetch synchronously
if !isRefreshing {
return try await fetchSettingsFromAPI()
}

// If we're here, it means a background refresh is in progress
// Wait for it to complete and return the cached result
while isRefreshing {
try await Task.sleep(nanoseconds: 100_000_000) // 100ms
if let cachedSettings = blog.rawBlockEditorSettings {
return cachedSettings
}
}

// If we still don't have settings after the refresh completed, throw an error
throw NSError(domain: "RawBlockEditorSettingsService", code: 2, userInfo: [NSLocalizedDescriptionKey: "Failed to fetch block editor settings"])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite
// MARK: - Dependencies

private let overlaysCoordinator: MySiteOverlaysCoordinator
private lazy var editorSettingsService: RawBlockEditorSettingsService? = {
guard let blog, FeatureFlag.newGutenberg.enabled || RemoteFeatureFlag.newGutenberg.enabled() else { return nil }
return RawBlockEditorSettingsService(blog: blog)
}()

// TODO: (reader) factor if out of `MySiteVC` for a production version
var isReaderAppModeEnabled = false
Expand Down Expand Up @@ -168,7 +172,7 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite
subscribeToPostPublished()
subscribeToWillEnterForeground()

if FeatureFlag.newGutenberg.enabled {
if FeatureFlag.newGutenberg.enabled || RemoteFeatureFlag.newGutenberg.enabled() {
GutenbergKit.EditorViewController.warmup()
}
}
Expand Down Expand Up @@ -381,6 +385,24 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite
hideBlogDetails()
showDashboard(for: blog)
}

if FeatureFlag.newGutenberg.enabled || RemoteFeatureFlag.newGutenberg.enabled() {
// Update editor settings service with new blog and fetch settings
editorSettingsService = RawBlockEditorSettingsService(blog: blog)
fetchEditorSettings()
}
}

private func fetchEditorSettings() {
guard let service = editorSettingsService else { return }

Task { @MainActor in
do {
_ = try await service.fetchSettings()
} catch {
DDLogError("Error fetching editor settings: \(error)")
}
}
}

@objc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,16 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
BlockEditorSettingsService(blog: post.blog, coreDataStack: ContextManager.shared)
}()

// New service for fetching raw block editor settings
lazy var rawBlockEditorSettingsService: RawBlockEditorSettingsService? = {
return RawBlockEditorSettingsService(blog: post.blog)
}()

// MARK: - GutenbergKit

private let editorViewController: GutenbergKit.EditorViewController
private var editorViewController: GutenbergKit.EditorViewController
private var activityIndicator: UIActivityIndicatorView?
private var hasEditorStarted = false

lazy var autosaver = Autosaver() {
self.performAutoSave()
Expand Down Expand Up @@ -202,7 +209,11 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
configureNavigationBar()
refreshInterface()

fetchBlockSettings()
// Show activity indicator while fetching settings
showActivityIndicator()

// Fetch block editor settings
fetchBlockEditorSettings()

// TODO: reimplement
// service?.syncJetpackSettingsForBlog(post.blog, success: { [weak self] in
Expand Down Expand Up @@ -316,6 +327,70 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
WordPressAppDelegate.crashLogging?.logJavaScriptException(exception, callback: callback)
}
}

// MARK: - Activity Indicator

private func showActivityIndicator() {
let indicator = UIActivityIndicatorView(style: .large)
indicator.color = .gray
indicator.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(indicator)

NSLayoutConstraint.activate([
indicator.centerXAnchor.constraint(equalTo: view.centerXAnchor),
indicator.centerYAnchor.constraint(equalTo: view.centerYAnchor)
])

indicator.startAnimating()
self.activityIndicator = indicator
}

private func hideActivityIndicator() {
activityIndicator?.stopAnimating()
activityIndicator?.removeFromSuperview()
activityIndicator = nil
}

// MARK: - Block Editor Settings

private func fetchBlockEditorSettings() {
guard let service = rawBlockEditorSettingsService else {
startEditor()
return
}

Task { @MainActor in
// Start the editor with default settings after 3 seconds
let timeoutTask = Task {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a clever way to address this – I think ideally we'd just use the timeout property on URLRequest, but we don't really own WordPressOrgRestApi. I guess we'll come back to this when it's time to adopt the Rust-layer REST API, and we can switch it over then.

try await Task.sleep(nanoseconds: 3_000_000_000) // 3 seconds
if !Task.isCancelled {
startEditor()
}
}

do {
let settings = try await service.fetchSettings()
timeoutTask.cancel()
startEditor(with: settings)
} catch {
timeoutTask.cancel()
DDLogError("Error fetching block editor settings: \(error)")
startEditor()
}
}
}

private func startEditor(with settings: [String: Any]? = nil) {
guard !hasEditorStarted else { return }
hasEditorStarted = true

if let settings {
var updatedConfig = self.editorViewController.configuration
updatedConfig.updateEditorSettings(settings)
self.editorViewController.updateConfiguration(updatedConfig)
}
self.editorViewController.startEditorSetup()
}
}

extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate {
Expand All @@ -327,6 +402,7 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate
// is still reflecting the actual startup time of the editor
editorSession.start()
}
self.hideActivityIndicator()
}

func editor(_ viewContoller: GutenbergKit.EditorViewController, didDisplayInitialContent content: String) {
Expand Down