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 9 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
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Button } from '@wordpress/components';
import { Button, Modal } from '@wordpress/components';
import { useSelect } from '@wordpress/data';
import { __ } from '@wordpress/i18n';
import { useCallback, useReducer } from 'react';
import { store as socialStore } from '../../social-store';
import { ShareList } from './share-list';
import styles from './styles.module.scss';

/**
Expand All @@ -10,6 +12,12 @@ import styles from './styles.module.scss';
* @return {import('react').ReactNode} - Share status modal component.
*/
export function ShareStatusModal() {
const [ isModalOpen, toggleModal ] = useReducer( state => ! state, false );

const handleOpenModal = useCallback( () => {
toggleModal();
}, [] );

const { featureFlags } = useSelect( select => {
const store = select( socialStore );
return {
Expand All @@ -23,7 +31,24 @@ export function ShareStatusModal() {

return (
<div className={ styles.wrapper }>
<Button variant="secondary">{ __( 'Review sharing status', 'jetpack' ) }</Button>{ ' ' }
{ isModalOpen && (
<Modal
onRequestClose={ toggleModal }
title={ __( 'Sharing status', 'jetpack' ) }
className={ styles.modal }
>
<ShareList />
<Button
className={ styles[ 'close-button' ] }
onClick={ toggleModal }
icon={ close }
label={ __( 'Close', 'jetpack' ) }
/>
</Modal>
) }
<Button variant="secondary" onClick={ handleOpenModal }>
{ __( 'Review sharing status', 'jetpack' ) }
</Button>
</div>
);
}
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;

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,44 @@
import { IconTooltip, Text } from '@automattic/jetpack-components';
import { __ } 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 ? __( 'Shared', 'jetpack' ) : __( 'Failed', 'jetpack' ) }
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,84 @@
.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
Expand Up @@ -41,6 +41,7 @@ export type ShareStatusItem = Pick<
timestamp: number;
service: string;
external_name: string;
external_id: string;
manzoorwanijk marked this conversation as resolved.
Show resolved Hide resolved
};

export type PostShareStatus = {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Normalizes the share status object.
* TODO: Optimize this function to avoid unnecessary iterations.
*
* @param {import( './types' ).PostShareStatus} shareStatus - Share status object.
* @return {import( './types' ).PostShareStatus | undefined} - Normalized share status object.
*/
export function normalizeShareStatus( shareStatus ) {
if ( ! shareStatus || ! ( 'shares' in shareStatus ) || ! shareStatus.done ) {
return;
}
// Sort shares to show the latest shares on the top.
shareStatus.shares.sort( ( a, b ) => b.timestamp - a.timestamp );
manzoorwanijk marked this conversation as resolved.
Show resolved Hide resolved

// Remove failed shares that have a successful share later.
shareStatus.shares = shareStatus.shares.filter( share => {
const hasSuccessfulShareLater = shareStatus.shares.some( otherShare => {
return (
otherShare.timestamp > share.timestamp &&
'success' === otherShare.status &&
otherShare.external_id === share.external_id &&
share.external_id // We added external_id later to the object
);
} );

if ( 'failure' === share.status ) {
return ! hasSuccessfulShareLater;
}

return true;
} );
Copy link
Contributor Author

@gmjuhasz gmjuhasz Aug 27, 2024

Choose a reason for hiding this comment

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

If we want to always show everything this part needs to be changed, or removed.

If we want to hide Retry for those that was already retried, we can update the status here to be retried or something like that.

p1724761254288879/1724758671.904879-slack-C02JJ910CNL

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, let's show the retry button as is and not hide them.

Copy link
Member

Choose a reason for hiding this comment

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

OK, let us then leave it as is for now and may be come back later to improve if needed.

I think it does make sense to treat it as activity log.


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