summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'plugins/jetpack/extensions/blocks/publicize')
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/connection-verify.js112
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/connection.js100
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/editor.js7
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/editor.scss100
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/form-unwrapped.js118
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/form.js72
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/index.js50
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/panel.js51
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/service-icon.js64
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/settings-button.js73
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/store/actions.js41
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/store/controls.js19
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/store/effects.js32
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/store/index.js24
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/store/middlewares.js40
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/store/reducer.js18
-rw-r--r--plugins/jetpack/extensions/blocks/publicize/store/selectors.js24
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 );
+}