Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import Foundation
import GutenbergKit
import Testing
import UIKit

@testable import WordPress
@testable import WordPressData

@MainActor
struct PostGBKEditorViewControllerTests {

@Test("presents the site media library for GutenbergKit requests")
func presentsSiteMediaLibrary() throws {
let context = ContextManager.forTesting().mainContext
let blog = BlogBuilder(context).build()
let viewController = PostGBKEditorViewController(
postId: nil,
postType: .post,
title: "",
content: "",
status: "draft",
blog: blog
)
let window = UIWindow()
window.rootViewController = viewController
window.makeKeyAndVisible()
viewController.loadViewIfNeeded()

let data = Data(
#"{"allowedTypes":["image"],"multiple":true,"value":[],"contextId":"test"}"#.utf8
)
let action = try JSONDecoder().decode(OpenMediaLibraryAction.self, from: data)

viewController.editor(
viewController.editorViewController,
didRequestMediaFromSiteMediaLibrary: action
)

let navigation = try #require(viewController.presentedViewController as? UINavigationController)
#expect(navigation.viewControllers.first is SiteMediaPickerViewController)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,19 +21,27 @@ struct GutenbergMediaType: OptionSet {
}

final class GutenbergMediaPickerHelper: NSObject {
private let post: AbstractPost
private let blog: Blog
private unowned let context: UIViewController

/// Media Library Data Source

var didPickMediaCallback: GutenbergMediaPickerHelperCallback?

init(context: UIViewController, post: AbstractPost) {
init(context: UIViewController, blog: Blog) {
self.context = context
self.post = post
self.blog = blog
}

func presetDevicePhotosPicker(filter: GutenbergMediaType, allowMultipleSelection: Bool, completion: @escaping GutenbergMediaPickerHelperCallback) {
convenience init(context: UIViewController, post: AbstractPost) {
self.init(context: context, blog: post.blog)
}

func presetDevicePhotosPicker(
filter: GutenbergMediaType,
allowMultipleSelection: Bool,
completion: @escaping GutenbergMediaPickerHelperCallback
) {
didPickMediaCallback = completion

var configuration = PHPickerConfiguration()
Expand All @@ -49,11 +57,21 @@ final class GutenbergMediaPickerHelper: NSObject {
context.present(picker, animated: true)
}

func presentSiteMediaPicker(filter: GutenbergMediaType, allowMultipleSelection: Bool, initialSelection: [Int] = [], completion: @escaping GutenbergMediaPickerHelperCallback) {
func presentSiteMediaPicker(
filter: GutenbergMediaType,
allowMultipleSelection: Bool,
initialSelection: [Int] = [],
completion: @escaping GutenbergMediaPickerHelperCallback
) {
didPickMediaCallback = completion
let initialMediaSelection = mapMediaIdsToMedia(initialSelection)
MediaPickerMenu(viewController: context, filter: .init(filter), isMultipleSelectionEnabled: allowMultipleSelection, initialSelection: initialMediaSelection)
.showSiteMediaPicker(blog: post.blog, delegate: self)
MediaPickerMenu(
viewController: context,
filter: .init(filter),
isMultipleSelectionEnabled: allowMultipleSelection,
initialSelection: initialMediaSelection
)
.showSiteMediaPicker(blog: blog, delegate: self)
}

private func mapMediaIdsToMedia(_ mediaIds: [Int]) -> [Media] {
Expand All @@ -66,12 +84,14 @@ final class GutenbergMediaPickerHelper: NSObject {
let fetchedMedia = try context.fetch(request) as? [Media] ?? []

// Create a dictionary for quick lookup
let mediaDict = Dictionary(uniqueKeysWithValues: fetchedMedia.compactMap { media -> (Int, Media)? in
if let mediaID = media.mediaID?.intValue {
return (mediaID, media)
let mediaDict = Dictionary(
uniqueKeysWithValues: fetchedMedia.compactMap { media -> (Int, Media)? in
if let mediaID = media.mediaID?.intValue {
return (mediaID, media)
}
return nil
}
return nil
})
)

// Map the original mediaIds to Media objects, preserving order
return mediaIds.compactMap { mediaDict[$0] }
Expand All @@ -80,17 +100,22 @@ final class GutenbergMediaPickerHelper: NSObject {
}
}

func presentCameraCaptureFullScreen(animated: Bool,
filter: GutenbergMediaType,
callback: @escaping GutenbergMediaPickerHelperCallback) {
func presentCameraCaptureFullScreen(
animated: Bool,
filter: GutenbergMediaType,
callback: @escaping GutenbergMediaPickerHelperCallback
) {
didPickMediaCallback = callback
MediaPickerMenu(viewController: context, filter: .init(filter))
.showCamera(delegate: self)
}
}

extension GutenbergMediaPickerHelper: ImagePickerControllerDelegate {
func imagePicker(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]) {
func imagePicker(
_ picker: UIImagePickerController,
didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey: Any]
) {
context.dismiss(animated: true) {
guard let mediaType = info[.mediaType] as? String else {
return
Expand All @@ -106,7 +131,7 @@ extension GutenbergMediaPickerHelper: ImagePickerControllerDelegate {
guard let videoURL = info[.mediaURL] as? URL else {
return
}
guard self.post.blog.canUploadVideo(from: videoURL) else {
guard self.blog.canUploadVideo(from: videoURL) else {
self.presentVideoLimitExceededAfterCapture(on: self.context)
return
}
Expand All @@ -122,7 +147,10 @@ extension GutenbergMediaPickerHelper: ImagePickerControllerDelegate {
extension GutenbergMediaPickerHelper: VideoLimitsAlertPresenter {}

extension GutenbergMediaPickerHelper: SiteMediaPickerViewControllerDelegate {
func siteMediaPickerViewController(_ viewController: SiteMediaPickerViewController, didFinishWithSelection selection: [Media]) {
func siteMediaPickerViewController(
_ viewController: SiteMediaPickerViewController,
didFinishWithSelection selection: [Media]
) {
context.dismiss(animated: true)
didPickMediaCallback?(selection)
didPickMediaCallback = nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi

let navigationBarManager: PostEditorNavigationBarManager

lazy var mediaPickerHelper: GutenbergMediaPickerHelper = {
GutenbergMediaPickerHelper(context: self, post: post)
}()

lazy var featuredImageHelper = NewGutenbergFeaturedImageHelper(post: post)

// MARK: - PostEditor
Expand Down Expand Up @@ -285,96 +281,13 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi
self.featuredImageHelper.setFeaturedImage(mediaID: mediaID)
}

// MARK: - Media Picker Helpers

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

let initialSelectionArray: [Int]
switch config.value {
case .single(let id):
initialSelectionArray = [id]
case .multiple(let ids):
initialSelectionArray = ids
case .none:
initialSelectionArray = []
}

mediaPickerHelper.presentSiteMediaPicker(
filter: flags,
allowMultipleSelection: config.multiple,
initialSelection: initialSelectionArray
) { [weak self] assets in
guard let self, let media = assets as? [Media], !media.isEmpty else {
return
}
let mediaInfos = media.map { item in
var metadata: [String: String] = [:]
if let videopressGUID = item.videopressGUID {
metadata["videopressGUID"] = videopressGUID
}
return MediaInfo(
id: item.mediaID?.int32Value,
url: item.remoteURL,
type: item.mediaTypeString,
caption: item.caption,
title: item.filename,
alt: item.alt,
metadata: [:]
)
}
if let jsonString = convertMediaInfoArrayToJSONString(mediaInfos) {
// Escape the string for JavaScript
let escapedJsonString = jsonString.replacingOccurrences(of: "'", with: "\\'")
editorViewController.setMediaUploadAttachment(escapedJsonString)
}
}
}

override func editorDidRequestLatestContent(
_ controller: GutenbergKit.EditorViewController
) -> (title: String, content: String)? {
// Return the current post title and content from Core Data.
// This is the authoritative source, updated via autosave.
(post.postTitle ?? "", post.content ?? "")
}

private func convertMediaInfoArrayToJSONString(_ mediaInfoArray: [MediaInfo]) -> String? {
do {
let jsonData = try JSONEncoder().encode(mediaInfoArray)
if let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
} catch {
DDLogError("Error encoding MediaInfo array: \(error)")
}
return nil
}

private func mediaFilterFlags(using filterArray: [OpenMediaLibraryAction.MediaType]) -> GutenbergMediaType {
var mediaType: Int = 0
for filter in filterArray {
switch filter {
case .image:
mediaType = mediaType | GutenbergMediaType.image.rawValue
case .video:
mediaType = mediaType | GutenbergMediaType.video.rawValue
case .audio:
mediaType = mediaType | GutenbergMediaType.audio.rawValue
case .other:
mediaType = mediaType | GutenbergMediaType.other.rawValue
case .any:
mediaType = mediaType | GutenbergMediaType.all.rawValue
@unknown default:
fatalError()
}
}

return GutenbergMediaType(rawValue: mediaType)
}
}

// MARK: - PostEditorNavigationBarManagerDelegate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import UIKit
import WebKit
import SafariServices
import GutenbergKit
import WordPressData
import WordPressShared
import WordPressUI

Expand All @@ -13,6 +14,8 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont
/* private */ let editorViewController: GutenbergKit.EditorViewController
private let status: String // TODO: Can be deleted?

private lazy var mediaPickerHelper = GutenbergMediaPickerHelper(context: self, blog: blog)

private var keyboardShowObserver: Any?
private var keyboardHideObserver: Any?
private var keyboardFrame = CGRect.zero
Expand Down Expand Up @@ -200,7 +203,47 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont
_ viewController: GutenbergKit.EditorViewController,
didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction
) {
// Do nothing
let flags = mediaFilterFlags(using: config.allowedTypes ?? [])

let initialSelectionArray: [Int]
switch config.value {
case .single(let id):
initialSelectionArray = [id]
case .multiple(let ids):
initialSelectionArray = ids
case .none:
initialSelectionArray = []
}

mediaPickerHelper.presentSiteMediaPicker(
filter: flags,
allowMultipleSelection: config.multiple,
initialSelection: initialSelectionArray
) { [weak self] assets in
guard let self, let media = assets as? [Media], !media.isEmpty else {
return
}
let mediaInfos = media.map { item in
var metadata: [String: String] = [:]
if let videopressGUID = item.videopressGUID {
metadata["videopressGUID"] = videopressGUID
}
return MediaInfo(
id: item.mediaID?.int32Value,
url: item.remoteURL,
type: item.mediaTypeString,
caption: item.caption,
title: item.filename,
alt: item.alt,
metadata: [:]

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Noting a pre-existing issue finding by Claude that we might address in a follow-up PR:

Is passing metadata: [:] here intentional? The metadata dict built just above with videopressGUID never gets used, so the GUID is dropped before it reaches the web editor — MediaInfo.metadata is an encoded field. No compiler warning since it is mutated inside the if.

Moved verbatim, so pre-existing — but worth flagging since this path now serves custom post types too. Looks like a one-word fix (metadata: metadata) if you want it here rather than a follow-up.

)
}
if let jsonString = convertMediaInfoArrayToJSONString(mediaInfos) {
// Escape the string for JavaScript
let escapedJsonString = jsonString.replacingOccurrences(of: "'", with: "\\'")
editorViewController.setMediaUploadAttachment(escapedJsonString)
}
}
}

func editor(_ viewController: GutenbergKit.EditorViewController, didTriggerAutocompleter type: String) {
Expand Down Expand Up @@ -244,6 +287,40 @@ class PostGBKEditorViewController: UIViewController, GutenbergKit.EditorViewCont
// Do nothing
nil
}

private func convertMediaInfoArrayToJSONString(_ mediaInfoArray: [MediaInfo]) -> String? {
do {
let jsonData = try JSONEncoder().encode(mediaInfoArray)
if let jsonString = String(data: jsonData, encoding: .utf8) {
return jsonString
}
} catch {
DDLogError("Error encoding MediaInfo array: \(error)")
}
return nil
}

private func mediaFilterFlags(using filterArray: [OpenMediaLibraryAction.MediaType]) -> GutenbergMediaType {
var mediaType: Int = 0
for filter in filterArray {
switch filter {
case .image:
mediaType = mediaType | GutenbergMediaType.image.rawValue
case .video:
mediaType = mediaType | GutenbergMediaType.video.rawValue
case .audio:
mediaType = mediaType | GutenbergMediaType.audio.rawValue
case .other:
mediaType = mediaType | GutenbergMediaType.other.rawValue
case .any:
mediaType = mediaType | GutenbergMediaType.all.rawValue
@unknown default:
fatalError()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Noting a pre-existing issue finding by Claude that we might address in a follow-up PR:

Could this fatalError() be a softer fallback? Keeping the @unknown default makes sense — it's required for a non-frozen enum from GutenbergKit, and it's what stops a new library case from being a breaking change. It's just that trapping in the body turns that safety net into a crash: a dependency bump alone arms it, and the next media-library request for the new type takes the app down mid-edit with unsaved content. It'd hit both editors now that this lives in the base class.

break looks sufficient — contributing no flag leaves an empty GutenbergMediaType, and both MediaPickerMenu.MediaFilter.init? and PHPickerFilter.init? already return nil for anything that isn't exactly .image/.video, which MediaPickerMenu treats as "no filter." So it degrades to an unfiltered picker rather than an empty one. (.all returns nil there too, so it'd behave identically — break just reads more honestly.) Fine as a follow-up if you'd rather keep this diff a pure refactor.

}
}

return GutenbergMediaType(rawValue: mediaType)
}
}

private extension PostGBKEditorViewController {
Expand Down