From bc3dc553925436054a04c261d2e189fba53e1d82 Mon Sep 17 00:00:00 2001 From: mzorz Date: Tue, 10 Nov 2020 14:44:27 -0300 Subject: [PATCH] Media files collection block mobile (#26721) * updated initial example html content file with story block * adding method requestStoryCreatorLoad() to the bridge * added new interface OnStoryCreatorRequestListener * passing mediaFiles and blockId over the bridge in requestStoryCreatorLoad() * prettier fix * added gray-700 color, missing if not after retiring and changes made in (#25213) * removed jetpack stories example block from initial demo load * Mobile Stories block (part2: on done) (#25771) * added mobile StoryUpdateProgress component and bridge code to send/receive save progress * updated WPAndroid bridge DeferredEventEmitter to handle Story save events separately * changed all Save event interface methods to use String ids instead of int, and removed serverMediaId params as these don't apply while saving locally * redefined upload/save state constants * added onStorySaveResult handling to bridge, and renamed STORY_SAVE_STATE_* events to MEDIA_SAVE_STATE_* where appropriate * checking for matches of mediaId in mediaFiles while saving to send save progress updates * added mediaModelCreated() method to the bridge, so a new ID can be assigned to a mediaFile in a Story block * mediaId should always be a string in mediaFiles so, converting to avoid strict comparison to fail * removed commented code * updated documentation * added missing implementation of method storySaveSync() in demo app * fixed prettier warning * Update packages/block-editor/src/components/story-update-progress/README.md Co-authored-by: Joel Dean * Update packages/block-editor/src/components/story-update-progress/README.md Co-authored-by: Joel Dean * Mobile Stories block (part3: refactor / rename) (#26005) * renames for generic media files collection block and BlockMediaUpdateProgres * referencing the right props method in finishMediaSaveWithFailure * mistaken renames of parameters in bridge methods * renamed more abstract/generic method names requestMediaFilesEditorLoad * removed extra whtie space * renamed argument type Co-authored-by: Joel Dean * Update packages/block-editor/src/components/block-media-update-progress/README.md Co-authored-by: Paul Von Schrottky * using array.some() to find element in array * prettified function call * removed commented code * Media Files Collection (part4: error handling) (#26008) * added mobile StoryUpdateProgress component and bridge code to send/receive save progress * updated WPAndroid bridge DeferredEventEmitter to handle Story save events separately * changed all Save event interface methods to use String ids instead of int, and removed serverMediaId params as these don't apply while saving locally * redefined upload/save state constants * added onStorySaveResult handling to bridge, and renamed STORY_SAVE_STATE_* events to MEDIA_SAVE_STATE_* where appropriate * checking for matches of mediaId in mediaFiles while saving to send save progress updates * added mediaModelCreated() method to the bridge, so a new ID can be assigned to a mediaFile in a Story block * mediaId should always be a string in mediaFiles so, converting to avoid strict comparison to fail * removed commented code * updated documentation * added missing implementation of method storySaveSync() in demo app * fixed prettier warning * renames for generic media files collection block and BlockMediaUpdateProgres * referencing the right props method in finishMediaSaveWithFailure * mistaken renames of parameters in bridge methods * renamed more abstract/generic method names requestMediaFilesEditorLoad * removed extra whtie space * renamed argument type * renamed event paramter name to easier to understand 'success' boolean, matching the native counterpart * added cancel and retry bridge methods specific for mediaFiles collection based blocks * added requestMediaFilesSaveCancelDialog bridge method * renamed bridge method mediaModelCreatedForFile for more general purpose mediaIdChanged * Add missing iOS bridge declarations * added jsdoc like description for methods * removed unneeded return statement in some bridge methods * Update packages/react-native-bridge/index.js Co-authored-by: Joel Dean * Update packages/react-native-bridge/index.js Co-authored-by: Joel Dean * Update packages/react-native-bridge/index.js Co-authored-by: Joel Dean * fixed typo, added punctuation Co-authored-by: eToledo Co-authored-by: Joel Dean * [RNMobile] added jetpack node_modules folder to cleanup step in JSbundle building (#26247) * added mobile StoryUpdateProgress component and bridge code to send/receive save progress * updated WPAndroid bridge DeferredEventEmitter to handle Story save events separately * changed all Save event interface methods to use String ids instead of int, and removed serverMediaId params as these don't apply while saving locally * redefined upload/save state constants * added onStorySaveResult handling to bridge, and renamed STORY_SAVE_STATE_* events to MEDIA_SAVE_STATE_* where appropriate * checking for matches of mediaId in mediaFiles while saving to send save progress updates * added mediaModelCreated() method to the bridge, so a new ID can be assigned to a mediaFile in a Story block * mediaId should always be a string in mediaFiles so, converting to avoid strict comparison to fail * removed commented code * updated documentation * added missing implementation of method storySaveSync() in demo app * fixed prettier warning * renames for generic media files collection block and BlockMediaUpdateProgres * referencing the right props method in finishMediaSaveWithFailure * mistaken renames of parameters in bridge methods * renamed more abstract/generic method names requestMediaFilesEditorLoad * removed extra whtie space * renamed argument type * renamed event paramter name to easier to understand 'success' boolean, matching the native counterpart * added cancel and retry bridge methods specific for mediaFiles collection based blocks * added requestMediaFilesSaveCancelDialog bridge method * renamed bridge method mediaModelCreatedForFile for more general purpose mediaIdChanged * added jetpack node_modules folder to cleanup step in JSbundle building tasks * Mobile Stories block - unit tests for BlockMediaUpdateProgress (#26423) * adding upload progress tests on BLockMediaUpdateProgress component (mediaFiles collection) * added save event tests for BlockMediaUpdateProgress component * ids in mediaFiles array are always strings, fixed that * updated function name passed by props * fixed mediaFiles const passing for saving, using tempMediaFiles * fixed state change for saveReset signal, was changing upload state instead * fixed typo * fixed using save state var * changed to more descriptive test names * removed commented line * renamed vars to match bridge signal name * Mobile stories block: hide behind feature flag (#26522) * hide Story block in non-DEV builds * prettified * passing enableStories feature flag in GutenbergProps and hiding block from picker if false * added space * renamed bridge GutenbergProps property to agnostic abstraction mediaFilesCollectionBlock * renamed var on Capabilities enum * removed stories specific block behavior * added Jdoc descriptions * [RNMobile] iOS side of bridge for media collection blocks (#26704) * Activate media files collection capabilitie on iOS example app * Implement iOS side of Bridge for Media Collection blocks * Update packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift Co-authored-by: Paul Von Schrottky Co-authored-by: Paul Von Schrottky Co-authored-by: Joel Dean Co-authored-by: Paul Von Schrottky Co-authored-by: eToledo --- packages/base-styles/_colors.native.scss | 1 + .../block-media-update-progress/README.md | 104 ++++ .../index.native.js | 299 +++++++++++ .../styles.native.scss | 9 + .../test/index.native.js | 500 ++++++++++++++++++ .../src/components/index.native.js | 1 + packages/edit-post/src/editor.native.js | 6 +- .../react-native-bridge/android/build.gradle | 2 +- .../GutenbergBridgeJS2Parent.java | 26 + .../RNReactNativeGutenbergBridgeModule.java | 29 + .../WPAndroidGlue/DeferredEventEmitter.java | 94 +++- .../mobile/WPAndroidGlue/GutenbergProps.kt | 3 + .../WPAndroidGlue/WPAndroidGlueCode.java | 98 +++- packages/react-native-bridge/index.js | 120 ++++- .../react-native-bridge/ios/Gutenberg.swift | 46 +- .../ios/GutenbergBridgeDelegate.swift | 33 +- .../ios/RNReactNativeGutenbergBridge.m | 6 + .../ios/RNReactNativeGutenbergBridge.swift | 38 ++ .../java/com/gutenberg/MainApplication.java | 31 +- .../GutenbergViewController.swift | 22 +- test/native/setup.js | 1 + 21 files changed, 1438 insertions(+), 31 deletions(-) create mode 100644 packages/block-editor/src/components/block-media-update-progress/README.md create mode 100644 packages/block-editor/src/components/block-media-update-progress/index.native.js create mode 100644 packages/block-editor/src/components/block-media-update-progress/styles.native.scss create mode 100644 packages/block-editor/src/components/block-media-update-progress/test/index.native.js diff --git a/packages/base-styles/_colors.native.scss b/packages/base-styles/_colors.native.scss index f4a0f6a05f5558..3e63958cc4624a 100644 --- a/packages/base-styles/_colors.native.scss +++ b/packages/base-styles/_colors.native.scss @@ -40,6 +40,7 @@ $gray: #87a6bc; $gray-light: lighten($gray, 33%); //#f3f6f8 $gray-dark: darken($gray, 38%); //#2e4453 $gray-900: #1a1a1a; +$gray-700: #757575; // $gray-text: ideal for standard, non placeholder text // $gray-text-min: minimum contrast needed for WCAG 2.0 AA on white background diff --git a/packages/block-editor/src/components/block-media-update-progress/README.md b/packages/block-editor/src/components/block-media-update-progress/README.md new file mode 100644 index 00000000000000..1302bb237d6f91 --- /dev/null +++ b/packages/block-editor/src/components/block-media-update-progress/README.md @@ -0,0 +1,104 @@ +BlockMediaUpdateProgress +=================== + +`BlockMediaUpdateProgress` shows a progress bar while the media files associated with a media-containing block are being saved first and uploaded later + +## Usage + +Usage example + +```jsx +import { ImageBackground, Text, View } from 'react-native'; +import { + BlockMediaUpdateProgress, +} from '@wordpress/block-editor'; + +function BlockUpdatingProgress( { url, id } ) { + return ( + { + return ( + + { isSaveFailed && + + { retryMessage } + + } + + ); + } } + /> + ); +} +``` + +## Props + +### mediaFiles + +A collection of media ID that identifies the current collection of files represented in this media container block. + +- Type: `Array` +- Required: Yes +- Platform: Mobile + +### renderContent + +Content to be rendered along with the progress bar, usually the thumbnail of the media being uploaded. + +- Type: `React components` +- Required: Yes +- Platform: Mobile + +It passes an object containing the following properties: + +`{ isUploadInProgress, isUploadFailed, isSaveInProgress, isSaveFailed, retryMessage }` + +### onUpdateMediaProgress + +Callback called when the progress of the upload is updated. + +- Type: `Function` +- Required: No +- Platform: Mobile + +The argument of the callback is an object containing the following properties: + +`{ mediaId, mediaUrl, progress, state }` + +### onFinishMediaUploadWithSuccess + +Callback called when the media file has been uploaded successfully. + +- Type: `Function` +- Required: No +- Platform: Mobile + +The argument of the callback is an object containing the following properties: + +`{ mediaId, mediaServerId, mediaUrl, progress, state }` + +### onFinishMediaUploadWithFailure + +Callback called when the media file couldn't be uploaded. + +- Type: `Function` +- Required: No +- Platform: Mobile + +The argument of the callback is an object containing the following properties: + +`{ mediaId, progress, state }` + + +### onMediaUploadStateReset + +Callback called when the media upload is reset + +- Type: `Function` +- Required: No +- Platform: Mobile diff --git a/packages/block-editor/src/components/block-media-update-progress/index.native.js b/packages/block-editor/src/components/block-media-update-progress/index.native.js new file mode 100644 index 00000000000000..279ef17d6e9987 --- /dev/null +++ b/packages/block-editor/src/components/block-media-update-progress/index.native.js @@ -0,0 +1,299 @@ +/** + * External dependencies + */ +import React from 'react'; +import { View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { Spinner } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { + subscribeMediaUpload, + subscribeMediaSave, +} from '@wordpress/react-native-bridge'; + +/** + * Internal dependencies + */ +import styles from './styles.scss'; + +export const MEDIA_UPLOAD_STATE_UPLOADING = 1; +export const MEDIA_UPLOAD_STATE_SUCCEEDED = 2; +export const MEDIA_UPLOAD_STATE_FAILED = 3; +export const MEDIA_UPLOAD_STATE_RESET = 4; + +export const MEDIA_SAVE_STATE_SAVING = 5; +export const MEDIA_SAVE_STATE_SUCCEEDED = 6; +export const MEDIA_SAVE_STATE_FAILED = 7; +export const MEDIA_SAVE_STATE_RESET = 8; +export const MEDIA_SAVE_FINAL_STATE_RESULT = 9; +export const MEDIA_SAVE_MEDIAID_CHANGED = 10; + +export class BlockMediaUpdateProgress extends React.Component { + constructor( props ) { + super( props ); + + this.state = { + progress: 0, + isSaveInProgress: false, + isSaveFailed: false, + isUploadInProgress: false, + isUploadFailed: false, + }; + + this.mediaUpload = this.mediaUpload.bind( this ); + this.mediaSave = this.mediaSave.bind( this ); + } + + componentDidMount() { + this.addMediaUploadListener(); + this.addMediaSaveListener(); + } + + componentWillUnmount() { + this.removeMediaUploadListener(); + this.removeMediaSaveListener(); + } + + mediaIdContainedInMediaFiles( mediaId, mediaFiles ) { + if ( mediaId !== undefined && mediaFiles !== undefined ) { + return mediaFiles.some( + ( element ) => element.id === mediaId.toString() + ); + } + return false; + } + + mediaUpload( payload ) { + const { mediaFiles } = this.props; + + if ( + this.mediaIdContainedInMediaFiles( payload.mediaId, mediaFiles ) === + false + ) { + return; + } + + switch ( payload.state ) { + case MEDIA_UPLOAD_STATE_UPLOADING: + this.updateMediaUploadProgress( payload ); + break; + case MEDIA_UPLOAD_STATE_SUCCEEDED: + this.finishMediaUploadWithSuccess( payload ); + break; + case MEDIA_UPLOAD_STATE_FAILED: + this.finishMediaUploadWithFailure( payload ); + break; + case MEDIA_UPLOAD_STATE_RESET: + this.mediaUploadStateReset( payload ); + break; + } + } + + mediaSave( payload ) { + const { mediaFiles } = this.props; + + if ( + this.mediaIdContainedInMediaFiles( payload.mediaId, mediaFiles ) === + false + ) { + return; + } + + switch ( payload.state ) { + case MEDIA_SAVE_STATE_SAVING: + this.updateMediaSaveProgress( payload ); + break; + case MEDIA_SAVE_STATE_SUCCEEDED: + this.finishMediaSaveWithSuccess( payload ); + break; + case MEDIA_SAVE_STATE_FAILED: + this.finishMediaSaveWithFailure( payload ); + break; + case MEDIA_SAVE_STATE_RESET: + this.mediaSaveStateReset( payload ); + break; + case MEDIA_SAVE_FINAL_STATE_RESULT: + this.finalSaveResult( payload ); + break; + case MEDIA_SAVE_MEDIAID_CHANGED: + this.mediaIdChanged( payload ); + break; + } + } + + // ---- Block media save actions + updateMediaSaveProgress( payload ) { + this.setState( { + progress: payload.progress, + isUploadInProgress: false, + isUploadFailed: false, + isSaveInProgress: true, + isSaveFailed: false, + } ); + if ( this.props.onUpdateMediaSaveProgress ) { + this.props.onUpdateMediaSaveProgress( payload ); + } + } + + finishMediaSaveWithSuccess( payload ) { + this.setState( { isSaveInProgress: false } ); + if ( this.props.onFinishMediaSaveWithSuccess ) { + this.props.onFinishMediaSaveWithSuccess( payload ); + } + } + + finishMediaSaveWithFailure( payload ) { + this.setState( { isSaveInProgress: false, isSaveFailed: true } ); + if ( this.props.onFinishMediaSaveWithFailure ) { + this.props.onFinishMediaSaveWithFailure( payload ); + } + } + + mediaSaveStateReset( payload ) { + this.setState( { isSaveInProgress: false, isSaveFailed: false } ); + if ( this.props.onMediaSaveStateReset ) { + this.props.onMediaSaveStateReset( payload ); + } + } + + finalSaveResult( payload ) { + this.setState( { + progress: payload.progress, + isUploadInProgress: false, + isUploadFailed: false, + isSaveInProgress: false, + isSaveFailed: ! payload.success, + } ); + if ( this.props.onFinalSaveResult ) { + this.props.onFinalSaveResult( payload ); + } + } + + mediaIdChanged( payload ) { + this.setState( { + isUploadInProgress: false, + isUploadFailed: false, + isSaveInProgress: false, + isSaveFailed: false, + } ); + if ( this.props.onMediaIdChanged ) { + this.props.onMediaIdChanged( payload ); + } + } + + // ---- Block media upload actions + updateMediaUploadProgress( payload ) { + this.setState( { + progress: payload.progress, + isUploadInProgress: true, + isUploadFailed: false, + isSaveInProgress: false, + isSaveFailed: false, + } ); + if ( this.props.onUpdateMediaUploadProgress ) { + this.props.onUpdateMediaUploadProgress( payload ); + } + } + + finishMediaUploadWithSuccess( payload ) { + this.setState( { isUploadInProgress: false, isSaveInProgress: false } ); + if ( this.props.onFinishMediaUploadWithSuccess ) { + this.props.onFinishMediaUploadWithSuccess( payload ); + } + } + + finishMediaUploadWithFailure( payload ) { + this.setState( { isUploadInProgress: false, isUploadFailed: true } ); + if ( this.props.onFinishMediaUploadWithFailure ) { + this.props.onFinishMediaUploadWithFailure( payload ); + } + } + + mediaUploadStateReset( payload ) { + this.setState( { isUploadInProgress: false, isUploadFailed: false } ); + if ( this.props.onMediaUploadStateReset ) { + this.props.onMediaUploadStateReset( payload ); + } + } + + addMediaUploadListener() { + //if we already have a subscription not worth doing it again + if ( this.subscriptionParentMediaUpload ) { + return; + } + this.subscriptionParentMediaUpload = subscribeMediaUpload( + ( payload ) => { + this.mediaUpload( payload ); + } + ); + } + + removeMediaUploadListener() { + if ( this.subscriptionParentMediaUpload ) { + this.subscriptionParentMediaUpload.remove(); + } + } + + addMediaSaveListener() { + //if we already have a subscription not worth doing it again + if ( this.subscriptionParentMediaSave ) { + return; + } + this.subscriptionParentMediaSave = subscribeMediaSave( ( payload ) => { + this.mediaSave( payload ); + } ); + } + + removeMediaSaveListener() { + if ( this.subscriptionParentMediaSave ) { + this.subscriptionParentMediaSave.remove(); + } + } + + render() { + const { renderContent = () => null } = this.props; + const { + isUploadInProgress, + isUploadFailed, + isSaveInProgress, + isSaveFailed, + } = this.state; + const showSpinner = + this.state.isUploadInProgress || this.state.isSaveInProgress; + const progress = this.state.progress * 100; + // eslint-disable-next-line @wordpress/i18n-no-collapsible-whitespace + const retryMessageSave = __( + 'Failed to save files.\nPlease tap for options.' + ); + // eslint-disable-next-line @wordpress/i18n-no-collapsible-whitespace + const retryMessageUpload = __( + 'Failed to upload files.\nPlease tap for options.' + ); + let retryMessage = retryMessageSave; + if ( isUploadFailed ) { + retryMessage = retryMessageUpload; + } + + return ( + + { showSpinner && ( + + + + ) } + { renderContent( { + isUploadInProgress, + isUploadFailed, + isSaveInProgress, + isSaveFailed, + retryMessage, + } ) } + + ); + } +} + +export default BlockMediaUpdateProgress; diff --git a/packages/block-editor/src/components/block-media-update-progress/styles.native.scss b/packages/block-editor/src/components/block-media-update-progress/styles.native.scss new file mode 100644 index 00000000000000..2178be1968b1c5 --- /dev/null +++ b/packages/block-editor/src/components/block-media-update-progress/styles.native.scss @@ -0,0 +1,9 @@ +.mediaUploadProgress { + flex: 1; + z-index: 1; +} + +.progressBar { + background-color: $gray-lighten-30; + z-index: 1; +} diff --git a/packages/block-editor/src/components/block-media-update-progress/test/index.native.js b/packages/block-editor/src/components/block-media-update-progress/test/index.native.js new file mode 100644 index 00000000000000..82016ab68d9b6d --- /dev/null +++ b/packages/block-editor/src/components/block-media-update-progress/test/index.native.js @@ -0,0 +1,500 @@ +/** + * External dependencies + */ +import { shallow } from 'enzyme'; + +/** + * WordPress dependencies + */ +import { sendMediaUpload, sendMediaSave } from '@wordpress/react-native-bridge'; + +/** + * Internal dependencies + */ +import { + BlockMediaUpdateProgress, + MEDIA_UPLOAD_STATE_UPLOADING, + MEDIA_UPLOAD_STATE_SUCCEEDED, + MEDIA_UPLOAD_STATE_FAILED, + MEDIA_UPLOAD_STATE_RESET, + MEDIA_SAVE_STATE_SAVING, + MEDIA_SAVE_STATE_SUCCEEDED, + MEDIA_SAVE_STATE_FAILED, + MEDIA_SAVE_STATE_RESET, + MEDIA_SAVE_FINAL_STATE_RESULT, + MEDIA_SAVE_MEDIAID_CHANGED, +} from '../'; + +jest.mock( '@wordpress/react-native-bridge', () => { + const callUploadCallback = ( payload ) => { + this.uploadCallBack( payload ); + }; + const callSaveCallback = ( payload ) => { + this.saveCallBack( payload ); + }; + const subscribeMediaUpload = ( callback ) => { + this.uploadCallBack = callback; + }; + const subscribeMediaSave = ( callback ) => { + this.saveCallBack = callback; + }; + const mediaSources = { + deviceCamera: 'DEVICE_CAMERA', + deviceLibrary: 'DEVICE_MEDIA_LIBRARY', + siteMediaLibrary: 'SITE_MEDIA_LIBRARY', + }; + return { + subscribeMediaUpload, + subscribeMediaSave, + sendMediaUpload: callUploadCallback, + sendMediaSave: callSaveCallback, + mediaSources, + }; +} ); + +const MEDIAID_LOCAL = 2; +const MEDIAID_TEMP = 'tempid-0-1'; + +const tempMediaFiles = [ + { + alt: '', + caption: '', + id: 'tempid-0-1', + link: '', + mime: 'image/jpeg', + type: 'image', + url: '', + }, + { + alt: '', + caption: '', + id: 'tempid-0-2', + link: '', + mime: 'image/jpeg', + type: 'image', + url: '', + }, +]; + +const localMediaFiles = [ + { + alt: '', + caption: '', + id: '2', + link: '', + mime: 'image/jpeg', + type: 'image', + url: '', + }, + { + alt: '', + caption: '', + id: '3', + link: '', + mime: 'image/jpeg', + type: 'image', + url: '', + }, +]; + +describe( 'BlockMediaUpdateProgress component', () => { + it( 'renders without crashing', () => { + const wrapper = shallow( + {} } /> + ); + expect( wrapper ).toBeTruthy(); + } ); + + it( 'upload: onUpdateMediaUploadProgress is called when a progress update payload is received', () => { + const progress = 10; + const payload = { + state: MEDIA_UPLOAD_STATE_UPLOADING, + mediaId: MEDIAID_LOCAL, + progress, + }; + + const onUpdateMediaUploadProgress = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payload ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + expect( wrapper.instance().state.isUploadInProgress ).toEqual( true ); + expect( wrapper.instance().state.isUploadFailed ).toEqual( false ); + expect( onUpdateMediaUploadProgress ).toHaveBeenCalledTimes( 1 ); + expect( onUpdateMediaUploadProgress ).toHaveBeenCalledWith( payload ); + } ); + + // UPLOAD tests + it( 'upload does not get affected by unrelated media uploads', () => { + const payload = { + state: MEDIA_UPLOAD_STATE_UPLOADING, + mediaId: 432, // id not belonging to assigned mediaFiles collection in test + progress: 20, + }; + const onUpdateMediaUploadProgress = jest.fn(); + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payload ); + + expect( wrapper.instance().state.progress ).toEqual( 0 ); + expect( onUpdateMediaUploadProgress ).toHaveBeenCalledTimes( 0 ); + } ); + + it( 'upload: onFinishMediaUploadWithSuccess is called when a success payload is received', () => { + const progress = 10; + const payloadSuccess = { + state: MEDIA_UPLOAD_STATE_SUCCEEDED, + mediaId: MEDIAID_LOCAL, + }; + const payloadUploading = { + state: MEDIA_UPLOAD_STATE_UPLOADING, + mediaId: MEDIAID_LOCAL, + progress, + }; + + const onFinishMediaUploadWithSuccess = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payloadUploading ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaUpload( payloadSuccess ); + + expect( wrapper.instance().state.isUploadInProgress ).toEqual( false ); + expect( onFinishMediaUploadWithSuccess ).toHaveBeenCalledTimes( 1 ); + expect( onFinishMediaUploadWithSuccess ).toHaveBeenCalledWith( + payloadSuccess + ); + } ); + + it( 'upload: onFinishMediaUploadWithFailure is called when a failed payload is received', () => { + const progress = 10; + const payloadFail = { + state: MEDIA_UPLOAD_STATE_FAILED, + mediaId: MEDIAID_LOCAL, + }; + const payloadUploading = { + state: MEDIA_UPLOAD_STATE_UPLOADING, + mediaId: MEDIAID_LOCAL, + progress, + }; + + const onFinishMediaUploadWithFailure = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payloadUploading ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaUpload( payloadFail ); + + expect( wrapper.instance().state.isUploadInProgress ).toEqual( false ); + expect( wrapper.instance().state.isUploadFailed ).toEqual( true ); + expect( onFinishMediaUploadWithFailure ).toHaveBeenCalledTimes( 1 ); + expect( onFinishMediaUploadWithFailure ).toHaveBeenCalledWith( + payloadFail + ); + } ); + + it( 'upload: onMediaUploadStateReset is called when a reset payload is received', () => { + const progress = 10; + const payloadReset = { + state: MEDIA_UPLOAD_STATE_RESET, + mediaId: MEDIAID_LOCAL, + }; + const payloadUploading = { + state: MEDIA_UPLOAD_STATE_UPLOADING, + mediaId: MEDIAID_LOCAL, + progress, + }; + + const onMediaUploadStateReset = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaUpload( payloadUploading ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaUpload( payloadReset ); + + expect( wrapper.instance().state.isUploadInProgress ).toEqual( false ); + expect( wrapper.instance().state.isUploadFailed ).toEqual( false ); + expect( onMediaUploadStateReset ).toHaveBeenCalledTimes( 1 ); + expect( onMediaUploadStateReset ).toHaveBeenCalledWith( payloadReset ); + } ); + + // SAVE tests + it( 'save does not get affected by unrelated media save events', () => { + const payload = { + state: MEDIA_SAVE_STATE_SAVING, + mediaId: 'tempid-432', // id not belonging to assigned mediaFiles collection in test + progress: 20, + }; + const onUpdateMediaSaveProgress = jest.fn(); + const wrapper = shallow( + {} } + /> + ); + + sendMediaSave( payload ); + + expect( wrapper.instance().state.progress ).toEqual( 0 ); + expect( onUpdateMediaSaveProgress ).toHaveBeenCalledTimes( 0 ); + } ); + + it( 'save: onFinishMediaSaveWithSuccess is called when a success payload is received', () => { + const progress = 10; + const payloadSuccess = { + state: MEDIA_SAVE_STATE_SUCCEEDED, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + }; + const payloadSaving = { + state: MEDIA_SAVE_STATE_SAVING, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + progress, + }; + + const onFinishMediaSaveWithSuccess = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaSave( payloadSaving ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaSave( payloadSuccess ); + + expect( wrapper.instance().state.isSaveInProgress ).toEqual( false ); + expect( onFinishMediaSaveWithSuccess ).toHaveBeenCalledTimes( 1 ); + expect( onFinishMediaSaveWithSuccess ).toHaveBeenCalledWith( + payloadSuccess + ); + } ); + + it( 'save: onFinishMediaSaveWithFailure is called when a failed payload is received', () => { + const progress = 10; + const payloadFail = { + state: MEDIA_SAVE_STATE_FAILED, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + }; + const payloadSaving = { + state: MEDIA_SAVE_STATE_SAVING, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + progress, + }; + + const onFinishMediaSaveWithFailure = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaSave( payloadSaving ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaSave( payloadFail ); + + expect( wrapper.instance().state.isSaveInProgress ).toEqual( false ); + expect( wrapper.instance().state.isSaveFailed ).toEqual( true ); + expect( onFinishMediaSaveWithFailure ).toHaveBeenCalledTimes( 1 ); + expect( onFinishMediaSaveWithFailure ).toHaveBeenCalledWith( + payloadFail + ); + } ); + + it( 'save: onMediaSaveStateReset is called when a reset payload is received', () => { + const progress = 10; + const payloadReset = { + state: MEDIA_SAVE_STATE_RESET, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + }; + const payloadSaving = { + state: MEDIA_SAVE_STATE_SAVING, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + progress, + }; + + const onMediaSaveStateReset = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaSave( payloadSaving ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaSave( payloadReset ); + + expect( wrapper.instance().state.isSaveInProgress ).toEqual( false ); + expect( wrapper.instance().state.isSaveFailed ).toEqual( false ); + expect( onMediaSaveStateReset ).toHaveBeenCalledTimes( 1 ); + expect( onMediaSaveStateReset ).toHaveBeenCalledWith( payloadReset ); + } ); + + it( 'save: onFinalSaveResult is called with fail result when fail result is received', () => { + const progress = 10; + const payloadFail = { + state: MEDIA_SAVE_FINAL_STATE_RESULT, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + success: false, + }; + const payloadSaving = { + state: MEDIA_SAVE_STATE_SAVING, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + progress, + }; + + const onFinalSaveResult = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaSave( payloadSaving ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaSave( payloadFail ); + + expect( wrapper.instance().state.isSaveInProgress ).toEqual( false ); + expect( wrapper.instance().state.isSaveFailed ).toEqual( true ); + expect( onFinalSaveResult ).toHaveBeenCalledTimes( 1 ); + expect( onFinalSaveResult ).toHaveBeenCalledWith( payloadFail ); + } ); + + it( 'save: onFinalSaveResult is called with success result when success result is received', () => { + const progress = 10; + const payloadSuccess = { + state: MEDIA_SAVE_FINAL_STATE_RESULT, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + success: true, + }; + const payloadSaving = { + state: MEDIA_SAVE_STATE_SAVING, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + progress, + }; + + const onFinalSaveResult = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaSave( payloadSaving ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaSave( payloadSuccess ); + + expect( wrapper.instance().state.isSaveInProgress ).toEqual( false ); + expect( wrapper.instance().state.isSaveFailed ).toEqual( false ); + expect( onFinalSaveResult ).toHaveBeenCalledTimes( 1 ); + expect( onFinalSaveResult ).toHaveBeenCalledWith( payloadSuccess ); + } ); + + it( 'save: listens to mediaId change and passes it up', () => { + const progress = 10; + const payloadMediaIdChange = { + state: MEDIA_SAVE_MEDIAID_CHANGED, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + newId: MEDIAID_LOCAL, + mediaUrl: 'file:///folder/someimage.jpg', + }; + const payloadSaving = { + state: MEDIA_SAVE_STATE_SAVING, + mediaId: MEDIAID_TEMP, // while saving, we have a tempid key + progress, + }; + + const onMediaIdChanged = jest.fn(); + + const wrapper = shallow( + {} } + /> + ); + + sendMediaSave( payloadSaving ); + + expect( wrapper.instance().state.progress ).toEqual( progress ); + + sendMediaSave( payloadMediaIdChange ); + + expect( wrapper.instance().state.isSaveInProgress ).toEqual( false ); + expect( wrapper.instance().state.isSaveFailed ).toEqual( false ); + expect( wrapper.instance().state.isUploadInProgress ).toEqual( false ); + expect( wrapper.instance().state.isUploadFailed ).toEqual( false ); + expect( onMediaIdChanged ).toHaveBeenCalledTimes( 1 ); + expect( onMediaIdChanged ).toHaveBeenCalledWith( payloadMediaIdChange ); + } ); +} ); diff --git a/packages/block-editor/src/components/index.native.js b/packages/block-editor/src/components/index.native.js index 8fa8e93b903040..2245d16a1511ba 100644 --- a/packages/block-editor/src/components/index.native.js +++ b/packages/block-editor/src/components/index.native.js @@ -28,6 +28,7 @@ export { MEDIA_TYPE_VIDEO, } from './media-upload'; export { default as MediaUploadProgress } from './media-upload-progress'; +export { default as BlockMediaUpdateProgress } from './block-media-update-progress'; export { default as URLInput } from './url-input'; export { default as BlockInvalidWarning } from './block-list/block-invalid-warning'; export { default as BlockCaption } from './block-caption'; diff --git a/packages/edit-post/src/editor.native.js b/packages/edit-post/src/editor.native.js index b227410684135c..633e870528a0f7 100644 --- a/packages/edit-post/src/editor.native.js +++ b/packages/edit-post/src/editor.native.js @@ -56,6 +56,11 @@ class Editor extends Component { // Omit hidden block types if exists and non-empty. if ( size( hiddenBlockTypes ) > 0 ) { + if ( settings.allowedBlockTypes === undefined ) { + // if no specific flags for allowedBlockTypes are set, assume `true` + // meaning allow all block types + settings.allowedBlockTypes = true; + } // Defer to passed setting for `allowedBlockTypes` if provided as // anything other than `true` (where `true` is equivalent to allow // all block types). @@ -190,7 +195,6 @@ export default compose( [ } ), withDispatch( ( dispatch ) => { const { switchEditorMode } = dispatch( 'core/edit-post' ); - return { switchEditorMode, }; diff --git a/packages/react-native-bridge/android/build.gradle b/packages/react-native-bridge/android/build.gradle index ff4c666f101bcc..31af74444cfffc 100644 --- a/packages/react-native-bridge/android/build.gradle +++ b/packages/react-native-bridge/android/build.gradle @@ -305,7 +305,7 @@ If they are changed, the isBundleUpToDate flag is switched to false. That flag i } } - def nodeModulesFolders = ["$mobileGutenbergRootDir/node_modules", "$mobileGutenbergRootDir/gutenberg/node_modules"] as String[] + def nodeModulesFolders = ["$mobileGutenbergRootDir/node_modules", "$mobileGutenbergRootDir/gutenberg/node_modules", "$mobileGutenbergRootDir/jetpack/node_modules"] as String[] task cleanupNodeModulesFolders(type: Delete) { doFirst { println "Deleting node_modules folders" diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java index cc5ba27356217d..cd9dcc79ed2b9b 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/GutenbergBridgeJS2Parent.java @@ -34,10 +34,23 @@ interface MediaUploadEventEmitter { void onMediaFileUploadFailed(int mediaId); } + interface MediaSaveEventEmitter { + void onSaveMediaFileClear(String mediaId); + void onMediaFileSaveProgress(String mediaId, float progress); + void onMediaFileSaveSucceeded(String mediaId, String mediaUrl); + void onMediaFileSaveFailed(String mediaId); + void onMediaCollectionSaveResult(String firstMediaIdInCollection, boolean success); + void onMediaIdChanged(final String oldId, final String newId, final String oldUrl); + } + interface ReplaceUnsupportedBlockCallback { void replaceUnsupportedBlock(String content, String blockId); } + interface ReplaceMediaFilesEditedBlockCallback { + void replaceMediaFilesEditedBlock(String mediaFiles, String blockId); + } + interface StarterPageTemplatesTooltipShownCallback { void onRequestStarterPageTemplatesTooltipShown(boolean tooltipShown); } @@ -122,6 +135,8 @@ public static GutenbergUserEvent getEnum(String eventName) { void mediaUploadSync(MediaSelectedCallback mediaSelectedCallback); + void mediaSaveSync(MediaSelectedCallback mediaSelectedCallback); + void requestImageFailedRetryDialog(int mediaId); void requestImageUploadCancelDialog(int mediaId); @@ -155,4 +170,15 @@ void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockCallback void setStarterPageTemplatesTooltipShown(boolean tooltipShown); void requestStarterPageTemplatesTooltipShown(StarterPageTemplatesTooltipShownCallback starterPageTemplatesTooltipShownCallback); + + void requestMediaFilesEditorLoad(ReplaceMediaFilesEditedBlockCallback replaceMediaFilesEditedBlockCallback, + ReadableArray mediaFiles, + String blockId + ); + + void requestMediaFilesFailedRetryDialog(ReadableArray mediaFiles); + + void requestMediaFilesUploadCancelDialog(ReadableArray mediaFiles); + + void requestMediaFilesSaveCancelDialog(ReadableArray mediaFiles); } diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java index 16f8034435409e..7d29419f91e3ec 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/ReactNativeGutenbergBridge/RNReactNativeGutenbergBridgeModule.java @@ -50,12 +50,15 @@ public class RNReactNativeGutenbergBridgeModule extends ReactContextBaseJavaModu private static final String MAP_KEY_UPDATE_HTML = "html"; private static final String MAP_KEY_UPDATE_TITLE = "title"; + public static final String MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_NEW_ID = "newId"; private static final String MAP_KEY_SHOW_NOTICE_MESSAGE = "message"; + public static final String MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_ID = "mediaId"; public static final String MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_URL = "mediaUrl"; public static final String MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_TYPE = "mediaType"; private static final String MAP_KEY_THEME_UPDATE_COLORS = "colors"; private static final String MAP_KEY_THEME_UPDATE_GRADIENTS = "gradients"; + public static final String MAP_KEY_MEDIA_FINAL_SAVE_RESULT_SUCCESS_VALUE = "success"; private static final String MAP_KEY_IS_PREFERRED_COLOR_SCHEME_DARK = "isPreferredColorSchemeDark"; @@ -203,6 +206,11 @@ public void mediaUploadSync() { mGutenbergBridgeJS2Parent.mediaUploadSync(getNewMediaSelectedCallback(false,null)); } + @ReactMethod + public void mediaSaveSync() { + mGutenbergBridgeJS2Parent.mediaSaveSync(getNewMediaSelectedCallback(true,null)); + } + @ReactMethod public void requestImageFailedRetryDialog(final int mediaId) { mGutenbergBridgeJS2Parent.requestImageFailedRetryDialog(mediaId); @@ -228,6 +236,27 @@ public void requestMediaEditor(String mediaUrl, final Callback onUploadMediaSele mGutenbergBridgeJS2Parent.requestMediaEditor(getNewMediaSelectedCallback(false, onUploadMediaSelected), mediaUrl); } + @ReactMethod + public void requestMediaFilesEditorLoad(ReadableArray mediaFiles, String blockId) { + mGutenbergBridgeJS2Parent.requestMediaFilesEditorLoad((savedMediaFiles, savedBlockId) -> + replaceBlock(savedMediaFiles, savedBlockId), mediaFiles, blockId); + } + + @ReactMethod + public void requestMediaFilesFailedRetryDialog(ReadableArray mediaFiles) { + mGutenbergBridgeJS2Parent.requestMediaFilesFailedRetryDialog(mediaFiles); + } + + @ReactMethod + public void requestMediaFilesUploadCancelDialog(ReadableArray mediaFiles) { + mGutenbergBridgeJS2Parent.requestMediaFilesUploadCancelDialog(mediaFiles); + } + + @ReactMethod + public void requestMediaFilesSaveCancelDialog(ReadableArray mediaFiles) { + mGutenbergBridgeJS2Parent.requestMediaFilesSaveCancelDialog(mediaFiles); + } + @ReactMethod public void editorDidEmitLog(String message, int logLevel) { mGutenbergBridgeJS2Parent.editorDidEmitLog(message, GutenbergBridgeJS2Parent.LogLevel.valueOf(logLevel)); diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java index e67e76a67b098b..013a89da92f514 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/DeferredEventEmitter.java @@ -9,14 +9,17 @@ import com.facebook.react.bridge.WritableNativeMap; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaUploadEventEmitter; +import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaSaveEventEmitter; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import static org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgeModule.MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_ID; +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; -public class DeferredEventEmitter implements MediaUploadEventEmitter { +public class DeferredEventEmitter implements MediaUploadEventEmitter, MediaSaveEventEmitter { public interface JSEventEmitter { void emitToJS(String eventName, @Nullable WritableMap data); } @@ -26,11 +29,19 @@ public interface JSEventEmitter { private static final int MEDIA_UPLOAD_STATE_FAILED = 3; private static final int MEDIA_UPLOAD_STATE_RESET = 4; + private static final int MEDIA_SAVE_STATE_SAVING = 5; + private static final int MEDIA_SAVE_STATE_SUCCEEDED = 6; + private static final int MEDIA_SAVE_STATE_FAILED = 7; + private static final int MEDIA_SAVE_STATE_RESET = 8; + private static final int MEDIA_SAVE_FINAL_STATE_RESULT = 9; + private static final int MEDIA_SAVE_MEDIAID_CHANGED = 10; + private static final String EVENT_NAME_MEDIA_UPLOAD = "mediaUpload"; + private static final String EVENT_NAME_MEDIA_SAVE = "mediaSave"; - private static final String MAP_KEY_MEDIA_FILE_UPLOAD_STATE = "state"; - private static final String MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_PROGRESS = "progress"; - private static final String MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_SERVER_ID = "mediaServerId"; + 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"; private static final String MAP_KEY_UPDATE_CAPABILITIES = "updateCapabilities"; @@ -86,12 +97,12 @@ private void setMediaFileUploadDataInJS(int state, int mediaId, String mediaUrl, private void setMediaFileUploadDataInJS(int state, int mediaId, String mediaUrl, float progress, int mediaServerId) { WritableMap writableMap = new WritableNativeMap(); - writableMap.putInt(MAP_KEY_MEDIA_FILE_UPLOAD_STATE, state); + writableMap.putInt(MAP_KEY_MEDIA_FILE_STATE, state); writableMap.putInt(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_ID, mediaId); writableMap.putString(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_URL, mediaUrl); - writableMap.putDouble(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_PROGRESS, progress); + writableMap.putDouble(MAP_KEY_MEDIA_FILE_MEDIA_ACTION_PROGRESS, progress); if (mediaServerId != MEDIA_SERVER_ID_UNKNOWN) { - writableMap.putInt(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_SERVER_ID, mediaServerId); + writableMap.putInt(MAP_KEY_MEDIA_FILE_MEDIA_SERVER_ID, mediaServerId); } if (isCriticalMessage(state)) { queueActionToJS(EVENT_NAME_MEDIA_UPLOAD, writableMap); @@ -100,8 +111,36 @@ private void setMediaFileUploadDataInJS(int state, int mediaId, String mediaUrl, } } + private void setMediaSaveResultDataInJS(int state, String mediaId, String mediaUrl, float progress) { + WritableMap writableMap = new WritableNativeMap(); + writableMap.putInt(MAP_KEY_MEDIA_FILE_STATE, state); + writableMap.putString(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_ID, mediaId); + writableMap.putString(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_URL, mediaUrl); + writableMap.putDouble(MAP_KEY_MEDIA_FILE_MEDIA_ACTION_PROGRESS, progress); + if (isCriticalMessage(state)) { + queueActionToJS(EVENT_NAME_MEDIA_SAVE, writableMap); + } else { + emitOrDrop(EVENT_NAME_MEDIA_SAVE, writableMap); + } + } + + private void setMediaSaveResultDataInJS(int state, String mediaId, boolean success, float progress) { + WritableMap writableMap = new WritableNativeMap(); + writableMap.putInt(MAP_KEY_MEDIA_FILE_STATE, state); + writableMap.putString(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_ID, mediaId); + writableMap.putBoolean(MAP_KEY_MEDIA_FINAL_SAVE_RESULT_SUCCESS_VALUE, success); + writableMap.putDouble(MAP_KEY_MEDIA_FILE_MEDIA_ACTION_PROGRESS, progress); + if (isCriticalMessage(state)) { + queueActionToJS(EVENT_NAME_MEDIA_SAVE, writableMap); + } else { + emitOrDrop(EVENT_NAME_MEDIA_SAVE, writableMap); + } + } + private boolean isCriticalMessage(int state) { - return state == MEDIA_UPLOAD_STATE_SUCCEEDED || state == MEDIA_UPLOAD_STATE_FAILED; + return state == MEDIA_UPLOAD_STATE_SUCCEEDED || state == MEDIA_UPLOAD_STATE_FAILED + || state == MEDIA_SAVE_STATE_SUCCEEDED || state == MEDIA_SAVE_STATE_FAILED + || state == MEDIA_SAVE_MEDIAID_CHANGED; } @Override @@ -124,6 +163,45 @@ public void onMediaFileUploadFailed(int mediaId) { setMediaFileUploadDataInJS(MEDIA_UPLOAD_STATE_FAILED, mediaId, null, 0); } + // Media file save events emitter + @Override + public void onSaveMediaFileClear(String mediaId) { + setMediaSaveResultDataInJS(MEDIA_SAVE_STATE_RESET, mediaId, null, 0); + } + + @Override + public void onMediaFileSaveProgress(String mediaId, float progress) { + setMediaSaveResultDataInJS(MEDIA_SAVE_STATE_SAVING, mediaId, null, progress); + } + + @Override + public void onMediaFileSaveSucceeded(String mediaId, String mediaUrl) { + setMediaSaveResultDataInJS(MEDIA_SAVE_STATE_SUCCEEDED, mediaId, mediaUrl, 1); + } + + @Override + public void onMediaFileSaveFailed(String mediaId) { + setMediaSaveResultDataInJS(MEDIA_SAVE_STATE_FAILED, mediaId, null, 0); + } + + @Override + public void onMediaCollectionSaveResult(String firstMediaIdInCollection, boolean success) { + setMediaSaveResultDataInJS(MEDIA_SAVE_FINAL_STATE_RESULT, firstMediaIdInCollection, success, success ? 1 : 0); + } + + @Override public void onMediaIdChanged(String oldId, String newId, String oldUrl) { + WritableMap writableMap = new WritableNativeMap(); + writableMap.putInt(MAP_KEY_MEDIA_FILE_STATE, MEDIA_SAVE_MEDIAID_CHANGED); + writableMap.putString(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_ID, oldId); + writableMap.putString(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_NEW_ID, newId); + writableMap.putString(MAP_KEY_MEDIA_FILE_UPLOAD_MEDIA_URL, oldUrl); + if (isCriticalMessage(MEDIA_SAVE_MEDIAID_CHANGED)) { + queueActionToJS(EVENT_NAME_MEDIA_SAVE, writableMap); + } else { + emitOrDrop(EVENT_NAME_MEDIA_SAVE, writableMap); + } + } + public void updateCapabilities(GutenbergProps gutenbergProps) { queueActionToJS(MAP_KEY_UPDATE_CAPABILITIES, Arguments.makeNativeMap(gutenbergProps.getUpdatedCapabilitiesProps())); } diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt index 6052b72b244b54..f9f65dd9c20838 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/GutenbergProps.kt @@ -3,6 +3,7 @@ package org.wordpress.mobile.WPAndroidGlue import android.os.Bundle data class GutenbergProps @JvmOverloads constructor( + val enableMediaFilesCollectionBlocks: Boolean, val enableMentions: Boolean, val enableUnsupportedBlockEditor: Boolean, val canEnableUnsupportedBlockEditor: Boolean, @@ -37,6 +38,7 @@ data class GutenbergProps @JvmOverloads constructor( fun getUpdatedCapabilitiesProps() = Bundle().apply { putBoolean(PROP_CAPABILITIES_MENTIONS, enableMentions) + putBoolean(PROP_CAPABILITIES_MEDIAFILES_COLLECTION_BLOCK, enableMediaFilesCollectionBlocks) putBoolean(PROP_CAPABILITIES_UNSUPPORTED_BLOCK_EDITOR, enableUnsupportedBlockEditor) putBoolean(PROP_CAPABILITIES_CAN_ENABLE_UNSUPPORTED_BLOCK_EDITOR, canEnableUnsupportedBlockEditor) putBoolean(PROP_CAPABILITIES_MODAL_LAYOUT_PICKER, isModalLayoutPickerEnabled) @@ -64,6 +66,7 @@ data class GutenbergProps @JvmOverloads constructor( private const val PROP_EDITOR_MODE_EDITOR = "editor" const val PROP_CAPABILITIES = "capabilities" + const val PROP_CAPABILITIES_MEDIAFILES_COLLECTION_BLOCK = "mediaFilesCollectionBlock" const val PROP_CAPABILITIES_MENTIONS = "mentions" const val PROP_CAPABILITIES_UNSUPPORTED_BLOCK_EDITOR = "unsupportedBlockEditor" const val PROP_CAPABILITIES_CAN_ENABLE_UNSUPPORTED_BLOCK_EDITOR = "canEnableUnsupportedBlockEditor" diff --git a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index e310b2b32eef72..86f261442afdce 100644 --- a/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java +++ b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java @@ -49,6 +49,7 @@ import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.GutenbergUserEvent; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.MediaSelectedCallback; +import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.ReplaceMediaFilesEditedBlockCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.GutenbergBridgeJS2Parent.ReplaceUnsupportedBlockCallback; import org.wordpress.mobile.ReactNativeGutenbergBridge.RNMedia; import org.wordpress.mobile.ReactNativeGutenbergBridge.RNReactNativeGutenbergBridgePackage; @@ -82,7 +83,8 @@ public class WPAndroidGlueCode { private boolean mAppendsMultipleSelectedToSiblingBlocks = false; private OnMediaLibraryButtonListener mOnMediaLibraryButtonListener; - private OnReattachQueryListener mOnReattachQueryListener; + private OnReattachMediaUploadQueryListener mOnReattachMediaUploadQueryListener; + private OnReattachMediaSavingQueryListener mOnReattachMediaSavingQueryListener; private OnEditorMountListener mOnEditorMountListener; private OnEditorAutosaveListener mOnEditorAutosaveListener; private OnImageFullscreenPreviewListener mOnImageFullscreenPreviewListener; @@ -92,6 +94,8 @@ public class WPAndroidGlueCode { private OnGutenbergDidSendButtonPressedActionListener mOnGutenbergDidSendButtonPressedActionListener; private ReplaceUnsupportedBlockCallback mReplaceUnsupportedBlockCallback; private OnStarterPageTemplatesTooltipShownEventListener mOnStarterPageTemplatesTooltipShownListener; + private OnMediaFilesCollectionBasedBlockEditorListener mOnMediaFilesCollectionBasedBlockEditorListener; + private ReplaceMediaFilesEditedBlockCallback mReplaceMediaFilesEditedBlockCallback; private boolean mIsEditorMounted; private String mContentHtml = ""; @@ -146,14 +150,25 @@ public interface OnMediaLibraryButtonListener { void onOtherMediaButtonClicked(String mediaSource, boolean allowMultipleSelection); } + public interface OnMediaFilesCollectionBasedBlockEditorListener { + void onRequestMediaFilesEditorLoad(ArrayList mediaFiles, String blockId); + void onCancelUploadForMediaCollection(ArrayList mediaFiles); + void onRetryUploadForMediaCollection(ArrayList mediaFiles); + void onCancelSaveForMediaCollection(ArrayList mediaFiles); + } + public interface OnImageFullscreenPreviewListener { void onImageFullscreenPreviewClicked(String mediaUrl); } - public interface OnReattachQueryListener { + public interface OnReattachMediaUploadQueryListener { void onQueryCurrentProgressForUploadingMedia(); } + public interface OnReattachMediaSavingQueryListener { + void onQueryCurrentProgressForSavingMedia(); + } + public interface OnEditorMountListener { void onEditorDidMount(ArrayList unsupportedBlockNames); } @@ -267,7 +282,13 @@ public void requestMediaImport(String url, MediaSelectedCallback mediaSelectedCa @Override public void mediaUploadSync(MediaSelectedCallback mediaSelectedCallback) { mMediaSelectedCallback = mediaSelectedCallback; - mOnReattachQueryListener.onQueryCurrentProgressForUploadingMedia(); + mOnReattachMediaUploadQueryListener.onQueryCurrentProgressForUploadingMedia(); + } + + @Override + public void mediaSaveSync(MediaSelectedCallback mediaSelectedCallback) { + mMediaSelectedCallback = mediaSelectedCallback; + mOnReattachMediaSavingQueryListener.onQueryCurrentProgressForSavingMedia(); } @Override @@ -399,6 +420,38 @@ public void requestStarterPageTemplatesTooltipShown(StarterPageTemplatesTooltipS boolean tooltipShown = mOnStarterPageTemplatesTooltipShownListener.onRequestStarterPageTemplatesTooltipShown(); starterPageTemplatesTooltipShownCallback.onRequestStarterPageTemplatesTooltipShown(tooltipShown); } + + @Override + public void requestMediaFilesEditorLoad( + ReplaceMediaFilesEditedBlockCallback replaceMediaFilesEditedBlockCallback, + ReadableArray mediaFiles, + String blockId + ) { + mReplaceMediaFilesEditedBlockCallback = replaceMediaFilesEditedBlockCallback; + mOnMediaFilesCollectionBasedBlockEditorListener + .onRequestMediaFilesEditorLoad(mediaFiles.toArrayList(), blockId); + } + + @Override + public void requestMediaFilesFailedRetryDialog(ReadableArray mediaFiles) { + mOnMediaFilesCollectionBasedBlockEditorListener.onRetryUploadForMediaCollection( + mediaFiles.toArrayList() + ); + } + + @Override + public void requestMediaFilesUploadCancelDialog(ReadableArray mediaFiles) { + mOnMediaFilesCollectionBasedBlockEditorListener.onCancelUploadForMediaCollection( + mediaFiles.toArrayList() + ); + } + + @Override + public void requestMediaFilesSaveCancelDialog(ReadableArray mediaFiles) { + mOnMediaFilesCollectionBasedBlockEditorListener.onCancelSaveForMediaCollection( + mediaFiles.toArrayList() + ); + } }, mIsDarkMode); return Arrays.asList( @@ -462,7 +515,8 @@ public void onCreateView(Context initContext, public void attachToContainer(ViewGroup viewGroup, OnMediaLibraryButtonListener onMediaLibraryButtonListener, - OnReattachQueryListener onReattachQueryListener, + OnReattachMediaUploadQueryListener onReattachMediaUploadQueryListener, + OnReattachMediaSavingQueryListener onReattachMediaSavingQueryListener, OnEditorMountListener onEditorMountListener, OnEditorAutosaveListener onEditorAutosaveListener, OnAuthHeaderRequestedListener onAuthHeaderRequestedListener, @@ -474,12 +528,14 @@ public void attachToContainer(ViewGroup viewGroup, OnGutenbergDidSendButtonPressedActionListener onGutenbergDidSendButtonPressedActionListener, AddMentionUtil addMentionUtil, OnStarterPageTemplatesTooltipShownEventListener onStarterPageTemplatesTooltipListener, + OnMediaFilesCollectionBasedBlockEditorListener onMediaFilesCollectionBasedBlockEditorListener, boolean isDarkMode) { MutableContextWrapper contextWrapper = (MutableContextWrapper) mReactRootView.getContext(); contextWrapper.setBaseContext(viewGroup.getContext()); mOnMediaLibraryButtonListener = onMediaLibraryButtonListener; - mOnReattachQueryListener = onReattachQueryListener; + mOnReattachMediaUploadQueryListener = onReattachMediaUploadQueryListener; + mOnReattachMediaSavingQueryListener = onReattachMediaSavingQueryListener; mOnEditorMountListener = onEditorMountListener; mOnEditorAutosaveListener = onEditorAutosaveListener; mRequestExecutor = fetchExecutor; @@ -490,6 +546,7 @@ public void attachToContainer(ViewGroup viewGroup, mOnGutenbergDidSendButtonPressedActionListener = onGutenbergDidSendButtonPressedActionListener; mAddMentionUtil = addMentionUtil; mOnStarterPageTemplatesTooltipShownListener = onStarterPageTemplatesTooltipListener; + mOnMediaFilesCollectionBasedBlockEditorListener = onMediaFilesCollectionBasedBlockEditorListener; sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener); @@ -857,6 +914,30 @@ public void clearMediaFileURL(final int mediaId) { mDeferredEventEmitter.onUploadMediaFileClear(mediaId); } + public void clearFileSaveStatus(final String mediaId) { + mDeferredEventEmitter.onSaveMediaFileClear(mediaId); + } + + public void mediaFileSaveProgress(final String mediaId, final float progress) { + mDeferredEventEmitter.onMediaFileSaveProgress(mediaId, progress); + } + + public void mediaFileSaveFailed(final String mediaId) { + mDeferredEventEmitter.onMediaFileSaveFailed(mediaId); + } + + public void mediaFileSaveSucceeded(final String mediaId, final String mediaUrl) { + mDeferredEventEmitter.onMediaFileSaveSucceeded(mediaId, mediaUrl); + } + + public void mediaCollectionFinalSaveResult(final String blockFirstMediaId, final boolean success) { + mDeferredEventEmitter.onMediaCollectionSaveResult(blockFirstMediaId, success); + } + + public void mediaIdChanged(final String oldId, final String newId, final String oldUrl) { + mDeferredEventEmitter.onMediaIdChanged(oldId, newId, oldUrl); + } + public void replaceUnsupportedBlock(String content, String blockId) { if (mReplaceUnsupportedBlockCallback != null) { mReplaceUnsupportedBlockCallback.replaceUnsupportedBlock(content, blockId); @@ -864,6 +945,13 @@ public void replaceUnsupportedBlock(String content, String blockId) { } } + public void replaceMediaFilesEditedBlock(String mediaFiles, String blockId) { + if (mReplaceMediaFilesEditedBlockCallback != null) { + mReplaceMediaFilesEditedBlockCallback.replaceMediaFilesEditedBlock(mediaFiles, blockId); + mReplaceMediaFilesEditedBlockCallback = null; + } + } + private boolean isMediaSelectedCallbackRegistered() { return mMediaSelectedCallback != null; } diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 84122ec16bbe31..f7d76c91d53cd5 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -67,10 +67,50 @@ export function subscribeUpdateHtml( callback ) { return gutenbergBridgeEvents.addListener( 'updateHtml', callback ); } +/** + * Request to subscribe to mediaUpload events + * + * When a media item exists as a local file and is to be uploaded, these are the generated events that are useful listening to. + * see subscribeMediaSave for events during a save operation. + * + * @param {Function} callback RN Callback function to be called with the following + * state and params: + * state: + * MEDIA_UPLOAD_STATE_SAVING: this is a progress update. Takes String mediaId, float progress. + * MEDIA_UPLOAD_STATE_SUCCEEDED: sent when one media is finished being saved. Takes String mediaId, String mediaUrl, String serverID + * (which is the remote id assigned to this file after having been uploaded). + * MEDIA_UPLOAD_STATE_FAILED: sent in case of saving failure (final state). Takes String mediaId. + * MEDIA_UPLOAD_STATE_RESET: sent when the progress and state needs be reset (a retry for example, for cleanup). Takes String mediaId. + */ export function subscribeMediaUpload( callback ) { return gutenbergBridgeEvents.addListener( 'mediaUpload', callback ); } +/** + * Request to subscribe to mediaSave events + * + * When a media item does not yet exist as a local file and is progressively being saved, these are the generated events that are useful listening to. + * see subscribeMediaUpload for events during an upload operation. + * + * @param {Function} callback RN Callback function to be called with the following + * state and params: + * Note that the first 4 states described are similar to upload events. + * state: + * MEDIA_SAVE_STATE_SAVING: this is a progress update. Takes String mediaId, float progress. + * MEDIA_SAVE_STATE_SUCCEEDED: sent when one media is finished being saved. Takes String mediaId, String mediaUrl. + * MEDIA_SAVE_STATE_FAILED: sent in case of saving failure (final state). Takes String mediaId. + * MEDIA_SAVE_STATE_RESET: sent when the progress and state needs be reset (a retry for example, for cleanup). Takes String mediaId. + * MEDIA_SAVE_FINAL_STATE_RESULT: used in media collections, sent when ALL media items in a collection have reached + * a final state (either FAILED or SUCCEEDED). Handy to know when to show a final state to the user, on + * a media collection based block when we don't know if there are still events to be received for other + * items in the collection. + * MEDIA_SAVE_MEDIAID_CHANGED: used when changing a media item id from a temporary id to a local file id, and then from a local file + * id to a remote file id. + */ +export function subscribeMediaSave( callback ) { + return gutenbergBridgeEvents.addListener( 'mediaSave', callback ); +} + export function subscribeMediaAppend( callback ) { return gutenbergBridgeEvents.addListener( 'mediaAppend', callback ); } @@ -158,11 +198,6 @@ export function requestUnsupportedBlockFallback( ); } -/** - * Messages the client that an action button was pressed. - * - * @param {string} htmlContent One of the values deffined on `actionButtons` constant object. - */ export function sendActionButtonPressedAction( buttonType ) { RNReactNativeGutenbergBridge.actionButtonPressed( buttonType ); } @@ -171,10 +206,26 @@ export function requestMediaImport( url, callback ) { return RNReactNativeGutenbergBridge.requestMediaImport( url, callback ); } +/** + * Request to start listening to upload events when in-progress uploads are in place + * + * For example, when media is being uploaded and the user re-enters the editor + * + */ export function mediaUploadSync() { return RNReactNativeGutenbergBridge.mediaUploadSync(); } +/** + * Request to start listening to save events when in-progress saves are in place + * + * For example, when media is being saved and the user re-enters the editor + * + */ +export function mediaSaveSync() { + return RNReactNativeGutenbergBridge.mediaSaveSync(); +} + export function requestImageFailedRetryDialog( mediaId ) { return RNReactNativeGutenbergBridge.requestImageFailedRetryDialog( mediaId @@ -244,4 +295,63 @@ export function setStarterPageTemplatesTooltipShown( tooltipShown ) { ); } +/** + * Request the host app to show the block for editing its mediaFiles collection + * + * For example, a mediaFiles collection editor can make special handling of visualization + * in this regard. + * + * @param {Array} mediaFiles the mediaFiles attribute of the block, containing data about each media item. + * @param {string} blockClientId the clientId of the block. + */ +export function requestMediaFilesEditorLoad( mediaFiles, blockClientId ) { + RNReactNativeGutenbergBridge.requestMediaFilesEditorLoad( + mediaFiles, + blockClientId + ); +} + +/** + * Request the host app to show a retry dialog for mediaFiles arrays which contained items that failed + * to upload + * + * For example, tapping on a failed-media overlay would trigger this request and a "Retry?" dialog + * would be presented to the user + * + * @param {Array} mediaFiles the mediaFiles attribute of the block, containing data about each media item + */ +export function requestMediaFilesFailedRetryDialog( mediaFiles ) { + RNReactNativeGutenbergBridge.requestMediaFilesFailedRetryDialog( + mediaFiles + ); +} + +/** + * Request the host app to show a cancel dialog for mediaFiles arrays currently being uploaded + * + * For example, tapping on a block containing mediaFiles that are currently being uplaoded would trigger this request + * and a "Cancel upload?" dialog would be presented to the user. + * + * @param {Array} mediaFiles the mediaFiles attribute of the block, containing data about each media item + */ +export function requestMediaFilesUploadCancelDialog( mediaFiles ) { + RNReactNativeGutenbergBridge.requestMediaFilesUploadCancelDialog( + mediaFiles + ); +} + +/** + * Request the host app to show a cancel dialog for mediaFiles arrays currently undergoing a save operation + * + * Save operations on mediaFiles collection could be lengthy so for example, tapping on a mediaFiles-type block + * currently being saved would trigger this request and a "Cancel save?" dialog would be presented to the user + * + * @param {Array} mediaFiles the mediaFiles attribute of the block, containing data about each media item. + */ +export function requestMediaFilesSaveCancelDialog( mediaFiles ) { + RNReactNativeGutenbergBridge.requestMediaFilesSaveCancelDialog( + mediaFiles + ); +} + export default RNReactNativeGutenbergBridge; diff --git a/packages/react-native-bridge/ios/Gutenberg.swift b/packages/react-native-bridge/ios/Gutenberg.swift index c8bf028e88705e..7a194e53e31a54 100644 --- a/packages/react-native-bridge/ios/Gutenberg.swift +++ b/packages/react-native-bridge/ios/Gutenberg.swift @@ -141,8 +141,33 @@ public class Gutenberg: NSObject { private func sendEvent(_ event: RNReactNativeGutenbergBridge.EventName, body: [String: Any]? = nil) { bridgeModule.sendEvent(withName: event.rawValue, body: body) } - + public func mediaUploadUpdate(id: Int32, state: MediaUploadState, progress: Float, url: URL?, serverID: Int32?) { + mediaUpdate(event: .mediaUpload, id: id, state: state, progress: progress, url: url, serverID: serverID) + } + + public func updateMediaSaveStatus(id: Int32, state: MediaSaveState, progress: Float, url: URL?, serverID: Int32?) { + mediaUpdate(event: .mediaSave, id: id, state: state, progress: progress, url: url, serverID: serverID) + } + + public func onMediaCollectionSaveResult(firstMediaIdInCollection: String, success: Bool) { + sendEvent(.mediaSave, body: [ + "state": MediaSaveEvent.result.rawValue, + "firstMediaIdInCollection": firstMediaIdInCollection, + "success": success, + ]) + } + + public func onMediaIdChanged(oldId: String, newId: String, oldUrl: URL) { + sendEvent(.mediaSave, body: [ + "state": MediaSaveEvent.idChange.rawValue, + "oldId": oldId, + "newId": newId, + "oldUrl": oldUrl, + ]) + } + + private func mediaUpdate(event: RNReactNativeGutenbergBridge.EventName, id: Int32, state: State, progress: Float, url: URL?, serverID: Int32?) { var data: [String: Any] = ["mediaId": id, "state": state.rawValue, "progress": progress]; if let url = url { data["mediaUrl"] = url.absoluteString @@ -150,7 +175,7 @@ public class Gutenberg: NSObject { if let serverID = serverID { data["mediaServerId"] = serverID } - sendEvent(.mediaUpload, body: data) + sendEvent(event, body: data) } public func appendMedia(id: Int32, url: URL, type: MediaType) { @@ -204,14 +229,27 @@ extension Gutenberg: RCTBridgeDelegate { } } +protocol MediaState: RawRepresentable {} + extension Gutenberg { - public enum MediaUploadState: Int { + public enum MediaUploadState: Int, MediaState { case uploading = 1 case succeeded = 2 case failed = 3 case reset = 4 } - + + public enum MediaSaveState: Int, MediaState { + case saving = 5 + case succeeded = 6 + case failed = 7 + case reset = 8 + } + + enum MediaSaveEvent: Int { + case result = 9 + case idChange = 10 + } } extension Gutenberg { diff --git a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift index 7f2b91db3a76ab..ce250c9c215183 100644 --- a/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift +++ b/packages/react-native-bridge/ios/GutenbergBridgeDelegate.swift @@ -14,6 +14,7 @@ public struct MediaInfo { /// Definition of capabilities to enable in the Block Editor public enum Capabilities: String { + case mediaFilesCollectionBlock case mentions case unsupportedBlockEditor case canEnableUnsupportedBlockEditor @@ -117,10 +118,10 @@ extension RCTLogLevel { } public enum GutenbergUserEvent { - + case editorSessionTemplateApply(_ template: String) case editorSessionTemplatePreview(_ template: String) - + init?(event: String, properties:[AnyHashable: Any]?) { switch event { case "editor_session_template_apply": @@ -210,7 +211,7 @@ public protocol GutenbergBridgeDelegate: class { /// Tells the delegate to display the media editor from a given URL /// func gutenbergDidRequestMediaEditor(with mediaUrl: URL, callback: @escaping MediaPickerDidPickMediaCallback) - + /// Tells the delegate that the editor needs to log a custom event /// - Parameter event: The event key to be logged func gutenbergDidLogUserEvent(_ event: GutenbergUserEvent) @@ -224,12 +225,26 @@ public protocol GutenbergBridgeDelegate: class { /// Tells the delegate that the editor requested to show the tooltip func gutenbergDidRequestStarterPageTemplatesTooltipShown() -> Bool - + /// Tells the delegate that the editor requested to set the tooltip's visibility - /// - Parameter tooltipShown: Tooltip's visibility value + /// - Parameter tooltipShown: Tooltip's visibility value func gutenbergDidRequestSetStarterPageTemplatesTooltipShown(_ tooltipShown: Bool) func gutenbergDidSendButtonPressedAction(_ buttonType: Gutenberg.ActionButtonType) + + // Media Collection + + /// Tells the delegate that a media collection block requested to reconnect with media save coordinator. + /// + func gutenbergDidRequestMediaSaveSync() + + func gutenbergDidRequestMediaFilesEditorLoad(_ mediaFiles: [String], blockId: String) + + func gutenbergDidRequestMediaFilesFailedRetryDialog(_ mediaFiles: [String]) + + func gutenbergDidRequestMediaFilesUploadCancelDialog(_ mediaFiles: [String]) + + func gutenbergDidRequestMediaFilesSaveCancelDialog(_ mediaFiles: [String]) } // MARK: - Optional GutenbergBridgeDelegate methods @@ -239,4 +254,12 @@ public extension GutenbergBridgeDelegate { func gutenbergDidLayout() { } func gutenbergDidRequestUnsupportedBlockFallback(for block: Block) { } func gutenbergDidSendButtonPressedAction(_ buttonType: Gutenberg.ActionButtonType) { } + + // Media Collection + + func gutenbergDidRequestMediaSaveSync() {} + func gutenbergDidRequestMediaFilesEditorLoad(_ mediaFiles: [String], blockId: String) { } + func gutenbergDidRequestMediaFilesFailedRetryDialog(_ mediaFiles: [String]) { } + func gutenbergDidRequestMediaFilesUploadCancelDialog(_ mediaFiles: [String]) { } + func gutenbergDidRequestMediaFilesSaveCancelDialog(_ mediaFiles: [String]) { } } diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m index 9b68bf074f161d..aafdce5c869aa0 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m @@ -23,6 +23,12 @@ @interface RCT_EXTERN_MODULE(RNReactNativeGutenbergBridge, NSObject) RCT_EXTERN_METHOD(addMention:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)rejecter) RCT_EXTERN_METHOD(requestStarterPageTemplatesTooltipShown:(RCTResponseSenderBlock)callback) RCT_EXTERN_METHOD(setStarterPageTemplatesTooltipShown:(BOOL)tooltipShown) +RCT_EXTERN_METHOD(requestMediaFilesEditorLoad:(NSArray *)mediaFiles blockId:(NSString *)blockId) +RCT_EXTERN_METHOD(requestMediaFilesFailedRetryDialog:(NSArray *)mediaFiles) +RCT_EXTERN_METHOD(requestMediaFilesUploadCancelDialog:(NSArray *)mediaFiles) +RCT_EXTERN_METHOD(requestMediaFilesSaveCancelDialog:(NSArray *)mediaFiles) +RCT_EXTERN_METHOD(onCancelUploadForMediaCollection:(NSArray *)mediaFiles) RCT_EXTERN_METHOD(actionButtonPressed:(NSString *)buttonType) +RCT_EXTERN_METHOD(mediaSaveSync) @end diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index 520b911a27e09e..24160bfd27ab18 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -288,6 +288,43 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { self.delegate?.gutenbergDidRequestSetStarterPageTemplatesTooltipShown(tooltipShown) } + @objc + func requestMediaFilesEditorLoad(_ mediaFiles: [String], blockId: String) { + DispatchQueue.main.async { + self.delegate?.gutenbergDidRequestMediaFilesEditorLoad(mediaFiles, blockId: blockId) + } + } + + @objc + func requestMediaFilesFailedRetryDialog(_ mediaFiles: [String]) { + DispatchQueue.main.async { + self.delegate?.gutenbergDidRequestMediaFilesFailedRetryDialog(mediaFiles) + } + } + + @objc + func requestMediaFilesUploadCancelDialog(_ mediaFiles: [String]) { + DispatchQueue.main.async { + self.delegate?.gutenbergDidRequestMediaFilesUploadCancelDialog(mediaFiles) + } + } + + @objc + func requestMediaFilesSaveCancelDialog(_ mediaFiles: [String]) { + DispatchQueue.main.async { + self.delegate?.gutenbergDidRequestMediaFilesSaveCancelDialog(mediaFiles) + } + } + + @objc + func mediaSaveSync() { + DispatchQueue.main.async { + if self.hasObservers { + self.delegate?.gutenbergDidRequestMediaSaveSync() + } + } + } + @objc func actionButtonPressed(_ buttonType: String) { guard let button = Gutenberg.ActionButtonType(rawValue: buttonType) else { @@ -321,6 +358,7 @@ extension RNReactNativeGutenbergBridge { case replaceBlock case updateCapabilities case showNotice + case mediaSave } public override func supportedEvents() -> [String]! { diff --git a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java index 66a4bfc431b872..311b234ffd05e7 100644 --- a/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java +++ b/packages/react-native-editor/android/app/src/main/java/com/gutenberg/MainApplication.java @@ -5,6 +5,7 @@ import android.content.res.Configuration; import android.os.Bundle; import android.util.Log; +import android.widget.Toast; import androidx.core.util.Consumer; @@ -83,6 +84,10 @@ public void requestMediaPickFromMediaLibrary(MediaSelectedCallback mediaSelected public void mediaUploadSync(MediaSelectedCallback mediaSelectedCallback) { } + @Override + public void mediaSaveSync(MediaSelectedCallback mediaSelectedCallback) { + } + @Override public void requestImageFailedRetryDialog(int mediaId) { } @@ -171,11 +176,35 @@ public void onAddMention(Consumer onSuccess) { onSuccess.accept("matt"); } + @Override + public void requestMediaFilesEditorLoad( + ReplaceMediaFilesEditedBlockCallback replaceMediaFilesEditedBlockCallback, + ReadableArray mediaFiles, + String blockId + ) { + Toast.makeText(MainApplication.this, "requestMediaFilesEditorLoad called", Toast.LENGTH_SHORT).show(); + } + + @Override + public void requestMediaFilesFailedRetryDialog(ReadableArray mediaFiles) { + Toast.makeText(MainApplication.this, "requestMediaFilesFailedRetryDialog called", Toast.LENGTH_SHORT).show(); + } + + @Override + public void requestMediaFilesUploadCancelDialog(ReadableArray mediaFiles) { + Toast.makeText(MainApplication.this, "requestMediaFilesUploadCancelDialog called", Toast.LENGTH_SHORT).show(); + } + + @Override + public void requestMediaFilesSaveCancelDialog(ReadableArray mediaFiles) { + Toast.makeText(MainApplication.this, "requestMediaFilesSaveCancelDialog called", Toast.LENGTH_SHORT).show(); + } + @Override public void gutenbergDidSendButtonPressedAction(String buttonType) { } - + }, isDarkMode()); return new ReactNativeHost(this) { diff --git a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift index 99f9051f0b101d..d23256220c2167 100644 --- a/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift +++ b/packages/react-native-editor/ios/GutenbergDemo/GutenbergViewController.swift @@ -50,7 +50,6 @@ class GutenbergViewController: UIViewController { } extension GutenbergViewController: GutenbergBridgeDelegate { - func gutenbergDidRequestFetch(path: String, completion: @escaping (Result) -> Void) { completion(Result.success([:])) } @@ -222,6 +221,26 @@ extension GutenbergViewController: GutenbergBridgeDelegate { func gutenbergDidRequestSetStarterPageTemplatesTooltipShown(_ tooltipShown: Bool) { print("Gutenberg requested setting tooltip flag") } + + func gutenbergDidRequestMediaSaveSync() { + print(#function) + } + + func gutenbergDidRequestMediaFilesEditorLoad(_ mediaFiles: [String], blockId: String) { + print(#function) + } + + func gutenbergDidRequestMediaFilesFailedRetryDialog(_ mediaFiles: [String]) { + print(#function) + } + + func gutenbergDidRequestMediaFilesUploadCancelDialog(_ mediaFiles: [String]) { + print(#function) + } + + func gutenbergDidRequestMediaFilesSaveCancelDialog(_ mediaFiles: [String]) { + print(#function) + } } extension GutenbergViewController: GutenbergWebDelegate { @@ -270,6 +289,7 @@ extension GutenbergViewController: GutenbergBridgeDataSource { .mentions: true, .unsupportedBlockEditor: unsupportedBlockEnabled, .canEnableUnsupportedBlockEditor: unsupportedBlockCanBeActivated, + .mediaFilesCollectionBlock: true, ] } diff --git a/test/native/setup.js b/test/native/setup.js index 5325be4e1fe869..d6125cc22fe688 100644 --- a/test/native/setup.js +++ b/test/native/setup.js @@ -30,6 +30,7 @@ jest.mock( '@wordpress/react-native-bridge', () => { editorDidMount: jest.fn(), editorDidAutosave: jest.fn(), subscribeMediaUpload: jest.fn(), + subscribeMediaSave: jest.fn(), getOtherMediaOptions: jest.fn(), requestMediaPicker: jest.fn(), requestUnsupportedBlockFallback: jest.fn(),