Skip to content

Commit 3abcd69

Browse files
committed
feat(avatar): support as nested profile block content
1 parent 2b76be5 commit 3abcd69

3 files changed

Lines changed: 165 additions & 20 deletions

File tree

src/blocks/avatar/block.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,6 @@
5050
"duotone": ".newspack-avatar-wrapper img"
5151
}
5252
},
53-
"usesContext": ["postId", "postType"],
53+
"usesContext": ["postId", "postType", "newspack-blocks/author"],
5454
"textdomain": "newspack-plugin"
5555
}

src/blocks/avatar/class-avatar-block.php

Lines changed: 92 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ public static function register_block() {
5353
__DIR__ . '/block.json',
5454
[
5555
'render_callback' => [ __CLASS__, 'render_block' ],
56-
'uses_context' => [ 'postId', 'postType' ],
56+
'uses_context' => [ 'postId', 'postType', 'newspack-blocks/author' ],
5757
]
5858
);
5959
}
@@ -68,15 +68,23 @@ public static function register_block() {
6868
* @return string The block HTML.
6969
*/
7070
public static function render_block( array $attributes, string $content, $block ) {
71+
$image_size = $attributes['size'] ?? 48;
72+
$link_to_author = $attributes['linkToAuthorArchive'] ?? false;
73+
74+
// Check for parent block context first (nested mode - single author).
75+
$author_from_parent = $block->context['newspack-blocks/author'] ?? null;
76+
if ( ! empty( $author_from_parent ) ) {
77+
return self::render_single_author_avatar( $author_from_parent, $attributes );
78+
}
79+
80+
// Standalone mode: get authors from post context.
7181
$post_id = $block->context['postId'] ?? null;
7282

7383
if ( empty( $post_id ) ) {
7484
return '';
7585
}
7686

77-
$image_size = $attributes['size'] ?? 48;
78-
$link_to_author = $attributes['linkToAuthorArchive'] ?? false;
79-
$authors = self::get_avatar_authors( $post_id );
87+
$authors = self::get_avatar_authors( $post_id );
8088

8189
if ( empty( $authors ) ) {
8290
return '';
@@ -135,6 +143,86 @@ class="<?php echo esc_attr( $class ); ?>"
135143
return ob_get_clean();
136144
}
137145

146+
/**
147+
* Render a single author's avatar from parent block context.
148+
*
149+
* @param array $author Author data from parent context.
150+
* @param array $attributes Block attributes.
151+
*
152+
* @return string The avatar HTML.
153+
*/
154+
public static function render_single_author_avatar( array $author, array $attributes ) {
155+
$image_size = $attributes['size'] ?? 48;
156+
$link_to_author = $attributes['linkToAuthorArchive'] ?? false;
157+
158+
// Get avatar URL from parent context.
159+
$avatar_url = '';
160+
if ( ! empty( $author['avatar'] ) ) {
161+
// If avatar is HTML, extract the src.
162+
if ( strpos( $author['avatar'], '<img' ) !== false ) {
163+
preg_match( '/src=["\']([^"\']+)["\']/', $author['avatar'], $matches );
164+
$avatar_url = $matches[1] ?? '';
165+
} else {
166+
$avatar_url = $author['avatar'];
167+
}
168+
}
169+
170+
// Fallback: try to get avatar by author ID.
171+
if ( empty( $avatar_url ) && ! empty( $author['id'] ) ) {
172+
$avatar_url = get_avatar_url( $author['id'], [ 'size' => $image_size * 2 ] );
173+
}
174+
175+
if ( empty( $avatar_url ) ) {
176+
return '';
177+
}
178+
179+
$author_name = esc_attr( $author['name'] ?? '' );
180+
$author_url = $author['url'] ?? '';
181+
182+
$wrapper_attributes = get_block_wrapper_attributes( [ 'style' => '--avatar-size: ' . esc_attr( $image_size ) . 'px;' ] );
183+
$duotone_preset = $attributes['style']['color']['duotone'] ?? null;
184+
$duotone_class = self::newspack_get_duotone_class_name( $duotone_preset );
185+
186+
$border_attributes = function_exists( 'get_block_core_avatar_border_attributes' )
187+
? get_block_core_avatar_border_attributes( $attributes )
188+
: [
189+
'class' => '',
190+
'style' => '',
191+
];
192+
193+
$class = 'avatar avatar-' . esc_attr( $image_size ) . ' photo wp-block-newspack-avatar__image ' . ( $border_attributes['class'] ?? '' );
194+
195+
ob_start();
196+
?>
197+
<div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
198+
<div class="newspack-avatar-wrapper <?php echo esc_attr( $duotone_class ); ?>">
199+
<?php if ( $link_to_author && ! empty( $author_url ) ) : ?>
200+
<a href="<?php echo esc_url( $author_url ); ?>" class="wp-block-newspack-avatar__link">
201+
<img
202+
src="<?php echo esc_url( $avatar_url ); ?>"
203+
class="<?php echo esc_attr( $class ); ?>"
204+
alt="<?php echo esc_attr( $author_name ); ?>"
205+
width="<?php echo esc_attr( $image_size ); ?>"
206+
height="<?php echo esc_attr( $image_size ); ?>"
207+
style="<?php echo esc_attr( $border_attributes['style'] ?? '' ); ?>"
208+
/>
209+
</a>
210+
<?php else : ?>
211+
<img
212+
src="<?php echo esc_url( $avatar_url ); ?>"
213+
class="<?php echo esc_attr( $class ); ?>"
214+
alt="<?php echo esc_attr( $author_name ); ?>"
215+
width="<?php echo esc_attr( $image_size ); ?>"
216+
height="<?php echo esc_attr( $image_size ); ?>"
217+
style="<?php echo esc_attr( $border_attributes['style'] ?? '' ); ?>"
218+
/>
219+
<?php endif; ?>
220+
</div>
221+
</div>
222+
<?php
223+
return ob_get_clean();
224+
}
225+
138226
/**
139227
* Get the authors whose avatars should be displayed.
140228
*

src/blocks/avatar/edit.jsx

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,27 @@ import {
1212
__experimentalUseBorderProps as useBorderProps,
1313
} from '@wordpress/block-editor';
1414
import { __ } from '@wordpress/i18n';
15-
import { useMemo } from '@wordpress/element';
15+
import { createContext, useContext, useMemo } from '@wordpress/element';
1616
import { PanelBody, RangeControl, ToggleControl } from '@wordpress/components';
1717
import { addQueryArgs, removeQueryArgs } from '@wordpress/url';
1818
/**
1919
* Internal dependencies
2020
*/
2121
import { useUserAvatar, usePostAuthors } from './hooks';
2222
import { useCustomByline, extractAuthorIdsFromByline } from '../../shared/hooks/use-custom-byline';
23+
24+
/**
25+
* Fallback context that always returns null.
26+
* Used when the shared AuthorContext from newspack-blocks is not available.
27+
*/
28+
const FallbackAuthorContext = createContext( null );
29+
30+
/**
31+
* Get the shared AuthorContext from newspack-blocks if available, otherwise use fallback.
32+
* This allows the avatar block to be used inside the Author Profile block's nested mode.
33+
*/
34+
const SharedAuthorContext = typeof window !== 'undefined' && window.NewspackAuthorContext ? window.NewspackAuthorContext : FallbackAuthorContext;
35+
2336
const AvatarInspectorControls = ( { setAttributes, attributes } ) => (
2437
<InspectorControls>
2538
<PanelBody title={ __( 'Settings', 'newspack-plugin' ) }>
@@ -120,14 +133,58 @@ const AvatarWrapper = ( { avatar, size, attributes, placeholder = false } ) => {
120133
};
121134

122135
const Edit = ( { attributes, context, setAttributes } ) => {
136+
const blockProps = useBlockProps();
137+
138+
// Check for parent block context first (nested mode - single author).
139+
const authorFromBlockContext = context[ 'newspack-blocks/author' ];
140+
const authorFromReactContext = useContext( SharedAuthorContext );
141+
const authorFromParent = authorFromBlockContext || authorFromReactContext;
142+
143+
// Hooks must be called unconditionally per React rules.
123144
const { postId, postType } = context;
124145
const avatar = useUserAvatar( { userId: attributes?.userId, postId, postType } );
125146
const allAuthors = usePostAuthors( { postId, postType } );
126147
const { bylineActive, bylineContent } = useCustomByline( postId, postType );
127-
const blockProps = useBlockProps();
128148

129-
// Text-only custom byline (no [Author] shortcodes) — show placeholder.
149+
// Memoize author ID extraction to avoid running regex on every render.
130150
const authorIds = useMemo( () => extractAuthorIdsFromByline( bylineContent ), [ bylineContent ] );
151+
152+
const renderAvatar = ( currentAvatar, key ) => (
153+
<AvatarWrapper key={ key } avatar={ currentAvatar } size={ attributes.size } attributes={ attributes } />
154+
);
155+
156+
// Nested mode: render single author from parent context.
157+
if ( authorFromParent ) {
158+
let avatarUrl = '';
159+
if ( authorFromParent.avatar ) {
160+
if ( authorFromParent.avatar.includes( '<img' ) ) {
161+
const match = authorFromParent.avatar.match( /src=["']([^"']+)["']/ );
162+
avatarUrl = match?.[ 1 ] || '';
163+
} else {
164+
avatarUrl = authorFromParent.avatar;
165+
}
166+
}
167+
168+
if ( ! avatarUrl ) {
169+
return null;
170+
}
171+
172+
const parentAvatar = {
173+
src: avatarUrl,
174+
alt: authorFromParent.name || '',
175+
minSize: 16,
176+
maxSize: 128,
177+
};
178+
179+
return (
180+
<>
181+
<AvatarInspectorControls attributes={ attributes } setAttributes={ setAttributes } />
182+
<div { ...blockProps }>{ renderAvatar( parentAvatar, 'nested-author' ) }</div>
183+
</>
184+
);
185+
}
186+
187+
// Text-only custom byline (no [Author] shortcodes) — show placeholder.
131188
const isTextOnlyByline = bylineActive && ( ! bylineContent || authorIds.length === 0 );
132189
if ( isTextOnlyByline ) {
133190
return (
@@ -138,28 +195,28 @@ const Edit = ( { attributes, context, setAttributes } ) => {
138195
);
139196
}
140197

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

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

148-
const renderAvatar = ( currentAvatar, key ) => (
149-
<AvatarWrapper key={ key } avatar={ currentAvatar } size={ attributes.size } attributes={ attributes } />
150-
);
151206
return (
152207
<>
153208
<AvatarInspectorControls attributes={ attributes } setAttributes={ setAttributes } />
154-
{ authors?.length
155-
? authors.map( ( author, index ) => {
156-
const currentAvatar = {
157-
src: author.avatarSrc,
158-
alt: author?.name || author?.display_name || '',
159-
};
160-
return renderAvatar( currentAvatar, author.id || index );
161-
} )
162-
: renderAvatar( avatar, 'single-author' ) }
209+
<div { ...blockProps }>
210+
{ authors?.length
211+
? authors.map( ( author, index ) => {
212+
const currentAvatar = {
213+
src: author.avatarSrc,
214+
alt: author?.name || author?.display_name || '',
215+
};
216+
return renderAvatar( currentAvatar, author.id || index );
217+
} )
218+
: renderAvatar( avatar, 'single-author' ) }
219+
</div>
163220
</>
164221
);
165222
};

0 commit comments

Comments
 (0)