diff --git a/Modules/Package.resolved b/Modules/Package.resolved
index e075b7450e38..6cf9aad38fe4 100644
--- a/Modules/Package.resolved
+++ b/Modules/Package.resolved
@@ -1,5 +1,5 @@
{
- "originHash" : "b1fb7c230ac6bba7fef0398912e86c03c8a9329d742c0c971d3945e9308ee8f2",
+ "originHash" : "db265698147730f917e4e590fe57a526e3b2a550a97ff973019fb8914dcf3987",
"pins" : [
{
"identity" : "alamofire",
@@ -149,8 +149,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/wordpress-mobile/GutenbergKit",
"state" : {
- "revision" : "3afb350113614db42bee4c30d00b1697209b25fc",
- "version" : "0.6.0"
+ "revision" : "e7c6471bf7b37c2a742d9106d32c83d7f5c89051"
}
},
{
diff --git a/Modules/Package.swift b/Modules/Package.swift
index 4a7456505b66..a28b43c615dd 100644
--- a/Modules/Package.swift
+++ b/Modules/Package.swift
@@ -54,7 +54,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-20250715"),
- .package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.6.0"),
+ .package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "e7c6471bf7b37c2a742d9106d32c83d7f5c89051"),
.package(
url: "https://github.com/Automattic/color-studio",
revision: "bf141adc75e2769eb469a3e095bdc93dc30be8de"
diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
index b0621431d471..e97531886cf8 100644
--- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
+++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
@@ -26,6 +26,7 @@ public enum FeatureFlag: Int, CaseIterable {
case pluginManagementOverhaul
case nativeJetpackConnection
case newsletterSubscribers
+ case nativeBlockInserter
/// Returns a boolean indicating if the feature is enabled.
///
@@ -82,6 +83,8 @@ public enum FeatureFlag: Int, CaseIterable {
return BuildConfiguration.current == .debug
case .newsletterSubscribers:
return true
+ case .nativeBlockInserter:
+ return BuildConfiguration.current == .debug
}
}
@@ -125,6 +128,7 @@ extension FeatureFlag {
case .readerGutenbergCommentComposer: "Gutenberg Comment Composer"
case .nativeJetpackConnection: "Native Jetpack Connection"
case .newsletterSubscribers: "Newsletter Subscribers"
+ 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 53e7e4df9f4a..d86791cd06f5 100644
--- a/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift
+++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift
@@ -91,4 +91,8 @@ extension CommentGutenbergEditorViewController: GutenbergKit.EditorViewControlle
func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: GutenbergKit.OpenMediaLibraryAction) {
// Do nothing
}
+
+ func getMediaPickerController(for viewController: GutenbergKit.EditorViewController, parameters: GutenbergKit.MediaPickerParameters) -> GutenbergKit.MediaPickerController? {
+ nil
+ }
}
diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPicker+GutenbergKit.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPicker+GutenbergKit.swift
new file mode 100644
index 000000000000..5afacbaa76ae
--- /dev/null
+++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPicker+GutenbergKit.swift
@@ -0,0 +1,12 @@
+import UIKit
+import GutenbergKit
+
+extension MediaPickerMenu.MediaFilter {
+ init?(_ filter: GutenbergKit.MediaPickerParameters.MediaFilter) {
+ switch filter {
+ case .images: self = .images
+ case .videos: self = .videos
+ case .all: return nil
+ }
+ }
+}
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..042bb6006f6f
--- /dev/null
+++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerController.swift
@@ -0,0 +1,159 @@
+import UIKit
+import GutenbergKit
+import WordPressData
+
+/// A adapter for GutenbergKit that manages media picker sources the editor.
+final class MediaPickerController: GutenbergKit.MediaPickerController {
+ private let blog: Blog
+ private let parameters: MediaPickerParameters
+ private var currentMediaPickerController: MediaPickerMenuController?
+ private var currentMediaPickerCompletion: (([MediaInfo]) -> Void)?
+
+ init(blog: Blog, parameters: MediaPickerParameters) {
+ self.blog = blog
+ self.parameters = parameters
+ }
+
+ var actions: [[MediaPickerAction]] {
+ // Create MediaPickerMenu with the configuration
+ let menu = MediaPickerMenu(
+ filter: convertFilter(parameters.filter),
+ isMultipleSelectionEnabled: parameters.isMultipleSelectionEnabled
+ )
+
+ // Create a controller to handle selections
+ let controller = MediaPickerMenuController()
+ controller.onSelection = { [weak self] selection in
+ guard let self else { return }
+ let mediaInfos = self.convertSelectionToMediaInfo(selection)
+ self.currentMediaPickerCompletion?(mediaInfos)
+ self.currentMediaPickerCompletion = nil
+ self.currentMediaPickerController = nil
+ }
+
+ // Store the controller to keep it alive
+ currentMediaPickerController = controller
+
+ // Define media sources with their identifiers
+ let sources: [(source: MediaPickerSource, id: MediaPickerID)] = [
+ (.playground, .imagePlayground),
+ (.siteMedia(blog: blog), .siteMedia),
+ (.photos, .applePhotos),
+ (.freePhotos(blog: blog), .freePhotos),
+ (.freeGIFs(blog: blog), .freeGIFs)
+ ]
+
+ // Create actions from enabled sources
+ let actionsWithGroups = sources.compactMap { source, id -> (action: MediaPickerAction, group: Int)? in
+ guard source.isEnabled else { return nil }
+
+ let uiAction = createUIAction(for: source, menu: menu, controller: controller)
+ guard let uiAction else { return nil }
+
+ let action = convertToMediaPickerAction(uiAction, id: id)
+
+ // Group 0: playground, site media, files
+ // Group 1: free photos, free gifs
+ let group = (id == .freePhotos || id == .freeGIFs) ? 1 : 0
+
+ return (action, group)
+ }
+
+ // Group actions
+ let firstGroup = actionsWithGroups.filter { $0.group == 0 }.map { $0.action }
+ let secondGroup = actionsWithGroups.filter { $0.group == 1 }.map { $0.action }
+
+ return [firstGroup, secondGroup].filter { !$0.isEmpty }
+ }
+
+ // MARK: - Private Methods
+
+ 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:
+ return menu.makeImagePlaygroundAction(delegate: controller)
+ case .siteMedia:
+ return menu.makeSiteMediaAction(blog: blog, delegate: controller)
+ case .photos:
+ return menu.makePhotosAction(delegate: controller)
+ case .freePhotos:
+ return menu.makeStockPhotos(blog: blog, delegate: controller)
+ case .freeGIFs:
+ return menu.makeFreeGIFAction(blog: blog, delegate: controller)
+ default:
+ return nil
+ }
+ }
+
+ private func convertToMediaPickerAction(_ uiAction: UIAction, id: MediaPickerID) -> MediaPickerAction {
+ MediaPickerAction(
+ id: id.rawValue,
+ title: uiAction.title,
+ image: uiAction.image ?? UIImage(),
+ perform: { [weak self] presentingViewController, completion in
+ guard let self else {
+ completion([])
+ return
+ }
+
+ // Store the completion handler for when selection is made
+ self.currentMediaPickerCompletion = completion
+
+ // Perform the original action
+ uiAction.performWithSender(nil, target: nil)
+ }
+ )
+ }
+
+ private func convertSelectionToMediaInfo(_ selection: MediaPickerSelection) -> [MediaInfo] {
+ var mediaInfos: [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.mediaTypeString,
+ caption: media.caption,
+ title: media.filename,
+ alt: media.alt,
+ metadata: metadata
+ )
+ mediaInfos.append(mediaInfo)
+
+ case .external(let asset):
+ let mediaInfo = MediaInfo(
+ id: nil,
+ url: asset.largeURL.absoluteString,
+ type: "image",
+ caption: asset.caption,
+ title: asset.name,
+ alt: nil,
+ metadata: [:]
+ )
+ mediaInfos.append(mediaInfo)
+
+ case .image, .pickerResult:
+ // These would need to be uploaded first
+ // For now, we skip them
+ break
+ }
+ }
+
+ return mediaInfos
+ }
+}
diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift
index 4f76d7b9e222..a9f56c8a7060 100644
--- a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift
+++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift
@@ -5,7 +5,7 @@ import WordPressShared
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)
@@ -17,7 +17,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)
}
}
}
@@ -26,7 +26,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)
}
}
}
@@ -35,7 +35,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)
}
}
}
@@ -45,7 +45,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")
}
@@ -56,7 +56,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 2001677e45b1..494e8d8dd90d 100644
--- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift
+++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift
@@ -1,12 +1,14 @@
import UIKit
import WordPressUI
import AsyncImageKit
+import BuildSettingsKit
import AutomatticTracks
import GutenbergKit
import SafariServices
import WordPressData
import WordPressShared
import WebKit
+import Photos
class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor {
let errorDomain: String = "GutenbergViewController.errorDomain"
@@ -133,6 +135,8 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
conf.content = post.content ?? ""
conf.postID = post.postID?.intValue != -1 ? post.postID?.intValue : nil
conf.postType = post is Page ? "page" : "post"
+ conf.enableNativeBlockInserter = FeatureFlag.nativeBlockInserter.enabled
+ conf.autoFocusOnLoad = FeatureFlag.nativeBlockInserter.enabled
self.editorViewController = GutenbergKit.EditorViewController(configuration: conf)
@@ -184,8 +188,8 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
view.pinSubviewToAllEdges(editorViewController.view)
editorViewController.didMove(toParent: self)
- if #available(iOS 16.4, *) {
- editorViewController.webView.isInspectable = true // TODO: should be diasble in production
+ if #available(iOS 16.4, *), BuildConfiguration.current == .debug {
+ editorViewController.webView.isInspectable = true
}
// Doesn't seem to do anything
@@ -426,6 +430,13 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate
throw URLError(.unknown)
}
+ /// Returns the available media picker sources for the given configuration
+ func getMediaPickerController(for viewController: GutenbergKit.EditorViewController, parameters: GutenbergKit.MediaPickerParameters) -> (any GutenbergKit.MediaPickerController)? {
+ MediaPickerController(blog: post.blog, parameters: parameters)
+ }
+
+ // MARK: - Media Picker Helpers
+
func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) {
let flags = mediaFilterFlags(using: config.allowedTypes ?? [])
diff --git a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme
index 968123f0debb..ff3f8291d8b5 100644
--- a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme
+++ b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme
@@ -122,15 +122,20 @@
value = "disable"
isEnabled = "NO">
+
+