Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Social: Add Social Share Status modal for published posts #39051

Merged
merged 17 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add share status log modal to published posts

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Modal } from '@wordpress/components';
import { useDispatch, useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { store as socialStore } from '../../social-store';
import { ShareList } from './share-list';
import styles from './styles.module.scss';

/**
Expand All @@ -14,13 +15,15 @@ export function ShareStatusModal() {
const { closeShareStatusModal } = useDispatch( socialStore );

return (
<Modal
className={ styles.modal }
onRequestClose={ closeShareStatusModal }
title={ __( 'Share status', 'jetpack' ) }
>
Content goes here
</Modal>
<div className={ styles.wrapper }>
<Modal
onRequestClose={ closeShareStatusModal }
title={ __( 'Sharing status', 'jetpack' ) }
className={ styles.modal }
>
<ShareList />
</Modal>
</div>
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { getDate, humanTimeDiff } from '@wordpress/date';
import ConnectionIcon from '../connection-icon';
import { ShareStatusAction } from './share-status-action';
import { ShareStatusLabel } from './share-status-label';
import styles from './styles.module.scss';

/**
*
* ShareInfo component
*
* @param {object} props - component props
* @param {object} props.share - share object
* @return {import('react').ReactNode} - React element
*/
export function ShareInfo( { share } ) {
const { service, external_name, profile_picture, timestamp, status, message } = share;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens external_name and profile_picture is undefined?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If external_name is undefined it won't be shown as with the case of linkedIN in your example, I'm not sure why linkedIn has no external name, but we might have to send down another data to fix that if for LinkedIn that's undefined all the time. If there is no profile picture, you get the basic connection icon as with LinkedIn in your case

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we fallback to the network logo if there is no profile picture?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConnectionIcon already does that, you can see it on Sid's screenshot for Linkedin


return (
<div className={ styles[ 'share-item' ] }>
<ConnectionIcon
serviceName={ service }
label={ external_name }
profilePicture={ profile_picture }
/>
<div className={ styles[ 'share-item-name-wrapper' ] }>
<div className={ styles[ 'share-item-name' ] }>{ external_name }</div>
</div>
<div>
{
// @ts-expect-error - humanTimeDiff is incorrectly typed, first argument can be a timestamp
humanTimeDiff( timestamp * 1000, getDate() )
}
</div>
<ShareStatusLabel status={ status } message={ message } />
<ShareStatusAction status={ status } shareLink={ 'success' === status ? message : '' } />
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Spinner } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { store as editorStore } from '@wordpress/editor';
import { __ } from '@wordpress/i18n';
import { store as socialStore } from '../../social-store';
import { ShareInfo } from './share-info';
import styles from './styles.module.scss';

/**
* ShareList component
*
* @return {import('react').ReactNode} - Share status modal component.
*/
export function ShareList() {
const { shareStatus } = useSelect( select => {
const store = select( socialStore );
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- `@wordpress/editor` is a nightmare to work with TypeScript
const _editorStore = select( editorStore ) as any;

return {
shareStatus: store.getPostShareStatus( _editorStore.getCurrentPostId() ),
};
}, [] );

return (
<div className="connection-management">
{ shareStatus.loading && (
<div className={ styles.spinner }>
<Spinner /> { __( 'Loading…', 'jetpack' ) }
</div>
) }
{ shareStatus.shares.length > 0 && (
<ul className={ styles[ 'share-log-list' ] }>
{ shareStatus.shares.map( ( share, idx ) => (
<li
key={ `${ share.external_id || share.connection_id }${ idx }}` }
className={ styles[ 'share-log-list-item' ] }
>
<ShareInfo share={ share } />
</li>
) ) }
</ul>
) }
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ExternalLink } from '@wordpress/components';
import { __ } from '@wordpress/i18n';
import styles from './styles.module.scss';

/**
*
* Share status action component.
*
* @param {object} props - component props
* @param {boolean} props.status - status of the share
* @param {string} props.shareLink - link to the share
* @return {import('react').ReactNode} - React element
*/
export function ShareStatusAction( { status, shareLink } ) {
return (
<div className={ styles[ 'share-status-action-wrapper' ] }>
{ 'success' !== status ? (
<span>Retry</span>
) : (
<ExternalLink className={ styles[ 'profile-link' ] } href={ shareLink }>
{ __( 'View', 'jetpack' ) }
</ExternalLink>
) }
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { IconTooltip, Text } from '@automattic/jetpack-components';
import { __, _x } from '@wordpress/i18n';
import { Icon, check } from '@wordpress/icons';
import clsx from 'clsx';
import React from 'react';
import styles from './styles.module.scss';

/**
*
* Share status label component.
*
* @param {object} props - component props
* @param {boolean} props.status - status of the share
* @param {string} props.message - link to the share, or error message if failed
* @return {import('react').ReactNode} - React element
*/
export function ShareStatusLabel( { status, message } ) {
const isSuccessful = 'success' === status;

const icon = isSuccessful ? (
<Icon className={ styles[ 'share-status-icon' ] } icon={ check } />
) : (
<IconTooltip
title={ __( 'Sharing failed with the following message:', 'jetpack' ) }
className={ styles[ 'share-status-icon-tooltip' ] }
>
<Text variant="body-small">{ message }</Text>
</IconTooltip>
);

return (
<div
className={ clsx( styles[ 'share-status-wrapper' ], {
[ styles[ 'share-status-success' ] ]: isSuccessful,
[ styles[ 'share-status-failure' ] ]: ! isSuccessful,
} ) }
>
<div className={ styles[ 'share-status-icon' ] }>{ icon }</div>
<div className={ styles[ 'share-status-label' ] }>
{ isSuccessful
? _x( 'Shared', 'The sharing is successful', 'jetpack' )
: __( 'Failed', 'jetpack' ) }
</div>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,88 @@
margin-top: 1rem;
padding-block: 1rem;
}

.wrapper {
margin-top: 1rem;
padding-block: 1rem;
}

.spinner{
margin: 0 1rem 1rem 1rem;
}

.modal {
width: 60rem;
}

.share-log-list {
outline: 1px solid var(--jp-gray-5);
border-radius: 4px;
margin: 0;
width: 100%;

.share-log-list-item {
margin-bottom: 0px;
padding: 0.8rem 1rem;

&:not(:last-child) {
border-bottom: 1px solid var(--jp-gray-5);
}
}

.share-item {
display: flex;
gap: 1rem;
align-items: center;
}
}

.share-item-name-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
overflow: auto;
}

.share-item-name {
display: flex;
align-items: center;
}

.share-status-wrapper {
display: flex;
align-items: center;
width: 5rem;

&.share-status-success {
color: var(--jp-green-50);
}

&.share-status-failure {
color: var(--jp-red-50);
height: 29px;
}
}

.share-status-label {
flex: 1;
}

.share-status-icon-tooltip {
width: 24px;
top: 2px;
margin-inline-start: 2px;

> button {
color: var(--jp-red-50) !important;
}
}

.share-status-icon {
fill: var(--jp-green-50);
}

.share-status-action-wrapper {
width: 3rem;
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import apiFetch from '@wordpress/api-fetch';
import { store as editorStore } from '@wordpress/editor';
import { normalizeShareStatus } from '../utils/share-status';
import { setConnections } from './actions/connection-data';
import { setJetpackSettings } from './actions/jetpack-settings';
import { fetchPostShareStatus, receivePostShareStaus } from './actions/share-status';
Expand Down Expand Up @@ -75,10 +76,12 @@ export function getPostShareStatus( postId ) {

try {
dispatch( fetchPostShareStatus( postId ) );
const result = await apiFetch( {
let result = await apiFetch( {
path: `jetpack/v4/social/share-status/${ postId }`,
} );

result = normalizeShareStatus( result );

dispatch( receivePostShareStaus( result, postId ) );
} catch ( error ) {
dispatch( fetchPostShareStatus( postId, false ) );
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { PostShareStatus } from '../social-store/types';

/**
* Normalizes the share status object.
*
* @param {PostShareStatus} shareStatus - Share status object.
* @return {PostShareStatus} - Normalized share status object.
*/
export function normalizeShareStatus( shareStatus: PostShareStatus ) {
if ( shareStatus && 'shares' in shareStatus && shareStatus.done ) {
// Sort shares to show the latest shares on the top.
shareStatus.shares.sort( ( a, b ) => b.timestamp - a.timestamp );
}

return shareStatus;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add share status log modal to published posts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add share status log modal to published posts
Loading