Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
6 changes: 3 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.10.0-alpha.0"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.10.0"),
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-20251101"),
.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 false
}
}

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,173 @@
import UIKit
import GutenbergKit
import WordPressData
import WordPressShared

/// A adapter for GutenbergKit that manages media picker sources the editor.
final class MediaPickerController: GutenbergKit.MediaPickerController {
private let blog: Blog

init(blog: Blog) {
self.blog = blog
}

func getActions(for parameters: MediaPickerParameters) -> [MediaPickerActionGroup] {
let menu = MediaPickerMenu(
filter: convertFilter(parameters.filter),
isMultipleSelectionEnabled: parameters.isMultipleSelectionEnabled
)

// Create a temporary controller just to extract action metadata
let tempController = MediaPickerMenuController()

// Define media sources with their identifiers
let sources: [(source: MediaPickerSource, id: MediaPickerID)] = [
(.siteMedia(blog: blog), .siteMedia),
(.freePhotos(blog: blog), .freePhotos),
(.freeGIFs(blog: blog), .freeGIFs)
]

// Create actions from enabled sources
let actions = sources.compactMap { source, id -> MediaPickerAction? in
guard source.isEnabled else { return nil }

let uiAction = createUIAction(for: source, menu: menu, controller: tempController)
guard let uiAction else { return nil }

return MediaPickerAction(
id: id.rawValue,
title: uiAction.title,
image: uiAction.image ?? UIImage()
)
}

return [MediaPickerActionGroup(id: "primary", actions: actions)]
.filter { !$0.actions.isEmpty }
}

func perform(_ action: MediaPickerAction, parameters: MediaPickerParameters, from presentingViewController: UIViewController) async -> [MediaInfo] {
// Find the source for this action
guard let pickerID = MediaPickerID(rawValue: action.id) else {
return []
}

let source = getSource(for: pickerID)
guard source.isEnabled else {
return []
}

// Create menu and controller
let menu = MediaPickerMenu(
filter: convertFilter(parameters.filter),
isMultipleSelectionEnabled: parameters.isMultipleSelectionEnabled
)

let controller = MediaPickerMenuController()

// Use continuation to wait for the selection
return await withCheckedContinuation { continuation in
controller.onSelection = { [weak self] selection in
guard let self else {
continuation.resume(returning: [])
return
}
let mediaInfos = self.convertSelectionToMediaInfo(selection)
continuation.resume(returning: mediaInfos)
}

// Create and perform the UIAction
if let uiAction = createUIAction(for: source, menu: menu, controller: controller) {
MainActor.assumeIsolated {
uiAction.performWithSender(nil, target: nil)
}
} else {
continuation.resume(returning: [])
}
}
}

// MARK: - Private Methods

private func getSource(for id: MediaPickerID) -> MediaPickerSource {
switch id {
case .imagePlayground: .playground
case .siteMedia: .siteMedia(blog: blog)
case .applePhotos: .photos
case .freePhotos: .freePhotos(blog: blog)
case .freeGIFs: .freeGIFs(blog: blog)
default: fatalError("Unsupported: \(id)")
}
}

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: menu.makeImagePlaygroundAction(delegate: controller)
case .siteMedia: menu.makeSiteMediaAction(blog: blog, delegate: controller)
case .photos: menu.makePhotosAction(delegate: controller)
case .freePhotos: menu.makeStockPhotos(blog: blog, delegate: controller)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the media providers succeeded in my testing accept for this "Free photos" provider. When selecting an image, the request always resulted in a 500 response.

I copied each request as a curl command from Proxyman. When comparing the commands, I noted that the GIF provider included the .gif file ending; contrastingly, the "Free photos" provider lacked a file ending. When I manually added a .jpg file ending, the request succeeded.

Do you experience this? What might causing these 500 responses?

case .freeGIFs: menu.makeFreeGIFAction(blog: blog, delegate: controller)
default: nil
}
}

private func convertSelectionToMediaInfo(_ selection: MediaPickerSelection) -> [MediaInfo] {
var output: [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.mimeType,
caption: media.caption,
title: media.filename,
alt: media.alt,
metadata: metadata
)
output.append(mediaInfo)

case .external(let asset):
let mediaInfo = MediaInfo(
id: nil,
url: asset.largeURL.absoluteString,
type: asset.largeURL.preferredMimeType,
caption: asset.caption,
title: asset.name,
alt: nil,
metadata: [:]
)
output.append(mediaInfo)

case .image, .pickerResult:
wpAssertionFailure("unused case")
break
}
}

return output
}
}

private extension URL {
var preferredMimeType: String {
if let mimeType = UTType(filenameExtension: pathExtension)?.preferredMIMEType {
return mimeType
} else {
return "application/octet-stream"
}
}
}
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 @@ -180,8 +182,13 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
self.editorSession = PostEditorAnalyticsSession(editor: .gutenbergKit, post: post)
self.navigationBarManager = navigationBarManager ?? PostEditorNavigationBarManager()

EditorLocalization.localize = getLocalizedString

let editorConfiguration = EditorConfiguration(blog: post.blog)
self.editorViewController = GutenbergKit.EditorViewController(configuration: editorConfiguration)
self.editorViewController = GutenbergKit.EditorViewController(
configuration: editorConfiguration,
mediaPicker: MediaPickerController(blog: post.blog)
)

self.blockEditorSettingsService = RawBlockEditorSettingsService(blog: post.blog)

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

self.editorViewController.updateConfiguration(updatedConfiguration)
Expand Down Expand Up @@ -569,6 +577,8 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate
// Do nothing
}

// MARK: - Media Picker Helpers

func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) {
let flags = mediaFilterFlags(using: config.allowedTypes ?? [])

Expand Down Expand Up @@ -1099,3 +1109,18 @@ private extension NewGutenbergViewController {
// Extend Gutenberg JavaScript exception struct to conform the protocol defined in the Crash Logging service
extension GutenbergJSException.StacktraceLine: @retroactive AutomatticTracks.JSStacktraceLine {}
extension GutenbergJSException: @retroactive AutomatticTracks.JSException {}

private func getLocalizedString(for value: GutenbergKit.EditorLocalizableString) -> String {
switch value {
case .showMore: NSLocalizedString("editor.blockInserter.showMore", value: "Show More", comment: "Button title to expand and show more blocks")
case .showLess: NSLocalizedString("editor.blockInserter.showLess", value: "Show Less", comment: "Button title to collapse and show fewer blocks")
case .search: NSLocalizedString("editor.blockInserter.search", value: "Search", comment: "Placeholder text for block search field")
case .insertBlock: NSLocalizedString("editor.blockInserter.insertBlock", value: "Insert Block", comment: "Context menu action to insert a block")
case .failedToInsertMedia: NSLocalizedString("editor.media.failedToInsert", value: "Failed to insert media", comment: "Error message when media insertion fails")
case .patterns: NSLocalizedString("editor.patterns.title", value: "Patterns", comment: "Navigation title for patterns view")
case .noPatternsFound: NSLocalizedString("editor.patterns.noPatternsFound", value: "No Patterns Found", comment: "Title shown when no patterns match the search")
case .insertPattern: NSLocalizedString("editor.patterns.insertPattern", value: "Insert Pattern", comment: "Context menu action to insert a pattern")
case .patternsCategoryUncategorized: NSLocalizedString("editor.patterns.uncategorized", value: "Uncategorized", comment: "Category name for patterns without a category")
case .patternsCategoryAll: NSLocalizedString("editor.patterns.all", value: "All", comment: "Category name for section showing all patterns")
}
}
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