Plugin Directory

source: jetpack/trunk/jetpack_vendor/automattic/jetpack-videopress/src/client/block-editor/hooks/use-video-data-update/index.ts @ 2832426

Last change on this file since 2832426 was 2832426, checked in by wpkaren, 20 months ago

Updating trunk to version 11.7-a.3

File size: 9.6 KB
Line 
1/**
2 * External dependencies
3 */
4import apiFetch from '@wordpress/api-fetch';
5import { usePrevious } from '@wordpress/compose';
6import { store as coreStore } from '@wordpress/core-data';
7import { useSelect, useDispatch } from '@wordpress/data';
8import { store as editorStore } from '@wordpress/editor';
9import { useEffect, useState, useCallback } from '@wordpress/element';
10import { __ } from '@wordpress/i18n';
11import debugFactory from 'debug';
12/**
13 * Internal dependencies
14 */
15import { getVideoPressUrl } from '../../../lib/url';
16import { uploadTrackForGuid } from '../../../lib/video-tracks';
17import { UploadTrackDataProps } from '../../../lib/video-tracks/types';
18import {
19        WPComV2VideopressGetMetaEndpointResponseProps,
20        WPComV2VideopressPostMetaEndpointBodyProps,
21} from '../../../types';
22import extractVideoChapters from '../../../utils/extract-video-chapters';
23import generateChaptersFile from '../../../utils/generate-chapters-file';
24import { snakeToCamel } from '../../../utils/map-object-keys-to-camel-case';
25import {
26        VideoBlockAttributes,
27        VideoBlockSetAttributesProps,
28        VideoId,
29} from '../../blocks/video/types';
30import useVideoData from '../use-video-data';
31import { VideoDataProps } from '../use-video-data/types';
32import { UseSyncMediaProps, UseSyncMediaOptionsProps, ArrangeTracksAttributesProps } from './types';
33
34const debug = debugFactory( 'videopress:video:use-sync-media' );
35
36/**
37 * Hook to update the media data by hitting the VideoPress API.
38 *
39 * @param {VideoId} id - Media ID.
40 * @returns {Function}  Update Promise handler.
41 */
42export default function useMediaDataUpdate( id: VideoId ) {
43        const updateMediaItem = data => {
44                return new Promise( ( resolve, reject ) => {
45                        apiFetch( {
46                                path: '/wpcom/v2/videopress/meta',
47                                method: 'POST',
48                                data: { id, ...data },
49                        } )
50                                .then( ( result: WPComV2VideopressGetMetaEndpointResponseProps ) => {
51                                        if ( 200 !== result?.data ) {
52                                                return reject( result );
53                                        }
54                                        resolve( result );
55                                } )
56                                .catch( reject );
57                } );
58        };
59
60        return updateMediaItem;
61}
62
63/*
64 * Fields list to keep in sync with block attributes.
65 */
66const videoFieldsToUpdate = [
67        'title',
68        'description',
69        'privacy_setting',
70        'rating',
71        'allow_download',
72        'display_embed',
73        'is_private',
74];
75
76/*
77 * Map object from video field name to block attribute name.
78 * Only register those fields that have a different attribute name.
79 */
80const mapFieldsToAttributes = {
81        privacy_setting: 'privacySetting',
82        allow_download: 'allowDownload',
83        display_embed: 'displayEmbed',
84        is_private: 'isPrivate',
85};
86
87/**
88 * Re-arrange the tracks to match the block attribute format.
89 * Also, check if the tracks is out of sync with the media item.
90 *
91 * @param {VideoDataProps} videoData        - Video data, provided by server.
92 * @param {VideoBlockAttributes} attributes - Block attributes.
93 * @returns {VideoBlockAttributes}            Video block attributes.
94 */
95function arrangeTracksAttributes(
96        videoData: VideoDataProps,
97        attributes: VideoBlockAttributes
98): ArrangeTracksAttributesProps {
99        if ( ! videoData?.tracks ) {
100                return [ [], false ];
101        }
102
103        const tracks = [];
104        let tracksOufOfSync = false;
105
106        Object.keys( videoData.tracks ).forEach( kind => {
107                for ( const srcLang in videoData.tracks[ kind ] ) {
108                        const track = videoData.tracks[ kind ][ srcLang ];
109                        const trackExistsInBlock = attributes.tracks.find( t => {
110                                return (
111                                        t.kind === kind && t.srcLang === srcLang && t.src === track.src && t.label === track.label
112                                );
113                        } );
114
115                        if ( ! trackExistsInBlock ) {
116                                debug( 'Track %o is out of sync. Set tracks attr', track.src );
117                                tracksOufOfSync = true;
118                        }
119
120                        tracks.push( {
121                                src: track.src,
122                                kind,
123                                srcLang,
124                                label: track.label,
125                        } );
126                }
127        } );
128
129        return [ tracks, tracksOufOfSync ];
130}
131
132/**
133 * React hook to keep the data in-sync
134 * between the media item and the block attributes.
135 *
136 * @param {object} attributes                - Block attributes.
137 * @param {Function} setAttributes           - Block attributes setter.
138 * @param {UseSyncMediaOptionsProps} options - Options.
139 * @returns {UseSyncMediaProps}                Hook API object.
140 */
141export function useSyncMedia(
142        attributes: VideoBlockAttributes,
143        setAttributes: VideoBlockSetAttributesProps,
144        options: UseSyncMediaOptionsProps
145): UseSyncMediaProps {
146        const { id, guid } = attributes;
147        const { videoData, isRequestingVideoData } = useVideoData( { id, guid } );
148
149        const isSaving = useSelect( select => select( editorStore ).isSavingPost(), [] );
150        const wasSaving = usePrevious( isSaving );
151        const invalidateResolution = useDispatch( coreStore ).invalidateResolution;
152
153        const [ initialState, setState ] = useState( {} );
154
155        const updateInitialState = useCallback( ( data: VideoDataProps ) => {
156                setState( current => ( { ...current, ...data } ) );
157        }, [] );
158
159        /*
160         * Media data => Block attributes (update)
161         *
162         * Populate block attributes with the media data,
163         * provided by the VideoPress API (useVideoData hook),
164         * when the block is mounted.
165         */
166        useEffect( () => {
167                if ( isRequestingVideoData ) {
168                        return;
169                }
170
171                if ( ! videoData || Object.keys( videoData ).length === 0 ) {
172                        return;
173                }
174
175                const attributesToUpdate: VideoBlockAttributes = {};
176
177                // Build an object with video data to use for the initial state.
178                const initialVideoData = videoFieldsToUpdate.reduce(
179                        ( acc, key ) => {
180                                if ( typeof videoData[ key ] === 'undefined' ) {
181                                        return acc;
182                                }
183
184                                let videoDataValue = videoData[ key ];
185
186                                // Cast privacy_setting to number to match the block attribute type.
187                                if ( 'privacy_setting' === key ) {
188                                        videoDataValue = Number( videoDataValue );
189                                }
190
191                                acc[ key ] = videoDataValue;
192                                const attrName = snakeToCamel( key );
193
194                                if ( videoDataValue !== attributes[ attrName ] ) {
195                                        debug(
196                                                '%o is out of sync. Updating %o attr from %o to %o ',
197                                                key,
198                                                attrName,
199                                                attributes[ attrName ],
200                                                videoDataValue
201                                        );
202                                        attributesToUpdate[ attrName ] = videoDataValue;
203                                }
204                                return acc;
205                        },
206                        {
207                                tracks: [],
208                        }
209                );
210
211                updateInitialState( initialVideoData );
212
213                if ( ! Object.keys( initialVideoData ).length ) {
214                        return;
215                }
216
217                const [ tracks, tracksOufOfSync ] = arrangeTracksAttributes( videoData, attributes );
218
219                // Sync video tracks if needed.
220                if ( tracksOufOfSync ) {
221                        attributesToUpdate.tracks = tracks;
222                }
223
224                if ( ! Object.keys( attributesToUpdate ).length ) {
225                        return;
226                }
227
228                debug( 'Updating attributes: ', attributesToUpdate );
229                setAttributes( attributesToUpdate );
230        }, [ videoData, isRequestingVideoData ] );
231
232        const updateMediaHandler = useMediaDataUpdate( id );
233
234        /*
235         * Block attributes => Media data (sync)
236         *
237         * Compare the current attribute values of the block
238         * with the initial state,
239         * and sync the media data if it detects changes on it
240         * (via the VideoPress API) when the post saves.
241         */
242        useEffect( () => {
243                if ( ! isSaving || wasSaving ) {
244                        return;
245                }
246
247                debug( 'Saving post action detected' );
248
249                if ( ! attributes?.id ) {
250                        return;
251                }
252
253                /*
254                 * Filter the attributes that have changed their values,
255                 * based on the initial state.
256                 */
257                const dataToUpdate: WPComV2VideopressPostMetaEndpointBodyProps = videoFieldsToUpdate.reduce(
258                        ( acc, key ) => {
259                                const attrName = mapFieldsToAttributes[ key ] || key;
260                                const stateValue = initialState[ key ];
261                                const attrValue = attributes[ attrName ];
262
263                                if ( initialState[ key ] !== attributes[ attrName ] ) {
264                                        debug( 'Field to sync %o: %o => %o: %o', key, stateValue, attrName, attrValue );
265                                        acc[ key ] = attributes[ attrName ];
266                                }
267                                return acc;
268                        },
269                        {}
270                );
271
272                // When nothing to update, bail out early.
273                if ( ! Object.keys( dataToUpdate ).length ) {
274                        return debug( 'No data to sync. Bail early' );
275                }
276
277                debug( 'Syncing data: ', dataToUpdate );
278
279                // Sync the block attributes data with the video data
280                updateMediaHandler( dataToUpdate ).then( () => {
281                        // Update local state with fresh video data.
282                        updateInitialState( dataToUpdate );
283
284                        // | Video Chapters feature |
285                        const chapters = extractVideoChapters( dataToUpdate?.description );
286
287                        if (
288                                options.isAutoGeneratedChapter &&
289                                attributes?.guid &&
290                                dataToUpdate?.description?.length &&
291                                chapters?.length
292                        ) {
293                                debug( 'Auto-generated chapter detected. Processing...' );
294                                const track: UploadTrackDataProps = {
295                                        label: __( 'English (auto-generated)', 'jetpack-videopress-pkg' ),
296                                        srcLang: 'en',
297                                        kind: 'chapters',
298                                        tmpFile: generateChaptersFile( dataToUpdate.description ),
299                                };
300
301                                debug( 'Auto-generated track: %o', track );
302
303                                uploadTrackForGuid( track, guid ).then( ( src: string ) => {
304                                        const autoGeneratedTrackIndex = attributes.tracks.findIndex(
305                                                t => t.kind === 'chapters' && t.srcLang === 'en'
306                                        );
307
308                                        const uploadedTrack = {
309                                                ...track,
310                                                src,
311                                        };
312
313                                        const tracks = [ ...attributes.tracks ];
314
315                                        if ( autoGeneratedTrackIndex > -1 ) {
316                                                debug( 'Updating %o auto-generated track', uploadedTrack.src );
317                                                tracks[ autoGeneratedTrackIndex ] = uploadedTrack;
318                                        } else {
319                                                debug( 'Adding auto-generated %o track', uploadedTrack.src );
320                                                tracks.push( uploadedTrack );
321                                        }
322
323                                        // Update block track attribute
324                                        setAttributes( { tracks } );
325
326                                        const videoPressUrl = getVideoPressUrl( guid, attributes );
327                                        invalidateResolution( 'getEmbedPreview', [ videoPressUrl ] );
328                                } );
329                        } else {
330                                const videoPressUrl = getVideoPressUrl( guid, attributes );
331                                invalidateResolution( 'getEmbedPreview', [ videoPressUrl ] );
332                        }
333                } );
334        }, [
335                isSaving,
336                wasSaving,
337                updateMediaHandler,
338                updateInitialState,
339                attributes,
340                initialState,
341                invalidateResolution,
342                videoFieldsToUpdate,
343        ] );
344
345        return {
346                forceInitialState: updateInitialState,
347                videoData,
348                isRequestingVideoData,
349        };
350}
Note: See TracBrowser for help on using the repository browser.