Skip to content

Add Conditional editor theme styles#25160

Merged
jkmassel merged 10 commits intorelease/26.6from
add/conditional-editor-theme-styles
Feb 5, 2026
Merged

Add Conditional editor theme styles#25160
jkmassel merged 10 commits intorelease/26.6from
add/conditional-editor-theme-styles

Conversation

@jkmassel
Copy link
Contributor

@jkmassel jkmassel commented Jan 22, 2026

Summary

Fix CMM-1163.

  • Use API to determine theme style support by querying the WordPress REST API for editor capabilities
  • Add some initial tests for WordPressClient feature detection and caching behaviour – there's more to do here, but that'll be a future PR.

Changes

WordPressClient Feature Detection

  • Added WordPressClientAPI protocol to abstract WordPress API access for testing
  • Added supports(_:forSiteId:) method to check API route availability for features
  • Results are cached internally using a single Task to avoid redundant API calls. In the future, we'll want a way to invalidate this, but we'll do that later.

Editor Capability Fetching

  • EditorDependencyManager now fetches and caches editor capabilities on My Site load
  • GutenbergSettings stores feature support per-blog using WordPressClient.Feature.stringValue as stable keys
  • Theme styles toggle visibility is now determined by API capability rather than hardcoded logic

Testing Instructions

  • Ensure CI passes
  • Add cpt.wpmt.co to the app. Ensure the editor loads as expected. Ensure that theme styles toggle cannot be activated in Site Settings.
  • Add jetpack.wpmt.co to the app. Ensure the editor loads. Ensure that theme styles toggle can be activated.
  • Add a WP.com simple site to the app. Ensure the editor loads. Ensure that theme styles toggle can be activated.

@jkmassel jkmassel requested a review from dcalhoun January 22, 2026 19:36
@jkmassel jkmassel added this to the 26.6 milestone Jan 22, 2026
@jkmassel jkmassel added the Gutenberg Editing and display of Gutenberg blocks. label Jan 22, 2026
@dangermattic
Copy link
Collaborator

dangermattic commented Jan 22, 2026

2 Warnings
⚠️ This PR is larger than 500 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
⚠️ This PR is assigned to the milestone 26.6 ❄️. The due date for this milestone has already passed.
Please assign it to a milestone with a later deadline or check whether the release for this milestone has already been finished.

Generated by 🚫 Danger

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jan 22, 2026

App Icon📲 You can test the changes from this Pull Request in WordPress by scanning the QR code below to install the corresponding build.
App NameWordPress
ConfigurationRelease-Alpha
Build Number30792
VersionPR #25160
Bundle IDorg.wordpress.alpha
Commit0727cc7
Installation URL191ui6avga14o
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jan 22, 2026

App Icon📲 You can test the changes from this Pull Request in Jetpack by scanning the QR code below to install the corresponding build.
App NameJetpack
ConfigurationRelease-Alpha
Build Number30792
VersionPR #25160
Bundle IDcom.jetpack.alpha
Commit0727cc7
Installation URL3pd81s7s7dfi0
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@wpmobilebot
Copy link
Contributor

wpmobilebot commented Jan 22, 2026

🤖 Build Failure Analysis

This build has failures. Claude has analyzed them - check the build annotations for details.

async let currentUserTask = try await api.users.retrieveMeWithEditContext().data
async let activeThemeTask = try await api.themes.listWithEditContext(
params: ThemeListParams(status: .active)
).data.first(where: { $0.status == .active })
Copy link
Contributor

Choose a reason for hiding this comment

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

If one of the requests fails, the whole loadSiteInfoTask fails, which means we won't get any of the "site info". That's probably not something we'd want here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Great point – resolved by 74db783

case .blockTheme: isBlockTheme
case .plugins: apiRoot.hasRoute(route: "/wp/v2/plugins")
case .applicationPasswordExtras: apiRoot.hasRoute(route: "/application-password-extras/v1/admin-ajax")
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: using the urlResolver property in WordPressAPI to find the URL should work on both self-hosted and WP.com sites, so we can merge the two similar branches into one.

let key = blog.locallyUniqueId + "-capabilities"

// Don't allow more than one concurrent invalidation
if self.prefetchTasks[key] != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

Unrelated to this PR: you can use OSAllocatedUnfairLock in LockingHashMap, which is Sendable, to get rid of the @unchecked declaration, which means it's impossible (as long as we continue to have the Swift compiler check in place) to forget use the lock.withLock.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The key is AnyHashable which isn't Sendable so I'm not sure if there's an easy way to do this?

