Skip to content

Commit

Permalink
feat: GutenbergKit Media Library support (#23721)
Browse files Browse the repository at this point in the history
* wip: GutenbergKit Media Library support

* fix: JSON encode media array and pass as string to WebView

* fix: Prevent global callback collisions

A single Media Library callback resulted in erroneously mutating
unselected blocks.

* refactor: Remove Media Library UUID usage

Unable to recreate the original erroneously replacement of media
attachments for unselected blocks. This now feels unnecessary.

* style: Remove unnecessary white space

Address lint warning.

* feat: Media Library preserves initial selection

Enable retaining selection for gallery addition/editing in the block
editor.

* refactor: Remove unnecessary preserveSelection parameter

Reusing the `initialSelection` reduces complexity.

* fix: Reinstate commented selection clearing code

This no longer disrupts initial selection as the setting the initial
selection now occurs within `setEditing` rather than initialization.

* refactor: Remove unnecessary application of initial selection

This appears to be unnecessary with the latest implementation.

* refactor: Remove unused `preserveSelection` parameter

* fix: Canceling media selection returns initial selection

Without returning the initial selection, canceling cleared out media
attached to Gallery blocks in the web-based editor, as it presumes the
existing media will be returned as "selected."

* fix: Discard initial selection for single select

Workaround existing logic that dismisses the dialog if `allowMuliple` is
true and selection is not empty. The existing logic allows for quickly
selecting a single media item, but inhibits the ability to showcase a
pre-existing selection for single-select contexts. This diverges from
the web experience, but is the easiest path forward to avoid a
significant refactor of the logic that would impact all other existing
use cases--Gutenberg Mobile, Aztec, etc.

* fix: Ensure correct media attachment sorting

Map the Media lookup results to the original array of media IDs so that
the sort order is preserved.

* build: Update GutenbergKit ref

* build: Update GutenbergKit ref

* docs: Expand existing documentation to include new parameter

* refactor: Update bridge method name to mirror GutenbergKit

* refactor: Rename `OpenMediaLibrary` to `OpenMediaLibraryAction`

Improve clarity.

* fix: Include VideoPress ID in metadata

* fix: Account for null `allowedTypes` values

The File block does not provide these value.

* fix: Assert main thread for `mapMediaIdsToMedia`

The returned `Media` entities are not thread-safe.

* build: Update GutenbergKit ref

* build: Update GutenbergKit ref
  • Loading branch information
dcalhoun authored Nov 12, 2024
1 parent 9f88062 commit 5b3871e
Show file tree
Hide file tree
Showing 7 changed files with 128 additions and 11 deletions.
2 changes: 1 addition & 1 deletion Modules/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ let package = Package(
.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/Automattic/wordpress-rs", revision: "alpha-swift-20240813"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "849118af582068f75807bc0f1265edeee4bf1b5e"),
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "6cc307e7fc24910697be5f71b7d70f465a9c0f63"),
.package(url: "https://github.com/Automattic/color-studio", branch: "trunk"),
],
targets: XcodeSupport.targets + [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/wordpress-mobile/GutenbergKit",
"state" : {
"revision" : "849118af582068f75807bc0f1265edeee4bf1b5e"
"revision" : "6cc307e7fc24910697be5f71b7d70f465a9c0f63"
}
},
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,37 @@ final class GutenbergMediaPickerHelper: NSObject {
context.present(picker, animated: true)
}

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

private func mapMediaIdsToMedia(_ mediaIds: [Int]) -> [Media] {
assert(Thread.isMainThread, "mapMediaIdsToMedia should only be called on the main thread")
let context = ContextManager.shared.mainContext
let request = NSFetchRequest<NSManagedObject>(entityName: "Media")
request.predicate = NSPredicate(format: "mediaID IN %@", mediaIds.map { NSNumber(value: $0) })

do {
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)
}
return nil
})

// Map the original mediaIds to Media objects, preserving order
return mediaIds.compactMap { mediaDict[$0] }
} catch {
return []
}
}

