Skip to content

Commit b435245

Browse files
committed
Add native block inserter
1 parent 81d1c36 commit b435245

File tree

10 files changed

+218
-11
lines changed

10 files changed

+218
-11
lines changed

Modules/Package.resolved

Lines changed: 11 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Modules/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ let package = Package(
5353
.package(url: "https://github.com/wordpress-mobile/NSURL-IDN", revision: "b34794c9a3f32312e1593d4a3d120572afa0d010"),
5454
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
5555
// We can't use wordpress-rs branches nor commits here. Only tags work.
56-
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.9.0"),
56+
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "e40ca1827fbc07ca3d173ae219a5e7c251116964"),
5757
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20251007"),
5858
.package(
5959
url: "https://github.com/Automattic/color-studio",

WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ public enum FeatureFlag: Int, CaseIterable {
2727
case mediaQuotaView
2828
case intelligence
2929
case newSupport
30+
case nativeBlockInserter
3031

3132
/// Returns a boolean indicating if the feature is enabled.
3233
///
@@ -86,6 +87,8 @@ public enum FeatureFlag: Int, CaseIterable {
8687
return (languageCode ?? "en").hasPrefix("en")
8788
case .newSupport:
8889
return false
90+
case .nativeBlockInserter:
91+
return BuildConfiguration.current == .debug
8992
}
9093
}
9194

@@ -130,6 +133,7 @@ extension FeatureFlag {
130133
case .mediaQuotaView: "Media Quota"
131134
case .intelligence: "Intelligence"
132135
case .newSupport: "New Support"
136+
case .nativeBlockInserter: "Native Block Inserter"
133137
}
134138
}
135139
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import UIKit
2+
import GutenbergKit
3+
4+
extension MediaPickerMenu.MediaFilter {
5+
init?(_ filter: GutenbergKit.MediaPickerParameters.MediaFilter) {
6+
switch filter {
7+
case .images: self = .images
8+
case .videos: self = .videos
9+
case .all: return nil
10+
}
11+
}
12+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import UIKit
2+
import GutenbergKit
3+
import WordPressData
4+
5+
/// A adapter for GutenbergKit that manages media picker sources the editor.
6+
final class MediaPickerController: GutenbergKit.MediaPickerController {
7+
private let blog: Blog
8+
private let parameters: MediaPickerParameters
9+
private var currentMediaPickerController: MediaPickerMenuController?
10+
private var currentMediaPickerCompletion: (([MediaInfo]) -> Void)?
11+
12+
init(blog: Blog, parameters: MediaPickerParameters) {
13+
self.blog = blog
14+
self.parameters = parameters
15+
}
16+
17+
var actions: [[MediaPickerAction]] {
18+
// Create MediaPickerMenu with the configuration
19+
let menu = MediaPickerMenu(
20+
filter: convertFilter(parameters.filter),
21+
isMultipleSelectionEnabled: parameters.isMultipleSelectionEnabled
22+
)
23+
24+
// Create a controller to handle selections
25+
let controller = MediaPickerMenuController()
26+
controller.onSelection = { [weak self] selection in
27+
guard let self else { return }
28+
let mediaInfos = self.convertSelectionToMediaInfo(selection)
29+
self.currentMediaPickerCompletion?(mediaInfos)
30+
self.currentMediaPickerCompletion = nil
31+
self.currentMediaPickerController = nil
32+
}
33+
34+
// Store the controller to keep it alive
35+
currentMediaPickerController = controller
36+
37+
// Define media sources with their identifiers
38+
let sources: [(source: MediaPickerSource, id: MediaPickerID)] = [
39+
(.playground, .imagePlayground),
40+
(.siteMedia(blog: blog), .siteMedia),
41+
(.photos, .applePhotos),
42+
(.freePhotos(blog: blog), .freePhotos),
43+
(.freeGIFs(blog: blog), .freeGIFs)
44+
]
45+
46+
// Create actions from enabled sources
47+
let actionsWithGroups = sources.compactMap { source, id -> (action: MediaPickerAction, group: Int)? in
48+
guard source.isEnabled else { return nil }
49+
50+
let uiAction = createUIAction(for: source, menu: menu, controller: controller)
51+
guard let uiAction else { return nil }
52+
53+
let action = convertToMediaPickerAction(uiAction, id: id)
54+
55+
// Group 0: playground, site media, files
56+
// Group 1: free photos, free gifs
57+
let group = (id == .freePhotos || id == .freeGIFs) ? 1 : 0
58+
59+
return (action, group)
60+
}
61+
62+
// Group actions
63+
let firstGroup = actionsWithGroups.filter { $0.group == 0 }.map { $0.action }
64+
let secondGroup = actionsWithGroups.filter { $0.group == 1 }.map { $0.action }
65+
66+
return [firstGroup, secondGroup].filter { !$0.isEmpty }
67+
}
68+
69+
// MARK: - Private Methods
70+
71+
private func convertFilter(_ filter: MediaPickerParameters.MediaFilter?) -> MediaPickerMenu.MediaFilter? {
72+
guard let filter else { return nil }
73+
switch filter {
74+
case .images: return .images
75+
case .videos: return .videos
76+
case .all: return nil
77+
}
78+
}
79+
80+
private func createUIAction(for source: MediaPickerSource, menu: MediaPickerMenu, controller: MediaPickerMenuController) -> UIAction? {
81+
switch source {
82+
case .playground:
83+
return menu.makeImagePlaygroundAction(delegate: controller)
84+
case .siteMedia:
85+
return menu.makeSiteMediaAction(blog: blog, delegate: controller)
86+
case .photos:
87+
return menu.makePhotosAction(delegate: controller)
88+
case .freePhotos:
89+
return menu.makeStockPhotos(blog: blog, delegate: controller)
90+
case .freeGIFs:
91+
return menu.makeFreeGIFAction(blog: blog, delegate: controller)
92+
default:
93+
return nil
94+
}
95+
}
96+
97+
private func convertToMediaPickerAction(_ uiAction: UIAction, id: MediaPickerID) -> MediaPickerAction {
98+
MediaPickerAction(
99+
id: id.rawValue,
100+
title: uiAction.title,
101+
image: uiAction.image ?? UIImage(),
102+
perform: { [weak self] presentingViewController, completion in
103+
guard let self else {
104+
completion([])
105+
return
106+
}
107+
108+
// Store the completion handler for when selection is made
109+
self.currentMediaPickerCompletion = completion
110+
111+
// Perform the original action
112+
uiAction.performWithSender(nil, target: nil)
113+
}
114+
)
115+
}
116+
117+
private func convertSelectionToMediaInfo(_ selection: MediaPickerSelection) -> [MediaInfo] {
118+
var mediaInfos: [MediaInfo] = []
119+
120+
for item in selection.items {
121+
switch item {
122+
case .media(let media):
123+
var metadata: [String: String] = [:]
124+
if let videopressGUID = media.videopressGUID {
125+
metadata["videopressGUID"] = videopressGUID
126+
}
127+
let mediaInfo = MediaInfo(
128+
id: media.mediaID?.int32Value,
129+
url: media.remoteURL,
130+
type: media.mediaTypeString,
131+
caption: media.caption,
132+
title: media.filename,
133+
alt: media.alt,
134+
metadata: metadata
135+
)
136+
mediaInfos.append(mediaInfo)
137+
138+
case .external(let asset):
139+
let mediaInfo = MediaInfo(
140+
id: nil,
141+
url: asset.largeURL.absoluteString,
142+
type: "image",
143+
caption: asset.caption,
144+
title: asset.name,
145+
alt: nil,
146+
metadata: [:]
147+
)
148+
mediaInfos.append(mediaInfo)
149+
150+
case .image, .pickerResult:
151+
// These would need to be uploaded first
152+
// For now, we skip them
153+
break
154+
}
155+
}
156+
157+
return mediaInfos
158+
}
159+
}

WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import WordPressData
66
final class MediaPickerMenuController: NSObject {
77
var onSelection: ((MediaPickerSelection) -> Void)?
88

9-
fileprivate func didSelect(_ items: [MediaPickerItem], source: String) {
9+
fileprivate func didSelect(_ items: [MediaPickerItem], source: MediaPickerID) {
1010
let selection = MediaPickerSelection(items: items, source: source)
1111
DispatchQueue.main.async {
1212
self.onSelection?(selection)
@@ -18,7 +18,7 @@ extension MediaPickerMenuController: PHPickerViewControllerDelegate {
1818
public func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
1919
picker.presentingViewController?.dismiss(animated: true)
2020
if !results.isEmpty {
21-
self.didSelect(results.map(MediaPickerItem.pickerResult), source: "apple_photos")
21+
self.didSelect(results.map(MediaPickerItem.pickerResult), source: .applePhotos)
2222
}
2323
}
2424
}
@@ -27,7 +27,7 @@ extension MediaPickerMenuController: ImagePickerControllerDelegate {
2727
func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
2828
picker.presentingViewController?.dismiss(animated: true)
2929
if let image = info[.originalImage] as? UIImage {
30-
self.didSelect([.image(image)], source: "camera")
30+
self.didSelect([.image(image)], source: .camera)
3131
}
3232
}
3333
}
@@ -36,7 +36,7 @@ extension MediaPickerMenuController: SiteMediaPickerViewControllerDelegate {
3636
func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) {
3737
viewController.presentingViewController?.dismiss(animated: true)
3838
if !selection.isEmpty {
39-
self.didSelect(selection.map(MediaPickerItem.media), source: "site_media")
39+
self.didSelect(selection.map(MediaPickerItem.media), source: .siteMedia)
4040
}
4141
}
4242
}
@@ -46,7 +46,7 @@ extension MediaPickerMenuController: ImagePlaygroundPickerDelegate {
4646

4747
viewController.presentingViewController?.dismiss(animated: true)
4848
if let data = try? Data(contentsOf: imageURL), let image = UIImage(data: data) {
49-
self.didSelect([.image(image)], source: "image_playground")
49+
self.didSelect([.image(image)], source: .imagePlayground)
5050
} else {
5151
wpAssertionFailure("failed to read the image created by ImagePlayground")
5252
}
@@ -57,7 +57,7 @@ extension MediaPickerMenuController: ExternalMediaPickerViewDelegate {
5757
func externalMediaPickerViewController(_ viewController: ExternalMediaPickerViewController, didFinishWithSelection selection: [ExternalMediaAsset]) {
5858
viewController.presentingViewController?.dismiss(animated: true)
5959
if !selection.isEmpty {
60-
let source = viewController.source == .tenor ? "free_gifs" : "free_photos"
60+
let source: MediaPickerID = viewController.source == .tenor ? .freeGIFs : .freePhotos
6161
self.didSelect(selection.map(MediaPickerItem.external), source: source)
6262
}
6363
}

WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ enum MediaPickerSource {
101101

102102
struct MediaPickerSelection {
103103
var items: [MediaPickerItem]
104-
var source: String
104+
var source: MediaPickerID
105105
}
106106

107107
enum MediaPickerItem {

WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,3 +53,12 @@ extension MediaPickerMenu.MediaFilter {
5353
}
5454
}
5555
}
56+
57+
enum MediaPickerID: String {
58+
case applePhotos = "apple_photos"
59+
case camera = "camera"
60+
case siteMedia = "site_media"
61+
case imagePlayground = "image_playground"
62+
case freeGIFs = "free_gifs"
63+
case freePhotos = "free_photos"
64+
}

WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import UIKit
22
import WordPressUI
33
import AsyncImageKit
4+
import BuildSettingsKit
45
import AutomatticTracks
56
import GutenbergKit
67
import SafariServices
78
import WordPressData
89
import WordPressShared
910
import WebKit
1011
import CocoaLumberjackSwift
12+
import Photos
1113

1214
class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor {
1315

@@ -397,6 +399,7 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
397399
.apply(settings) { $0.setEditorSettings($1) }
398400
.setTitle(post.postTitle ?? "")
399401
.setContent(post.content ?? "")
402+
.setNativeInserterEnabled(FeatureFlag.nativeBlockInserter)
400403
.build()
401404

402405
self.editorViewController.updateConfiguration(updatedConfiguration)
@@ -569,6 +572,13 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate
569572
throw URLError(.unknown)
570573
}
571574

575+
/// Returns the available media picker sources for the given configuration
576+
func getMediaPickerController(for viewController: GutenbergKit.EditorViewController, parameters: GutenbergKit.MediaPickerParameters) -> (any GutenbergKit.MediaPickerController)? {
577+
MediaPickerController(blog: post.blog, parameters: parameters)
578+
}
579+
580+
// MARK: - Media Picker Helpers
581+
572582
func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) {
573583
let flags = mediaFilterFlags(using: config.allowedTypes ?? [])
574584

WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
value = "disable"
123123
isEnabled = "NO">
124124
</EnvironmentVariable>
125+
<EnvironmentVariable
126+
key = "GUTENBERG_EDITOR_URL"
127+
value = "http://localhost:5173/"
128+
isEnabled = "NO">
129+
</EnvironmentVariable>
125130
</EnvironmentVariables>
126131
<AdditionalOptions>
127132
<AdditionalOption

0 commit comments

Comments
 (0)