We could drop LockingHashMap once our minimum iOS target is 18.0 in favour of the Swift Mutex type though.

Copy link
Contributor

Choose a reason for hiding this comment

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

You could keep a strong-typed key and use OSAllocatedUnfairLock<[CacheKey: CacheResult]>. Again, not really related to this PR, probably something for future PRs if needed.

I'm looking into GBK's custom post type support, and I think I'll need to update how this cache works (I probably need to use (Blog, PostType) as the cache key).

@dcalhoun
Copy link
Member

dcalhoun commented Jan 22, 2026

Noting some early testing results...

Good:

  • Theme styles correctly loaded for WPCOM Simple sites.
  • The Use theme styles toggle was correctly disabled for cpt.wpmt.co.

Bad:

  • With theme styles enabled, theme styles were missing from the editor for WoW/Atomic sites and jetpack.wpmt.co. WoW correctly showed (presumably cached) theme styles before I logged out and back in; then theme styles were missing.
  • The Use theme styles toggle resets when navigating away from the Site Settings view or toggling an adjacent setting.
Theme styles settings reset

theme-styles-settings.MP4

@jkmassel jkmassel force-pushed the add/conditional-editor-theme-styles branch 2 times, most recently from cfc334e to bc417dc Compare January 22, 2026 23:11
@wpmobilebot wpmobilebot modified the milestones: 26.6, 26.7 Jan 23, 2026
@wpmobilebot
Copy link
Contributor

Version 26.6 has now entered code-freeze, so the milestone of this PR has been updated to 26.7.

@dcalhoun
Copy link
Member

I updated the PR description to link this to CMM-1163.

Also, should we target the release/26.6 branch?

@jkmassel jkmassel changed the base branch from trunk to release/26.6 February 3, 2026 20:26
@jkmassel jkmassel force-pushed the add/conditional-editor-theme-styles branch from 74db783 to 796e829 Compare February 3, 2026 20:48
@jkmassel
Copy link
Contributor Author

jkmassel commented Feb 3, 2026

should we target the release/26.6 branch

Yep, I've rebased atop that branch.

case .success(let user): return user
case .failure(let error):
self.loadCurrentUserTask = newCurrentUserTask()
throw error
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be better to await self.loadCurrentUserTask.value? This throw returns the preivous fetch attempt's error. I don't think that's something the caller wants when calling try await client.fetchCurrentUser()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm I think you're probably right – I've taken another run at it in be373d5.

The way it'll work now is if if the first run fails, it'll drop the error but subsequent fetches will propagate it. I think this is an ok trade-off?

case .blockEditorSettings: apiRoot.hasRoute(route: "/wp-block-editor/v1/sites/\(siteId)/settings")
case .blockTheme: isBlockTheme
case .plugins: apiRoot.hasRoute(route: "/wp/v2/plugins")
case .applicationPasswordExtras: apiRoot.hasRoute(route: "/application-password-extras/v1/admin-ajax")
Copy link
Contributor

Choose a reason for hiding this comment

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

Do the plugins and applicationPasswordExtras cases need /sites/\(siteId)/?

BTW, I'm not sure if this comment's suggestion would work, maybe worth give it ago https://github.com/wordpress-mobile/WordPress-iOS/pull/25160/changes#r2718516922

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure they do, but this works and I'd like to do a separate PR that adopts the wprs path management.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I believe the applicationPasswordExtras endpoint requires the site ID if fetched from the WP.com API. Presumably the plugins would need it also.

// These tasks need to be manually restated here because we can't use the task constructors
self.loadSiteInfoTask = Task { try await api.apiRoot.get().data }
self.loadCurrentUserTask = Task { try await api.users.retrieveMeWithEditContext().data }
self.loadActiveThemeTask = Task { try await api.themes.listWithEditContext(params: ThemeListParams(status: .active)).data.first }
Copy link
Contributor

Choose a reason for hiding this comment

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

Call Task { self.refresh() } instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That still violates task isolation – you can't call self from inside any of these tasks.

let key = blog.locallyUniqueId + "-capabilities"

// Don't allow more than one concurrent invalidation
if self.prefetchTasks[key] != nil {
Copy link
Contributor

Choose a reason for hiding this comment

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

You could keep a strong-typed key and use OSAllocatedUnfairLock<[CacheKey: CacheResult]>. Again, not really related to this PR, probably something for future PRs if needed.

I'm looking into GBK's custom post type support, and I think I'll need to update how this cache works (I probably need to use (Blog, PostType) as the cache key).

}

// Refresh editor capabilities
EditorDependencyManager.shared.fetchEditorCapabilities(for: blog)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this code need the if RemoteFeatureFlag.newGutenberg.enabled(), like the one above?