func presentCameraCaptureFullScreen(animated: Bool,
filter: WPMediaType,
callback: @escaping GutenbergMediaPickerHelperCallback) {
Expand Down
9 changes: 7 additions & 2 deletions WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct MediaPickerMenu {
weak var presentingViewController: UIViewController?
var filter: MediaFilter?
var isMultipleSelectionEnabled: Bool
var initialSelection: [Media]

enum MediaFilter {
case images
Expand All @@ -21,12 +22,15 @@ struct MediaPickerMenu {
/// - viewController: The view controller to use for presentation.
/// - filter: By default, `nil` – allow all content types.
/// - isMultipleSelectionEnabled: By default, `false`.
/// - initialSelection: By default, `[]`.
init(viewController: UIViewController,
filter: MediaFilter? = nil,
isMultipleSelectionEnabled: Bool = false) {
isMultipleSelectionEnabled: Bool = false,
initialSelection: [Media] = []) {
self.presentingViewController = viewController
self.filter = filter
self.isMultipleSelectionEnabled = isMultipleSelectionEnabled
self.initialSelection = initialSelection
}
}

Expand Down Expand Up @@ -185,7 +189,8 @@ extension MediaPickerMenu {
let viewController = SiteMediaPickerViewController(
blog: blog,
filter: filter.map { [$0.mediaType] },
allowsMultipleSelection: isMultipleSelectionEnabled
allowsMultipleSelection: isMultipleSelectionEnabled,
initialSelection: initialSelection
)
viewController.delegate = delegate
let navigation = UINavigationController(rootViewController: viewController)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult
fatalError("init(coder:) has not been implemented")
}

private func setInitialSelection(_ media: [Media]) {
updateSelection {
for item in media {
selection.add(item)
}
}
}

func embed(in parentViewController: UIViewController) {
parentViewController.addChild(self)
parentViewController.view.addSubview(view)
Expand Down Expand Up @@ -159,14 +167,19 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult
func setEditing(
_ isEditing: Bool,
allowsMultipleSelection: Bool = true,
isSelectionOrdered: Bool = false
isSelectionOrdered: Bool = false,
initialSelection: [Media]? = nil
) {
guard self.isEditing != isEditing else { return }
self.isEditing = isEditing
self.allowsMultipleSelection = allowsMultipleSelection
self.isSelectionOrdered = isSelectionOrdered

deselectAll()
if let selectedMedia = initialSelection, allowsMultipleSelection {
setInitialSelection(selectedMedia)
} else {
deselectAll()
}
}

private func updateSelection(_ perform: () -> Void) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ protocol SiteMediaPickerViewControllerDelegate: AnyObject {
final class SiteMediaPickerViewController: UIViewController, SiteMediaCollectionViewControllerDelegate {
private let blog: Blog
private let allowsMultipleSelection: Bool
private let initialSelection: [Media]

private let collectionViewController: SiteMediaCollectionViewController
private let toolbarItemTitle = SiteMediaSelectionTitleView()
Expand All @@ -22,9 +23,11 @@ final class SiteMediaPickerViewController: UIViewController, SiteMediaCollection
/// - blog: The site that contains the media
/// - filter: The types of media to display. By default, `nil` (show everything).
/// - allowsMultipleSelection: `false` by default.
init(blog: Blog, filter: Set<MediaType>? = nil, allowsMultipleSelection: Bool = false) {
/// - initialSelection: `[]` by default.
init(blog: Blog, filter: Set<MediaType>? = nil, allowsMultipleSelection: Bool = false, initialSelection: [Media] = []) {
self.blog = blog
self.allowsMultipleSelection = allowsMultipleSelection
self.initialSelection = initialSelection
self.collectionViewController = SiteMediaCollectionViewController(blog: blog, filter: filter, isShowingPendingUploads: false)

super.init(nibName: nil, bundle: nil)
Expand Down Expand Up @@ -65,7 +68,7 @@ final class SiteMediaPickerViewController: UIViewController, SiteMediaCollection
// MARK: - Actions

private func buttonCancelTapped() {
delegate?.siteMediaPickerViewController(self, didFinishWithSelection: [])
delegate?.siteMediaPickerViewController(self, didFinishWithSelection: initialSelection)
}

@objc private func buttonDoneTapped() {
Expand All @@ -75,7 +78,7 @@ final class SiteMediaPickerViewController: UIViewController, SiteMediaCollection
// MARK: - Selection

private func startSelection() {
collectionViewController.setEditing(true, allowsMultipleSelection: allowsMultipleSelection, isSelectionOrdered: true)
collectionViewController.setEditing(true, allowsMultipleSelection: allowsMultipleSelection, isSelectionOrdered: true, initialSelection: initialSelection)

if allowsMultipleSelection, toolbarItems == nil {
var toolbarItems: [UIBarButtonItem] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
SupportCoordinator(controllerToShowFrom: topmostPresentedViewController, tag: .editorHelp)
}()

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

// MARK: - PostEditor

private(set) lazy var postEditorStateContext: PostEditorStateContext = {
Expand Down Expand Up @@ -335,6 +339,73 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate
func editor(_ viewController: GutenbergKit.EditorViewController, performRequest: GutenbergKit.EditorNetworkRequest) async throws -> GutenbergKit.EditorNetworkResponse {
throw URLError(.unknown)
}

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] else {
self?.editorViewController.setMediaUploadAttachment("[]")
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)
}
}
}

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

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

return WPMediaType(rawValue: mediaType)
}
}

private struct NewGutenbergNetworkClient: GutenbergKit.EditorNetworkingClient {
Expand Down

0 comments on commit 5b3871e

Please sign in to comment.