From 898c8d721c91190907603290637bdd27b9c92bd1 Mon Sep 17 00:00:00 2001 From: mzorz Date: Tue, 10 Nov 2020 14:43:46 -0300 Subject: [PATCH] Mobile stories block (#17140) * basic scaffolding for the Story block on mobile editor * added flex alignment and actual mediaFiles attribute rendering * applying styles to stories block view * removed unused alignment * added StoryEditingButton and showing it when the story block is selected * connecting StoryEditingButton with bridge's requestStoryCreatorLoad * updated requestStoryCreatorLoad call to pass block's mediaFiles and clientId over the bridge * added new component StoryUpdateProgress * Revert "added new component StoryUpdateProgress", not belonging to this branch This reverts commit 86fb9569e2b17b252f78f58a67151e7f64b8c81d. * fixed lint warnings * changed the no longer existing color dark-gray-500 removed in https://github.com/WordPress/gutenberg/commit/162bc50a487f987d1c3250d769f07a28598fe3ae with gray-700 * added package-lock.json * Mobile Stories block (part 2): introduce StoryUpdateProgress (#17222) * Revert "Revert "added new component StoryUpdateProgress", not belonging to this branch" This reverts commit 34349b18976191249fe6d38efc9a1f8796a7485b. * added mediaSave statuses listeners definitions * moved StoryEdit to a React.Component class and implemented StoryUpdateProgress overlay * added onStorySaveResult handling to Story block * edit mode: replacing urls by id for saving process * added onMediaModelCreated() callback so we can re-assign the mediaID to the mediaFiles attribute of a Story block once such a mediaModel is created * update the mediaFile id and URL of a given story frame when finished uploading succesfully * removed commented imports * added explicit TODO comments to make sure to follow up on them for error handling * make sure to call mediaUploadSync and storySaveSync if any of the mediaFiles contained in this block is not a remote url - also call onRemoveBlockCheckUpload() action under the same conditions in componentWillUnmount() * Mobile Stories block (part 3): rename using BlockMediaUpdateProgress (#17456) * using BlockMediaUpdateProgress * method rename * updated (automatically modified) package-lock.json * removed package-lock.json * moved condition check for http or https to helper method isUrlRemote() * calling mediaUploadSync and mediaSaveSync regardless of ids, we always will want to receive updates on all 3 stages * fixed spelling * Mobile Stories block (part 4): error handling (#17458) * Revert "Revert "added new component StoryUpdateProgress", not belonging to this branch" This reverts commit 34349b18976191249fe6d38efc9a1f8796a7485b. * added mediaSave statuses listeners definitions * moved StoryEdit to a React.Component class and implemented StoryUpdateProgress overlay * added onStorySaveResult handling to Story block * edit mode: replacing urls by id for saving process * added onMediaModelCreated() callback so we can re-assign the mediaID to the mediaFiles attribute of a Story block once such a mediaModel is created * update the mediaFile id and URL of a given story frame when finished uploading succesfully * removed commented imports * added explicit TODO comments to make sure to follow up on them for error handling * make sure to call mediaUploadSync and storySaveSync if any of the mediaFiles contained in this block is not a remote url - also call onRemoveBlockCheckUpload() action under the same conditions in componentWillUnmount() * using BlockMediaUpdateProgress * method rename * updated story block to represent error state * added cancel and retry bridge methods specific for mediaFiles collection based blocks * using a deep copy of mediaFiles when replacing ids and mediaUrl, given mediaFiles attribute needs to be replaced again and cannot be modified in place as per React conventions * added requestMediaFilesSaveCancelDialog bridge method * changed props name onMediaModelCreated to more generic onMediaIdChanged * removed commented code * replaced for loops with map * Mobile Stories block (part 5): add empty placeholder (#17539) * Revert "Revert "added new component StoryUpdateProgress", not belonging to this branch" This reverts commit 34349b18976191249fe6d38efc9a1f8796a7485b. * added mediaSave statuses listeners definitions * moved StoryEdit to a React.Component class and implemented StoryUpdateProgress overlay * added onStorySaveResult handling to Story block * edit mode: replacing urls by id for saving process * added onMediaModelCreated() callback so we can re-assign the mediaID to the mediaFiles attribute of a Story block once such a mediaModel is created * update the mediaFile id and URL of a given story frame when finished uploading succesfully * removed commented imports * added explicit TODO comments to make sure to follow up on them for error handling * make sure to call mediaUploadSync and storySaveSync if any of the mediaFiles contained in this block is not a remote url - also call onRemoveBlockCheckUpload() action under the same conditions in componentWillUnmount() * using BlockMediaUpdateProgress * method rename * updated story block to represent error state * added cancel and retry bridge methods specific for mediaFiles collection based blocks * using a deep copy of mediaFiles when replacing ids and mediaUrl, given mediaFiles attribute needs to be replaced again and cannot be modified in place as per React conventions * added requestMediaFilesSaveCancelDialog bridge method * changed props name onMediaModelCreated to more generic onMediaIdChanged * removed commented code * added MediaPlaceholder, wrapped in a pointerEvents=none View so we can handle media picking from the StoryComposer * replaced for loops with map * importing defined const MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO instead of re-defining * made placeholder container look as per any other media placeholder, reservign the Story style container for populated Story blocks * Mobile Stories block (part 6): rewrote StoryEdit class as a function (#17549) * Revert "Revert "added new component StoryUpdateProgress", not belonging to this branch" This reverts commit 34349b18976191249fe6d38efc9a1f8796a7485b. * added mediaSave statuses listeners definitions * moved StoryEdit to a React.Component class and implemented StoryUpdateProgress overlay * added onStorySaveResult handling to Story block * edit mode: replacing urls by id for saving process * added onMediaModelCreated() callback so we can re-assign the mediaID to the mediaFiles attribute of a Story block once such a mediaModel is created * update the mediaFile id and URL of a given story frame when finished uploading succesfully * removed commented imports * added explicit TODO comments to make sure to follow up on them for error handling * make sure to call mediaUploadSync and storySaveSync if any of the mediaFiles contained in this block is not a remote url - also call onRemoveBlockCheckUpload() action under the same conditions in componentWillUnmount() * using BlockMediaUpdateProgress * method rename * updated story block to represent error state * added cancel and retry bridge methods specific for mediaFiles collection based blocks * using a deep copy of mediaFiles when replacing ids and mediaUrl, given mediaFiles attribute needs to be replaced again and cannot be modified in place as per React conventions * added requestMediaFilesSaveCancelDialog bridge method * changed props name onMediaModelCreated to more generic onMediaIdChanged * removed commented code * added MediaPlaceholder, wrapped in a pointerEvents=none View so we can handle media picking from the StoryComposer * replaced for loops with map * rewrote StoryEdit class as a function * removed unused imports * removed unused function * story block: set inserter support to false for web, but keep it for mobile (#17690) * renamed params to avoid name shadowing --- extensions/blocks/contact-info/edit.native.js | 7 +- extensions/blocks/story/edit.native.js | 240 ++++++++++++++++++ extensions/blocks/story/editor.native.scss | 97 +++++++ .../blocks/story/icon-customize.native.js | 10 + extensions/blocks/story/index.js | 5 +- .../story/story-editing-button.native.js | 30 +++ extensions/editor.native.js | 1 + extensions/shared/icons.scss | 2 +- 8 files changed, 388 insertions(+), 4 deletions(-) create mode 100644 extensions/blocks/story/edit.native.js create mode 100644 extensions/blocks/story/editor.native.scss create mode 100644 extensions/blocks/story/icon-customize.native.js create mode 100644 extensions/blocks/story/story-editing-button.native.js diff --git a/extensions/blocks/contact-info/edit.native.js b/extensions/blocks/contact-info/edit.native.js index 425983394738f..649c6b248879e 100644 --- a/extensions/blocks/contact-info/edit.native.js +++ b/extensions/blocks/contact-info/edit.native.js @@ -23,7 +23,12 @@ const TEMPLATE = [ [ 'jetpack/email' ], [ 'jetpack/phone' ], [ 'jetpack/address' const ContactInfoEdit = () => { return ( - + ); }; diff --git a/extensions/blocks/story/edit.native.js b/extensions/blocks/story/edit.native.js new file mode 100644 index 0000000000000..af7b5768b8ad5 --- /dev/null +++ b/extensions/blocks/story/edit.native.js @@ -0,0 +1,240 @@ +/** + * External dependencies + */ +import { Text, View, TouchableWithoutFeedback } from 'react-native'; +/** + * WordPress dependencies + */ +import { Image } from '@wordpress/components'; +import { + BlockIcon, + MediaPlaceholder, + BlockMediaUpdateProgress, + MEDIA_TYPE_IMAGE, + MEDIA_TYPE_VIDEO, +} from '@wordpress/block-editor'; +import { __, sprintf } from '@wordpress/i18n'; +import { useEffect, useState } from '@wordpress/element'; +import { getProtocol } from '@wordpress/url'; +import { + requestMediaFilesFailedRetryDialog, + requestMediaFilesSaveCancelDialog, + requestMediaFilesUploadCancelDialog, + mediaUploadSync, + mediaSaveSync, + requestMediaFilesEditorLoad, +} from '@wordpress/react-native-bridge'; + +/** + * Internal dependencies + */ +import { icon } from '.'; +import styles from './editor.scss'; +import StoryEditingButton from './story-editing-button'; + +const StoryEdit = ( { attributes, isSelected, clientId, setAttributes, onFocus } ) => { + const { mediaFiles } = attributes; + const hasContent = !! mediaFiles.length; + + // setup state vars + const [ isUploadInProgress, setUploadInProgress ] = useState( false ); + + const [ isSaveInProgress, setSaveInProgress ] = useState( false ); + + const [ didUploadFail, setUploadFail ] = useState( false ); + + const [ didSaveFail, setSaveFail ] = useState( false ); + + // sync with local media store + useEffect( mediaUploadSync, [] ); + + useEffect( mediaSaveSync, [] ); + + function onEditButtonTapped() { + // let's open the Story Creator and load this block in there + requestMediaFilesEditorLoad( mediaFiles, clientId ); + } + + // upload state handling methods + function updateMediaUploadProgress( payload ) { + if ( payload.mediaUrl ) { + setAttributes( { url: payload.mediaUrl } ); + } + if ( ! isUploadInProgress ) { + setUploadInProgress( true ); + } + } + + function finishMediaUploadWithSuccess( payload ) { + // find the mediaFiles item that needs to change via its id, and apply the new URL + const updatedMediaFiles = replaceNewIdInMediaFilesByOldId( + payload.mediaId, + payload.mediaServerId, + payload.mediaUrl + ); + setAttributes( { mediaFiles: updatedMediaFiles } ); + setUploadInProgress( false ); + } + + function finishMediaUploadWithFailure( payload ) { + // should anything be done on media upload failure, do it here + setUploadInProgress( false ); + setUploadFail( true ); + } + + function mediaUploadStateReset() { + setUploadInProgress( false ); + } + + // save state handling methods + function updateMediaSaveProgress( payload ) { + if ( payload.mediaUrl ) { + setAttributes( { url: payload.mediaUrl } ); + } + if ( ! isSaveInProgress ) { + setSaveInProgress( true ); + } + } + + function replaceMediaUrlInMediaFilesById( mediaId, mediaUrl ) { + if ( mediaId !== undefined ) { + const newMediaFiles = mediaFiles.map( mediaFile => { + if ( mediaFile.id === mediaId.toString() ) { + // we need to deep copy because attributes can't be modified in-place + return { ...mediaFile, url: mediaUrl, link: mediaUrl }; + } + return { ...mediaFile }; + } ); + return newMediaFiles; + } + return mediaFiles; + } + + function replaceNewIdInMediaFilesByOldId( oldId, mediaId, mediaUrl ) { + if ( mediaId !== undefined ) { + const newMediaFiles = mediaFiles.map( mediaFile => { + if ( mediaFile.id === oldId.toString() ) { + // we need to deep copy because attributes can't be modified in-place + return { ...mediaFile, id: mediaId, url: mediaUrl, link: mediaUrl }; + } + return { ...mediaFile }; + } ); + return newMediaFiles; + } + return mediaFiles; + } + + function finishMediaSaveWithSuccess( payload ) { + // find the mediaFiles item that needs to change via its id, and apply the new URL + const updatedMediaFiles = replaceMediaUrlInMediaFilesById( payload.mediaId, payload.mediaUrl ); + setAttributes( { mediaFiles: updatedMediaFiles } ); + setSaveInProgress( false ); + } + + function finishMediaSaveWithFailure( payload ) { + // should anything be done on save failure on one single item in the media collection, do it here + setSaveInProgress( false ); + } + + function mediaSaveStateReset() { + setSaveInProgress( false ); + } + + function onStorySaveResult( payload ) { + // when story result ends up in failure, the failed overlay will be set in BlockMediaUpdateProgress + setSaveInProgress( false ); + setSaveFail( ! payload.success ); + } + + function onMediaIdChanged( payload ) { + const updatedMediaFiles = replaceNewIdInMediaFilesByOldId( + payload.mediaId, + payload.newId, + payload.mediaUrl + ); + setAttributes( { mediaFiles: updatedMediaFiles } ); + setSaveInProgress( false ); + } + + function onStoryPressed() { + if ( isUploadInProgress ) { + // issue cancellation for all media files involved + requestMediaFilesUploadCancelDialog( mediaFiles ); + } else if ( isSaveInProgress ) { + requestMediaFilesSaveCancelDialog( mediaFiles ); + } else if ( didUploadFail ) { + requestMediaFilesFailedRetryDialog( mediaFiles ); + } else { + // open the editor + onEditButtonTapped(); + } + } + + const mediaPlaceholder = ( + // TODO this we are wrapping in a pointerEvents=none because we don't want to + // trigger the ADD MEDIA bottom sheet just yet, but only give the placedholder the right appearance. + + } + labels={ { + title: __( 'Story' ), + instructions: __( 'ADD MEDIA' ), + } } + allowedTypes={ [ MEDIA_TYPE_IMAGE, MEDIA_TYPE_VIDEO ] } + onFocus={ onFocus } + /> + + ); + + return ( + + + { ! hasContent && mediaPlaceholder } + { hasContent && ( + + { ! isUploadInProgress && ! isSaveInProgress && isSelected && ( + + ) } + { + return ( + + ); + } } + /> + + ) } + + + ); +}; + +export default StoryEdit; diff --git a/extensions/blocks/story/editor.native.scss b/extensions/blocks/story/editor.native.scss new file mode 100644 index 0000000000000..ed453ce32ac8f --- /dev/null +++ b/extensions/blocks/story/editor.native.scss @@ -0,0 +1,97 @@ + +@import './player/variables.scss'; + +.wp-story-container { + height: 320px; + width: 180px; + margin-left: auto; + margin-right: auto; + position: relative; + list-style: none; + padding: 0; + z-index: 1; + border-radius: 15px; + overflow: hidden; + box-shadow: 0 2px 12px rgba(0, 0, 0, 0.25); + + .wp-story-wrapper { + display: block; + position: absolute; + height: auto; + bottom: 0; + top: 0; + left: 0; + right: 0; + z-index: -1; + border-radius: 15px; + background-color: $wp-story-background-color; + } + + .wp-story-slide { + display: flex; + height: 100%; + width: 100%; + + figure { + align-items: center; + display: flex; + height: 100%; + width: 100%; + justify-content: center; + margin: 0; + position: relative; + overflow: hidden; + object-fit: contain; + } + + } + + .wp-story-image, .wp-story-video { + display: block; + height: auto; + width: auto; + max-height: 100%; + max-width: 100%; + margin: 0; + border: 0; + + &.wp-story-crop-wide { + max-width: revert; + } + + &.wp-story-crop-narrow { + max-height: revert; + } + } + +} + +.editContainer { + width: 44px; + height: 44px; + position: absolute; + top: 0; + right: 0; + z-index: 2; +} + +.edit { + width: 30px; + height: 30px; + background-color: $gray-dark; + border-radius: 22px; + position: absolute; + top: 5px; + right: 5px; +} + +.iconCustomize { + fill: #fff; + position: absolute; + top: 7px; + left: 7px; +} + +.content-placeholder { + flex: 1; +} diff --git a/extensions/blocks/story/icon-customize.native.js b/extensions/blocks/story/icon-customize.native.js new file mode 100644 index 0000000000000..57fc39c7c29f1 --- /dev/null +++ b/extensions/blocks/story/icon-customize.native.js @@ -0,0 +1,10 @@ +/** + * WordPress dependencies + */ +import { Path, SVG } from '@wordpress/components'; + +export default ( + + + +); diff --git a/extensions/blocks/story/index.js b/extensions/blocks/story/index.js index 2b6f4e18b1977..2c6674f4a63d9 100644 --- a/extensions/blocks/story/index.js +++ b/extensions/blocks/story/index.js @@ -1,7 +1,8 @@ /** - * External dependencies + * WordPress dependencies */ import { __, _x } from '@wordpress/i18n'; +import { Platform } from '@wordpress/element'; /** * Internal dependencies @@ -57,7 +58,7 @@ export const settings = { attributes, supports: { html: false, - inserter: false, + inserter: Platform.OS !== 'web', // false for web, true for mobile }, icon: { src: icon, diff --git a/extensions/blocks/story/story-editing-button.native.js b/extensions/blocks/story/story-editing-button.native.js new file mode 100644 index 0000000000000..e90d4f5d30ae0 --- /dev/null +++ b/extensions/blocks/story/story-editing-button.native.js @@ -0,0 +1,30 @@ +/** + * External dependencies + */ +import { TouchableWithoutFeedback, View } from 'react-native'; + +/** + * WordPress dependencies + */ +import { Icon } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import SvgIconCustomize from './icon-customize'; +import styles from './editor.scss'; + +const StoryEditingButton = ( { onEditButtonTapped } ) => { + return ( + + + + { /* { mediaOptions() } */ } + + + + + ); +}; + +export default StoryEditingButton; diff --git a/extensions/editor.native.js b/extensions/editor.native.js index cb3628e4bbb2b..c960343124e6f 100644 --- a/extensions/editor.native.js +++ b/extensions/editor.native.js @@ -5,3 +5,4 @@ import './shared/block-category'; // Register blocks import './blocks/contact-info/editor'; +import './blocks/story/editor'; diff --git a/extensions/shared/icons.scss b/extensions/shared/icons.scss index 49d0c9cdf265d..bdc4336532b32 100644 --- a/extensions/shared/icons.scss +++ b/extensions/shared/icons.scss @@ -1,7 +1,7 @@ @import './styles/gutenberg-base-styles.scss'; .jetpack-gutenberg-social-icon { - fill: $dark-gray-500; + fill: $gray-700; &.is-facebook { fill: var( --color-facebook );