From 60a97635ca00b257ab638d6bb02b5dd95dbd2a9b Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 20 May 2024 15:53:17 +0200 Subject: [PATCH 1/7] Mobile - Add onVoiceToContent bridge function --- .../RNReactNativeGutenbergBridgeModule.java | 8 ++++++++ .../mobile/WPAndroidGlue/WPAndroidGlueCode.java | 4 ++++ packages/react-native-bridge/index.js | 13 +++++++++++++ packages/react-native-bridge/ios/Gutenberg.swift | 5 +++++ .../ios/RNReactNativeGutenbergBridge.swift | 1 + test/native/setup.js | 1 + 6 files changed, 32 insertions(+) diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index 4f7066a5bd47d0..77fbea79cc8754 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -66,6 +66,7 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu private static final String EVENT_NAME_ON_UNDO_PRESSED = "onUndoPressed"; private static final String EVENT_NAME_ON_REDO_PRESSED = "onRedoPressed"; + private static final String EVENT_NAME_ON_VOICE_TO_CONTENT = "onVoiceToContent"; private static final String MAP_KEY_UPDATE_HTML = "html"; private static final String MAP_KEY_UPDATE_TITLE = "title"; @@ -91,6 +92,7 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu private static final String MAP_KEY_REPLACE_BLOCK_HTML = "html"; private static final String MAP_KEY_REPLACE_BLOCK_BLOCK_ID = "clientId"; + private static final String MAP_KEY_VOICE_TO_CONTENT = "content"; public static final String MAP_KEY_FEATURED_IMAGE_ID = "featuredImageId"; public static final String MAP_KEY_IS_CONNECTED = "isConnected"; @@ -214,6 +216,12 @@ public void onRedoPressed() { emitToJS(EVENT_NAME_ON_REDO_PRESSED, null); } + public void onVoiceToContent(String content) { + WritableMap writableMap = new WritableNativeMap(); + writableMap.putString(MAP_KEY_VOICE_TO_CONTENT, content); + emitToJS(EVENT_NAME_ON_VOICE_TO_CONTENT, writableMap); + } + @ReactMethod public void addListener(String eventName) { // Keep: Required for RN built in Event Emitter Calls. diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index 8677c1737c52fb..27638e5e9bc97a 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -846,6 +846,10 @@ public void onRedoPressed() { mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onRedoPressed(); } + public void onVoiceToContent(String content) { + mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onVoiceToContent(content); + } + public void setTitle(String title) { mTitleInitialized = true; mTitle = title; diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 50e21fe86a6833..9876659cc7f96b 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -202,6 +202,19 @@ export function subscribeConnectionStatus( callback ) { ); } +/** + * Subscribes a callback function to the 'onVoiceToContent' event. + * This event is triggered with markdown content that will be passed to the block editor + * to be converted into blocks. + * + * @param {Function} callback - The function to be called when the 'onVoiceToContent' event is triggered. + * This function receives markdown content as an argument. + * @return {Object} - The listener object that was added to the event. + */ +export function subscribeVoiceToContent( callback ) { + return gutenbergBridgeEvents.addListener( 'onVoiceToContent', callback ); +} + export function requestConnectionStatus( callback ) { return RNReactNativeGutenbergBridge.requestConnectionStatus( callback ); } diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index 2b293e21919795..6a05c314b2312d 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -221,6 +221,11 @@ public class Gutenberg: UIResponder { var data: [String: Any] = ["isConnected": isConnected] bridgeModule.sendEventIfNeeded(.connectionStatusChange, body: data) } + + public func onVoiceToContent(content: String) { + let payload: [String: Any] = ["content": content] + bridgeModule.sendEventIfNeeded(.onVoiceToContent, body: payload) + } private func properties(from editorSettings: GutenbergEditorSettings?) -> [String : Any] { var settingsUpdates = [String : Any]() diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index aa115331ec2d87..639d9df0ddcb31 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -422,6 +422,7 @@ extension RNReactNativeGutenbergBridge { case onUndoPressed case onRedoPressed case connectionStatusChange + case onVoiceToContent } public override func supportedEvents() -> [String]! { diff --git a/test/native/setup.js b/test/native/setup.js index 12b61d15553cae..151ee4f197e539 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -109,6 +109,7 @@ jest.mock( '@wordpress/react-native-bridge', () => { subscribeOnUndoPressed: jest.fn(), subscribeOnRedoPressed: jest.fn(), subscribeConnectionStatus: jest.fn( () => ( { remove: jest.fn() } ) ), + subscribeVoiceToContent: jest.fn(), requestConnectionStatus: jest.fn( ( callback ) => callback( true ) ), editorDidMount: jest.fn(), showAndroidSoftKeyboard: jest.fn(), From cf9b8547c6be9deb0c35f060b0d3cfeae9ee334d Mon Sep 17 00:00:00 2001 From: Gerardo Date: Mon, 20 May 2024 15:56:47 +0200 Subject: [PATCH 2/7] Mobile - Provider - Add listener for the onVoiceToContent functionality that adds support to parse markdown content into blocks --- .../src/components/provider/index.native.js | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index 31c49fa3dc7fd3..89d5811454c031 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -21,6 +21,7 @@ import RNReactNativeGutenbergBridge, { subscribeUpdateCapabilities, subscribeShowNotice, subscribeShowEditorHelp, + subscribeVoiceToContent, } from '@wordpress/react-native-bridge'; import { Component } from '@wordpress/element'; import { count as wordCount } from '@wordpress/wordcount'; @@ -30,6 +31,7 @@ import { getUnregisteredTypeHandlerName, getBlockType, createBlock, + pasteHandler, } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; @@ -82,6 +84,7 @@ class NativeEditorProvider extends Component { ); this.onHardwareBackPress = this.onHardwareBackPress.bind( this ); + this.onVoiceToContent = this.onVoiceToContent.bind( this ); this.getEditorSettings = memize( ( settings, capabilities ) => ( { @@ -200,6 +203,12 @@ class NativeEditorProvider extends Component { this.onHardwareBackPress ); + this.subscriptionOnVoiceToContent = subscribeVoiceToContent( + ( content ) => { + this.onVoiceToContent( content ); + } + ); + // Request current block impressions from native app. requestBlockTypeImpressions( ( storedImpressions ) => { const impressions = { ...NEW_BLOCK_TYPES, ...storedImpressions }; @@ -263,6 +272,10 @@ class NativeEditorProvider extends Component { if ( this.hardwareBackPressListener ) { this.hardwareBackPressListener.remove(); } + + if ( this.subscriptionOnVoiceToContent ) { + this.subscriptionOnVoiceToContent.remove(); + } } getThemeColors( { rawStyles, rawFeatures } ) { @@ -303,6 +316,17 @@ class NativeEditorProvider extends Component { return false; } + onVoiceToContent( { content } ) { + const { insertBlocks } = this.props; + const blocks = pasteHandler( { + plainText: content, + } ); + + if ( blocks ) { + insertBlocks( blocks, undefined, undefined, false ); + } + } + serializeToNativeAction() { const title = this.props.title; let html; @@ -428,6 +452,7 @@ const ComposedNativeProvider = compose( [ clearSelectedBlock, updateSettings, insertBlock, + insertBlocks, replaceBlock, } = dispatch( blockEditorStore ); const { addEntities, receiveEntityRecords } = dispatch( coreStore ); @@ -439,6 +464,7 @@ const ComposedNativeProvider = compose( [ updateEditorSettings, addEntities, insertBlock, + insertBlocks, createSuccessNotice, createErrorNotice, clearSelectedBlock, From ff539585c36e4948adf5c9b6701d33ed49ad9326 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 23 May 2024 12:09:42 +0200 Subject: [PATCH 3/7] Adds integration test for onVoiceToContent --- .../test/__snapshots__/editor.native.js.snap | 82 +++++++++++++++++++ packages/edit-post/src/test/editor.native.js | 49 +++++++++++ 2 files changed, 131 insertions(+) diff --git a/packages/edit-post/src/test/__snapshots__/editor.native.js.snap b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap index 8b820cd38f11bd..650b2f32fb9663 100644 --- a/packages/edit-post/src/test/__snapshots__/editor.native.js.snap +++ b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap @@ -19,3 +19,85 @@ exports[`Editor appends media correctly for allowed types and skips unsupported
" `; + +exports[`Editor on voice to content parses markdown into blocks 1`] = ` +" +

Sample Document

+ + + +

Lorem ipsum dolor sit amet, consectetur adipiscing
elit.

+ + + +

Overview

+ + + +
    +
  • Lorem ipsum dolor sit amet
  • + + + +
  • Consectetur adipiscing
    elit
  • + + + +
  • Integer nec odio
  • +
+ + + +

Details

+ + + +
    +
  1. Sed cursus ante dapibus diam
  2. + + + +
  3. Nulla quis sem at nibh elementum imperdiet
  4. + + + +
  5. Duis sagittis ipsum ## Mixed Lists
  6. +
+ + + +
    +
  • Key Points:
  • +
+ + + +
    +
  1. Lorem ipsum dolor sit amet
  2. + + + +

  3. Consectetur adipiscing elit
  4. + + + +
  5. Integer nec odio
  6. +
+ + + +
    +
  • Additional Info:
    -
    Sed cursus ante dapibus diam
  • + + + +
  • Nulla quis sem at nibh elementum imperdiet
  • +
+" +`; + +exports[`Editor on voice to content parses standard text into blocks 1`] = ` +" +

Lorem ipsum dolor sit amet. Qui rerum quae sed sciunt
animi rem voluptate quas aut impedit accusamus ut

+" +`; diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js index 0de2c528b2452a..159450c0b51ae3 100644 --- a/packages/edit-post/src/test/editor.native.js +++ b/packages/edit-post/src/test/editor.native.js @@ -20,6 +20,7 @@ import { requestMediaImport, subscribeMediaAppend, subscribeParentToggleHTMLMode, + subscribeVoiceToContent, } from '@wordpress/react-native-bridge'; setupCoreBlocks(); @@ -34,6 +35,11 @@ subscribeMediaAppend.mockImplementation( ( callback ) => { mediaAppendCallback = callback; } ); +let onVoiceToContentCallback; +subscribeVoiceToContent.mockImplementation( ( callback ) => { + onVoiceToContentCallback = callback; +} ); + const MEDIA = [ { localId: 1, @@ -149,4 +155,47 @@ describe( 'Editor', () => { screen.queryAllByLabelText( 'Open Settings' ); expect( openBlockSettingsButton.length ).toBe( 0 ); } ); + + describe( 'on voice to content', () => { + it( 'parses markdown into blocks', async () => { + // Arrange + await initializeEditor(); + + // Act + act( () => { + onVoiceToContentCallback( { + content: `# Sample Document\nLorem ipsum dolor sit amet, consectetur adipiscing + elit.\n## Overview\n- Lorem ipsum dolor sit amet\n- Consectetur adipiscing + elit\n- Integer nec odio\n## Details\n1. Sed cursus ante dapibus diam\n2. + Nulla quis sem at nibh elementum imperdiet\n3. Duis sagittis ipsum\n + ## Mixed Lists\n- Key Points:\n 1. Lorem ipsum dolor sit amet\n 2. + Consectetur adipiscing elit\n 3. Integer nec odio\n- Additional Info:\n - + Sed cursus ante dapibus diam\n - Nulla quis sem at nibh elementum imperdiet\n`, + } ); + } ); + + // Assert + // Needed to for the "Processed HTML piece" log. + expect( console ).toHaveLogged(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + + it( 'parses standard text into blocks', async () => { + // Arrange + await initializeEditor(); + + // Act + act( () => { + onVoiceToContentCallback( { + content: `Lorem ipsum dolor sit amet. Qui rerum quae sed sciunt + animi rem voluptate quas aut impedit accusamus ut`, + } ); + } ); + + // Assert + // Needed to for the "Processed HTML piece" log. + expect( console ).toHaveLogged(); + expect( getEditorHtml() ).toMatchSnapshot(); + } ); + } ); } ); From efa9d0d42f62c6b7bcd966c005ad840663ff6d6b Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 30 May 2024 16:25:12 +0200 Subject: [PATCH 4/7] Rename onVoiceToContent to onContentUpdate --- .../src/test/__snapshots__/editor.native.js.snap | 4 ++-- packages/edit-post/src/test/editor.native.js | 14 +++++++------- .../src/components/provider/index.native.js | 16 ++++++++-------- .../RNReactNativeGutenbergBridgeModule.java | 15 ++++++++++----- .../mobile/WPAndroidGlue/WPAndroidGlueCode.java | 5 +++-- packages/react-native-bridge/index.js | 14 +++++++------- packages/react-native-bridge/ios/Gutenberg.swift | 9 ++++++--- .../ios/RNReactNativeGutenbergBridge.swift | 2 +- test/native/setup.js | 2 +- 9 files changed, 45 insertions(+), 36 deletions(-) diff --git a/packages/edit-post/src/test/__snapshots__/editor.native.js.snap b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap index 650b2f32fb9663..974664faa6dd23 100644 --- a/packages/edit-post/src/test/__snapshots__/editor.native.js.snap +++ b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap @@ -20,7 +20,7 @@ exports[`Editor appends media correctly for allowed types and skips unsupported " `; -exports[`Editor on voice to content parses markdown into blocks 1`] = ` +exports[`Editor on content update parses markdown into blocks 1`] = ` "

Sample Document

@@ -96,7 +96,7 @@ exports[`Editor on voice to content parses markdown into blocks 1`] = ` " `; -exports[`Editor on voice to content parses standard text into blocks 1`] = ` +exports[`Editor on content update parses standard text into blocks 1`] = ` "

Lorem ipsum dolor sit amet. Qui rerum quae sed sciunt
animi rem voluptate quas aut impedit accusamus ut

" diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js index 159450c0b51ae3..0b4007543442d1 100644 --- a/packages/edit-post/src/test/editor.native.js +++ b/packages/edit-post/src/test/editor.native.js @@ -20,7 +20,7 @@ import { requestMediaImport, subscribeMediaAppend, subscribeParentToggleHTMLMode, - subscribeVoiceToContent, + subscribeToContentUpdate, } from '@wordpress/react-native-bridge'; setupCoreBlocks(); @@ -35,9 +35,9 @@ subscribeMediaAppend.mockImplementation( ( callback ) => { mediaAppendCallback = callback; } ); -let onVoiceToContentCallback; -subscribeVoiceToContent.mockImplementation( ( callback ) => { - onVoiceToContentCallback = callback; +let onContentUpdateCallback; +subscribeToContentUpdate.mockImplementation( ( callback ) => { + onContentUpdateCallback = callback; } ); const MEDIA = [ @@ -156,14 +156,14 @@ describe( 'Editor', () => { expect( openBlockSettingsButton.length ).toBe( 0 ); } ); - describe( 'on voice to content', () => { + describe( 'on content update', () => { it( 'parses markdown into blocks', async () => { // Arrange await initializeEditor(); // Act act( () => { - onVoiceToContentCallback( { + onContentUpdateCallback( { content: `# Sample Document\nLorem ipsum dolor sit amet, consectetur adipiscing elit.\n## Overview\n- Lorem ipsum dolor sit amet\n- Consectetur adipiscing elit\n- Integer nec odio\n## Details\n1. Sed cursus ante dapibus diam\n2. @@ -186,7 +186,7 @@ describe( 'Editor', () => { // Act act( () => { - onVoiceToContentCallback( { + onContentUpdateCallback( { content: `Lorem ipsum dolor sit amet. Qui rerum quae sed sciunt animi rem voluptate quas aut impedit accusamus ut`, } ); diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index 89d5811454c031..a7e05052b9acce 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -21,7 +21,7 @@ import RNReactNativeGutenbergBridge, { subscribeUpdateCapabilities, subscribeShowNotice, subscribeShowEditorHelp, - subscribeVoiceToContent, + subscribeToContentUpdate, } from '@wordpress/react-native-bridge'; import { Component } from '@wordpress/element'; import { count as wordCount } from '@wordpress/wordcount'; @@ -84,7 +84,7 @@ class NativeEditorProvider extends Component { ); this.onHardwareBackPress = this.onHardwareBackPress.bind( this ); - this.onVoiceToContent = this.onVoiceToContent.bind( this ); + this.onContentUpdate = this.onContentUpdate.bind( this ); this.getEditorSettings = memize( ( settings, capabilities ) => ( { @@ -203,9 +203,9 @@ class NativeEditorProvider extends Component { this.onHardwareBackPress ); - this.subscriptionOnVoiceToContent = subscribeVoiceToContent( - ( content ) => { - this.onVoiceToContent( content ); + this.subscriptionOnContentUpdate = subscribeToContentUpdate( + ( data ) => { + this.onContentUpdate( data ); } ); @@ -273,8 +273,8 @@ class NativeEditorProvider extends Component { this.hardwareBackPressListener.remove(); } - if ( this.subscriptionOnVoiceToContent ) { - this.subscriptionOnVoiceToContent.remove(); + if ( this.subscriptionOnContentUpdate ) { + this.subscriptionOnContentUpdate.remove(); } } @@ -316,7 +316,7 @@ class NativeEditorProvider extends Component { return false; } - onVoiceToContent( { content } ) { + onContentUpdate( { content } ) { const { insertBlocks } = this.props; const blocks = pasteHandler( { plainText: content, diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index 77fbea79cc8754..442e703fe0043c 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -66,7 +66,7 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu private static final String EVENT_NAME_ON_UNDO_PRESSED = "onUndoPressed"; private static final String EVENT_NAME_ON_REDO_PRESSED = "onRedoPressed"; - private static final String EVENT_NAME_ON_VOICE_TO_CONTENT = "onVoiceToContent"; + private static final String EVENT_NAME_ON_CONTENT_UPDATE = "onContentUpdate"; private static final String MAP_KEY_UPDATE_HTML = "html"; private static final String MAP_KEY_UPDATE_TITLE = "title"; @@ -92,7 +92,8 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu private static final String MAP_KEY_REPLACE_BLOCK_HTML = "html"; private static final String MAP_KEY_REPLACE_BLOCK_BLOCK_ID = "clientId"; - private static final String MAP_KEY_VOICE_TO_CONTENT = "content"; + private static final String MAP_KEY_UPDATE_CONTENT_TITLE = "title"; + private static final String MAP_KEY_UPDATE_CONTENT = "content"; public static final String MAP_KEY_FEATURED_IMAGE_ID = "featuredImageId"; public static final String MAP_KEY_IS_CONNECTED = "isConnected"; @@ -216,10 +217,14 @@ public void onRedoPressed() { emitToJS(EVENT_NAME_ON_REDO_PRESSED, null); } - public void onVoiceToContent(String content) { + public void onContentUpdate(String title, String content) { WritableMap writableMap = new WritableNativeMap(); - writableMap.putString(MAP_KEY_VOICE_TO_CONTENT, content); - emitToJS(EVENT_NAME_ON_VOICE_TO_CONTENT, writableMap); + + if (title != null) { + writableMap.putString(MAP_KEY_UPDATE_CONTENT_TITLE, title); + } + writableMap.putString(MAP_KEY_UPDATE_CONTENT, content); + emitToJS(EVENT_NAME_ON_CONTENT_UPDATE, writableMap); } @ReactMethod diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index 27638e5e9bc97a..7d42800a4b4011 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -846,8 +846,9 @@ public void onRedoPressed() { mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onRedoPressed(); } - public void onVoiceToContent(String content) { - mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onVoiceToContent(content); + public void onContentUpdate(String title, String content) { + mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule() + .onContentUpdate(title, content); } public void setTitle(String title) { diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 9876659cc7f96b..da16f75e161dac 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -203,16 +203,16 @@ export function subscribeConnectionStatus( callback ) { } /** - * Subscribes a callback function to the 'onVoiceToContent' event. - * This event is triggered with markdown content that will be passed to the block editor + * Subscribes a callback function to the 'onContentUpdate' event. + * This event is triggered with content that will be passed to the block editor * to be converted into blocks. * - * @param {Function} callback - The function to be called when the 'onVoiceToContent' event is triggered. - * This function receives markdown content as an argument. - * @return {Object} - The listener object that was added to the event. + * @param {Function} callback The function to be called when the 'onContentUpdate' event is triggered. + * This function receives content plain text/markdown as an argument. + * @return {Object} The listener object that was added to the event. */ -export function subscribeVoiceToContent( callback ) { - return gutenbergBridgeEvents.addListener( 'onVoiceToContent', callback ); +export function subscribeToContentUpdate( callback ) { + return gutenbergBridgeEvents.addListener( 'onContentUpdate', callback ); } export function requestConnectionStatus( callback ) { diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index 6a05c314b2312d..1b8f5eee8f3a82 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -222,9 +222,12 @@ public class Gutenberg: UIResponder { bridgeModule.sendEventIfNeeded(.connectionStatusChange, body: data) } - public func onVoiceToContent(content: String) { - let payload: [String: Any] = ["content": content] - bridgeModule.sendEventIfNeeded(.onVoiceToContent, body: payload) + public func onContentUpdate(title: String? = nil, content: String) { + var payload: [String: Any] = ["content": content] + if let title = title { + payload["title"] = title + } + bridgeModule.sendEventIfNeeded(.onContentUpdate, body: payload) } private func properties(from editorSettings: GutenbergEditorSettings?) -> [String : Any] { diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index 639d9df0ddcb31..96c3a8f25e0cb8 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -422,7 +422,7 @@ extension RNReactNativeGutenbergBridge { case onUndoPressed case onRedoPressed case connectionStatusChange - case onVoiceToContent + case onContentUpdate } public override func supportedEvents() -> [String]! { diff --git a/test/native/setup.js b/test/native/setup.js index 151ee4f197e539..e93d2248bd03cb 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -109,7 +109,7 @@ jest.mock( '@wordpress/react-native-bridge', () => { subscribeOnUndoPressed: jest.fn(), subscribeOnRedoPressed: jest.fn(), subscribeConnectionStatus: jest.fn( () => ( { remove: jest.fn() } ) ), - subscribeVoiceToContent: jest.fn(), + subscribeToContentUpdate: jest.fn(), requestConnectionStatus: jest.fn( ( callback ) => callback( true ) ), editorDidMount: jest.fn(), showAndroidSoftKeyboard: jest.fn(), From af7ef3df3047a432226122a5e3885e3f64fc1b65 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 30 May 2024 16:47:22 +0200 Subject: [PATCH 5/7] On Content Update, use the same functionality as in the Post Title component --- .../test/__snapshots__/editor.native.js.snap | 12 +-- packages/edit-post/src/test/editor.native.js | 52 ++++++--- .../src/components/post-title/index.native.js | 101 ++++++++++++------ .../src/components/provider/index.native.js | 26 +++-- 4 files changed, 128 insertions(+), 63 deletions(-) diff --git a/packages/edit-post/src/test/__snapshots__/editor.native.js.snap b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap index 974664faa6dd23..76bb42d5a2ccea 100644 --- a/packages/edit-post/src/test/__snapshots__/editor.native.js.snap +++ b/packages/edit-post/src/test/__snapshots__/editor.native.js.snap @@ -21,11 +21,7 @@ exports[`Editor appends media correctly for allowed types and skips unsupported `; exports[`Editor on content update parses markdown into blocks 1`] = ` -" -

Sample Document

- - - +"

Lorem ipsum dolor sit amet, consectetur adipiscing
elit.

@@ -95,9 +91,3 @@ exports[`Editor on content update parses markdown into blocks 1`] = ` " `; - -exports[`Editor on content update parses standard text into blocks 1`] = ` -" -

Lorem ipsum dolor sit amet. Qui rerum quae sed sciunt
animi rem voluptate quas aut impedit accusamus ut

-" -`; diff --git a/packages/edit-post/src/test/editor.native.js b/packages/edit-post/src/test/editor.native.js index 0b4007543442d1..acafc4d68d42a5 100644 --- a/packages/edit-post/src/test/editor.native.js +++ b/packages/edit-post/src/test/editor.native.js @@ -7,6 +7,7 @@ import { fireEvent, getBlock, getEditorHtml, + getEditorTitle, initializeEditor, screen, setupCoreBlocks, @@ -157,45 +158,72 @@ describe( 'Editor', () => { } ); describe( 'on content update', () => { + const MARKDOWN = `# Sample Document\nLorem ipsum dolor sit amet, consectetur adipiscing + elit.\n## Overview\n- Lorem ipsum dolor sit amet\n- Consectetur adipiscing + elit\n- Integer nec odio\n## Details\n1. Sed cursus ante dapibus diam\n2. + Nulla quis sem at nibh elementum imperdiet\n3. Duis sagittis ipsum\n + ## Mixed Lists\n- Key Points:\n 1. Lorem ipsum dolor sit amet\n 2. + Consectetur adipiscing elit\n 3. Integer nec odio\n- Additional Info:\n - + Sed cursus ante dapibus diam\n - Nulla quis sem at nibh elementum imperdiet\n`; + it( 'parses markdown into blocks', async () => { // Arrange - await initializeEditor(); + await initializeEditor( { + initialTitle: null, + } ); // Act act( () => { onContentUpdateCallback( { - content: `# Sample Document\nLorem ipsum dolor sit amet, consectetur adipiscing - elit.\n## Overview\n- Lorem ipsum dolor sit amet\n- Consectetur adipiscing - elit\n- Integer nec odio\n## Details\n1. Sed cursus ante dapibus diam\n2. - Nulla quis sem at nibh elementum imperdiet\n3. Duis sagittis ipsum\n - ## Mixed Lists\n- Key Points:\n 1. Lorem ipsum dolor sit amet\n 2. - Consectetur adipiscing elit\n 3. Integer nec odio\n- Additional Info:\n - - Sed cursus ante dapibus diam\n - Nulla quis sem at nibh elementum imperdiet\n`, + content: MARKDOWN, } ); } ); // Assert // Needed to for the "Processed HTML piece" log. expect( console ).toHaveLogged(); + expect( getEditorTitle() ).toBe( 'Sample Document' ); expect( getEditorHtml() ).toMatchSnapshot(); } ); + it( 'parses a markdown heading into a title', async () => { + // Arrange + await initializeEditor( { + initialTitle: null, + } ); + + // Act + act( () => { + onContentUpdateCallback( { + content: `# Sample Document`, + } ); + } ); + + // Assert + // Needed to for the "Processed HTML piece" log. + expect( console ).toHaveLogged(); + expect( getEditorTitle() ).toBe( 'Sample Document' ); + expect( getEditorHtml() ).toBe( '' ); + } ); + it( 'parses standard text into blocks', async () => { // Arrange - await initializeEditor(); + await initializeEditor( { + initialTitle: null, + } ); // Act act( () => { onContentUpdateCallback( { - content: `Lorem ipsum dolor sit amet. Qui rerum quae sed sciunt - animi rem voluptate quas aut impedit accusamus ut`, + content: `Lorem ipsum dolor sit amet`, } ); } ); // Assert // Needed to for the "Processed HTML piece" log. expect( console ).toHaveLogged(); - expect( getEditorHtml() ).toMatchSnapshot(); + expect( getEditorTitle() ).toBe( 'Lorem ipsum dolor sit amet' ); + expect( getEditorHtml() ).toBe( '' ); } ); } ); } ); diff --git a/packages/editor/src/components/post-title/index.native.js b/packages/editor/src/components/post-title/index.native.js index dc7534c28a79fe..3d8987e8ce2e3b 100644 --- a/packages/editor/src/components/post-title/index.native.js +++ b/packages/editor/src/components/post-title/index.native.js @@ -18,11 +18,67 @@ import { store as blockEditorStore, RichText } from '@wordpress/block-editor'; import { store as editorStore } from '@wordpress/editor'; import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; +/** @typedef {import('./types').RichTextValue} RichTextValue */ + /** * Internal dependencies */ import styles from './style.scss'; +/** + * Inserts content with title + * + * This function processes the given content and title, updating the title + * and content based on certain conditions. If the content is an array of + * blocks, it will check the first block for a heading or paragraph to use + * as the title. If the content is a string, it will strip HTML and update + * the title and content accordingly. + * + * @param {string} title The post title. + * @param {Array | string} content The content to be processed. It can be an array of blocks or a string. + * @param {Function} onUpdateTitle Callback function to update the title. + * @param {Function} onUpdateContent Callback function to update the content. + * @param {RichTextValue} value The initial value object, default is an object with empty text. + */ +export function insertContentWithTitle( + title, + content, + onUpdateTitle, + onUpdateContent, + value = create( { text: '' } ) +) { + if ( ! content.length ) { + return; + } + + if ( typeof content !== 'string' ) { + const [ firstBlock ] = content; + + if ( + ! title && + ( firstBlock.name === 'core/heading' || + firstBlock.name === 'core/paragraph' ) + ) { + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( firstBlock.attributes.content ); + onUpdateTitle( contentNoHTML ); + onUpdateContent( content.slice( 1 ) ); + } else { + onUpdateContent( content ); + } + } else { + // Strip HTML to avoid unwanted HTML being added to the title. + // In the majority of cases it is assumed that HTML in the title + // is undesirable. + const contentNoHTML = stripHTML( content ); + + const newValue = insert( value, create( { html: contentNoHTML } ) ); + onUpdateTitle( toHTMLString( { value: newValue } ) ); + } +} + class PostTitle extends Component { constructor( props ) { super( props ); @@ -59,45 +115,24 @@ class PostTitle extends Component { } onPaste( { value, plainText, html } ) { - const { title, onInsertBlockAfter, onUpdate } = this.props; + const { + title, + onInsertBlockAfter: onInsertBlocks, + onUpdate, + } = this.props; const content = pasteHandler( { HTML: html, plainText, } ); - if ( ! content.length ) { - return; - } - - if ( typeof content !== 'string' ) { - const [ firstBlock ] = content; - - if ( - ! title && - ( firstBlock.name === 'core/heading' || - firstBlock.name === 'core/paragraph' ) - ) { - // Strip HTML to avoid unwanted HTML being added to the title. - // In the majority of cases it is assumed that HTML in the title - // is undesirable. - const contentNoHTML = stripHTML( - firstBlock.attributes.content - ); - onUpdate( contentNoHTML ); - onInsertBlockAfter( content.slice( 1 ) ); - } else { - onInsertBlockAfter( content ); - } - } else { - // Strip HTML to avoid unwanted HTML being added to the title. - // In the majority of cases it is assumed that HTML in the title - // is undesirable. - const contentNoHTML = stripHTML( content ); - - const newValue = insert( value, create( { html: contentNoHTML } ) ); - onUpdate( toHTMLString( { value: newValue } ) ); - } + insertContentWithTitle( + title, + content, + onUpdate, + onInsertBlocks, + value + ); } setRef( richText ) { diff --git a/packages/editor/src/components/provider/index.native.js b/packages/editor/src/components/provider/index.native.js index a7e05052b9acce..335f39afd414db 100644 --- a/packages/editor/src/components/provider/index.native.js +++ b/packages/editor/src/components/provider/index.native.js @@ -69,6 +69,7 @@ import { store as coreStore } from '@wordpress/core-data'; * Internal dependencies */ import EditorProvider from './index.js'; +import { insertContentWithTitle } from '../post-title'; class NativeEditorProvider extends Component { constructor() { @@ -316,15 +317,19 @@ class NativeEditorProvider extends Component { return false; } - onContentUpdate( { content } ) { - const { insertBlocks } = this.props; - const blocks = pasteHandler( { - plainText: content, + onContentUpdate( { content: rawContent } ) { + const { + editTitle, + onClearPostTitleSelection, + onInsertBlockAfter: onInsertBlocks, + title, + } = this.props; + const content = pasteHandler( { + plainText: rawContent, } ); - if ( blocks ) { - insertBlocks( blocks, undefined, undefined, false ); - } + insertContentWithTitle( title, content, editTitle, onInsertBlocks ); + onClearPostTitleSelection(); } serializeToNativeAction() { @@ -447,6 +452,7 @@ const ComposedNativeProvider = compose( [ resetEditorBlocks, updateEditorSettings, switchEditorMode, + togglePostTitleSelection, } = dispatch( editorStore ); const { clearSelectedBlock, @@ -480,6 +486,12 @@ const ComposedNativeProvider = compose( [ switchMode( mode ) { switchEditorMode( mode ); }, + onInsertBlockAfter( blocks ) { + insertBlocks( blocks, undefined, undefined, false ); + }, + onClearPostTitleSelection() { + togglePostTitleSelection( false ); + }, replaceBlock, }; } ), From 5462a962b2024268f4f0e419ede13a97c3a259c3 Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 30 May 2024 16:57:35 +0200 Subject: [PATCH 6/7] Remove setting the title along side with the content --- .../RNReactNativeGutenbergBridgeModule.java | 6 +----- .../wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java | 5 ++--- packages/react-native-bridge/ios/Gutenberg.swift | 5 +---- 3 files changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index 442e703fe0043c..315765edddf108 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -92,7 +92,6 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu private static final String MAP_KEY_REPLACE_BLOCK_HTML = "html"; private static final String MAP_KEY_REPLACE_BLOCK_BLOCK_ID = "clientId"; - private static final String MAP_KEY_UPDATE_CONTENT_TITLE = "title"; private static final String MAP_KEY_UPDATE_CONTENT = "content"; public static final String MAP_KEY_FEATURED_IMAGE_ID = "featuredImageId"; @@ -217,12 +216,9 @@ public void onRedoPressed() { emitToJS(EVENT_NAME_ON_REDO_PRESSED, null); } - public void onContentUpdate(String title, String content) { + public void onContentUpdate(String content) { WritableMap writableMap = new WritableNativeMap(); - if (title != null) { - writableMap.putString(MAP_KEY_UPDATE_CONTENT_TITLE, title); - } writableMap.putString(MAP_KEY_UPDATE_CONTENT, content); emitToJS(EVENT_NAME_ON_CONTENT_UPDATE, writableMap); } diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index 7d42800a4b4011..4477dfc115b7c4 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -846,9 +846,8 @@ public void onRedoPressed() { mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onRedoPressed(); } - public void onContentUpdate(String title, String content) { - mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule() - .onContentUpdate(title, content); + public void onContentUpdate(String content) { + mRnReactNativeGutenbergBridgePackage.getRNReactNativeGutenbergBridgeModule().onContentUpdate(content); } public void setTitle(String title) { diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index 1b8f5eee8f3a82..2273801f1eeb9c 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -222,11 +222,8 @@ public class Gutenberg: UIResponder { bridgeModule.sendEventIfNeeded(.connectionStatusChange, body: data) } - public func onContentUpdate(title: String? = nil, content: String) { + public func onContentUpdate(content: String) { var payload: [String: Any] = ["content": content] - if let title = title { - payload["title"] = title - } bridgeModule.sendEventIfNeeded(.onContentUpdate, body: payload) } From 2735ff5e68247118da5804016c73d326ff62443d Mon Sep 17 00:00:00 2001 From: Gerardo Date: Thu, 30 May 2024 17:01:40 +0200 Subject: [PATCH 7/7] Update changelog --- packages/react-native-editor/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index b5ec9c7c0c321d..beb32d70e60725 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -11,6 +11,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] Prevent deleting content when backspacing in the first Paragraph block [#62069] +- [internal] Adds new bridge functionality for updating content [#61796] ## 1.119.0 - [internal] Remove circular dependencies within the components package [#61102]