diff --git a/projects/packages/videopress/changelog/rnmobile-detect-when-post-is-saved b/projects/packages/videopress/changelog/rnmobile-detect-when-post-is-saved new file mode 100644 index 0000000000000..1496493750471 --- /dev/null +++ b/projects/packages/videopress/changelog/rnmobile-detect-when-post-is-saved @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +VideoPress block: Sync metadata when post is manually saved on native diff --git a/projects/packages/videopress/src/client/block-editor/blocks/video/edit.native.js b/projects/packages/videopress/src/client/block-editor/blocks/video/edit.native.js index 99d8bbb1b2164..0c7e568d8e960 100644 --- a/projects/packages/videopress/src/client/block-editor/blocks/video/edit.native.js +++ b/projects/packages/videopress/src/client/block-editor/blocks/video/edit.native.js @@ -8,7 +8,7 @@ import { store as blockEditorStore, } from '@wordpress/block-editor'; import { createBlock } from '@wordpress/blocks'; -import { PanelBody, BottomSheet } from '@wordpress/components'; +import { PanelBody } from '@wordpress/components'; import { useDispatch, useSelect } from '@wordpress/data'; import { useState, useCallback, useEffect } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; @@ -68,7 +68,7 @@ export default function VideoPressEdit( { [ clientId ] ); const { replaceBlock } = useDispatch( blockEditorStore ); - const { createErrorNotice, createSuccessNotice } = useDispatch( noticesStore ); + const { createErrorNotice } = useDispatch( noticesStore ); // Display upload progress in case the editor is closed and re-opened // while the upload is in progress. @@ -89,22 +89,7 @@ export default function VideoPressEdit( { [ setAttributes ] ); - // TODO: This is a temporary solution for manually saving block settings. - // This will be removed and replaced with a more robust solution before the block is shipped to users. - // See: https://github.com/Automattic/jetpack/pull/29996 - const [ blockSettingsSaved, setBlockSettingsSaved ] = useState( false ); - - const onSaveBlockSettings = () => { - setBlockSettingsSaved( true ); - createSuccessNotice( 'Block settings saved.' ); - }; - - const { videoData } = useSyncMedia( - attributes, - setAttributes, - blockSettingsSaved, - setBlockSettingsSaved - ); + const { videoData } = useSyncMedia( attributes, setAttributes ); const { private_enabled_for_site: privateEnabledForSite } = videoData; const handleDoneUpload = useCallback( @@ -231,9 +216,6 @@ export default function VideoPressEdit( { - - - ) } diff --git a/projects/packages/videopress/src/client/block-editor/hooks/use-sync-media/index.native.js b/projects/packages/videopress/src/client/block-editor/hooks/use-sync-media/index.native.js new file mode 100644 index 0000000000000..b3765ff94ef54 --- /dev/null +++ b/projects/packages/videopress/src/client/block-editor/hooks/use-sync-media/index.native.js @@ -0,0 +1,264 @@ +/** + * External dependencies + */ +import { store as coreStore } from '@wordpress/core-data'; +import { useDispatch } from '@wordpress/data'; +import { useEffect, useState, useCallback } from '@wordpress/element'; +import { subscribePostSaveEvent } from '@wordpress/react-native-bridge'; +import debugFactory from 'debug'; +/** + * Internal dependencies + */ +import { getVideoPressUrl } from '../../../lib/url'; +import { snakeToCamel } from '../../../utils/map-object-keys-to-camel-case'; +import useVideoData from '../use-video-data'; +import useMediaDataUpdate from '../use-video-data-update'; + +const debug = debugFactory( 'videopress:video:use-sync-media' ); + +/* + * Fields list to keep in sync with block attributes. + */ +const videoFieldsToUpdate = [ + 'post_id', + 'title', + 'description', + 'privacy_setting', + 'rating', + 'allow_download', + 'display_embed', + 'is_private', + 'duration', +]; + +/* + * Map object from video field name to block attribute name. + * Only register those fields that have a different attribute name. + */ +const mapFieldsToAttributes = { + privacy_setting: 'privacySetting', + allow_download: 'allowDownload', + display_embed: 'displayEmbed', + is_private: 'isPrivate', + post_id: 'id', +}; + +/* + * Fields list that should invalidate the resolution of the embed (player) + * when some of them change. + * + * Keep in mind some field changes + * are handled inidrectly by the VideoPress video URL, + * for instance, `loop`, `autoplay`, `color`, etc... + */ +const invalidateEmbedResolutionFields = [ + 'title', + 'privacy_setting', + 'is_private', + 'allow_download', + 'display_embed', +]; + +/** + * React hook to keep the data in-sync + * between the media item and the block attributes. + * + * @param {object} attributes - Block attributes. + * @param {Function} setAttributes - Block attributes setter. + * @returns {object} - Hook API object. + */ +export function useSyncMedia( attributes, setAttributes ) { + const { id, guid, isPrivate } = attributes; + const { videoData, isRequestingVideoData } = useVideoData( { + id, + guid, + skipRatingControl: true, + maybeIsPrivate: isPrivate, + } ); + + // In native, it's not currently possible to access a post's saved state from the editor store. + // We therefore need to listen to a native event emitter to know when a post has just been saved. + const [ postHasBeenJustSaved, setPostHasBeenJustSaved ] = useState( false ); + + useEffect( () => { + const subscription = subscribePostSaveEvent( () => setPostHasBeenJustSaved( true ) ); + + return () => { + subscription?.remove(); + }; + }, [] ); + + const invalidateResolution = useDispatch( coreStore ).invalidateResolution; + + const [ initialState, setState ] = useState( {} ); + + const [ error, setError ] = useState( null ); + + const updateInitialState = useCallback( data => { + setState( current => ( { ...current, ...data } ) ); + }, [] ); + + /* + * Media data => Block attributes (update) + * + * Populate block attributes with the media data, + * provided by the VideoPress API (useVideoData hook), + * when the block is mounted. + */ + useEffect( () => { + if ( isRequestingVideoData ) { + return; + } + + // Bail early if the video data is not available. + if ( + ! videoData || + Object.keys( videoData ).filter( key => videoFieldsToUpdate.includes( key ) ).length === 0 + ) { + return; + } + + const attributesToUpdate = {}; + + // Build an object with video data to use for the initial state. + const initialVideoData = videoFieldsToUpdate.reduce( ( acc, key ) => { + if ( typeof videoData[ key ] === 'undefined' ) { + return acc; + } + + let videoDataValue = videoData[ key ]; + + // Cast privacy_setting to number to match the block attribute type. + if ( 'privacy_setting' === key ) { + videoDataValue = Number( videoDataValue ); + } + + acc[ key ] = videoDataValue; + const attrName = mapFieldsToAttributes[ key ] || snakeToCamel( key ); + + if ( videoDataValue !== attributes[ attrName ] ) { + debug( + '%o is out of sync. Updating %o attr from %o to %o ', + key, + attrName, + attributes[ attrName ], + videoDataValue + ); + attributesToUpdate[ attrName ] = videoDataValue; + } + return acc; + }, {} ); + + updateInitialState( initialVideoData ); + debug( 'Initial state: ', initialVideoData ); + + if ( ! Object.keys( initialVideoData ).length || ! Object.keys( attributesToUpdate ).length ) { + return; + } + + debug( 'Updating attributes: ', attributesToUpdate ); + setAttributes( attributesToUpdate ); + }, [ videoData, isRequestingVideoData ] ); + + const updateMediaHandler = useMediaDataUpdate( id ); + + /* + * Block attributes => Media data (sync) + * + * Compare the current attribute values of the block + * with the initial state, + * and sync the media data if it detects changes on it + * (via the VideoPress API) when the post saves. + */ + useEffect( () => { + if ( ! postHasBeenJustSaved ) { + return; + } + + debug( '%o Post has been just saved. Syncing...', attributes?.guid ); + + setPostHasBeenJustSaved( false ); + + if ( ! attributes?.id ) { + debug( '%o No media ID found. Impossible to sync. Bail early', attributes?.guid ); + return; + } + + /* + * Filter the attributes that have changed their values, + * based on the initial state. + */ + const dataToUpdate = videoFieldsToUpdate.reduce( ( acc, key ) => { + const attrName = mapFieldsToAttributes[ key ] || key; + const stateValue = initialState[ key ]; + const attrValue = attributes[ attrName ]; + + if ( initialState[ key ] !== attributes[ attrName ] ) { + debug( 'Field to sync %o: %o => %o: %o', key, stateValue, attrName, attrValue ); + acc[ key ] = attributes[ attrName ]; + } + return acc; + }, {} ); + + // When nothing to update, bail out early. + if ( ! Object.keys( dataToUpdate ).length ) { + return debug( 'No data to sync. Bail early' ); + } + + debug( 'Syncing data: ', dataToUpdate ); + + // Sync the block attributes data with the video data + updateMediaHandler( dataToUpdate ) + .then( () => { + // Update local state with fresh video data. + updateInitialState( dataToUpdate ); + + /* + * Update isPrivate attribute: + * `is_private` is a read-only metadata field. + * The VideoPress API provides its value + * and depends on the `privacy_setting` + * and `private_enabled_for_site` fields. + */ + if ( dataToUpdate.privacy_setting ) { + const isPrivateVideo = + dataToUpdate.privacy_setting !== 2 + ? dataToUpdate.privacy_setting === 1 + : videoData.private_enabled_for_site; + + debug( 'Updating isPrivate attribute: %o', isPrivateVideo ); + setAttributes( { isPrivate: isPrivateVideo } ); + } + + const shouldInvalidateResolution = Object.keys( dataToUpdate ).filter( key => + invalidateEmbedResolutionFields.includes( key ) + ); + + if ( shouldInvalidateResolution?.length ) { + debug( 'Invalidate resolution because of %o', shouldInvalidateResolution.join( ', ' ) ); + + const videoPressUrl = getVideoPressUrl( attributes.guid, attributes ); + invalidateResolution( 'getEmbedPreview', [ videoPressUrl ] ); + } + } ) + .catch( ( updateMediaError: Error ) => { + debug( '%o Error while syncing data: %o', attributes?.guid, updateMediaError ); + setError( updateMediaError ); + } ); + }, [ + postHasBeenJustSaved, + updateMediaHandler, + updateInitialState, + attributes, + initialState, + invalidateResolution, + videoFieldsToUpdate, + ] ); + + return { + forceInitialState: updateInitialState, + videoData, + isRequestingVideoData, + error, + }; +} diff --git a/projects/packages/videopress/src/client/block-editor/hooks/use-sync-media/index.ts b/projects/packages/videopress/src/client/block-editor/hooks/use-sync-media/index.ts index c77fc8ad3b424..6ef88b4346828 100644 --- a/projects/packages/videopress/src/client/block-editor/hooks/use-sync-media/index.ts +++ b/projects/packages/videopress/src/client/block-editor/hooks/use-sync-media/index.ts @@ -5,7 +5,7 @@ import { usePrevious } from '@wordpress/compose'; import { store as coreStore } from '@wordpress/core-data'; import { useSelect, useDispatch } from '@wordpress/data'; import { store as editorStore } from '@wordpress/editor'; -import { useEffect, useState, useCallback, Platform } from '@wordpress/element'; +import { useEffect, useState, useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import debugFactory from 'debug'; /** @@ -30,8 +30,6 @@ import { useVideoPosterData } from '../use-video-poster-data'; import type { UseSyncMedia, ArrangeTracksAttributesProps } from './types'; import type { UploadTrackDataProps } from '../../../lib/video-tracks/types'; -const isNative = Platform.isNative; - const debug = debugFactory( 'videopress:video:use-sync-media' ); /* @@ -147,15 +145,11 @@ function arrangeTracksAttributes( * * @param {object} attributes - Block attributes. * @param {Function} setAttributes - Block attributes setter. - * @param {boolean} blockSettingsSaved - Temporary attribute to track when block settings have been saved on native. - * @param {Function} setBlockSettingsSaved - Temporary attribute to track manually control when settings are synchronised on native. * @returns {UseSyncMedia} Hook API object. */ export function useSyncMedia( attributes: VideoBlockAttributes, - setAttributes: VideoBlockSetAttributesProps, - blockSettingsSaved = false, - setBlockSettingsSaved + setAttributes: VideoBlockSetAttributesProps ): UseSyncMedia { const { id, guid, isPrivate } = attributes; const { videoData, isRequestingVideoData } = useVideoData( { @@ -276,7 +270,7 @@ export function useSyncMedia( const updateMediaHandler = useMediaDataUpdate( id ); - const postHasBeenJustSaved = isNative ? blockSettingsSaved : !! ( wasSaving && ! isSaving ); + const postHasBeenJustSaved = !! ( wasSaving && ! isSaving ); /* * Video frame poster: Block attributes => Frame poster generation @@ -299,10 +293,6 @@ export function useSyncMedia( return; } - if ( isNative ) { - setBlockSettingsSaved( false ); - } - debug( '%o Post has been just saved. Syncing...', attributes?.guid ); if ( ! attributes?.id ) { @@ -366,8 +356,7 @@ export function useSyncMedia( isOverwriteChapterAllowed && attributes?.guid && dataToUpdate?.description?.length && - validateChapters( chapters ) && - ! isNative + validateChapters( chapters ) ) { debug( 'Autogenerated chapter detected. Processing...' ); const track: UploadTrackDataProps = {