diff --git a/packages/block-editor/src/components/media-upload/index.native.js b/packages/block-editor/src/components/media-upload/index.native.js index 05aabd9a9a3cd..8f793e4c0fa3f 100644 --- a/packages/block-editor/src/components/media-upload/index.native.js +++ b/packages/block-editor/src/components/media-upload/index.native.js @@ -39,6 +39,7 @@ export const OPTION_TAKE_VIDEO = __( 'Take a Video' ); export const OPTION_TAKE_PHOTO = __( 'Take a Photo' ); export const OPTION_TAKE_PHOTO_OR_VIDEO = __( 'Take a Photo or Video' ); export const OPTION_INSERT_FROM_URL = __( 'Insert from URL' ); +export const OPTION_WORDPRESS_MEDIA_LIBRARY = __( 'WordPress Media Library' ); const URL_MEDIA_SOURCE = 'URL'; @@ -78,6 +79,8 @@ export class MediaUpload extends Component { } getAllSources() { + const { onSelectURL } = this.props; + const cameraImageSource = { id: mediaSources.deviceCamera, // ID is the value sent to native. value: mediaSources.deviceCamera + '-IMAGE', // This is needed to diferenciate image-camera from video-camera sources. @@ -124,16 +127,17 @@ export class MediaUpload extends Component { id: URL_MEDIA_SOURCE, value: URL_MEDIA_SOURCE, label: __( 'Insert from URL' ), - types: [ MEDIA_TYPE_AUDIO ], + types: [ MEDIA_TYPE_AUDIO, MEDIA_TYPE_IMAGE ], icon: globe, }; + // Only include `urlSource` option if `onSelectURL` prop is present, in order to match the web behavior. const internalSources = [ deviceLibrarySource, cameraImageSource, cameraVideoSource, siteLibrarySource, - urlSource, + ...( onSelectURL ? [ urlSource ] : [] ), ]; return internalSources.concat( this.state.otherMediaOptions ); diff --git a/packages/block-editor/src/components/media-upload/test/index.native.js b/packages/block-editor/src/components/media-upload/test/index.native.js index e54a9f5219321..73cbf41b7dfc1 100644 --- a/packages/block-editor/src/components/media-upload/test/index.native.js +++ b/packages/block-editor/src/components/media-upload/test/index.native.js @@ -20,6 +20,7 @@ import { OPTION_TAKE_VIDEO, OPTION_TAKE_PHOTO, OPTION_INSERT_FROM_URL, + OPTION_WORDPRESS_MEDIA_LIBRARY, } from '../index'; const MEDIA_URL = 'http://host.media.type'; @@ -33,8 +34,11 @@ describe( 'MediaUpload component', () => { expect( wrapper ).toBeTruthy(); } ); - it( 'shows right media capture option for media type', () => { - const expectOptionForMediaType = ( mediaType, expectedOption ) => { + describe( 'Media capture options for different media block types', () => { + const expectOptionForMediaType = async ( + mediaType, + expectedOptions + ) => { const wrapper = render( { ); fireEvent.press( wrapper.getByText( 'Open Picker' ) ); - wrapper.getByText( expectedOption ); + expectedOptions.forEach( ( item ) => { + const option = wrapper.getByText( item ); + expect( option ).toBeVisible(); + } ); }; - expectOptionForMediaType( MEDIA_TYPE_IMAGE, OPTION_TAKE_PHOTO ); - expectOptionForMediaType( MEDIA_TYPE_VIDEO, OPTION_TAKE_VIDEO ); - expectOptionForMediaType( MEDIA_TYPE_AUDIO, OPTION_INSERT_FROM_URL ); + + it( 'shows the correct media capture options for the Image block', () => { + expectOptionForMediaType( MEDIA_TYPE_IMAGE, [ + OPTION_TAKE_PHOTO, + OPTION_WORDPRESS_MEDIA_LIBRARY, + OPTION_INSERT_FROM_URL, + ] ); + } ); + + it( 'shows the correct media capture options for the Video block', () => { + expectOptionForMediaType( MEDIA_TYPE_VIDEO, [ + OPTION_TAKE_VIDEO, + OPTION_WORDPRESS_MEDIA_LIBRARY, + ] ); + } ); + + it( 'shows the correct media capture options for the Audio block', () => { + expectOptionForMediaType( MEDIA_TYPE_AUDIO, [ + OPTION_INSERT_FROM_URL, + ] ); + } ); } ); const expectMediaPickerForOption = ( diff --git a/packages/block-library/src/image/edit.native.js b/packages/block-library/src/image/edit.native.js index f9025bf2333f9..24df9fce2866a 100644 --- a/packages/block-library/src/image/edit.native.js +++ b/packages/block-library/src/image/edit.native.js @@ -1,7 +1,12 @@ /** * External dependencies */ -import { View, TouchableWithoutFeedback } from 'react-native'; +import { + ActivityIndicator, + Image as RNImage, + TouchableWithoutFeedback, + View, +} from 'react-native'; import { useRoute } from '@react-navigation/native'; /** @@ -45,7 +50,7 @@ import { blockSettingsScreens, } from '@wordpress/block-editor'; import { __, _x, sprintf } from '@wordpress/i18n'; -import { getProtocol, hasQueryArg } from '@wordpress/url'; +import { getProtocol, hasQueryArg, isURL } from '@wordpress/url'; import { doAction, hasAction } from '@wordpress/hooks'; import { compose, withPreferredColorScheme } from '@wordpress/compose'; import { withSelect, withDispatch } from '@wordpress/data'; @@ -57,6 +62,7 @@ import { } from '@wordpress/icons'; import { store as coreStore } from '@wordpress/core-data'; import { store as editPostStore } from '@wordpress/edit-post'; +import { store as noticesStore } from '@wordpress/notices'; /** * Internal dependencies @@ -207,6 +213,7 @@ export class ImageEdit extends Component { this.onImagePressed = this.onImagePressed.bind( this ); this.onSetFeatured = this.onSetFeatured.bind( this ); this.onFocusCaption = this.onFocusCaption.bind( this ); + this.onSelectURL = this.onSelectURL.bind( this ); this.updateAlignment = this.updateAlignment.bind( this ); this.accessibilityLabelCreator = this.accessibilityLabelCreator.bind( this @@ -461,6 +468,45 @@ export class ImageEdit extends Component { } ); } + onSelectURL( newURL ) { + const { + createErrorNotice, + imageDefaultSize, + setAttributes, + } = this.props; + + if ( isURL( newURL ) ) { + this.setState( { + isFetchingImage: true, + } ); + + // Use RN's Image.getSize to determine if URL is a valid image + RNImage.getSize( + newURL, + () => { + setAttributes( { + url: newURL, + id: undefined, + width: undefined, + height: undefined, + sizeSlug: imageDefaultSize, + } ); + this.setState( { + isFetchingImage: false, + } ); + }, + () => { + createErrorNotice( __( 'Image file not found.' ) ); + this.setState( { + isFetchingImage: false, + } ); + } + ); + } else { + createErrorNotice( __( 'Invalid URL.' ) ); + } + } + onFocusCaption() { if ( this.props.onFocus ) { this.props.onFocus(); @@ -484,6 +530,14 @@ export class ImageEdit extends Component { ); } + showLoadingIndicator() { + return ( + + + + ); + } + getWidth() { const { attributes } = this.props; const { align, width } = attributes; @@ -611,7 +665,7 @@ export class ImageEdit extends Component { } render() { - const { isCaptionSelected } = this.state; + const { isCaptionSelected, isFetchingImage } = this.state; const { attributes, isSelected, @@ -713,9 +767,11 @@ export class ImageEdit extends Component { if ( ! url ) { return ( + { isFetchingImage && this.showLoadingIndicator() } { return ( + { isFetchingImage && + this.showLoadingIndicator() } { return getImageComponent( open, getMediaOptions ); } } @@ -881,7 +940,10 @@ export default compose( [ }; } ), withDispatch( ( dispatch ) => { + const { createErrorNotice } = dispatch( noticesStore ); + return { + createErrorNotice, closeSettingsBottomSheet() { dispatch( editPostStore ).closeGeneralSidebar(); }, diff --git a/packages/block-library/src/image/styles.native.scss b/packages/block-library/src/image/styles.native.scss index 476bb8b2be471..0bcec21b57189 100644 --- a/packages/block-library/src/image/styles.native.scss +++ b/packages/block-library/src/image/styles.native.scss @@ -57,3 +57,14 @@ .removeFeaturedButton { color: $alert-red; } + +.image__loading { + align-items: center; + background-color: rgba(10, 10, 10, 0.5); + flex: 1; + height: 100%; + justify-content: center; + position: absolute; + width: 100%; + z-index: 1; +}