diff options
Diffstat (limited to 'plugins/jetpack/extensions/blocks/publicize')
17 files changed, 945 insertions, 0 deletions
diff --git a/plugins/jetpack/extensions/blocks/publicize/connection-verify.js b/plugins/jetpack/extensions/blocks/publicize/connection-verify.js new file mode 100644 index 00000000..030ebb11 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/connection-verify.js @@ -0,0 +1,112 @@ +/** + * Publicize connections verification component. + * + * Component to create Ajax request to check + * all connections. If any connection tests failed, + * a refresh link may be provided to the user. If + * no connection tests fail, this component will + * not render anything. + */ + +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Button, Notice } from '@wordpress/components'; +import { Component, Fragment } from '@wordpress/element'; +import { compose } from '@wordpress/compose'; +import { withDispatch, withSelect } from '@wordpress/data'; + +class PublicizeConnectionVerify extends Component { + componentDidMount() { + this.props.refreshConnections(); + } + + /** + * Opens up popup so user can refresh connection + * + * Displays pop up with to specified URL where user + * can refresh a specific connection. + * + * @param {object} event Event instance for onClick. + */ + refreshConnectionClick = event => { + const { href, title } = event.target; + event.preventDefault(); + // open a popup window + // when it is closed, kick off the tests again + const popupWin = window.open( href, title, '' ); + const popupTimer = window.setInterval( () => { + if ( false !== popupWin.closed ) { + window.clearInterval( popupTimer ); + this.props.refreshConnections(); + } + }, 500 ); + }; + + renderRefreshableConnections() { + const { failedConnections } = this.props; + const refreshableConnections = failedConnections.filter( connection => connection.can_refresh ); + + if ( refreshableConnections.length ) { + return ( + <Notice className="jetpack-publicize-notice" isDismissible={ false } status="error"> + <p> + { __( + 'Before you hit Publish, please refresh the following connection(s) to make sure we can Publicize your post:', + 'jetpack' + ) } + </p> + { refreshableConnections.map( connection => ( + <Button + href={ connection.refresh_url } + isSmall + key={ connection.id } + onClick={ this.refreshConnectionClick } + title={ connection.refresh_text } + > + { connection.refresh_text } + </Button> + ) ) } + </Notice> + ); + } + + return null; + } + + renderNonRefreshableConnections() { + const { failedConnections } = this.props; + const nonRefreshableConnections = failedConnections.filter( + connection => ! connection.can_refresh + ); + + if ( nonRefreshableConnections.length ) { + return nonRefreshableConnections.map( connection => ( + <Notice className="jetpack-publicize-notice" isDismissible={ false } status="error"> + <p>{ connection.test_message }</p> + </Notice> + ) ); + } + + return null; + } + + render() { + return ( + <Fragment> + { this.renderRefreshableConnections() } + { this.renderNonRefreshableConnections() } + </Fragment> + ); + } +} + +export default compose( [ + withSelect( select => ( { + failedConnections: select( 'jetpack/publicize' ).getFailedConnections(), + } ) ), + withDispatch( dispatch => ( { + refreshConnections: dispatch( 'jetpack/publicize' ).refreshConnectionTestResults, + } ) ), +] )( PublicizeConnectionVerify ); diff --git a/plugins/jetpack/extensions/blocks/publicize/connection.js b/plugins/jetpack/extensions/blocks/publicize/connection.js new file mode 100644 index 00000000..071a275d --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/connection.js @@ -0,0 +1,100 @@ +/** + * Publicize connection form component. + * + * Component to display connection label and a + * checkbox to enable/disable the connection for sharing. + */ + +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { Component } from '@wordpress/element'; +import { Disabled, FormToggle, Notice, ExternalLink } from '@wordpress/components'; +import { withSelect } from '@wordpress/data'; +import { includes } from 'lodash'; + +/** + * Internal dependencies + */ +import PublicizeServiceIcon from './service-icon'; +import getSiteFragment from '../../shared/get-site-fragment'; + +class PublicizeConnection extends Component { + /** + * Displays a message when a connection requires reauthentication. We used this when migrating LinkedIn API usage from v1 to v2, + * since the prevous OAuth1 tokens were incompatible with OAuth2. + * + * @returns {object|?null} Notice about reauthentication + */ + maybeDisplayLinkedInNotice = () => + this.connectionNeedsReauth() && ( + <Notice className="jetpack-publicize-notice" isDismissible={ false } status="error"> + <p> + { __( + 'Your LinkedIn connection needs to be reauthenticated ' + + 'to continue working – head to Sharing to take care of it.', + 'jetpack' + ) } + </p> + <ExternalLink href={ `https://wordpress.com/marketing/connections/${ getSiteFragment() }` }> + { __( 'Go to Sharing settings', 'jetpack' ) } + </ExternalLink> + </Notice> + ); + + /** + * Check whether the connection needs to be reauthenticated. + * + * @returns {boolean} True if connection must be reauthenticated. + */ + connectionNeedsReauth = () => includes( this.props.mustReauthConnections, this.props.name ); + + onConnectionChange = () => { + const { id } = this.props; + this.props.toggleConnection( id ); + }; + + connectionIsFailing() { + const { failedConnections, name } = this.props; + return failedConnections.some( connection => connection.service_name === name ); + } + + render() { + const { disabled, enabled, id, label, name } = this.props; + const fieldId = 'connection-' + name + '-' + id; + // Genericon names are dash separated + const serviceName = name.replace( '_', '-' ); + + let toggle = ( + <FormToggle + id={ fieldId } + className="jetpack-publicize-connection-toggle" + checked={ enabled } + onChange={ this.onConnectionChange } + /> + ); + + if ( disabled || this.connectionIsFailing() || this.connectionNeedsReauth() ) { + toggle = <Disabled>{ toggle }</Disabled>; + } + + return ( + <li> + { this.maybeDisplayLinkedInNotice() } + <div className="publicize-jetpack-connection-container"> + <label htmlFor={ fieldId } className="jetpack-publicize-connection-label"> + <PublicizeServiceIcon serviceName={ serviceName } /> + <span className="jetpack-publicize-connection-label-copy">{ label }</span> + </label> + { toggle } + </div> + </li> + ); + } +} + +export default withSelect( select => ( { + failedConnections: select( 'jetpack/publicize' ).getFailedConnections(), + mustReauthConnections: select( 'jetpack/publicize' ).getMustReauthConnections(), +} ) )( PublicizeConnection ); diff --git a/plugins/jetpack/extensions/blocks/publicize/editor.js b/plugins/jetpack/extensions/blocks/publicize/editor.js new file mode 100644 index 00000000..9adee220 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/editor.js @@ -0,0 +1,7 @@ +/** + * Internal dependencies + */ +import { name, settings } from '.'; +import registerJetpackPlugin from '../../shared/register-jetpack-plugin'; + +registerJetpackPlugin( name, settings ); diff --git a/plugins/jetpack/extensions/blocks/publicize/editor.scss b/plugins/jetpack/extensions/blocks/publicize/editor.scss new file mode 100644 index 00000000..0704a5fa --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/editor.scss @@ -0,0 +1,100 @@ +@import '../../shared/styles/gutenberg-colors.scss'; + +.jetpack-publicize-message-box { + background-color: $light-gray-300; + border-radius: 4px; +} + +.jetpack-publicize-message-box textarea { + width: 100%; +} + +.jetpack-publicize-character-count { + padding-bottom: 5px; + padding-left: 5px; +} + +.jetpack-publicize__connections-list { + list-style-type: none; + margin: 13px 0; +} + +.publicize-jetpack-connection-container { + display: flex; +} + +.jetpack-publicize-gutenberg-social-icon { + fill: $dark-gray-500; + margin-right: 5px; + + &.is-facebook { + fill: var( --color-facebook ); + } + &.is-twitter { + fill: var( --color-twitter ); + } + &.is-linkedin { + fill: var( --color-linkedin ); + } + &.is-tumblr { + fill: var( --color-tumblr ); + } +} + +.jetpack-publicize-connection-label { + flex: 1; + margin-right: 5px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + + .jetpack-publicize-gutenberg-social-icon, + .jetpack-publicize-connection-label-copy { + display: inline-block; + vertical-align: middle; + } +} + +.jetpack-publicize-connection-toggle { + margin-top: 3px; +} + +.jetpack-publicize-notice { + &.components-notice { + margin-left: 0; + margin-right: 0; + margin-bottom: 13px; + } + + .components-button + .components-button { + margin-top: 5px; + } +} + +.jetpack-publicize-message-note { + display: inline-block; + margin-bottom: 4px; + margin-top: 13px; +} + +.jetpack-publicize-add-connection-wrapper { + margin: 15px 0; +} + +.jetpack-publicize-add-connection-container { + display: flex; + + a { + cursor: pointer; + } + + span { + vertical-align: middle; + } +} + +.jetpack-publicize__connections-list { + .components-notice { + margin: 5px 0 10px; + } +} diff --git a/plugins/jetpack/extensions/blocks/publicize/form-unwrapped.js b/plugins/jetpack/extensions/blocks/publicize/form-unwrapped.js new file mode 100644 index 00000000..04efc7eb --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/form-unwrapped.js @@ -0,0 +1,118 @@ +/** + * Publicize sharing form component. + * + * Displays text area and connection list to allow user + * to select connections to share to and write a custom + * sharing message. + */ + +/** + * External dependencies + */ +import classnames from 'classnames'; +import { __, _n, sprintf } from '@wordpress/i18n'; +import { Component, Fragment } from '@wordpress/element'; +import { uniqueId } from 'lodash'; + +/** + * Internal dependencies + */ +import PublicizeConnection from './connection'; +import PublicizeSettingsButton from './settings-button'; + +export const MAXIMUM_MESSAGE_LENGTH = 256; + +class PublicizeFormUnwrapped extends Component { + state = { + hasEditedShareMessage: false, + }; + + fieldId = uniqueId( 'jetpack-publicize-message-field-' ); + + /** + * Check to see if form should be disabled. + * + * Checks full connection list to determine if all are disabled. + * If they all are, it returns true to disable whole form. + * + * @return {boolean} True if whole form should be disabled. + */ + isDisabled() { + return this.props.connections.every( connection => ! connection.toggleable ); + } + + getShareMessage() { + const { shareMessage, defaultShareMessage } = this.props; + return ! this.state.hasEditedShareMessage && shareMessage === '' + ? defaultShareMessage + : shareMessage; + } + + onMessageChange = event => { + const { messageChange } = this.props; + this.setState( { hasEditedShareMessage: true } ); + messageChange( event ); + }; + + render() { + const { connections, toggleConnection, refreshCallback } = this.props; + const shareMessage = this.getShareMessage(); + const charactersRemaining = MAXIMUM_MESSAGE_LENGTH - shareMessage.length; + const characterCountClass = classnames( 'jetpack-publicize-character-count', { + 'wpas-twitter-length-limit': charactersRemaining <= 0, + } ); + + return ( + <div id="publicize-form"> + <ul className="jetpack-publicize__connections-list"> + { connections.map( ( { display_name, enabled, id, service_name, toggleable } ) => ( + <PublicizeConnection + disabled={ ! toggleable } + enabled={ enabled } + key={ id } + id={ id } + label={ display_name } + name={ service_name } + toggleConnection={ toggleConnection } + /> + ) ) } + </ul> + <PublicizeSettingsButton refreshCallback={ refreshCallback } /> + { connections.some( connection => connection.enabled ) && ( + <Fragment> + <label className="jetpack-publicize-message-note" htmlFor={ this.fieldId }> + { __( 'Customize your message', 'jetpack' ) } + </label> + <div className="jetpack-publicize-message-box"> + <textarea + id={ this.fieldId } + value={ shareMessage } + onChange={ this.onMessageChange } + disabled={ this.isDisabled() } + maxLength={ MAXIMUM_MESSAGE_LENGTH } + placeholder={ __( + "Write a message for your audience here. If you leave this blank, we'll use the post title as the message.", + 'jetpack' + ) } + rows={ 4 } + /> + <div className={ characterCountClass }> + { sprintf( + _n( + '%d character remaining', + '%d characters remaining', + charactersRemaining, + 'jetpack' + ), + charactersRemaining + ) } + </div> + </div> + </Fragment> + ) } + </div> + ); + } +} + +export default PublicizeFormUnwrapped; diff --git a/plugins/jetpack/extensions/blocks/publicize/form.js b/plugins/jetpack/extensions/blocks/publicize/form.js new file mode 100644 index 00000000..cb76b54f --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/form.js @@ -0,0 +1,72 @@ +/** + * Higher Order Publicize sharing form composition. + * + * Uses Gutenberg data API to dispatch publicize form data to + * editor post data in format to match 'publicize' field schema. + */ + +/** + * External dependencies + */ +import { get } from 'lodash'; +import { compose } from '@wordpress/compose'; +import { withSelect, withDispatch } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import PublicizeFormUnwrapped, { MAXIMUM_MESSAGE_LENGTH } from './form-unwrapped'; + +const PublicizeForm = compose( [ + withSelect( select => { + const meta = select( 'core/editor' ).getEditedPostAttribute( 'meta' ); + const postTitle = select( 'core/editor' ).getEditedPostAttribute( 'title' ); + const message = get( meta, [ 'jetpack_publicize_message' ], '' ); + + return { + connections: select( 'core/editor' ).getEditedPostAttribute( + 'jetpack_publicize_connections' + ), + defaultShareMessage: postTitle.substr( 0, MAXIMUM_MESSAGE_LENGTH ), + shareMessage: message.substr( 0, MAXIMUM_MESSAGE_LENGTH ), + }; + } ), + withDispatch( ( dispatch, { connections } ) => ( { + /** + * Toggle connection enable/disable state based on checkbox. + * + * Saves enable/disable value to connections property in editor + * in field 'jetpack_publicize_connections'. + * + * @param {number} id ID of the connection being enabled/disabled + */ + toggleConnection( id ) { + const newConnections = connections.map( connection => ( { + ...connection, + enabled: connection.id === id ? ! connection.enabled : connection.enabled, + } ) ); + + dispatch( 'core/editor' ).editPost( { + jetpack_publicize_connections: newConnections, + } ); + }, + + /** + * Handler for when sharing message is edited. + * + * Saves edited message to state and to the editor + * in field 'jetpack_publicize_message'. + * + * @param {object} event Change event data from textarea element. + */ + messageChange( event ) { + dispatch( 'core/editor' ).editPost( { + meta: { + jetpack_publicize_message: event.target.value, + }, + } ); + }, + } ) ), +] )( PublicizeFormUnwrapped ); + +export default PublicizeForm; diff --git a/plugins/jetpack/extensions/blocks/publicize/index.js b/plugins/jetpack/extensions/blocks/publicize/index.js new file mode 100644 index 00000000..9d553873 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/index.js @@ -0,0 +1,50 @@ +/** + * Top-level Publicize plugin for Gutenberg editor. + * + * Hooks into Gutenberg's PluginPrePublishPanel + * to display Jetpack's Publicize UI in the pre-publish flow. + * + * It also hooks into our dedicated Jetpack plugin sidebar and + * displays the Publicize UI there. + */ + +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { PanelBody } from '@wordpress/components'; +import { PluginPrePublishPanel } from '@wordpress/edit-post'; +import { PostTypeSupportCheck } from '@wordpress/editor'; + +/** + * Internal dependencies + */ +import './editor.scss'; +import './store'; +import JetpackPluginSidebar from '../../shared/jetpack-plugin-sidebar'; +import PublicizePanel from './panel'; + +export const name = 'publicize'; + +export const settings = { + render: () => ( + <PostTypeSupportCheck supportKeys="publicize"> + <JetpackPluginSidebar> + <PanelBody title={ __( 'Share this post', 'jetpack' ) }> + <PublicizePanel /> + </PanelBody> + </JetpackPluginSidebar> + <PluginPrePublishPanel + initialOpen + id="publicize-title" + title={ + <span id="publicize-defaults" key="publicize-title-span"> + { __( 'Share this post', 'jetpack' ) } + </span> + } + > + <PublicizePanel /> + </PluginPrePublishPanel> + </PostTypeSupportCheck> + ), +}; diff --git a/plugins/jetpack/extensions/blocks/publicize/panel.js b/plugins/jetpack/extensions/blocks/publicize/panel.js new file mode 100644 index 00000000..81735c48 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/panel.js @@ -0,0 +1,51 @@ +/** + * Publicize sharing panel component. + * + * Displays Publicize notifications if no + * services are connected or displays form if + * services are connected. + */ + +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import { compose } from '@wordpress/compose'; +import { Fragment } from '@wordpress/element'; +import { withDispatch, withSelect } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import PublicizeConnectionVerify from './connection-verify'; +import PublicizeForm from './form'; +import PublicizeSettingsButton from './settings-button'; + +const PublicizePanel = ( { connections, refreshConnections } ) => ( + <Fragment> + { connections && connections.some( connection => connection.enabled ) && ( + <PublicizeConnectionVerify /> + ) } + <div> + { __( "Connect and select the accounts where you'd like to share your post.", 'jetpack' ) } + </div> + { connections && connections.length > 0 && ( + <PublicizeForm refreshCallback={ refreshConnections } /> + ) } + { connections && 0 === connections.length && ( + <PublicizeSettingsButton + className="jetpack-publicize-add-connection-wrapper" + refreshCallback={ refreshConnections } + /> + ) } + </Fragment> +); + +export default compose( [ + withSelect( select => ( { + connections: select( 'core/editor' ).getEditedPostAttribute( 'jetpack_publicize_connections' ), + } ) ), + withDispatch( dispatch => ( { + refreshConnections: dispatch( 'core/editor' ).refreshPost, + } ) ), +] )( PublicizePanel ); diff --git a/plugins/jetpack/extensions/blocks/publicize/service-icon.js b/plugins/jetpack/extensions/blocks/publicize/service-icon.js new file mode 100644 index 00000000..dc5dc392 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/service-icon.js @@ -0,0 +1,64 @@ +/** + * External dependencies + */ +import { G, Icon, Path, Rect, SVG } from '@wordpress/components'; + +/** + * Module variables + */ +// @TODO: Import those from https://github.com/Automattic/social-logos when that's possible. +// Currently we can't directly import icons from there, because all icons are bundled in a single file. +// This means that to import an icon from there, we'll need to add the entire bundle with all icons to our build. +// In the future we'd want to export each icon in that repo separately, and then import them separately here. +const FacebookIcon = ( + <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <Rect x="0" fill="none" width="24" height="24" /> + <G> + <Path d="M20.007 3H3.993C3.445 3 3 3.445 3 3.993v16.013c0 .55.445.994.993.994h8.62v-6.97H10.27V11.31h2.346V9.31c0-2.325 1.42-3.59 3.494-3.59.993 0 1.847.073 2.096.106v2.43h-1.438c-1.128 0-1.346.537-1.346 1.324v1.734h2.69l-.35 2.717h-2.34V21h4.587c.548 0 .993-.445.993-.993V3.993c0-.548-.445-.993-.993-.993z" /> + </G> + </SVG> +); +const TwitterIcon = ( + <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <Rect x="0" fill="none" width="24" height="24" /> + <G> + <Path d="M22.23 5.924c-.736.326-1.527.547-2.357.646.847-.508 1.498-1.312 1.804-2.27-.793.47-1.67.812-2.606.996C18.325 4.498 17.258 4 16.078 4c-2.266 0-4.103 1.837-4.103 4.103 0 .322.036.635.106.935-3.41-.17-6.433-1.804-8.457-4.287-.353.607-.556 1.312-.556 2.064 0 1.424.724 2.68 1.825 3.415-.673-.022-1.305-.207-1.86-.514v.052c0 1.988 1.415 3.647 3.293 4.023-.344.095-.707.145-1.08.145-.265 0-.522-.026-.773-.074.522 1.63 2.038 2.817 3.833 2.85-1.404 1.1-3.174 1.757-5.096 1.757-.332 0-.66-.02-.98-.057 1.816 1.164 3.973 1.843 6.29 1.843 7.547 0 11.675-6.252 11.675-11.675 0-.178-.004-.355-.012-.53.802-.578 1.497-1.3 2.047-2.124z" /> + </G> + </SVG> +); +const LinkedinIcon = ( + <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <Rect x="0" fill="none" width="24" height="24" /> + <G> + <Path d="M19.7 3H4.3C3.582 3 3 3.582 3 4.3v15.4c0 .718.582 1.3 1.3 1.3h15.4c.718 0 1.3-.582 1.3-1.3V4.3c0-.718-.582-1.3-1.3-1.3zM8.34 18.338H5.666v-8.59H8.34v8.59zM7.003 8.574c-.857 0-1.55-.694-1.55-1.548 0-.855.692-1.548 1.55-1.548.854 0 1.547.694 1.547 1.548 0 .855-.692 1.548-1.546 1.548zm11.335 9.764h-2.67V14.16c0-.995-.017-2.277-1.387-2.277-1.39 0-1.6 1.086-1.6 2.206v4.248h-2.668v-8.59h2.56v1.174h.036c.357-.675 1.228-1.387 2.527-1.387 2.703 0 3.203 1.78 3.203 4.092v4.71z" /> + </G> + </SVG> +); +const TumblrIcon = ( + <SVG xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <Rect x="0" fill="none" width="24" height="24" /> + <G> + <Path d="M19 3H5c-1.105 0-2 .895-2 2v14c0 1.105.895 2 2 2h14c1.105 0 2-.895 2-2V5c0-1.105-.895-2-2-2zm-5.57 14.265c-2.445.042-3.37-1.742-3.37-2.998V10.6H8.922V9.15c1.703-.615 2.113-2.15 2.21-3.026.006-.06.053-.084.08-.084h1.645V8.9h2.246v1.7H12.85v3.495c.008.476.182 1.13 1.08 1.107.3-.008.698-.094.907-.194l.54 1.6c-.205.297-1.12.642-1.946.657z" /> + </G> + </SVG> +); + +export default ( { serviceName } ) => { + const defaultProps = { + className: `jetpack-publicize-gutenberg-social-icon is-${ serviceName }`, + size: 24, + }; + + switch ( serviceName ) { + case 'facebook': + return <Icon icon={ FacebookIcon } { ...defaultProps } />; + case 'twitter': + return <Icon icon={ TwitterIcon } { ...defaultProps } />; + case 'linkedin': + return <Icon icon={ LinkedinIcon } { ...defaultProps } />; + case 'tumblr': + return <Icon icon={ TumblrIcon } { ...defaultProps } />; + } + + return null; +}; diff --git a/plugins/jetpack/extensions/blocks/publicize/settings-button.js b/plugins/jetpack/extensions/blocks/publicize/settings-button.js new file mode 100644 index 00000000..8e22ee82 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/settings-button.js @@ -0,0 +1,73 @@ +/** + * Publicize settings button component. + * + * Component which allows user to click to open settings + * in a new window/tab. If window/tab is closed, then + * connections will be automatically refreshed. + */ + +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; +import classnames from 'classnames'; +import { Component } from '@wordpress/element'; +import { ExternalLink } from '@wordpress/components'; + +/** + * Internal dependencies + */ +import getSiteFragment from '../../shared/get-site-fragment'; + +class PublicizeSettingsButton extends Component { + getButtonLink() { + const siteFragment = getSiteFragment(); + + // If running in WP.com wp-admin or in Calypso, we redirect to Calypso sharing settings. + if ( siteFragment ) { + return `https://wordpress.com/marketing/connections/${ siteFragment }`; + } + + // If running in WordPress.org wp-admin we redirect to Sharing settings in wp-admin. + return 'options-general.php?page=sharing&publicize_popup=true'; + } + + /** + * Opens up popup so user can view/modify connections + * + * @param {object} event Event instance for onClick. + */ + settingsClick = event => { + const href = this.getButtonLink(); + const { refreshCallback } = this.props; + event.preventDefault(); + /** + * Open a popup window, and + * when it is closed, refresh connections + */ + const popupWin = window.open( href, '', '' ); + const popupTimer = window.setInterval( () => { + if ( false !== popupWin.closed ) { + window.clearInterval( popupTimer ); + refreshCallback(); + } + }, 500 ); + }; + + render() { + const className = classnames( + 'jetpack-publicize-add-connection-container', + this.props.className + ); + + return ( + <div className={ className }> + <ExternalLink onClick={ this.settingsClick }> + { __( 'Connect an account', 'jetpack' ) } + </ExternalLink> + </div> + ); + } +} + +export default PublicizeSettingsButton; diff --git a/plugins/jetpack/extensions/blocks/publicize/store/actions.js b/plugins/jetpack/extensions/blocks/publicize/store/actions.js new file mode 100644 index 00000000..e5b71694 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/store/actions.js @@ -0,0 +1,41 @@ +/** + * Returns an action object used in signalling that + * we're setting the Publicize connection test results. + * + * @param {Array} results Connection test results. + * + * @return {Object} Action object. + */ +export function setConnectionTestResults( results ) { + return { + type: 'SET_CONNECTION_TEST_RESULTS', + results, + }; +} + +/** + * Returns an action object used in signalling that + * we're refreshing the Publicize connection test results. + * + * @return {Object} Action object. + */ +export function refreshConnectionTestResults() { + return { + type: 'REFRESH_CONNECTION_TEST_RESULTS', + }; +} + +/** + * Returns an action object used in signalling that + * we're initiating a fetch request to the REST API. + * + * @param {String} path API endpoint path. + * + * @return {Object} Action object. + */ +export function fetchFromAPI( path ) { + return { + type: 'FETCH_FROM_API', + path, + }; +} diff --git a/plugins/jetpack/extensions/blocks/publicize/store/controls.js b/plugins/jetpack/extensions/blocks/publicize/store/controls.js new file mode 100644 index 00000000..afe6eccd --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/store/controls.js @@ -0,0 +1,19 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Trigger an API Fetch request. + * + * @param {Object} action Action Object. + * + * @return {Promise} Fetch request promise. + */ +const fetchFromApi = ( { path } ) => { + return apiFetch( { path } ); +}; + +export default { + FETCH_FROM_API: fetchFromApi, +}; diff --git a/plugins/jetpack/extensions/blocks/publicize/store/effects.js b/plugins/jetpack/extensions/blocks/publicize/store/effects.js new file mode 100644 index 00000000..594c8b72 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/store/effects.js @@ -0,0 +1,32 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; + +/** + * Internal dependencies + */ +import { setConnectionTestResults } from './actions'; + +/** + * Effect handler which will refresh the connection test results. + * + * @param {Object} action Action which had initiated the effect handler. + * @param {Object} store Store instance. + * + * @return {Object} Refresh connection test results action. + */ +export async function refreshConnectionTestResults( action, store ) { + const { dispatch } = store; + + try { + const results = await apiFetch( { path: '/wpcom/v2/publicize/connection-test-results' } ); + return dispatch( setConnectionTestResults( results ) ); + } catch ( error ) { + // Refreshing connections failed + } +} + +export default { + REFRESH_CONNECTION_TEST_RESULTS: refreshConnectionTestResults, +}; diff --git a/plugins/jetpack/extensions/blocks/publicize/store/index.js b/plugins/jetpack/extensions/blocks/publicize/store/index.js new file mode 100644 index 00000000..337167bc --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/store/index.js @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { registerStore } from '@wordpress/data'; + +/** + * Internal dependencies + */ +import * as actions from './actions'; +import * as selectors from './selectors'; +import applyMiddlewares from './middlewares'; +import controls from './controls'; +import reducer from './reducer'; + +const store = registerStore( 'jetpack/publicize', { + actions, + controls, + reducer, + selectors, +} ); + +applyMiddlewares( store ); + +export default store; diff --git a/plugins/jetpack/extensions/blocks/publicize/store/middlewares.js b/plugins/jetpack/extensions/blocks/publicize/store/middlewares.js new file mode 100644 index 00000000..1403b808 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/store/middlewares.js @@ -0,0 +1,40 @@ +/** + * External dependencies + */ +import refx from 'refx'; +import { flowRight } from 'lodash'; + +/** + * Internal dependencies + */ +import effects from './effects'; + +/** + * Applies the custom middlewares used specifically in the Publicize extension. + * + * @param {Object} store Store Object. + * + * @return {Object} Update Store Object. + */ +export default function applyMiddlewares( store ) { + const middlewares = [ refx( effects ) ]; + + let enhancedDispatch = () => { + throw new Error( + 'Dispatching while constructing your middleware is not allowed. ' + + 'Other middleware would not be applied to this dispatch.' + ); + }; + let chain = []; + + const middlewareAPI = { + getState: store.getState, + dispatch: ( ...args ) => enhancedDispatch( ...args ), + }; + chain = middlewares.map( middleware => middleware( middlewareAPI ) ); + enhancedDispatch = flowRight( ...chain )( store.dispatch ); + + store.dispatch = enhancedDispatch; + + return store; +} diff --git a/plugins/jetpack/extensions/blocks/publicize/store/reducer.js b/plugins/jetpack/extensions/blocks/publicize/store/reducer.js new file mode 100644 index 00000000..80af0701 --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/store/reducer.js @@ -0,0 +1,18 @@ +/** + * Reducer managing Publicize connection test results. + * + * @param {Object} state Current state. + * @param {Object} action Dispatched action. + * + * @return {Object} Updated state. + */ +export default function( state = [], action ) { + switch ( action.type ) { + case 'SET_CONNECTION_TEST_RESULTS': + return action.results; + case 'REFRESH_CONNECTION_TEST_RESULTS': + return []; + } + + return state; +} diff --git a/plugins/jetpack/extensions/blocks/publicize/store/selectors.js b/plugins/jetpack/extensions/blocks/publicize/store/selectors.js new file mode 100644 index 00000000..db86a4fe --- /dev/null +++ b/plugins/jetpack/extensions/blocks/publicize/store/selectors.js @@ -0,0 +1,24 @@ +/** + * Returns the failed Publicize connections. + * + * @param {Object} state State object. + * + * @return {Array} List of connections. + */ +export function getFailedConnections( state ) { + return state.filter( connection => false === connection.test_success ); +} + +/** + * Returns a list of Publicize connection service names that require reauthentication from users. + * iFor example, when LinkedIn switched its API from v1 to v2. + * + * @param {Object} state State object. + * + * @return {Array} List of service names that need reauthentication. + */ +export function getMustReauthConnections( state ) { + return state + .filter( connection => 'must_reauth' === connection.test_success ) + .map( connection => connection.service_name ); +} |