diff --git a/Modules/Package.resolved b/Modules/Package.resolved index e075b7450e38..6cf9aad38fe4 100644 --- a/Modules/Package.resolved +++ b/Modules/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b1fb7c230ac6bba7fef0398912e86c03c8a9329d742c0c971d3945e9308ee8f2", + "originHash" : "db265698147730f917e4e590fe57a526e3b2a550a97ff973019fb8914dcf3987", "pins" : [ { "identity" : "alamofire", @@ -149,8 +149,7 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/wordpress-mobile/GutenbergKit", "state" : { - "revision" : "3afb350113614db42bee4c30d00b1697209b25fc", - "version" : "0.6.0" + "revision" : "e7c6471bf7b37c2a742d9106d32c83d7f5c89051" } }, { diff --git a/Modules/Package.swift b/Modules/Package.swift index 4a7456505b66..a28b43c615dd 100644 --- a/Modules/Package.swift +++ b/Modules/Package.swift @@ -54,7 +54,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-20250715"), - .package(url: "https://github.com/wordpress-mobile/GutenbergKit", from: "0.6.0"), + .package(url: "https://github.com/wordpress-mobile/GutenbergKit", revision: "e7c6471bf7b37c2a742d9106d32c83d7f5c89051"), .package( url: "https://github.com/Automattic/color-studio", revision: "bf141adc75e2769eb469a3e095bdc93dc30be8de" diff --git a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift index b0621431d471..e97531886cf8 100644 --- a/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift +++ b/WordPress/Classes/Utility/BuildInformation/FeatureFlag.swift @@ -26,6 +26,7 @@ public enum FeatureFlag: Int, CaseIterable { case pluginManagementOverhaul case nativeJetpackConnection case newsletterSubscribers + case nativeBlockInserter /// Returns a boolean indicating if the feature is enabled. /// @@ -82,6 +83,8 @@ public enum FeatureFlag: Int, CaseIterable { return BuildConfiguration.current == .debug case .newsletterSubscribers: return true + case .nativeBlockInserter: + return BuildConfiguration.current == .debug } } @@ -125,6 +128,7 @@ extension FeatureFlag { case .readerGutenbergCommentComposer: "Gutenberg Comment Composer" case .nativeJetpackConnection: "Native Jetpack Connection" case .newsletterSubscribers: "Newsletter Subscribers" + case .nativeBlockInserter: "Native Block Inserter" } } } diff --git a/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift b/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift index 53e7e4df9f4a..d86791cd06f5 100644 --- a/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift +++ b/WordPress/Classes/ViewRelated/Comments/Controllers/Editor/CommentGutenbergEditorViewController.swift @@ -91,4 +91,8 @@ extension CommentGutenbergEditorViewController: GutenbergKit.EditorViewControlle func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: GutenbergKit.OpenMediaLibraryAction) { // Do nothing } + + func getMediaPickerController(for viewController: GutenbergKit.EditorViewController, parameters: GutenbergKit.MediaPickerParameters) -> GutenbergKit.MediaPickerController? { + nil + } } diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPicker+GutenbergKit.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPicker+GutenbergKit.swift new file mode 100644 index 000000000000..5afacbaa76ae --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPicker+GutenbergKit.swift @@ -0,0 +1,12 @@ +import UIKit +import GutenbergKit + +extension MediaPickerMenu.MediaFilter { + init?(_ filter: GutenbergKit.MediaPickerParameters.MediaFilter) { + switch filter { + case .images: self = .images + case .videos: self = .videos + case .all: return nil + } + } +} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerController.swift new file mode 100644 index 000000000000..042bb6006f6f --- /dev/null +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerController.swift @@ -0,0 +1,159 @@ +import UIKit +import GutenbergKit +import WordPressData + +/// A adapter for GutenbergKit that manages media picker sources the editor. +final class MediaPickerController: GutenbergKit.MediaPickerController { + private let blog: Blog + private let parameters: MediaPickerParameters + private var currentMediaPickerController: MediaPickerMenuController? + private var currentMediaPickerCompletion: (([MediaInfo]) -> Void)? + + init(blog: Blog, parameters: MediaPickerParameters) { + self.blog = blog + self.parameters = parameters + } + + var actions: [[MediaPickerAction]] { + // Create MediaPickerMenu with the configuration + let menu = MediaPickerMenu( + filter: convertFilter(parameters.filter), + isMultipleSelectionEnabled: parameters.isMultipleSelectionEnabled + ) + + // Create a controller to handle selections + let controller = MediaPickerMenuController() + controller.onSelection = { [weak self] selection in + guard let self else { return } + let mediaInfos = self.convertSelectionToMediaInfo(selection) + self.currentMediaPickerCompletion?(mediaInfos) + self.currentMediaPickerCompletion = nil + self.currentMediaPickerController = nil + } + + // Store the controller to keep it alive + currentMediaPickerController = controller + + // Define media sources with their identifiers + let sources: [(source: MediaPickerSource, id: MediaPickerID)] = [ + (.playground, .imagePlayground), + (.siteMedia(blog: blog), .siteMedia), + (.photos, .applePhotos), + (.freePhotos(blog: blog), .freePhotos), + (.freeGIFs(blog: blog), .freeGIFs) + ] + + // Create actions from enabled sources + let actionsWithGroups = sources.compactMap { source, id -> (action: MediaPickerAction, group: Int)? in + guard source.isEnabled else { return nil } + + let uiAction = createUIAction(for: source, menu: menu, controller: controller) + guard let uiAction else { return nil } + + let action = convertToMediaPickerAction(uiAction, id: id) + + // Group 0: playground, site media, files + // Group 1: free photos, free gifs + let group = (id == .freePhotos || id == .freeGIFs) ? 1 : 0 + + return (action, group) + } + + // Group actions + let firstGroup = actionsWithGroups.filter { $0.group == 0 }.map { $0.action } + let secondGroup = actionsWithGroups.filter { $0.group == 1 }.map { $0.action } + + return [firstGroup, secondGroup].filter { !$0.isEmpty } + } + + // MARK: - Private Methods + + 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: + return menu.makeImagePlaygroundAction(delegate: controller) + case .siteMedia: + return menu.makeSiteMediaAction(blog: blog, delegate: controller) + case .photos: + return menu.makePhotosAction(delegate: controller) + case .freePhotos: + return menu.makeStockPhotos(blog: blog, delegate: controller) + case .freeGIFs: + return menu.makeFreeGIFAction(blog: blog, delegate: controller) + default: + return nil + } + } + + private func convertToMediaPickerAction(_ uiAction: UIAction, id: MediaPickerID) -> MediaPickerAction { + MediaPickerAction( + id: id.rawValue, + title: uiAction.title, + image: uiAction.image ?? UIImage(), + perform: { [weak self] presentingViewController, completion in + guard let self else { + completion([]) + return + } + + // Store the completion handler for when selection is made + self.currentMediaPickerCompletion = completion + + // Perform the original action + uiAction.performWithSender(nil, target: nil) + } + ) + } + + private func convertSelectionToMediaInfo(_ selection: MediaPickerSelection) -> [MediaInfo] { + var mediaInfos: [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.mediaTypeString, + caption: media.caption, + title: media.filename, + alt: media.alt, + metadata: metadata + ) + mediaInfos.append(mediaInfo) + + case .external(let asset): + let mediaInfo = MediaInfo( + id: nil, + url: asset.largeURL.absoluteString, + type: "image", + caption: asset.caption, + title: asset.name, + alt: nil, + metadata: [:] + ) + mediaInfos.append(mediaInfo) + + case .image, .pickerResult: + // These would need to be uploaded first + // For now, we skip them + break + } + } + + return mediaInfos + } +} diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift index 4f76d7b9e222..a9f56c8a7060 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Helpers/MediaPickerMenuController.swift @@ -5,7 +5,7 @@ import WordPressShared 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) @@ -17,7 +17,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) } } } @@ -26,7 +26,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) } } } @@ -35,7 +35,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) } } } @@ -45,7 +45,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") } @@ -56,7 +56,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) } } diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift index cd5fb43356ab..a14709a39caa 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/MediaPicker.swift @@ -101,7 +101,7 @@ enum MediaPickerSource { struct MediaPickerSelection { var items: [MediaPickerItem] - var source: String + var source: MediaPickerID } enum MediaPickerItem { diff --git a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift index c4e2f8ddafdb..a6e8dad5a995 100644 --- a/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift +++ b/WordPress/Classes/ViewRelated/Media/MediaPicker/Menu/MediaPickerMenu.swift @@ -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" +} diff --git a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift index 2001677e45b1..494e8d8dd90d 100644 --- a/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift +++ b/WordPress/Classes/ViewRelated/NewGutenberg/NewGutenbergViewController.swift @@ -1,12 +1,14 @@ import UIKit import WordPressUI import AsyncImageKit +import BuildSettingsKit import AutomatticTracks import GutenbergKit import SafariServices import WordPressData import WordPressShared import WebKit +import Photos class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor { let errorDomain: String = "GutenbergViewController.errorDomain" @@ -133,6 +135,8 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor conf.content = post.content ?? "" conf.postID = post.postID?.intValue != -1 ? post.postID?.intValue : nil conf.postType = post is Page ? "page" : "post" + conf.enableNativeBlockInserter = FeatureFlag.nativeBlockInserter.enabled + conf.autoFocusOnLoad = FeatureFlag.nativeBlockInserter.enabled self.editorViewController = GutenbergKit.EditorViewController(configuration: conf) @@ -184,8 +188,8 @@ class NewGutenbergViewController: UIViewController, PostEditor, PublishingEditor view.pinSubviewToAllEdges(editorViewController.view) editorViewController.didMove(toParent: self) - if #available(iOS 16.4, *) { - editorViewController.webView.isInspectable = true // TODO: should be diasble in production + if #available(iOS 16.4, *), BuildConfiguration.current == .debug { + editorViewController.webView.isInspectable = true } // Doesn't seem to do anything @@ -426,6 +430,13 @@ extension NewGutenbergViewController: GutenbergKit.EditorViewControllerDelegate throw URLError(.unknown) } + /// Returns the available media picker sources for the given configuration + func getMediaPickerController(for viewController: GutenbergKit.EditorViewController, parameters: GutenbergKit.MediaPickerParameters) -> (any GutenbergKit.MediaPickerController)? { + MediaPickerController(blog: post.blog, parameters: parameters) + } + + // MARK: - Media Picker Helpers + func editor(_ viewController: GutenbergKit.EditorViewController, didRequestMediaFromSiteMediaLibrary config: OpenMediaLibraryAction) { let flags = mediaFilterFlags(using: config.allowedTypes ?? []) diff --git a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme index 968123f0debb..ff3f8291d8b5 100644 --- a/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme +++ b/WordPress/WordPress.xcodeproj/xcshareddata/xcschemes/Jetpack.xcscheme @@ -122,15 +122,20 @@ value = "disable" isEnabled = "NO"> + +