@objc public func swiftRefreshSettings() {
// Refresh editor capabilities
EditorDependencyManager.shared.fetchEditorCapabilities(for: self.blog)
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this code need if RemoteFeatureFlag.newGutenberg.enabled()?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure it's strictly needed but doesn't hurt – added in f22e21f

@jkmassel jkmassel requested a review from crazytonyli February 4, 2026 15:48
Copy link
Member

@dcalhoun dcalhoun left a comment

Choose a reason for hiding this comment

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

The latest iteration tested well for me. I did not encounter any oddities while testing WP.com Simple, WoW, and self-hosted sites. I tested variations with and without the Gutenberg plugin. I tested GutenbergKit, Gutenberg Mobile, and Aztec.

case .blockEditorSettings: apiRoot.hasRoute(route: "/wp-block-editor/v1/sites/\(siteId)/settings")
case .blockTheme: isBlockTheme
case .plugins: apiRoot.hasRoute(route: "/wp/v2/plugins")
case .applicationPasswordExtras: apiRoot.hasRoute(route: "/application-password-extras/v1/admin-ajax")
Copy link
Member

Choose a reason for hiding this comment

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

Yes, I believe the applicationPasswordExtras endpoint requires the site ID if fetched from the WP.com API. Presumably the plugins would need it also.

…iewController.m

Co-authored-by: David Calhoun <github@davidcalhoun.me>
@jkmassel
Copy link
Contributor Author

jkmassel commented Feb 4, 2026

Yes, I believe the applicationPasswordExtras endpoint requires the site ID if fetched from the WP.com API. Presumably the plugins would need it also.

Sites with plugins shouldn't be going through the WP.com APIs, we should be talking to those sites directly. Is the Application Password extras endpoint live? I'd suggest addressing this in a future PR either way to get this landed though.

@dcalhoun
Copy link
Member

dcalhoun commented Feb 4, 2026

Yes, I believe the applicationPasswordExtras endpoint requires the site ID if fetched from the WP.com API. Presumably the plugins would need it also.

Sites with plugins shouldn't be going through the WP.com APIs, we should be talking to those sites directly. Is the Application Password extras endpoint live? I'd suggest addressing this in a future PR either way to get this landed though.

I agree with both points. I intended to add additional context, not imply we need to address it now.

No, the Application Passwords Extras endpoint is not yet live.

@jkmassel jkmassel enabled auto-merge (squash) February 4, 2026 23:05
@crazytonyli crazytonyli modified the milestones: 26.7, 26.6 ❄️ Feb 5, 2026
@sonarqubecloud
Copy link

sonarqubecloud bot commented Feb 5, 2026

@jkmassel jkmassel merged commit d3343a2 into release/26.6 Feb 5, 2026
26 of 32 checks passed
@jkmassel jkmassel deleted the add/conditional-editor-theme-styles branch February 5, 2026 00:59
crazytonyli pushed a commit that referenced this pull request Feb 5, 2026
* Use API to determine theme style support

* Add Tests

* Split up WordPressClient cache fetches

* Fix a warmup call introduced in the rebase

* Fix theme styles persistence

* Only fetch editor capabilities if new gutenberg is enabled

* Address suggestions

* Default to `true` for `isThemeStylesEnabled`

* Update WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController.m

Co-authored-by: David Calhoun <github@davidcalhoun.me>

---------

Co-authored-by: David Calhoun <github@davidcalhoun.me>
github-merge-queue bot pushed a commit that referenced this pull request Feb 5, 2026
* Exclude sensitive headers from Pulse logs (#25213)

* Add Conditional editor theme styles (#25160)

* Use API to determine theme style support

* Add Tests

* Split up WordPressClient cache fetches

* Fix a warmup call introduced in the rebase

* Fix theme styles persistence

* Only fetch editor capabilities if new gutenberg is enabled

* Address suggestions

* Default to `true` for `isThemeStylesEnabled`

* Update WordPress/Classes/ViewRelated/Blog/Site Settings/SiteSettingsViewController.m

Co-authored-by: David Calhoun <github@davidcalhoun.me>

---------

Co-authored-by: David Calhoun <github@davidcalhoun.me>

* Update strings for localization

* Bump version number

---------

Co-authored-by: Tony Li <tony.li@automattic.com>
Co-authored-by: Jeremy Massel <1123407+jkmassel@users.noreply.github.com>
Co-authored-by: David Calhoun <github@davidcalhoun.me>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Gutenberg Editing and display of Gutenberg blocks.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants