Skip to content
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
14 changes: 11 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 @@ -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: "e40ca1827fbc07ca3d173ae219a5e7c251116964"),
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20251007"),
.package(
url: "https://github.com/Automattic/color-studio",
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 @@ -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.
///
Expand Down Expand Up @@ -86,6 +87,8 @@ public enum FeatureFlag: Int, CaseIterable {
return (languageCode ?? "en").hasPrefix("en")
case .newSupport:
return false
case .nativeBlockInserter:
return BuildConfiguration.current == .debug
}
}

Expand Down Expand Up @@ -130,6 +133,7 @@ extension FeatureFlag {
case .mediaQuotaView: "Media Quota"
case .intelligence: "Intelligence"
case .newSupport: "New Support"
case .nativeBlockInserter: "Native Block Inserter"
}
}
}
Expand Down
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 @@ -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)
Expand All @@ -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)
}
}
}
Expand All @@ -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)
}
}
}
Expand All @@ -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)
}
}
}
Expand All @@ -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")
}
Expand All @@ -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)
}
}
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,13 +1,15 @@
import UIKit
import WordPressUI
import AsyncImageKit
import BuildSettingsKit
import AutomatticTracks
import GutenbergKit
import SafariServices
import WordPressData
import WordPressShared
import WebKit
import CocoaLumberjackSwift
import Photos

class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor {

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

self.editorViewController.updateConfiguration(updatedConfiguration)
Expand Down Expand Up @@ -569,6 +572,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,6 +122,11 @@
value = "disable"
isEnabled = "NO">
</EnvironmentVariable>
<EnvironmentVariable
key = "GUTENBERG_EDITOR_URL"
value = "http://localhost:5173/"
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
<AdditionalOption
Expand Down