diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index 7c4348270aaa0a..285675c1585ab2 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -26,6 +26,7 @@ import { LinkSettingsNavigation, BottomSheetTextControl, FooterMessageLink, + Badge, } from '@wordpress/components'; import { BlockCaption, @@ -437,6 +438,7 @@ export class ImageEdit extends Component { image, clientId, imageDefaultSize, + featuredImageId, } = this.props; const { align, url, alt, id, sizeSlug, className } = attributes; @@ -445,6 +447,8 @@ export class ImageEdit extends Component { imageDefaultSize, ] ); + const isFeaturedImage = featuredImageId === attributes.id; + const getToolbarEditButton = ( open ) => ( @@ -507,7 +511,7 @@ export class ImageEdit extends Component { }; const getImageComponent = ( openMediaOptions, getMediaOptions ) => ( - <> + - + ); return ( @@ -591,12 +595,14 @@ export default compose( [ withSelect( ( select, props ) => { const { getMedia } = select( coreStore ); const { getSettings } = select( blockEditorStore ); + const { getEditedPostAttribute } = select( 'core/editor' ); const { attributes: { id, url }, isSelected, } = props; const { imageSizes, imageDefaultSize } = getSettings(); const isNotFileUrl = id && getProtocol( url ) !== 'file:'; + const featuredImageId = getEditedPostAttribute( 'featured_media' ); const shouldGetMedia = ( isSelected && isNotFileUrl ) || @@ -610,6 +616,7 @@ export default compose( [ image: shouldGetMedia ? getMedia( id ) : null, imageSizes, imageDefaultSize, + featuredImageId, }; } ), withPreferredColorScheme, diff --git a/packages/components/src/index.native.js b/packages/components/src/index.native.js index 27ecb9022ef672..e910f3386c7e70 100644 --- a/packages/components/src/index.native.js +++ b/packages/components/src/index.native.js @@ -89,6 +89,7 @@ export { default as ImageEditingButton } from './mobile/image/image-editing-butt export { default as InserterButton } from './mobile/inserter-button'; export { setClipboard, getClipboard } from './mobile/clipboard'; export { default as AudioPlayer } from './mobile/audio-player'; +export { default as Badge } from './mobile/badge'; // Utils export { colorsUtils } from './mobile/color-settings/utils'; diff --git a/packages/components/src/mobile/badge/README.md b/packages/components/src/mobile/badge/README.md new file mode 100644 index 00000000000000..8e6453935f985f --- /dev/null +++ b/packages/components/src/mobile/badge/README.md @@ -0,0 +1,31 @@ +# Badge + +The Badge component is designed to be wrapped around another component. It adds a "badge" with some text to the child component's upper left. + +An example can be found in the image block. After setting an image as featured, a "featured" badge overlays the block. + +### Usage + +```jsx +return + + +; +``` + +### Props + +#### label + +The text that will be displayed within the Badge component. + +- Type: `String` +- Required: Yes + +#### show + +An optional boolean to determine whether the badge is displayed. + +- Type: `Boolean` +- Required: No +- Default: `true` diff --git a/packages/components/src/mobile/badge/index.native.js b/packages/components/src/mobile/badge/index.native.js new file mode 100644 index 00000000000000..7111257920dc9c --- /dev/null +++ b/packages/components/src/mobile/badge/index.native.js @@ -0,0 +1,27 @@ +/** + * External dependencies + */ +import { View, Text } from 'react-native'; + +/** + * WordPress dependencies + */ +import { withPreferredColorScheme } from '@wordpress/compose'; + +/** + * Internal dependencies + */ +import styles from './style.scss'; + +const Badge = ( { label, children, show = true } ) => { + return ( + <> + { children } + + { show && { label } } + + + ); +}; + +export default withPreferredColorScheme( Badge ); diff --git a/packages/components/src/mobile/badge/style.scss b/packages/components/src/mobile/badge/style.scss new file mode 100644 index 00000000000000..beae07d5360d01 --- /dev/null +++ b/packages/components/src/mobile/badge/style.scss @@ -0,0 +1,15 @@ +.badgeContainer { + position: absolute; + top: 0; + left: 0; + z-index: 2; +} + +.badge { + padding: 10px; + margin: 8px; + color: #fff; + border-radius: 3px; + background-color: $gray-70; + border-color: $gray-70; +} diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index 01fdca23b2604e..cc79e0fb006c3d 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -13,8 +13,12 @@ import { EditorProvider } from '@wordpress/editor'; import { parse, serialize, store as blocksStore } from '@wordpress/blocks'; import { withDispatch, withSelect } from '@wordpress/data'; import { compose } from '@wordpress/compose'; -import { subscribeSetFocusOnTitle } from '@wordpress/react-native-bridge'; +import { + subscribeSetFocusOnTitle, + subscribeFeaturedImageIdNativeUpdated, +} from '@wordpress/react-native-bridge'; import { SlotFillProvider } from '@wordpress/components'; +import { store as coreStore } from '@wordpress/core-data'; /** * Internal dependencies @@ -77,6 +81,8 @@ class Editor extends Component { } componentDidMount() { + const { editEntityRecord, postType, postId } = this.props; + this.subscriptionParentSetFocusOnTitle = subscribeSetFocusOnTitle( () => { if ( this.postTitleRef ) { @@ -84,12 +90,24 @@ class Editor extends Component { } } ); + + this.subscriptionParentFeaturedImageIdNativeUpdated = subscribeFeaturedImageIdNativeUpdated( + ( payload ) => { + editEntityRecord( 'postType', postType, postId, { + featured_media: payload.featuredImageId, + } ); + } + ); } componentWillUnmount() { if ( this.subscriptionParentSetFocusOnTitle ) { this.subscriptionParentSetFocusOnTitle.remove(); } + + if ( this.subscribeFeaturedImageIdNativeUpdated ) { + this.subscribeFeaturedImageIdNativeUpdated.remove(); + } } setTitleRef( titleRef ) { @@ -107,6 +125,7 @@ class Editor extends Component { post, postId, postType, + featuredImageId, initialHtml, ...props } = this.props; @@ -124,6 +143,7 @@ class Editor extends Component { title: { raw: props.initialTitle || '', }, + featured_media: featuredImageId, content: { // make sure the post content is in sync with gutenberg store // to avoid marking the post as modified when simply loaded @@ -173,8 +193,10 @@ export default compose( [ } ), withDispatch( ( dispatch ) => { const { switchEditorMode } = dispatch( editPostStore ); + const { editEntityRecord } = dispatch( coreStore ); return { switchEditorMode, + editEntityRecord, }; } ), ] )( Editor ); diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index ed3490932c5920..01ba63621525d8 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -44,6 +44,10 @@ interface MediaSaveEventEmitter { void onReplaceMediaFilesEditedBlock(final String mediaFiles, final String blockId); } + interface FeaturedImageEmitter { + void sendToJSFeaturedImageId(int mediaId); + } + interface ReplaceUnsupportedBlockCallback { void replaceUnsupportedBlock(String content, String blockId); } 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 5c440f4d4cde72..7cdd5d9c33cee1 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 @@ -68,6 +68,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"; + public static final String MAP_KEY_FEATURED_IMAGE_ID = "featuredImageId"; + private boolean mIsDarkMode; public RNReactNativeGutenbergBridgeModule(ReactApplicationContext reactContext, diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java index 8643d4ca37b0b5..0755ff3c68a925 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java @@ -10,6 +10,7 @@ import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaUploadEventEmitter; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaSaveEventEmitter; +import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.FeaturedImageEmitter; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; @@ -18,8 +19,9 @@ import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_NEW_ID; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_URL; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FINAL_SAVE_RESULT_SUCCESS_VALUE; +import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_FEATURED_IMAGE_ID; -public class DeferredEventEmitter implements MediaUploadEventEmitter, MediaSaveEventEmitter { +public class DeferredEventEmitter implements MediaUploadEventEmitter, MediaSaveEventEmitter, FeaturedImageEmitter { public interface JSEventEmitter { void emitToJS(String eventName, @Nullable WritableMap data); } @@ -40,6 +42,8 @@ public interface JSEventEmitter { private static final String EVENT_NAME_MEDIA_SAVE = "mediaSave"; private static final String EVENT_NAME_MEDIA_REPLACE_BLOCK = "replaceBlock"; + private static final String EVENT_FEATURED_IMAGE_ID_NATIVE_UPDATED = "featuredImageIdNativeUpdated"; + private static final String MAP_KEY_MEDIA_FILE_STATE = "state"; private static final String MAP_KEY_MEDIA_FILE_MEDIA_ACTION_PROGRESS = "progress"; private static final String MAP_KEY_MEDIA_FILE_MEDIA_SERVER_ID = "mediaServerId"; @@ -205,6 +209,12 @@ public void onMediaCollectionSaveResult(String firstMediaIdInCollection, boolean } } + public void sendToJSFeaturedImageId(int mediaId) { + WritableMap writableMap = new WritableNativeMap(); + writableMap.putInt(MAP_KEY_FEATURED_IMAGE_ID, mediaId); + queueActionToJS(EVENT_FEATURED_IMAGE_ID_NATIVE_UPDATED, writableMap); + } + @Override public void onReplaceMediaFilesEditedBlock(String mediaFiles, String blockId) { WritableMap writableMap = new WritableNativeMap(); writableMap.putString(MAP_KEY_REPLACE_BLOCK_HTML, mediaFiles); diff --git a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt index 93e32a1da6a64c..5e1e712cb414ab 100644 --- a/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt +++ b/packages/react-native-bridge/android/react-native-bridge/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt @@ -12,6 +12,7 @@ data class GutenbergProps @JvmOverloads constructor( val enableAudioBlock: Boolean, val localeSlug: String, val postType: String, + val featuredImageId: Int, val editorTheme: Bundle?, val translations: Bundle, val isDarkMode: Boolean, @@ -23,6 +24,7 @@ data class GutenbergProps @JvmOverloads constructor( putString(PROP_INITIAL_TITLE, "") putString(PROP_LOCALE, localeSlug) putString(PROP_POST_TYPE, postType) + putInt(PROP_INITIAL_FEATURED_IMAGE_ID, featuredImageId) putBundle(PROP_TRANSLATIONS, translations) putBoolean(PROP_INITIAL_HTML_MODE_ENABLED, htmlModeEnabled) @@ -56,6 +58,7 @@ data class GutenbergProps @JvmOverloads constructor( private const val PROP_INITIAL_TITLE = "initialTitle" private const val PROP_INITIAL_HTML_MODE_ENABLED = "initialHtmlModeEnabled" private const val PROP_POST_TYPE = "postType" + private const val PROP_INITIAL_FEATURED_IMAGE_ID = "featuredImageId" private const val PROP_LOCALE = "locale" private const val PROP_TRANSLATIONS = "translations" private const val PROP_COLORS = "colors" 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 5a8adc32b49f7e..ba2ecc8aca7067 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 @@ -956,6 +956,10 @@ public void mediaIdChanged(final String oldId, final String newId, final String mDeferredEventEmitter.onMediaIdChanged(oldId, newId, oldUrl); } + public void sendToJSFeaturedImageId(int mediaId) { + mDeferredEventEmitter.sendToJSFeaturedImageId(mediaId); + } + public void replaceUnsupportedBlock(String content, String blockId) { if (mReplaceUnsupportedBlockCallback != null) { mReplaceUnsupportedBlockCallback.replaceUnsupportedBlock(content, blockId); diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 431b4936f63df1..73099bd1eff3db 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -62,6 +62,15 @@ export function subscribeUpdateHtml( callback ) { return gutenbergBridgeEvents.addListener( 'updateHtml', callback ); } +export function subscribeFeaturedImageIdNativeUpdated( callback ) { + return isAndroid + ? gutenbergBridgeEvents.addListener( + 'featuredImageIdNativeUpdated', + callback + ) + : undefined; +} + /** * Request to subscribe to mediaUpload events * diff --git a/packages/react-native-editor/CHANGELOG.md b/packages/react-native-editor/CHANGELOG.md index 04ee026e69469d..35c22aabbdadb7 100644 --- a/packages/react-native-editor/CHANGELOG.md +++ b/packages/react-native-editor/CHANGELOG.md @@ -12,6 +12,7 @@ For each user feature we should also add a importance categorization label to i ## Unreleased - [*] Image block: Improve text entry for long alt text. [#29670] +- [*] Image block: Add a "featured" banner. (Android only) [#30806] ## 1.50.0 diff --git a/packages/react-native-editor/src/index.js b/packages/react-native-editor/src/index.js index 5887201b6d10fa..81abf5bd7eb9c7 100644 --- a/packages/react-native-editor/src/index.js +++ b/packages/react-native-editor/src/index.js @@ -70,6 +70,7 @@ const setupInitHooks = () => { initialData, initialTitle, postType, + featuredImageId, colors, gradients, } = props; @@ -93,6 +94,7 @@ const setupInitHooks = () => { initialHtmlModeEnabled: props.initialHtmlModeEnabled, initialTitle, postType, + featuredImageId, capabilities, colors, gradients, diff --git a/test/native/setup.js b/test/native/setup.js index abc27aacf1f1af..9c89c8845d999a 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -31,6 +31,7 @@ jest.mock( '@wordpress/react-native-bridge', () => { subscribeSetTitle: jest.fn(), subscribeSetFocusOnTitle: jest.fn(), subscribeUpdateHtml: jest.fn(), + subscribeFeaturedImageIdNativeUpdated: jest.fn(), subscribeMediaAppend: jest.fn(), subscribeAndroidModalClosed: jest.fn(), subscribeUpdateTheme: jest.fn(),