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
4 changes: 4 additions & 0 deletions .github/changelog/block-visibility-fediverse
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Add per-block Fediverse visibility toggle to control which blocks are shared to Mastodon and other Fediverse platforms.
11 changes: 11 additions & 0 deletions build/block-visibility/block.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build/block-visibility/plugin.asset.php

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build/block-visibility/plugin.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build/reply/block.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

41 changes: 41 additions & 0 deletions includes/class-blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ public static function enqueue_editor_assets() {
$asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/pre-publish-panel/plugin.asset.php';
$plugin_url = plugins_url( 'build/pre-publish-panel/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
wp_enqueue_script( 'activitypub-pre-publish-panel', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );

$asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/block-visibility/plugin.asset.php';
$plugin_url = plugins_url( 'build/block-visibility/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
wp_enqueue_script( 'activitypub-block-visibility', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
}

/**
Expand Down Expand Up @@ -946,6 +950,7 @@ public static function add_directions( $content, $selector, $attributes ) {
* @param object $post The post object.
*/
public static function add_post_transformation_callbacks( $post ) {
\add_filter( 'render_block', array( self::class, 'maybe_hide_block' ), 5, 2 );
\add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 );

// Only transform reply link if it's the first block in the post.
Expand All @@ -963,12 +968,48 @@ public static function add_post_transformation_callbacks( $post ) {
* @return string The updated content.
*/
public static function remove_post_transformation_callbacks( $content ) {
\remove_filter( 'render_block', array( self::class, 'maybe_hide_block' ), 5 );
\remove_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ) );
\remove_filter( 'render_block_activitypub/reply', array( self::class, 'generate_reply_link' ) );

return $content;
}

/**
* Strip blocks not marked for federation during content rendering.
*
* @since unreleased
*
* @param string $block_content The rendered block content.
* @param array $block The parsed block data.
*
* @return string The block content, or empty string if hidden.
*/
public static function maybe_hide_block( $block_content, $block ) {
if ( ! self::is_federated( $block ) ) {
return '';
}

return $block_content;
}

/**
* Whether a block should be included in federated content.
*
* Checks `metadata.blockVisibility.fediverse`. Defaults to true.
*
* @since unreleased
*
* @param array $block The parsed block data.
*
* @return bool True if the block should be federated.
*/
public static function is_federated( $block ) {
$visibility = $block['attrs']['metadata']['blockVisibility']['fediverse'] ?? true;

return false !== $visibility;
}

/**
* Generate HTML @ link for reply block.
*
Expand Down
9 changes: 9 additions & 0 deletions includes/transformer/class-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,10 @@ protected function get_in_reply_to() {
$blocks = \parse_blocks( $this->item->post_content );

foreach ( $blocks as $block ) {
if ( ! Blocks::is_federated( $block ) ) {
continue;
}

if ( 'activitypub/reply' === $block['blockName'] && isset( $block['attrs']['url'] ) ) {

// Check if the URL has been validated as ActivityPub. Default to true for backwards compatibility.
Expand Down Expand Up @@ -914,6 +918,11 @@ protected function get_block_attachments( $media, $max_media ) {
*/
protected function get_media_from_blocks( $blocks, $media ) {
foreach ( $blocks as $block ) {
// Skip blocks hidden from the Fediverse.
if ( ! Blocks::is_federated( $block ) ) {
continue;
}

// Recurse into inner blocks.
if ( ! empty( $block['innerBlocks'] ) ) {
$media = $this->get_media_from_blocks( $block['innerBlocks'], $media );
Expand Down
1 change: 1 addition & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"@wordpress/editor": "^14.38.0",
"@wordpress/element": "^6.0.0",
"@wordpress/env": "^10.39.0",
"@wordpress/hooks": "^4.36.0",
"@wordpress/html-entities": "^4.36.0",
"@wordpress/i18n": "^6.0.0",
"@wordpress/icons": "^11.4.0",
Expand Down
11 changes: 11 additions & 0 deletions src/block-visibility/block.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "activitypub/block-visibility",
"title": "ActivityPub Block Visibility",
"category": "widgets",
"description": "Adds Fediverse visibility controls to all blocks.",
"icon": "screenoptions",
"textdomain": "activitypub",
"editorScript": "file:./plugin.js"
}
79 changes: 79 additions & 0 deletions src/block-visibility/plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { addFilter } from '@wordpress/hooks';
import { createHigherOrderComponent } from '@wordpress/compose';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, ToggleControl } from '@wordpress/components';
import { __ } from '@wordpress/i18n';

/**
* Adds a "Fediverse" panel to the block inspector for all blocks,
* allowing users to control whether a block is included in federated content.
*/
const withFediverseVisibility = createHigherOrderComponent( ( BlockEdit ) => {
return ( props ) => {
const { attributes, setAttributes, isSelected } = props;

// Get the current fediverse visibility state (default: true).
const metadata = attributes?.metadata || {};
const blockVisibility = metadata?.blockVisibility || {};
const isFediverseVisible = blockVisibility?.fediverse !== false;

/**
* Update the fediverse visibility in block metadata.
*
* @param {boolean} value Whether the block should be visible on the Fediverse.
*/
const onChange = ( value ) => {
const updatedBlockVisibility = { ...blockVisibility };

if ( value ) {
// Remove the fediverse key when true (default state).
delete updatedBlockVisibility.fediverse;
} else {
updatedBlockVisibility.fediverse = false;
}

// Clean up: if blockVisibility is empty, remove it from metadata.
const updatedMetadata = { ...metadata };
if ( Object.keys( updatedBlockVisibility ).length === 0 ) {
delete updatedMetadata.blockVisibility;
} else {
updatedMetadata.blockVisibility = updatedBlockVisibility;
}

// Clean up: if metadata is empty, remove it entirely.
if ( Object.keys( updatedMetadata ).length === 0 ) {
setAttributes( { metadata: undefined } );
} else {
setAttributes( { metadata: updatedMetadata } );
}
};

return (
<>
<BlockEdit { ...props } />
{ isSelected && (
<InspectorControls>
<PanelBody title={ __( 'Fediverse ⁂', 'activitypub' ) } initialOpen={ false }>
<ToggleControl
__nextHasNoMarginBottom
label={ __( 'Share to the Fediverse', 'activitypub' ) }
help={
isFediverseVisible
? __(
'This block will be shared to Mastodon and other Fediverse platforms.',
'activitypub'
)
: __( 'This block will only be visible on your site.', 'activitypub' )
}
checked={ isFediverseVisible }
onChange={ onChange }
/>
</PanelBody>
</InspectorControls>
) }
</>
);
};
}, 'withFediverseVisibility' );

addFilter( 'editor.BlockEdit', 'activitypub/block-visibility', withFediverseVisibility );
1 change: 1 addition & 0 deletions src/reply/block.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"inserter": true,
"reusable": false,
"lock": false,
"visibility": false,
"innerBlocks": {
"allowedBlocks": [ "core/embed" ]
}
Expand Down
78 changes: 78 additions & 0 deletions tests/phpunit/tests/includes/class-test-blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -1027,4 +1027,82 @@ public function test_render_extra_fields_block_preserves_html() {
$this->assertStringContainsString( '<strong>my site</strong>', $output );
$this->assertStringContainsString( '<a href="https://test.com"', $output );
}

/**
* Test is_federated returns true by default.
*
* @covers ::is_federated
*/
public function test_is_federated_defaults_to_true() {
$block = array( 'attrs' => array() );
$this->assertTrue( Blocks::is_federated( $block ) );
}

/**
* Test is_federated returns true when no metadata exists.
*
* @covers ::is_federated
*/
public function test_is_federated_without_metadata() {
$block = array( 'attrs' => array( 'metadata' => array() ) );
$this->assertTrue( Blocks::is_federated( $block ) );
}

/**
* Test is_federated returns true when fediverse is explicitly true.
*
* @covers ::is_federated
*/
public function test_is_federated_when_explicitly_true() {
$block = array(
'attrs' => array(
'metadata' => array(
'blockVisibility' => array( 'fediverse' => true ),
),
),
);
$this->assertTrue( Blocks::is_federated( $block ) );
}

/**
* Test is_federated returns false when fediverse is false.
*
* @covers ::is_federated
*/
public function test_is_federated_returns_false_when_hidden() {
$block = array(
'attrs' => array(
'metadata' => array(
'blockVisibility' => array( 'fediverse' => false ),
),
),
);
$this->assertFalse( Blocks::is_federated( $block ) );
}

/**
* Test maybe_hide_block returns content for federated blocks.
*
* @covers ::maybe_hide_block
*/
public function test_maybe_hide_block_keeps_visible_block() {
$block = array( 'attrs' => array() );
$this->assertSame( '<p>Hello</p>', Blocks::maybe_hide_block( '<p>Hello</p>', $block ) );
}

/**
* Test maybe_hide_block returns empty string for hidden blocks.
*
* @covers ::maybe_hide_block
*/
public function test_maybe_hide_block_strips_hidden_block() {
$block = array(
'attrs' => array(
'metadata' => array(
'blockVisibility' => array( 'fediverse' => false ),
),
),
);
$this->assertSame( '', Blocks::maybe_hide_block( '<p>Secret</p>', $block ) );
}
}
58 changes: 58 additions & 0 deletions tests/phpunit/tests/includes/transformer/class-test-post.php
Original file line number Diff line number Diff line change
Expand Up @@ -2110,4 +2110,62 @@ public function test_to_tombstone_to_array_without_deleted() {
$this->assertSame( 'Tombstone', $array['type'] );
$this->assertSame( 'Article', $array['formerType'] );
}

/**
* Test get_media_from_blocks skips blocks hidden from the Fediverse.
*
* @covers ::get_media_from_blocks
*/
public function test_get_media_from_blocks_skips_hidden_blocks() {
$post_id = self::factory()->post->create(
array(
'post_content' => '<!-- wp:image {"id":123,"metadata":{"blockVisibility":{"fediverse":false}}} --><figure class="wp-block-image"><img src="hidden.jpg" alt="Hidden" /></figure><!-- /wp:image --><!-- wp:image {"id":456} --><figure class="wp-block-image"><img src="visible.jpg" alt="Visible" /></figure><!-- /wp:image -->',
)
);
$post = get_post( $post_id );

$transformer = new Post( $post );
$media = array(
'image' => array(),
'audio' => array(),
'video' => array(),
);

$reflection = new \ReflectionClass( Post::class );
$method = $reflection->getMethod( 'get_media_from_blocks' );
if ( \PHP_VERSION_ID < 80100 ) {
$method->setAccessible( true );
}

$blocks = parse_blocks( $post->post_content );
$result = $method->invoke( $transformer, $blocks, $media );

$this->assertCount( 1, $result['image'] );
$this->assertSame( 456, $result['image'][0]['id'] );
}

/**
* Test get_in_reply_to skips reply blocks hidden from the Fediverse.
*
* @covers ::get_in_reply_to
*/
public function test_get_in_reply_to_skips_hidden_reply_block() {
$post_id = self::factory()->post->create(
array(
'post_content' => '<!-- wp:activitypub/reply {"url":"https://example.com/hidden","metadata":{"blockVisibility":{"fediverse":false}}} /--><!-- wp:paragraph --><p>Hello</p><!-- /wp:paragraph -->',
)
);
$post = get_post( $post_id );

$transformer = new Post( $post );

$reflection = new \ReflectionClass( Post::class );
$method = $reflection->getMethod( 'get_in_reply_to' );
if ( \PHP_VERSION_ID < 80100 ) {
$method->setAccessible( true );
}

$result = $method->invoke( $transformer );
$this->assertNull( $result );
}
}
Loading