diff --git a/Modules/Package.swift b/Modules/Package.swift index e3cb397670b8..704b911a3908 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -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"), ], diff --git a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved index 72e7a8c77931..302e7c504c9e 100644 --- a/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "645dd3ef573ab8e20ff04cddccb0ffcc29b7a5f354c0eb4d8f44d125b5a8694d", + "originHash" : "c7fc894bd07cdfaf1241217c60af90ce11e6474d9488e6d63dd1e81668a87272", "pins" : [ { "identity" : "alamofire", @@ -150,7 +150,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "fc369073730384c8946bee15ec8ff7c763cf69c9" + "revision" : "fa72e630203e7472d55f4abedfd5c462d2333584" } }, { diff --git a/WordPress/Classes/Models/Blog/Blog+RawBlockEditorSettings.swift b/WordPress/Classes/Models/Blog/Blog+RawBlockEditorSettings.swift new file mode 100644 index 000000000000..29889c9170d1 --- /dev/null +++ b/WordPress/Classes/Models/Blog/Blog+RawBlockEditorSettings.swift @@ -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) + } + } +} diff --git a/WordPress/Classes/Services/RawBlockEditorSettingsService.swift b/WordPress/Classes/Services/RawBlockEditorSettingsService.swift new file mode 100644 index 000000000000..e5ed260db38f --- /dev/null +++ b/WordPress/Classes/Services/RawBlockEditorSettingsService.swift @@ -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] { + // 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"]) + } +} diff --git a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift index becc44a14f12..e70cee4926bd 100644 --- a/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift +++ b/WordPress/Classes/ViewRelated/Blog/My Site/MySiteViewController.swift @@ -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 @@ -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() } } @@ -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 diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index c14a7556b513..b518b0a2f38a 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -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() @@ -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 @@ -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 { + 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 { @@ -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) {