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..2f9844bc7a22be --- /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 IDs that identify the current story upload. + +- 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..0ff813f88c4721 --- /dev/null +++ b/packages/block-editor/src/components/block-media-update-progress/index.native.js @@ -0,0 +1,293 @@ +/** + * 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_MEDIAMODEL_CREATED = 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 ) { + for ( let i = 0; i < this.props.mediaFiles.length; i++ ) { + if ( mediaFiles[ i ].id === mediaId.toString() ) { + return true; + } + } + } + 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_MEDIAMODEL_CREATED: + this.mediaModelCreated( 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( { isUploadInProgress: false, isUploadFailed: 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 ); + } + } + + mediaModelCreated( payload ) { + this.setState( { + isUploadInProgress: false, + isUploadFailed: false, + isSaveInProgress: false, + isSaveFailed: false, + } ); + if ( this.props.onMediaModelCreated ) { + this.props.onMediaModelCreated( 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 retryMessage = __( + 'Failed to save files.\nPlease tap for options.' + ); + + 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/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/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 d05ef0cdf20e00..89cb3ccf3b5adc 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,12 +34,21 @@ 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 onMediaModelCreatedForFile(final String oldId, final String newId, final String oldUrl); + } + interface ReplaceUnsupportedBlockCallback { void replaceUnsupportedBlock(String content, String blockId); } - interface ReplaceStoryEditedBlockCallback { - void replaceStoryBlock(String mediaFiles, String blockId); + interface ReplaceMediaFilesEditedBlockCallback { + void replaceMediaFilesEditedBlock(String mediaFiles, String blockId); } interface StarterPageTemplatesTooltipShownCallback { @@ -126,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); @@ -160,7 +171,7 @@ void gutenbergDidRequestUnsupportedBlockFallback(ReplaceUnsupportedBlockCallback void requestStarterPageTemplatesTooltipShown(StarterPageTemplatesTooltipShownCallback starterPageTemplatesTooltipShownCallback); - void requestStoryCreatorLoad(ReplaceStoryEditedBlockCallback replaceUnsupportedBlockCallback, + void requestMediaFilesEditorLoad(ReplaceMediaFilesEditedBlockCallback replaceMediaFilesEditedBlockCallback, ReadableArray mediaFiles, String blockId ); 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 8d84545cf04614..e64c9d357c2be2 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 @@ -49,11 +49,13 @@ 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"; 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 = "mediaFinalSaveResult"; private static final String MAP_KEY_IS_PREFERRED_COLOR_SCHEME_DARK = "isPreferredColorSchemeDark"; @@ -195,6 +197,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); @@ -221,8 +228,8 @@ public void requestMediaEditor(String mediaUrl, final Callback onUploadMediaSele } @ReactMethod - public void requestStoryCreatorLoad(ReadableArray mediaFiles, String blockId) { - mGutenbergBridgeJS2Parent.requestStoryCreatorLoad((savedMediaFiles, savedBlockId) -> + public void requestMediaFilesEditorLoad(ReadableArray mediaFiles, String blockId) { + mGutenbergBridgeJS2Parent.requestMediaFilesEditorLoad((savedMediaFiles, savedBlockId) -> replaceBlock(savedMediaFiles, savedBlockId), mediaFiles, blockId); } 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..98b473322a32f4 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; -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_MEDIAMODEL_CREATED = 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); + 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_MEDIAMODEL_CREATED; } @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 onMediaModelCreatedForFile(String oldId, String newId, String oldUrl) { + WritableMap writableMap = new WritableNativeMap(); + writableMap.putInt(MAP_KEY_MEDIA_FILE_STATE, MEDIA_SAVE_MEDIAMODEL_CREATED); + 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_MEDIAMODEL_CREATED)) { + 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/WPAndroidGlueCode.java b/packages/react-native-bridge/android/src/main/java/org/wordpress/mobile/WPAndroidGlue/WPAndroidGlueCode.java index b30cc12f9af5b4..9c61321afd67fc 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,7 +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.ReplaceStoryEditedBlockCallback; +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; @@ -83,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; @@ -93,8 +94,8 @@ public class WPAndroidGlueCode { private OnGutenbergDidSendButtonPressedActionListener mOnGutenbergDidSendButtonPressedActionListener; private ReplaceUnsupportedBlockCallback mReplaceUnsupportedBlockCallback; private OnStarterPageTemplatesTooltipShownEventListener mOnStarterPageTemplatesTooltipShownListener; - private OnStoryCreatorLoadRequestListener mOnStoryCreatorLoadRequestListener; - private ReplaceStoryEditedBlockCallback mReplaceStoryEditedBlockCallback; + private OnMediaFilesEditorLoadRequestListener mOnMediaFilesEditorLoadRequestListener; + private ReplaceMediaFilesEditedBlockCallback mReplaceMediaFilesEditedBlockCallback; private boolean mIsEditorMounted; private String mContentHtml = ""; @@ -149,18 +150,22 @@ public interface OnMediaLibraryButtonListener { void onOtherMediaButtonClicked(String mediaSource, boolean allowMultipleSelection); } - public interface OnStoryCreatorLoadRequestListener { - void onRequestStoryCreatorLoad(ArrayList mediaFiles, String blockId); + public interface OnMediaFilesEditorLoadRequestListener { + void onRequestMediaFilesEditorLoad(ArrayList mediaFiles, String blockId); } 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); } @@ -273,7 +278,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 @@ -407,13 +418,13 @@ public void requestStarterPageTemplatesTooltipShown(StarterPageTemplatesTooltipS } @Override - public void requestStoryCreatorLoad( - ReplaceStoryEditedBlockCallback replaceStoryEditedBlockCallback, + public void requestMediaFilesEditorLoad( + ReplaceMediaFilesEditedBlockCallback replaceMediaFilesEditedBlockCallback, ReadableArray mediaFiles, String blockId ) { - mReplaceStoryEditedBlockCallback = replaceStoryEditedBlockCallback; - mOnStoryCreatorLoadRequestListener.onRequestStoryCreatorLoad(mediaFiles.toArrayList(), blockId); + mReplaceMediaFilesEditedBlockCallback = replaceMediaFilesEditedBlockCallback; + mOnMediaFilesEditorLoadRequestListener.onRequestMediaFilesEditorLoad(mediaFiles.toArrayList(), blockId); } }, mIsDarkMode); @@ -478,7 +489,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, @@ -490,13 +502,14 @@ public void attachToContainer(ViewGroup viewGroup, OnGutenbergDidSendButtonPressedActionListener onGutenbergDidSendButtonPressedActionListener, AddMentionUtil addMentionUtil, OnStarterPageTemplatesTooltipShownEventListener onStarterPageTemplatesTooltipListener, - OnStoryCreatorLoadRequestListener onStoryCreatorLoadRequestListener, + OnMediaFilesEditorLoadRequestListener onMediaFilesEditorLoadRequestListener, 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; @@ -507,7 +520,7 @@ public void attachToContainer(ViewGroup viewGroup, mOnGutenbergDidSendButtonPressedActionListener = onGutenbergDidSendButtonPressedActionListener; mAddMentionUtil = addMentionUtil; mOnStarterPageTemplatesTooltipShownListener = onStarterPageTemplatesTooltipListener; - mOnStoryCreatorLoadRequestListener = onStoryCreatorLoadRequestListener; + mOnMediaFilesEditorLoadRequestListener = onMediaFilesEditorLoadRequestListener; sAddCookiesInterceptor.setOnAuthHeaderRequestedListener(onAuthHeaderRequestedListener); @@ -867,6 +880,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 mediaModelCreatedForFile(final String oldId, final String newId, final String oldUrl) { + mDeferredEventEmitter.onMediaModelCreatedForFile(oldId, newId, oldUrl); + } + public void replaceUnsupportedBlock(String content, String blockId) { if (mReplaceUnsupportedBlockCallback != null) { mReplaceUnsupportedBlockCallback.replaceUnsupportedBlock(content, blockId); @@ -874,10 +911,10 @@ public void replaceUnsupportedBlock(String content, String blockId) { } } - public void replaceStoryEditedBlock(String mediaFiles, String blockId) { - if (mReplaceStoryEditedBlockCallback != null) { - mReplaceStoryEditedBlockCallback.replaceStoryBlock(mediaFiles, blockId); - mReplaceStoryEditedBlockCallback = null; + public void replaceMediaFilesEditedBlock(String mediaFiles, String blockId) { + if (mReplaceMediaFilesEditedBlockCallback != null) { + mReplaceMediaFilesEditedBlockCallback.replaceMediaFilesEditedBlock(mediaFiles, blockId); + mReplaceMediaFilesEditedBlockCallback = null; } } diff --git a/packages/react-native-bridge/index.js b/packages/react-native-bridge/index.js index 14595fa71d918f..c411f06dfd4716 100644 --- a/packages/react-native-bridge/index.js +++ b/packages/react-native-bridge/index.js @@ -71,6 +71,10 @@ export function subscribeMediaUpload( callback ) { return gutenbergBridgeEvents.addListener( 'mediaUpload', callback ); } +export function subscribeMediaSave( callback ) { + return gutenbergBridgeEvents.addListener( 'mediaSave', callback ); +} + export function subscribeMediaAppend( callback ) { return gutenbergBridgeEvents.addListener( 'mediaAppend', callback ); } @@ -175,6 +179,10 @@ export function mediaUploadSync() { return RNReactNativeGutenbergBridge.mediaUploadSync(); } +export function mediaSaveSync() { + return RNReactNativeGutenbergBridge.mediaSaveSync(); +} + export function requestImageFailedRetryDialog( mediaId ) { return RNReactNativeGutenbergBridge.requestImageFailedRetryDialog( mediaId @@ -244,8 +252,8 @@ export function setStarterPageTemplatesTooltipShown( tooltipShown ) { ); } -export function requestStoryCreatorLoad( mediaFiles, blockClientId ) { - return RNReactNativeGutenbergBridge.requestStoryCreatorLoad( +export function requestMediaFilesEditorLoad( mediaFiles, blockClientId ) { + return RNReactNativeGutenbergBridge.requestMediaFilesEditorLoad( mediaFiles, blockClientId ); diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m index 618c1c73937f8b..46b813cfc660de 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.m @@ -23,7 +23,7 @@ @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(requestStoryCreatorLoad:(NSArray *)mediaFiles blockId:(NSString *)blockId) +RCT_EXTERN_METHOD(requestMediaFilesEditorLoad:(NSArray *)mediaFiles blockId:(NSString *)blockId) RCT_EXTERN_METHOD(actionButtonPressed:(NSString *)buttonType) @end diff --git a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift index f2c81ecc085abb..c7dbfa38e6632e 100644 --- a/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift +++ b/packages/react-native-bridge/ios/RNReactNativeGutenbergBridge.swift @@ -289,11 +289,11 @@ public class RNReactNativeGutenbergBridge: RCTEventEmitter { } @objc - func requestStoryCreatorLoad(_ mediaFilesArray: [AnyObject], blockId: String) { + func requestMediaFilesEditorLoad(_ mediaFilesArray: [AnyObject], blockId: String) { // TODO actually implement the delegate call on iOS let mediaFiles = mediaFilesArray.compactMap { $0 as? String } // DispatchQueue.main.async { - // self.delegate?.gutenbergDidRequestStoryCreatorLoad(mediaFiles: mediaFiles, blockId: blockId) + // self.delegate?.gutenbergDidRequestMediaFilesEditorLoad(mediaFiles: mediaFiles, blockId: blockId) // } } 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 b0fac206f0c1ac..a32138a0dba2e8 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 @@ -84,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) { } @@ -173,14 +177,14 @@ public void onAddMention(Consumer onSuccess) { } @Override - public void requestStoryCreatorLoad( - ReplaceStoryEditedBlockCallback replaceStoryEditedBlockCallback, + public void requestMediaFilesEditorLoad( + ReplaceMediaFilesEditedBlockCallback replaceMediaFilesEditedBlockCallback, ReadableArray mediaFiles, String blockId ) { // mReplaceStoryEditedBlockCallback = replaceStoryEditedBlockCallback; - // mOnStoryCreatorLoadRequestListener.onRequestStoryCreatorLoad(content, blockId); - Toast.makeText(MainApplication.this, "requestStoryCreatorLoad called", Toast.LENGTH_SHORT).show(); + // mOnMediaFilesEditorLoadRequestListener.onRequestMediaFilesEditorLoad(content, blockId); + Toast.makeText(MainApplication.this, "requestMediaFilesEditorLoad called", Toast.LENGTH_SHORT).show(); } @Override 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(),