Skip to content
Draft
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
2 changes: 1 addition & 1 deletion src/blocks/avatar/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,6 @@
"duotone": ".newspack-avatar-wrapper img"
}
},
"usesContext": ["postId", "postType"],
"usesContext": ["postId", "postType", "newspack-blocks/author"],
"textdomain": "newspack-plugin"
}
96 changes: 92 additions & 4 deletions src/blocks/avatar/class-avatar-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public static function register_block() {
__DIR__ . '/block.json',
[
'render_callback' => [ __CLASS__, 'render_block' ],
'uses_context' => [ 'postId', 'postType' ],
'uses_context' => [ 'postId', 'postType', 'newspack-blocks/author' ],
]
);
}
Expand All @@ -68,15 +68,23 @@ public static function register_block() {
* @return string The block HTML.
*/
public static function render_block( array $attributes, string $content, $block ) {
$image_size = $attributes['size'] ?? 48;
$link_to_author = $attributes['linkToAuthorArchive'] ?? false;

// Check for parent block context first (nested mode - single author).
$author_from_parent = $block->context['newspack-blocks/author'] ?? null;
if ( ! empty( $author_from_parent ) ) {
return self::render_single_author_avatar( $author_from_parent, $attributes );
}

// Standalone mode: get authors from post context.
$post_id = $block->context['postId'] ?? null;

if ( empty( $post_id ) ) {
return '';
}

$image_size = $attributes['size'] ?? 48;
$link_to_author = $attributes['linkToAuthorArchive'] ?? false;
$authors = self::get_avatar_authors( $post_id );
$authors = self::get_avatar_authors( $post_id );

if ( empty( $authors ) ) {
return '';
Expand Down Expand Up @@ -135,6 +143,86 @@ class="<?php echo esc_attr( $class ); ?>"
return ob_get_clean();
}

/**
* Render a single author's avatar from parent block context.
*
* @param array $author Author data from parent context.
* @param array $attributes Block attributes.
*
* @return string The avatar HTML.
*/
public static function render_single_author_avatar( array $author, array $attributes ) {
$image_size = $attributes['size'] ?? 48;
$link_to_author = $attributes['linkToAuthorArchive'] ?? false;

// Get avatar URL from parent context.
$avatar_url = '';
if ( ! empty( $author['avatar'] ) ) {
// If avatar is HTML, extract the src.
if ( strpos( $author['avatar'], '<img' ) !== false ) {
preg_match( '/src=["\']([^"\']+)["\']/', $author['avatar'], $matches );
$avatar_url = $matches[1] ?? '';
} else {
$avatar_url = $author['avatar'];
}
}

// Fallback: try to get avatar by author ID.
if ( empty( $avatar_url ) && ! empty( $author['id'] ) ) {
$avatar_url = get_avatar_url( $author['id'], [ 'size' => $image_size * 2 ] );
}

if ( empty( $avatar_url ) ) {
return '';
}

$author_name = esc_attr( $author['name'] ?? '' );
$author_url = $author['url'] ?? '';

$wrapper_attributes = get_block_wrapper_attributes( [ 'style' => '--avatar-size: ' . esc_attr( $image_size ) . 'px;' ] );
$duotone_preset = $attributes['style']['color']['duotone'] ?? null;
$duotone_class = self::newspack_get_duotone_class_name( $duotone_preset );

$border_attributes = function_exists( 'get_block_core_avatar_border_attributes' )
? get_block_core_avatar_border_attributes( $attributes )
: [
'class' => '',
'style' => '',
];

$class = 'avatar avatar-' . esc_attr( $image_size ) . ' photo wp-block-newspack-avatar__image ' . ( $border_attributes['class'] ?? '' );

ob_start();
?>
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
<div class="newspack-avatar-wrapper <?php echo esc_attr( $duotone_class ); ?>">
<?php if ( $link_to_author && ! empty( $author_url ) ) : ?>
<a href="<?php echo esc_url( $author_url ); ?>" class="wp-block-newspack-avatar__link">
<img
src="<?php echo esc_url( $avatar_url ); ?>"
class="<?php echo esc_attr( $class ); ?>"
alt="<?php echo esc_attr( $author_name ); ?>"
width="<?php echo esc_attr( $image_size ); ?>"
height="<?php echo esc_attr( $image_size ); ?>"
style="<?php echo esc_attr( $border_attributes['style'] ?? '' ); ?>"
/>
</a>
<?php else : ?>
<img
src="<?php echo esc_url( $avatar_url ); ?>"
class="<?php echo esc_attr( $class ); ?>"
alt="<?php echo esc_attr( $author_name ); ?>"
width="<?php echo esc_attr( $image_size ); ?>"
height="<?php echo esc_attr( $image_size ); ?>"
style="<?php echo esc_attr( $border_attributes['style'] ?? '' ); ?>"
/>
<?php endif; ?>
</div>
</div>
<?php
return ob_get_clean();
}

/**
* Get the authors whose avatars should be displayed.
*
Expand Down
87 changes: 72 additions & 15 deletions src/blocks/avatar/edit.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,27 @@ import {
__experimentalUseBorderProps as useBorderProps,
} from '@wordpress/block-editor';
import { __ } from '@wordpress/i18n';
import { useMemo } from '@wordpress/element';
import { createContext, useContext, useMemo } from '@wordpress/element';
import { PanelBody, RangeControl, ToggleControl } from '@wordpress/components';
import { addQueryArgs, removeQueryArgs } from '@wordpress/url';
/**
* Internal dependencies
*/
import { useUserAvatar, usePostAuthors } from './hooks';
import { useCustomByline, extractAuthorIdsFromByline } from '../../shared/hooks/use-custom-byline';

/**
* Fallback context that always returns null.
* Used when the shared AuthorContext from newspack-blocks is not available.
*/
const FallbackAuthorContext = createContext( null );

/**
* Get the shared AuthorContext from newspack-blocks if available, otherwise use fallback.
* This allows the avatar block to be used inside the Author Profile block's nested mode.
*/
const SharedAuthorContext = typeof window !== 'undefined' && window.NewspackAuthorContext ? window.NewspackAuthorContext : FallbackAuthorContext;

const AvatarInspectorControls = ( { setAttributes, attributes } ) => (
<InspectorControls>
<PanelBody title={ __( 'Settings', 'newspack-plugin' ) }>
Expand Down Expand Up @@ -110,14 +123,58 @@ const AvatarWrapper = ( { avatar, size, attributes, placeholder = false } ) => {
};

const Edit = ( { attributes, context, setAttributes } ) => {
const blockProps = useBlockProps();

// Check for parent block context first (nested mode - single author).
const authorFromBlockContext = context[ 'newspack-blocks/author' ];
const authorFromReactContext = useContext( SharedAuthorContext );
const authorFromParent = authorFromBlockContext || authorFromReactContext;

// Hooks must be called unconditionally per React rules.
const { postId, postType } = context;
const avatar = useUserAvatar( { userId: attributes?.userId, postId, postType } );
const allAuthors = usePostAuthors( { postId, postType } );
const { bylineActive, bylineContent } = useCustomByline( postId, postType );
const blockProps = useBlockProps();

// Text-only custom byline (no [Author] shortcodes) — show placeholder.
// Memoize author ID extraction to avoid running regex on every render.
const authorIds = useMemo( () => extractAuthorIdsFromByline( bylineContent ), [ bylineContent ] );

const renderAvatar = ( currentAvatar, key ) => (
<AvatarWrapper key={ key } avatar={ currentAvatar } size={ attributes.size } attributes={ attributes } />
);

// Nested mode: render single author from parent context.
if ( authorFromParent ) {
let avatarUrl = '';
if ( authorFromParent.avatar ) {
if ( authorFromParent.avatar.includes( '<img' ) ) {
const match = authorFromParent.avatar.match( /src=["']([^"']+)["']/ );
avatarUrl = match?.[ 1 ] || '';
} else {
avatarUrl = authorFromParent.avatar;
}
}

if ( ! avatarUrl ) {
return null;
}

const parentAvatar = {
src: avatarUrl,
alt: authorFromParent.name || '',
minSize: 16,
maxSize: 128,
};

return (
<>
<AvatarInspectorControls attributes={ attributes } setAttributes={ setAttributes } />
<div { ...blockProps }>{ renderAvatar( parentAvatar, 'nested-author' ) }</div>
</>
);
}

// Text-only custom byline (no [Author] shortcodes) — show placeholder.
const isTextOnlyByline = bylineActive && ( ! bylineContent || authorIds.length === 0 );
if ( isTextOnlyByline ) {
return (
Expand All @@ -128,28 +185,28 @@ const Edit = ( { attributes, context, setAttributes } ) => {
);
}

// Standalone mode: get authors from post context.
const authors = allAuthors?.length ? allAuthors : null;

// Wait until we have something to render
if ( ! avatar?.src && ! authors?.length ) {
return <div { ...blockProps }>{ __( 'Loading avatar…', 'newspack-plugin' ) }</div>;
}

const renderAvatar = ( currentAvatar, key ) => (
<AvatarWrapper key={ key } avatar={ currentAvatar } size={ attributes.size } attributes={ attributes } />
);
return (
<>
<AvatarInspectorControls attributes={ attributes } setAttributes={ setAttributes } />
{ authors?.length
? authors.map( ( author, index ) => {
const currentAvatar = {
src: author.avatarSrc,
alt: author?.name || author?.display_name || '',
};
return renderAvatar( currentAvatar, author.id || index );
} )
: renderAvatar( avatar, 'single-author' ) }
<div { ...blockProps }>
{ authors?.length
? authors.map( ( author, index ) => {
const currentAvatar = {
src: author.avatarSrc,
alt: author?.name || author?.display_name || '',
};
return renderAvatar( currentAvatar, author.id || index );
} )
: renderAvatar( avatar, 'single-author' ) }
</div>
</>
);
};
Expand Down
Loading