diff --git a/Modules/Package.resolved b/Modules/Package.resolved
index 05d0519d67a2..ec1f7f793b23 100644
--- a/Modules/Package.resolved
+++ b/Modules/Package.resolved
@@ -1,5 +1,5 @@
{
- "originHash" : "0018852d4b5daf8ccfef53dba310fdb313c4511d7c5ade6fe3ef42fa2490ca4e",
+ "originHash" : "c309bd4a10359e813e6b0c4b70e353fb5e054a0a93db9045a8dbb3c7ba8b1c05",
"pins" : [
{
"identity" : "alamofire",
@@ -149,8 +149,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/wordpress-mobile/GutenbergKit",
"state" : {
- "revision" : "06a322f5fe222c4991abca407b7392f76bcff9d8",
- "version" : "0.9.0"
+ "revision" : "ed8ddd5a8af42b092834e6a3cfbd9944e71b9d59"
}
},
{
@@ -295,6 +294,15 @@
"version" : "8.0.4"
}
},
+ {
+ "identity" : "svgview",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/exyte/SVGView.git",
+ "state" : {
+ "revision" : "6465962facdd25cb96eaebc35603afa2f15d2c0d",
+ "version" : "1.0.6"
+ }
+ },
{
"identity" : "svprogresshud",
"kind" : "remoteSourceControl",
diff --git a/Modules/Package.swift b/Modules/Package.swift
index 6fa743000e7a..d3e7d4fc28ef 100644
--- a/Modules/Package.swift
+++ b/Modules/Package.swift
@@ -53,7 +53,7 @@ let package = Package(
.package(url: "https://github.com/wordpress-mobile/NSURL-IDN", revision: "b34794c9a3f32312e1593d4a3d120572afa0d010"),
.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/wordpress-mobile/GutenbergKit", from: "0.9.0"),
+ .package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "ed8ddd5a8af42b092834e6a3cfbd9944e71b9d59"),
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20251101"),
.package(
url: "https://github.com/Automattic/color-studio",
diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
index e13eb38ce187..80bb1f1046d5 100644
--- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
+++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
@@ -27,6 +27,7 @@ public enum FeatureFlag: Int, CaseIterable {
case mediaQuotaView
case intelligence
case newSupport
+ case nativeBlockInserter
/// Returns a boolean indicating if the feature is enabled.
///
@@ -86,6 +87,8 @@ public enum FeatureFlag: Int, CaseIterable {
return (languageCode ?? "en").hasPrefix("en")
case .newSupport:
return false
+ case .nativeBlockInserter:
+ return false
}
}
@@ -130,6 +133,7 @@ extension FeatureFlag {
case .mediaQuotaView: "Media Quota"
case .intelligence: "Intelligence"
case .newSupport: "New Support"
+ case .nativeBlockInserter: "Native Block Inserter"
}
}
}
diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift
index a021a6386c88..df7c8f99a240 100644
--- a/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift
+++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift
@@ -104,4 +104,8 @@ extension CommentGutenbergEditorViewController: GutenbergKit.EditorViewControlle
func editor(_ viewController: GutenbergKit.EditorViewController, didCloseModalDialog dialogType: String) {
// Do nothing
}
+
+ func editor(_ viewController: GutenbergKit.EditorViewController, didLogMessage message: String, level: GutenbergKit.LogLevel) {
+ // Do nothing
+ }
}
diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerController.swift
new file mode 100644
index 000000000000..3e187ceeec2e
--- /dev/null
+++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerController.swift
@@ -0,0 +1,173 @@
+import UIKit
+import GutenbergKit
+import WordPressData
+import WordPressShared
+
+/// A adapter for GutenbergKit that manages media picker sources the editor.
+final class MediaPickerController: GutenbergKit.MediaPickerController {
+ private let blog: Blog
+
+ init(blog: Blog) {
+ self.blog = blog
+ }
+
+ func getActions(for parameters: MediaPickerParameters) -> [MediaPickerActionGroup] {
+ let menu = MediaPickerMenu(
+ filter: convertFilter(parameters.filter),
+ isMultipleSelectionEnabled: parameters.isMultipleSelectionEnabled
+ )
+
+ // Create a temporary controller just to extract action metadata
+ let tempController = MediaPickerMenuController()
+
+ // Define media sources with their identifiers
+ let sources: [(source: MediaPickerSource, id: MediaPickerID)] = [
+ (.siteMedia(blog: blog), .siteMedia),
+ (.freePhotos(blog: blog), .freePhotos),
+ (.freeGIFs(blog: blog), .freeGIFs)
+ ]
+
+ // Create actions from enabled sources
+ let actions = sources.compactMap { source, id -> MediaPickerAction? in
+ guard source.isEnabled else { return nil }
+
+ let uiAction = createUIAction(for: source, menu: menu, controller: tempController)
+ guard let uiAction else { return nil }
+
+ return MediaPickerAction(
+ id: id.rawValue,
+ title: uiAction.title,
+ image: uiAction.image ?? UIImage()
+ )
+ }
+
+ return [MediaPickerActionGroup(id: "primary", actions: actions)]
+ .filter { !$0.actions.isEmpty }
+ }
+
+ func perform(_ action: MediaPickerAction, parameters: MediaPickerParameters, from presentingViewController: UIViewController) async -> [MediaInfo] {
+ // Find the source for this action
+ guard let pickerID = MediaPickerID(rawValue: action.id) else {
+ return []
+ }
+
+ let source = getSource(for: pickerID)
+ guard source.isEnabled else {
+ return []
+ }
+
+ // Create menu and controller
+ let menu = MediaPickerMenu(
+ filter: convertFilter(parameters.filter),
+ isMultipleSelectionEnabled: parameters.isMultipleSelectionEnabled
+ )
+
+ let controller = MediaPickerMenuController()
+
+ // Use continuation to wait for the selection
+ return await withCheckedContinuation { continuation in
+ controller.onSelection = { [weak self] selection in
+ guard let self else {
+ continuation.resume(returning: [])
+ return
+ }
+ let mediaInfos = self.convertSelectionToMediaInfo(selection)
+ continuation.resume(returning: mediaInfos)
+ }
+
+ // Create and perform the UIAction
+ if let uiAction = createUIAction(for: source, menu: menu, controller: controller) {
+ MainActor.assumeIsolated {
+ uiAction.performWithSender(nil, target: nil)
+ }
+ } else {
+ continuation.resume(returning: [])
+ }
+ }
+ }
+
+ // MARK: - Private Methods
+
+ private func getSource(for id: MediaPickerID) -> MediaPickerSource {
+ switch id {
+ case .imagePlayground: .playground
+ case .siteMedia: .siteMedia(blog: blog)
+ case .applePhotos: .photos
+ case .freePhotos: .freePhotos(blog: blog)
+ case .freeGIFs: .freeGIFs(blog: blog)
+ default: fatalError("Unsupported: \(id)")
+ }
+ }
+
+ private func convertFilter(_ filter: MediaPickerParameters.MediaFilter?) -> MediaPickerMenu.MediaFilter? {
+ guard let filter else { return nil }
+ switch filter {
+ case .images: return .images
+ case .videos: return .videos
+ case .all: return nil
+ }
+ }
+
+ private func createUIAction(for source: MediaPickerSource, menu: MediaPickerMenu, controller: MediaPickerMenuController) -> UIAction? {
+ switch source {
+ case .playground: menu.makeImagePlaygroundAction(delegate: controller)
+ case .siteMedia: menu.makeSiteMediaAction(blog: blog, delegate: controller)
+ case .photos: menu.makePhotosAction(delegate: controller)
+ case .freePhotos: menu.makeStockPhotos(blog: blog, delegate: controller)
+ case .freeGIFs: menu.makeFreeGIFAction(blog: blog, delegate: controller)
+ default: nil
+ }
+ }
+
+ private func convertSelectionToMediaInfo(_ selection: MediaPickerSelection) -> [MediaInfo] {
+ var output: [MediaInfo] = []
+
+ for item in selection.items {
+ switch item {
+ case .media(let media):
+ var metadata: [String: String] = [:]
+ if let videopressGUID = media.videopressGUID {
+ metadata["videopressGUID"] = videopressGUID
+ }
+ let mediaInfo = MediaInfo(
+ id: media.mediaID?.int32Value,
+ url: media.remoteURL,
+ type: media.mimeType,
+ caption: media.caption,
+ title: media.filename,
+ alt: media.alt,
+ metadata: metadata
+ )
+ output.append(mediaInfo)
+
+ case .external(let asset):
+ let mediaInfo = MediaInfo(
+ id: nil,
+ url: asset.largeURL.absoluteString,
+ type: asset.largeURL.preferredMimeType,
+ caption: asset.caption,
+ title: asset.name,
+ alt: nil,
+ metadata: [:]
+ )
+ output.append(mediaInfo)
+
+ case .image, .pickerResult:
+ wpAssertionFailure("unused case")
+ break
+ }
+ }
+
+ return output
+ }
+}
+
+private extension URL {
+ var preferredMimeType: String {
+ if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType {
+ return mimeType
+ } else {
+ return "application/octet-stream"
+ }
+ }
+}
diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift
index b3541871521c..51d66452422e 100644
--- a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift
+++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift
@@ -6,7 +6,7 @@ import WordPressData
final class MediaPickerMenuController: NSObject {
var onSelection: ((MediaPickerSelection) -> Void)?
- fileprivate func didSelect(_ items: [MediaPickerItem], source: String) {
+ fileprivate func didSelect(_ items: [MediaPickerItem], source: MediaPickerID) {
let selection = MediaPickerSelection(items: items, source: source)
DispatchQueue.main.async {
self.onSelection?(selection)
@@ -18,7 +18,7 @@ extension MediaPickerMenuController: PHPickerViewControllerDelegate {
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
picker.presentingViewController?.dismiss(animated: true)
if !results.isEmpty {
- self.didSelect(results.map(MediaPickerItem.pickerResult), source: "apple_photos")
+ self.didSelect(results.map(MediaPickerItem.pickerResult), source: .applePhotos)
}
}
}
@@ -27,7 +27,7 @@ extension MediaPickerMenuController: ImagePickerControllerDelegate {
func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
picker.presentingViewController?.dismiss(animated: true)
if let image = info[.originalImage] as? UIImage {
- self.didSelect([.image(image)], source: "camera")
+ self.didSelect([.image(image)], source: .camera)
}
}
}
@@ -36,7 +36,7 @@ extension MediaPickerMenuController: SiteMediaPickerViewControllerDelegate {
func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) {
viewController.presentingViewController?.dismiss(animated: true)
if !selection.isEmpty {
- self.didSelect(selection.map(MediaPickerItem.media), source: "site_media")
+ self.didSelect(selection.map(MediaPickerItem.media), source: .siteMedia)
}
}
}
@@ -46,7 +46,7 @@ extension MediaPickerMenuController: ImagePlaygroundPickerDelegate {
viewController.presentingViewController?.dismiss(animated: true)
if let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) {
- self.didSelect([.image(image)], source: "image_playground")
+ self.didSelect([.image(image)], source: .imagePlayground)
} else {
wpAssertionFailure("failed to read the image created by ImagePlayground")
}
@@ -57,7 +57,7 @@ extension MediaPickerMenuController: ExternalMediaPickerViewDelegate {
func externalMediaPickerViewController(_ viewController: ExternalMediaPickerViewController, didFinishWithSelection selection: [ExternalMediaAsset]) {
viewController.presentingViewController?.dismiss(animated: true)
if !selection.isEmpty {
- let source = viewController.source == .tenor ? "free_gifs" : "free_photos"
+ let source: MediaPickerID = viewController.source == .tenor ? .freeGIFs : .freePhotos
self.didSelect(selection.map(MediaPickerItem.external), source: source)
}
}
diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift
index cd5fb43356ab..a14709a39caa 100644
--- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift
+++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift
@@ -101,7 +101,7 @@ enum MediaPickerSource {
struct MediaPickerSelection {
var items: [MediaPickerItem]
- var source: String
+ var source: MediaPickerID
}
enum MediaPickerItem {
diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift
index c4e2f8ddafdb..a6e8dad5a995 100644
--- a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift
+++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift
@@ -53,3 +53,12 @@ extension MediaPickerMenu.MediaFilter {
}
}
}
+
+enum MediaPickerID: String {
+ case applePhotos = "apple_photos"
+ case camera = "camera"
+ case siteMedia = "site_media"
+ case imagePlayground = "image_playground"
+ case freeGIFs = "free_gifs"
+ case freePhotos = "free_photos"
+}
diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift
index fca8dff431f9..62d4625ab5b0 100644
--- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift
+++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift
@@ -1,6 +1,7 @@
import UIKit
import WordPressUI
import AsyncImageKit
+import BuildSettingsKit
import AutomatticTracks
import GutenbergKit
import SafariServices
@@ -8,6 +9,7 @@ import WordPressData
import WordPressShared
import WebKit
import CocoaLumberjackSwift
+import Photos
class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor {
@@ -180,8 +182,13 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
self.editorSession = PostEditorAnalyticsSession(editor: .gutenbergKit, post: post)
self.navigationBarManager = navigationBarManager ?? PostEditorNavigationBarManager()
+ EditorLocalization.localize = getLocalizedString
+
let editorConfiguration = EditorConfiguration(blog: post.blog)
- self.editorViewController = GutenbergKit.EditorViewController(configuration: editorConfiguration)
+ self.editorViewController = GutenbergKit.EditorViewController(
+ configuration: editorConfiguration,
+ mediaPicker: MediaPickerController(blog: post.blog)
+ )
self.blockEditorSettingsService = RawBlockEditorSettingsService(blog: post.blog)
@@ -397,6 +404,7 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
.apply(settings) { $0.setEditorSettings($1) }
.setTitle(post.postTitle ?? "")
.setContent(post.content ?? "")
+ .setNativeInserterEnabled(FeatureFlag.nativeBlockInserter.enabled)
.build()
self.editorViewController.updateConfiguration(updatedConfiguration)
@@ -565,10 +573,12 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate
}
}
- func editor(_ viewController: GutenbergKit.EditorViewController, performRequest: GutenbergKit.EditorNetworkRequest) async throws -> GutenbergKit.EditorNetworkResponse {
- throw URLError(.unknown)
+ func editor(_ viewController: GutenbergKit.EditorViewController, didLogMessage message: String, level: GutenbergKit.LogLevel) {
+ // Do nothing
}
+ // MARK: - Media Picker Helpers
+
func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) {
let flags = mediaFilterFlags(using: config.allowedTypes ?? [])
@@ -1099,3 +1109,18 @@ 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")
+ }
+}
diff --git a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme
index 8b7fb8b87607..148389f3fadf 100644
--- a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme
+++ b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme
@@ -122,6 +122,11 @@
value = "disable"
isEnabled = "NO">
+
+