Skip to content

Commit 449e825

Browse files
committed
Add native block inserter
1 parent a252e96 commit 449e825

File tree

11 files changed

+269
-14
lines changed

11 files changed

+269
-14
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: "91abfb14058fa0ee750f2a643157b91fd540a2b9"),
5757
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20251101"),
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 true
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
}

WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,8 @@ extension CommentGutenbergEditorViewController: GutenbergKit.EditorViewControlle
104104
func editor(_ viewController: GutenbergKit.EditorViewController, didCloseModalDialog dialogType: String) {
105105
// Do nothing
106106
}
107+
108+
func editor(_ viewController: GutenbergKit.EditorViewController, didLogMessage message: String, level: GutenbergKit.LogLevel) {
109+
// Do nothing
110+
}
107111
}
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: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
9+
init(blog: Blog) {
10+
self.blog = blog
11+
}
12+
13+
func getActions(for parameters: MediaPickerParameters) -> [MediaPickerActionGroup] {
14+
// Create MediaPickerMenu with the configuration
15+
let menu = MediaPickerMenu(
16+
filter: convertFilter(parameters.filter),
17+
isMultipleSelectionEnabled: parameters.isMultipleSelectionEnabled
18+
)
19+
20+
// Create a temporary controller just to extract action metadata
21+
let tempController = MediaPickerMenuController()
22+
23+
// Define media sources with their identifiers
24+
let sources: [(source: MediaPickerSource, id: MediaPickerID)] = [
25+
(.playground, .imagePlayground),
26+
(.siteMedia(blog: blog), .siteMedia),
27+
(.photos, .applePhotos),
28+
(.freePhotos(blog: blog), .freePhotos),
29+
(.freeGIFs(blog: blog), .freeGIFs)
30+
]
31+
32+
// Create actions from enabled sources
33+
let actionsWithGroups = sources.compactMap { source, id -> (action: MediaPickerAction, group: Int)? in
34+
guard source.isEnabled else { return nil }
35+
36+
let uiAction = createUIAction(for: source, menu: menu, controller: tempController)
37+
guard let uiAction else { return nil }
38+
39+
let action = MediaPickerAction(
40+
id: id.rawValue,
41+
title: uiAction.title,
42+
image: uiAction.image ?? UIImage()
43+
)
44+
45+
// Group 0: playground, site media, files
46+
// Group 1: free photos, free gifs
47+
let group = (id == .freePhotos || id == .freeGIFs) ? 1 : 0
48+
49+
return (action, group)
50+
}
51+
52+
// Group actions
53+
let firstGroup = actionsWithGroups.filter { $0.group == 0 }.map { $0.action }
54+
let secondGroup = actionsWithGroups.filter { $0.group == 1 }.map { $0.action }
55+
56+
let groups = [
57+
MediaPickerActionGroup(id: "primary", actions: firstGroup),
58+
MediaPickerActionGroup(id: "secondary", actions: secondGroup)
59+
].filter { !$0.actions.isEmpty }
60+
61+
return groups
62+
}
63+
64+
func perform(_ action: MediaPickerAction, parameters: MediaPickerParameters, from presentingViewController: UIViewController) async -> [MediaInfo] {
65+
// Find the source for this action
66+
guard let pickerID = MediaPickerID(rawValue: action.id) else {
67+
return []
68+
}
69+
70+
let source = getSource(for: pickerID)
71+
guard source.isEnabled else {
72+
return []
73+
}
74+
75+
// Create menu and controller
76+
let menu = MediaPickerMenu(
77+
filter: convertFilter(parameters.filter),
78+
isMultipleSelectionEnabled: parameters.isMultipleSelectionEnabled
79+
)
80+
81+
let controller = MediaPickerMenuController()
82+
83+
// Use continuation to wait for the selection
84+
return await withCheckedContinuation { continuation in
85+
controller.onSelection = { [weak self] selection in
86+
guard let self else {
87+
continuation.resume(returning: [])
88+
return
89+
}
90+
let mediaInfos = self.convertSelectionToMediaInfo(selection)
91+
continuation.resume(returning: mediaInfos)
92+
}
93+
94+
// Create and perform the UIAction
95+
if let uiAction = createUIAction(for: source, menu: menu, controller: controller) {
96+
uiAction.performWithSender(nil, target: nil)
97+
} else {
98+
continuation.resume(returning: [])
99+
}
100+
}
101+
}
102+
103+
// MARK: - Private Methods
104+
105+
private func getSource(for id: MediaPickerID) -> MediaPickerSource {
106+
switch id {
107+
case .imagePlayground:
108+
return .playground
109+
case .siteMedia:
110+
return .siteMedia(blog: blog)
111+
case .applePhotos:
112+
return .photos
113+
case .freePhotos:
114+
return .freePhotos(blog: blog)
115+
case .freeGIFs:
116+
return .freeGIFs(blog: blog)
117+
}
118+
}
119+
120+
private func convertFilter(_ filter: MediaPickerParameters.MediaFilter?) -> MediaPickerMenu.MediaFilter? {
121+
guard let filter else { return nil }
122+
switch filter {
123+
case .images: return .images
124+
case .videos: return .videos
125+
case .all: return nil
126+
}
127+
}
128+
129+
private func createUIAction(for source: MediaPickerSource, menu: MediaPickerMenu, controller: MediaPickerMenuController) -> UIAction? {
130+
switch source {
131+
case .playground:
132+
return menu.makeImagePlaygroundAction(delegate: controller)
133+
case .siteMedia:
134+
return menu.makeSiteMediaAction(blog: blog, delegate: controller)
135+
case .photos:
136+
return menu.makePhotosAction(delegate: controller)
137+
case .freePhotos:
138+
return menu.makeStockPhotos(blog: blog, delegate: controller)
139+
case .freeGIFs:
140+
return menu.makeFreeGIFAction(blog: blog, delegate: controller)
141+
default:
142+
return nil
143+
}
144+
}
145+
146+
private func convertSelectionToMediaInfo(_ selection: MediaPickerSelection) -> [MediaInfo] {
147+
var mediaInfos: [MediaInfo] = []
148+
149+
for item in selection.items {
150+
switch item {
151+
case .media(let media):
152+
var metadata: [String: String] = [:]
153+
if let videopressGUID = media.videopressGUID {
154+
metadata["videopressGUID"] = videopressGUID
155+
}
156+
let mediaInfo = MediaInfo(
157+
id: media.mediaID?.int32Value,
158+
url: media.remoteURL,
159+
type: media.mediaTypeString,
160+
caption: media.caption,
161+
title: media.filename,
162+
alt: media.alt,
163+
metadata: metadata
164+
)
165+
mediaInfos.append(mediaInfo)
166+
167+
case .external(let asset):
168+
let mediaInfo = MediaInfo(
169+
id: nil,
170+
url: asset.largeURL.absoluteString,
171+
type: "image",
172+
caption: asset.caption,
173+
title: asset.name,
174+
alt: nil,
175+
metadata: [:]
176+
)
177+
mediaInfos.append(mediaInfo)
178+
179+
case .image, .pickerResult:
180+
// These would need to be uploaded first
181+
// For now, we skip them
182+
break
183+
}
184+
}
185+
186+
return mediaInfos
187+
}
188+
}

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+
}

0 commit comments

Comments
 (0)