Files

280 lines
7.8 KiB
TypeScript

import jetpackAnalytics from '@automattic/jetpack-analytics';
import apiFetch from '@wordpress/api-fetch';
import { dispatch, select } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { addQueryArgs } from '@wordpress/url';
import { waitFor } from '../../wait-for';
import { store as mediaStore } from '../store';
import { MediaSource } from './types';
// Pexels constants
const PEXELS_ID = 'pexels';
const PEXELS_NAME = __( 'Pexels Free Photos', 'jetpack' );
const PEXELS_SEARCH_PLACEHOLDER = __( 'Search Pexels Free Photos', 'jetpack' );
const DEFAULT_PEXELS_SEARCH: MediaSearch = {
per_page: 10,
search: 'mountain',
};
// Google Photos constants
const GOOGLE_PHOTOS_ID = 'google_photos';
const GOOGLE_PHOTOS_NAME = __( 'Google Photos', 'jetpack' );
const GOOGLE_PHOTOS_SEARCH_PLACEHOLDER = __( 'Search Google Photos', 'jetpack' );
const DEFAULT_GOOGLE_PHOTOS_SEARCH: MediaSearch = {
per_page: 25,
search: '',
};
/**
* External media endpoints.
*/
// eslint-disable-next-line no-shadow
enum WpcomMediaEndpoints {
List = '/wpcom/v2/external-media/list/',
}
/**
* WPCOM media type of the WPCOM Media Api.
*/
// eslint-disable-next-line no-shadow
enum WpcomMediaItemType {
Image = 'image',
Video = 'video',
}
/**
* Gutenberg media category search DTO.
*/
type MediaSearch = {
per_page: number;
search: string;
};
/**
* Gutenberg media item DTO.
*/
type MediaItem = {
caption: string;
previewUrl: string;
title: string;
url: string;
};
/**
* WPCOM media list item DTO.
*/
type WpcomMediaItem = {
URL: string;
caption: string;
// Sometimes the title is null, so we need to handle that case.
title: string | null;
name: string | null;
file: string;
thumbnails: {
thumbnail: string;
};
type: WpcomMediaItemType;
};
/**
* WPCOM media list response DTO.
*/
type WpcomMediaResponse = {
found: number;
media: WpcomMediaItem[];
};
/**
* Get media URL for a given MediaSource.
*
* @param {MediaSource} source - MediaSource to get URL for.
* @param {MediaSearch} mediaSearch - MediaCategorySearch to filter for.
* @returns {string} Media URL.
*/
const getMediaApiUrl = ( source: MediaSource, mediaSearch: MediaSearch ) =>
addQueryArgs( `${ WpcomMediaEndpoints.List }${ source }`, {
...( mediaSearch.search && { search: mediaSearch.search } ),
number: mediaSearch.per_page || 25,
path: 'recent',
} );
/**
* Maps a WPCOM media item to a Gutenberg media item.
*
* @param {WpcomMediaItem} item - WPCOM media list item to map.
* @returns {MediaItem} Mapped media category item.
*/
const mapWpcomMediaToMedia = ( item: WpcomMediaItem ): MediaItem => ( {
caption: item?.caption ?? '',
previewUrl: item.thumbnails.thumbnail,
title: item?.title ?? item?.name ?? item.file,
url: item.URL,
} );
/**
* Builds a Gutenberg media category object.
*
* @param {string} name - Name of the media category.
* @param {string} label - Label of the media category.
* @param {string} searchPlaceholder - Search placeholder of the media category.
* @param {MediaSource} source - MediaSource of the media category.
* @param {MediaSearch} defaultSearch - Default search of the media category.
* @returns {object} Media category object.
*/
const buildMediaCategory = (
name: string,
label: string,
searchPlaceholder: string,
source: MediaSource,
defaultSearch: MediaSearch
) => ( {
name: name,
labels: {
name: label,
search_items: searchPlaceholder,
},
mediaType: 'image',
fetch: async ( mediaCategorySearch: MediaSearch ) =>
await apiFetch( {
path: getMediaApiUrl( source, {
per_page: defaultSearch.per_page,
search:
mediaCategorySearch?.search === '' ? defaultSearch.search : mediaCategorySearch.search,
} ),
method: 'GET',
} )
.then( ( response: WpcomMediaResponse ) => {
const mediaItems = response.media
.filter( wpcomMediaItem => wpcomMediaItem.type === WpcomMediaItemType.Image )
.map( mapWpcomMediaToMedia );
jetpackAnalytics.tracks.recordEvent( 'jetpack_editor_media_inserter_external_source', {
mediaSource: source.toString(),
results: mediaItems.length,
search:
mediaCategorySearch?.search === '' ? defaultSearch.search : mediaCategorySearch.search,
} );
return mediaItems;
} )
// Null object pattern, we don't want to break if the API fails.
.catch( () => [] ),
getReportUrl: null,
isExternalResource: true,
} );
/**
* Get Google Photos media category.
*
* @returns {object} Google Photos media category.
*/
const googlePhotosProvider = () =>
buildMediaCategory(
GOOGLE_PHOTOS_ID,
GOOGLE_PHOTOS_NAME,
GOOGLE_PHOTOS_SEARCH_PLACEHOLDER,
MediaSource.GooglePhotos,
DEFAULT_GOOGLE_PHOTOS_SEARCH
);
/**
* Checks if a given MediaSource is connected and calls the callback with the response.
*
* @param {MediaSource} source - MediaSource to check.
* @returns {void}
*/
const isMediaSourceConnected = async ( source: MediaSource ) =>
apiFetch< boolean | WpcomMediaResponse >( {
path: getMediaApiUrl( source, DEFAULT_PEXELS_SEARCH ),
method: 'GET',
} )
.then( response => response )
.catch( () => false );
/**
* Checks if the inserter is opened.
*
* @returns {boolean} True if the inserter is opened false otherwise.
*/
const isInserterOpened = (): boolean => {
// Prior to WP 6.5, the isInserterOpened selector was available in core/edit-post.
// In WP 6.5, it was moved to core/editor. This check is to support both versions of WordPress.
// @to-do: remove exception when Jetpack requires WordPress 6.5.
const selectIsInserterOpened =
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
( select( 'core/editor' ) as any )?.isInserterOpened ??
select( 'core/edit-post' )?.isInserterOpened;
const editorIsInserterOpened = selectIsInserterOpened?.();
return (
editorIsInserterOpened ||
select( 'core/edit-site' )?.isInserterOpened() ||
select( 'core/edit-widgets' )?.isInserterOpened()
);
};
const registerInInserter = ( mediaCategoryProvider: () => object ) =>
// Remove as soon @types/wordpress__block-editor is up to date
// eslint-disable-next-line
// @ts-ignore
dispatch( 'core/block-editor' )?.registerInserterMediaCategory?.( mediaCategoryProvider() );
/**
* Get Pexels media category.
*
* @returns {object} Pexels media category.
*/
const pexelsProvider = () =>
buildMediaCategory(
PEXELS_ID,
PEXELS_NAME,
PEXELS_SEARCH_PLACEHOLDER,
MediaSource.Pexels,
DEFAULT_PEXELS_SEARCH
);
/**
* Checks if a given MediaSource is authenticated in the store.
*
* @param {MediaSource} source - MediaSource to check.
* @returns {boolean} True if the MediaSource is authenticated false otherwise.
*/
const isAuthenticatedByWithMediaComponent = ( source: MediaSource ) =>
!! select( mediaStore ).isAuthenticated( source );
/**
* Adds Google Photos to the media inserter if/when it's connected.
* We will not remove Google Photos from the inserter if the user disconnects Google Photos during runtime.
*/
export const addGooglePhotosToMediaInserter = async () => {
waitFor( isInserterOpened ).then( async () => {
const isConnected = await isMediaSourceConnected( MediaSource.GooglePhotos );
if ( isConnected ) {
registerInInserter( googlePhotosProvider );
return;
}
waitFor( () => isAuthenticatedByWithMediaComponent( MediaSource.GooglePhotos ) ).then( () =>
registerInInserter( googlePhotosProvider )
);
} );
};
/**
* Adds Pexels to the media inserter. There is no need to check if it's connected because it's always connected.
*/
export const addPexelsToMediaInserter = () => {
waitFor( isInserterOpened ).then( () => registerInInserter( pexelsProvider ) );
};
/**
* Authenticates a given MediaSource.
*
* @param {MediaSource} source - MediaSource to authenticate.
* @param {boolean} isAuthenticated - True if the MediaSource is authenticated false otherwise.
*/
export const authenticateMediaSource = ( source: MediaSource, isAuthenticated: boolean ) => {
dispatch( mediaStore ).setAuthenticated( source, isAuthenticated );
};