diff --git a/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift new file mode 100644 index 000000000000..e5a06596f534 --- /dev/null +++ b/Tests/KeystoneTests/Tests/Features/Gutenberg/PostGBKEditorViewControllerTests.swift @@ -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) + } +} diff --git a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaPickerHelper.swift b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaPickerHelper.swift index d3c917580c20..75963550f770 100644 --- a/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaPickerHelper.swift +++ b/WordPress/Classes/ViewRelated/Gutenberg/GutenbergMediaPickerHelper.swift @@ -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() @@ -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] { @@ -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] } @@ -80,9 +100,11 @@ 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) @@ -90,7 +112,10 @@ final class GutenbergMediaPickerHelper: NSObject { } 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 @@ -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 } @@ -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 diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index da9dbb146837..0109635afcc2 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -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 @@ -285,55 +281,6 @@ 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)? { @@ -341,40 +288,6 @@ class NewGutenbergViewController: PostGBKEditorViewController, PostEditor, Publi // 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 diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift index 2fe894311e9a..1e4dae14129b 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/PostGBKEditorViewController.swift @@ -3,6 +3,7 @@ import UIKit import WebKit import SafariServices import GutenbergKit +import WordPressData import WordPressShared import WordPressUI @@ -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 @@ -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: [:] + ) + } + 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) { @@ -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() + } + } + + return GutenbergMediaType(rawValue: mediaType) + } } private extension PostGBKEditorViewController {