Skip to content

Integrate Native Block Inserter #24708

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: trunk
Choose a base branch
from
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 @@ -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"
Expand Down
4 changes: 4 additions & 0 deletions WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -82,6 +83,8 @@ public enum FeatureFlag: Int, CaseIterable {
return BuildConfiguration.current == .debug
case .newsletterSubscribers:
return true
case .nativeBlockInserter:
return BuildConfiguration.current == .debug
}
}

Expand Down Expand Up @@ -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"
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
}
}
Expand All @@ -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)
}
}
}
Expand All @@ -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)
}
}
}
Expand All @@ -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")
}
Expand All @@ -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)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ enum MediaPickerSource {

struct MediaPickerSelection {
var items: [MediaPickerItem]
var source: String
var source: MediaPickerID
}

enum MediaPickerItem {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ?? [])

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,15 +122,20 @@
value = "disable"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "GUTENBERG_EDITOR_URL"
value = "http://localhost:5173/"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
<AdditionalOption
key = "MallocStackLogging"
key = "PrefersMallocStackLoggingLite"
value = ""
isEnabled = "YES">
</AdditionalOption>
<AdditionalOption
key = "PrefersMallocStackLoggingLite"
key = "MallocStackLogging"
value = ""
isEnabled = "YES">
</AdditionalOption>
Expand Down