Skip to content

Commit 5b3871e

Browse files
authored
feat: GutenbergKit Media Library support (#23721)
* 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
1 parent 9f88062 commit 5b3871e

File tree

7 files changed

+128
-11
lines changed

7 files changed

+128
-11
lines changed

Modules/Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ let package = Package(
4747
.package(url: "https://github.com/zendesk/support_sdk_ios", from: "8.0.3"),
4848
// We can't use wordpress-rs branches nor commits here. Only tags work.
4949
.package(url: "https://github.com/Automattic/wordpress-rs", revision: "alpha-swift-20240813"),
50-
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "849118af582068f75807bc0f1265edeee4bf1b5e"),
50+
.package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "6cc307e7fc24910697be5f71b7d70f465a9c0f63"),
5151
.package(url: "https://github.com/Automattic/color-studio", branch: "trunk"),
5252
],
5353
targets: XcodeSupport.targets + [

WordPress.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaPickerHelper.swift

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,37 @@ final class GutenbergMediaPickerHelper: NSObject {
3838
context.present(picker, animated: true)
3939
}
4040

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

48+
private func mapMediaIdsToMedia(_ mediaIds: [Int]) -> [Media] {
49+
assert(Thread.isMainThread, "mapMediaIdsToMedia should only be called on the main thread")
50+
let context = ContextManager.shared.mainContext
51+
let request = NSFetchRequest<NSManagedObject>(entityName: "Media")
52+
request.predicate = NSPredicate(format: "mediaID IN %@", mediaIds.map { NSNumber(value: $0) })
53+
54+
do {
55+
let fetchedMedia = try context.fetch(request) as? [Media] ?? []
56+
57+
// Create a dictionary for quick lookup
58+
let mediaDict = Dictionary(uniqueKeysWithValues: fetchedMedia.compactMap { media -> (Int, Media)? in
59+
if let mediaID = media.mediaID?.intValue {
60+
return (mediaID, media)
61+
}
62+
return nil
63+
})
64+
65+
// Map the original mediaIds to Media objects, preserving order
66+
return mediaIds.compactMap { mediaDict[$0] }
67+
} catch {
68+
return []
69+
}
70+
}
71+
4772
func presentCameraCaptureFullScreen(animated: Bool,
4873
filter: WPMediaType,
4974
callback: @escaping GutenbergMediaPickerHelperCallback) {

WordPress/Classes/ViewRelated/Media/MediaPickerMenu.swift

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ struct MediaPickerMenu {
99
weak var presentingViewController: UIViewController?
1010
var filter: MediaFilter?
1111
var isMultipleSelectionEnabled: Bool
12+
var initialSelection: [Media]
1213

1314
enum MediaFilter {
1415
case images
@@ -21,12 +22,15 @@ struct MediaPickerMenu {
2122
/// - viewController: The view controller to use for presentation.
2223
/// - filter: By default, `nil` – allow all content types.
2324
/// - isMultipleSelectionEnabled: By default, `false`.
25+
/// - initialSelection: By default, `[]`.
2426
init(viewController: UIViewController,
2527
filter: MediaFilter? = nil,
26-
isMultipleSelectionEnabled: Bool = false) {
28+
isMultipleSelectionEnabled: Bool = false,
29+
initialSelection: [Media] = []) {
2730
self.presentingViewController = viewController
2831
self.filter = filter
2932
self.isMultipleSelectionEnabled = isMultipleSelectionEnabled
33+
self.initialSelection = initialSelection
3034
}
3135
}
3236

@@ -185,7 +189,8 @@ extension MediaPickerMenu {
185189
let viewController = SiteMediaPickerViewController(
186190
blog: blog,
187191
filter: filter.map { [$0.mediaType] },
188-
allowsMultipleSelection: isMultipleSelectionEnabled
192+
allowsMultipleSelection: isMultipleSelectionEnabled,
193+
initialSelection: initialSelection
189194
)
190195
viewController.delegate = delegate
191196
let navigation = UINavigationController(rootViewController: viewController)

WordPress/Classes/ViewRelated/Media/SiteMedia/Controllers/SiteMediaCollectionViewController.swift

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,14 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult
7373
fatalError("init(coder:) has not been implemented")
7474
}
7575

76+
private func setInitialSelection(_ media: [Media]) {
77+
updateSelection {
78+
for item in media {
79+
selection.add(item)
80+
}
81+
}
82+
}
83+
7684
func embed(in parentViewController: UIViewController) {
7785
parentViewController.addChild(self)
7886
parentViewController.view.addSubview(view)
@@ -159,14 +167,19 @@ final class SiteMediaCollectionViewController: UIViewController, NSFetchedResult
159167
func setEditing(
160168
_ isEditing: Bool,
161169
allowsMultipleSelection: Bool = true,
162-
isSelectionOrdered: Bool = false
170+
isSelectionOrdered: Bool = false,
171+
initialSelection: [Media]? = nil
163172
) {
164173
guard self.isEditing != isEditing else { return }
165174
self.isEditing = isEditing
166175
self.allowsMultipleSelection = allowsMultipleSelection
167176
self.isSelectionOrdered = isSelectionOrdered
168177

169-
deselectAll()
178+
if let selectedMedia = initialSelection, allowsMultipleSelection {
179+
setInitialSelection(selectedMedia)
180+
} else {
181+
deselectAll()
182+
}
170183
}
171184

172185
private func updateSelection(_ perform: () -> Void) {

WordPress/Classes/ViewRelated/Media/SiteMedia/SiteMediaPickerViewController.swift

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ protocol SiteMediaPickerViewControllerDelegate: AnyObject {
99
final class SiteMediaPickerViewController: UIViewController, SiteMediaCollectionViewControllerDelegate {
1010
private let blog: Blog
1111
private let allowsMultipleSelection: Bool
12+
private let initialSelection: [Media]
1213

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

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

6770
private func buttonCancelTapped() {
68-
delegate?.siteMediaPickerViewController(self, didFinishWithSelection: [])
71+
delegate?.siteMediaPickerViewController(self, didFinishWithSelection: initialSelection)
6972
}
7073

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

7780
private func startSelection() {
78-
collectionViewController.setEditing(true, allowsMultipleSelection: allowsMultipleSelection, isSelectionOrdered: true)
81+
collectionViewController.setEditing(true, allowsMultipleSelection: allowsMultipleSelection, isSelectionOrdered: true, initialSelection: initialSelection)
7982

8083
if allowsMultipleSelection, toolbarItems == nil {
8184
var toolbarItems: [UIBarButtonItem] = []

WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor
2121
SupportCoordinator(controllerToShowFrom: topmostPresentedViewController, tag: .editorHelp)
2222
}()
2323

24+
lazy var mediaPickerHelper: GutenbergMediaPickerHelper = {
25+
return GutenbergMediaPickerHelper(context: self, post: post)
26+
}()
27+
2428
// MARK: - PostEditor
2529

2630
private(set) lazy var postEditorStateContext: PostEditorStateContext = {
@@ -335,6 +339,73 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate
335339
func editor(_ viewController: GutenbergKit.EditorViewController, performRequest: GutenbergKit.EditorNetworkRequest) async throws -> GutenbergKit.EditorNetworkResponse {
336340
throw URLError(.unknown)
337341
}
342+
343+
func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) {
344+
let flags = mediaFilterFlags(using: config.allowedTypes ?? [])
345+
346+
let initialSelectionArray: [Int]
347+
switch config.value {
348+
case .single(let id):
349+
initialSelectionArray = [id]
350+
case .multiple(let ids):
351+
initialSelectionArray = ids
352+
case .none:
353+
initialSelectionArray = []
354+
}
355+
356+
mediaPickerHelper.presentSiteMediaPicker(filter: flags, allowMultipleSelection: config.multiple, initialSelection: initialSelectionArray) { [weak self] assets in
357+
guard let self, let media = assets as? [Media] else {
358+
self?.editorViewController.setMediaUploadAttachment("[]")
359+
return
360+
}
361+
let mediaInfos = media.map { item in
362+
var metadata: [String: String] = [:]
363+
if let videopressGUID = item.videopressGUID {
364+
metadata["videopressGUID"] = videopressGUID
365+
}
366+
return MediaInfo(id: item.mediaID?.int32Value, url: item.remoteURL, type: item.mediaTypeString, caption: item.caption, title: item.filename, alt: item.alt, metadata: [:])
367+
}
368+
if let jsonString = convertMediaInfoArrayToJSONString(mediaInfos) {
369+
// Escape the string for JavaScript
370+
let escapedJsonString = jsonString.replacingOccurrences(of: "'", with: "\\'")
371+
editorViewController.setMediaUploadAttachment(escapedJsonString)
372+
}
373+
}
374+
}
375+
376+
private func convertMediaInfoArrayToJSONString(_ mediaInfoArray: [MediaInfo]) -> String? {
377+
do {
378+
let jsonData = try JSONEncoder().encode(mediaInfoArray)
379+
if let jsonString = String(data: jsonData, encoding: .utf8) {
380+
return jsonString
381+
}
382+
} catch {
383+
print("Error encoding MediaInfo array: \(error)")
384+
}
385+
return nil
386+
}
387+
388+
private func mediaFilterFlags(using filterArray: [OpenMediaLibraryAction.MediaType]) -> WPMediaType {
389+
var mediaType: Int = 0
390+
for filter in filterArray {
391+
switch filter {
392+
case .image:
393+
mediaType = mediaType | WPMediaType.image.rawValue
394+
case .video:
395+
mediaType = mediaType | WPMediaType.video.rawValue
396+
case .audio:
397+
mediaType = mediaType | WPMediaType.audio.rawValue
398+
case .other:
399+
mediaType = mediaType | WPMediaType.other.rawValue
400+
case .any:
401+
mediaType = mediaType | WPMediaType.all.rawValue
402+
@unknown default:
403+
fatalError()
404+
}
405+
}
406+
407+
return WPMediaType(rawValue: mediaType)
408+
}
338409
}
339410

340411
private struct NewGutenbergNetworkClient: GutenbergKit.EditorNetworkingClient {

0 commit comments

Comments
 (0)