Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 2 additions & 3 deletions Modules/Package.resolved

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

2 changes: 1 addition & 1 deletion Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ let package = Package(
.package(url: "https://github.com/wordpress-mobile/wpxmlrpc", from: "0.9.0"),
.package(url: "https://github.com/wordpress-mobile/NSURL-IDN", revision: "b34794c9a3f32312e1593d4a3d120572afa0d010"),
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.13.0"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "6c3a438dc9a6e5dbe118810d9b98052086261a0b"),
// We can't use wordpress-rs branches nor commits here. Only tags work.
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20260114"),
.package(
Expand Down
17 changes: 17 additions & 0 deletions WordPress/Classes/Utility/Editor/EditorDependencyManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ final class EditorDependencyManager: Sendable {
/// Cached dependencies keyed by blog's ObjectID string representation.
private let cache = LockingHashMap<EditorDependencies>()

/// Tracks the `newGutenbergPlugins` flag value at the time the cache was last populated.
/// Used to detect when the flag changes and invalidate all stale entries.
private let pluginsFlagLock = NSLock()
private var _lastPluginsFlagValue: Bool?

/// Currently running prefetch tasks, keyed by blog's ObjectID string.
private let prefetchTasks = LockingTaskHashMap<Void, Never>()

Expand Down Expand Up @@ -83,6 +88,16 @@ final class EditorDependencyManager: Sendable {
return nil
}

// Check if the plugins flag changed since we last cached
let currentPluginsFlagValue = RemoteFeatureFlag.newGutenbergPlugins.enabled()
let lastFlagValue = pluginsFlagLock.withLock { _lastPluginsFlagValue }
if let lastFlagValue, lastFlagValue != currentPluginsFlagValue {
// Flag changed - invalidate all cached entries
DDLogInfo("EditorDependencyManager: Plugins flag changed (\(lastFlagValue) -> \(currentPluginsFlagValue)), invalidating all cached dependencies")
cache.removeAll()
prefetchTasks.removeAll()
}
Comment on lines +94 to +99
Copy link
Member Author

Choose a reason for hiding this comment

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

This is the fix for the unexpected "Plugin loading failed, using default editor configuration" warning.

GutenbergKit's preloading logic returns an empty bundle if plugins is disabled. The app preloaded and cached an empty asset bundle, then continued utilizing it even after the plugins feature was enabled. Invalidating the cache ensure GutenbergKit preloads an appropriate bundle.


// Don't prefetch if we already have cached dependencies
if cache[key] != nil {
return nil
Expand All @@ -95,6 +110,7 @@ final class EditorDependencyManager: Sendable {
do {
let dependencies = try await service.prepare()
self.cache[key] = dependencies
self.pluginsFlagLock.withLock { self._lastPluginsFlagValue = currentPluginsFlagValue }
} catch {
// Prefetch failed - editor will fall back to async loading
DDLogError("EditorDependencyManager: Failed to prefetch dependencies: \(error)")
Expand Down Expand Up @@ -145,6 +161,7 @@ final class EditorDependencyManager: Sendable {
/// Clears all cached dependencies.
func invalidateAll() {
cache.removeAll()
pluginsFlagLock.withLock { _lastPluginsFlagValue = nil }
prefetchTasks.removeAll()
// No need to use `removeAll` for the `invalidationTasks`
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import Foundation
import UIKit
import CoreData
import GutenbergKit
import WordPressData
import WordPressKit
import WordPressCore
Expand Down Expand Up @@ -30,6 +31,9 @@ final class BlogDashboardViewModel {

private var blog: Blog

/// Tracks the last blog for which the editor was warmed up to avoid redundant warmups.
private static var lastWarmedUpBlogID: NSManagedObjectID?

private var error: Error?

private let wordpressClient: WordPressClient?
Expand Down Expand Up @@ -128,7 +132,7 @@ final class BlogDashboardViewModel {
}

func viewWillAppear() {
EditorDependencyManager.shared.prefetchDependencies(for: self.blog)
warmUpEditorIfNeeded(for: self.blog)
Copy link
Member Author

Choose a reason for hiding this comment

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

Introduced to avoid unnecessary preloading if GutenbergKit is disabled.

quickActionsViewModel.viewWillAppear()
}

Expand All @@ -146,7 +150,7 @@ final class BlogDashboardViewModel {
self.loadCardsFromCache()
self.loadCards()

EditorDependencyManager.shared.prefetchDependencies(for: blog)
warmUpEditorIfNeeded(for: blog)
}

func clearEditorCache(_ completion: @escaping () -> Void) {
Expand Down Expand Up @@ -185,6 +189,30 @@ final class BlogDashboardViewModel {

private extension BlogDashboardViewModel {

/// Warms up the editor for the given blog.
///
/// This performs two operations:
/// 1. WebKit warmup (once per blog) - pre-compiles HTML/JS
/// 2. Data prefetch (always called) - fetches settings, assets, preload list
///
/// The prefetch is always called because `EditorDependencyManager` handles its own
/// caching and needs to detect when the plugins feature flag changes.
func warmUpEditorIfNeeded(for blog: Blog) {
guard RemoteFeatureFlag.newGutenberg.enabled() else {
return
}

// WebKit warmup - only needed once per blog (shaves ~100-200ms)
if blog.objectID != Self.lastWarmedUpBlogID {
Self.lastWarmedUpBlogID = blog.objectID
let configuration = EditorConfiguration(blog: blog)
GutenbergKit.EditorViewController.warmup(configuration: configuration)
}

// Data prefetch - always call to allow EditorDependencyManager to detect flag changes
EditorDependencyManager.shared.prefetchDependencies(for: blog)
}

func registerNotifications() {
NotificationCenter.default.addObserver(self, selector: #selector(showDraftsCardIfNeeded), name: .newPostCreated, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(showScheduledCardIfNeeded), name: .newPostScheduled, object: nil)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite
}

private var currentSection: Section = .dashboard
private static var lastWarmedUpBlogID: NSManagedObjectID?

@objc
private(set) lazy var scrollView: UIScrollView = {
Expand Down Expand Up @@ -341,29 +340,6 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite
configureNavBarAppearance(animated: true)
}

// MARK: - Editor Warmup

/// Warms up the editor for the given blog if it hasn't been warmed up already.
/// This avoids duplicative warmups when the site hasn't changed.
private func warmUpEditorIfNeeded(for blog: Blog) {
guard blog.objectID != Self.lastWarmedUpBlogID else {
// Editor already warmed up for this blog
return
}

Self.lastWarmedUpBlogID = blog.objectID

let configuration = EditorConfiguration(blog: blog)

// 1. WebKit warmup - pre-compile HTML/JS (shaves ~100-200ms)
GutenbergKit.EditorViewController.warmup(configuration: configuration)

// 2. Data prefetch - pre-fetch settings, assets, preload list via EditorDependencyManager
Task {
await EditorDependencyManager.shared.prefetchDependencies(for: blog)
}
}

// MARK: - Main Blog

/// This VC is prepared to either show the details for a blog, or show a no-results VC configured to let the user know they have no blogs.
Expand Down Expand Up @@ -403,9 +379,6 @@ final class MySiteViewController: UIViewController, UIScrollViewDelegate, NoSite
showDashboard(for: blog)
}

if RemoteFeatureFlag.newGutenberg.enabled() {
warmUpEditorIfNeeded(for: blog)
}
Comment on lines -406 to -408
Copy link
Member Author

Choose a reason for hiding this comment

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

Consolidated to BlogDashboardViewModel as it appears to be duplication to have this in both places.

}

@objc
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,6 @@ final class PostListViewController: AbstractPostListViewController, InteractiveP
refreshNoResultsViewController = { [weak self] in
self?.handleRefreshNoResultsViewController($0)
}

if let blog = self.blog {
EditorDependencyManager.shared.prefetchDependencies(for: blog)
}
Comment on lines -42 to -45
Copy link
Member Author

Choose a reason for hiding this comment

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

Removed as this feels duplicative of BlogDashboardViewModel. It's difficult (impossible?) to navigate to the post list without first navigating to My Site.

}

private lazy var createButtonCoordinator: CreateButtonCoordinator = {
Expand Down