From 24a217791057ebbea97c821686368b563875a9ae Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 24 Feb 2026 13:45:57 +0100 Subject: [PATCH 1/5] Add pre-publish panel suggesting post formats for better federation When the object type setting is "Post Format", analyze post content and suggest a more appropriate format (Image, Gallery, Video, Audio, Status) before publishing. This helps image/video posts get shared as Notes instead of Articles, making them visible on media-focused platforms like Pixelfed and Vernissage. Also raises ACTIVITYPUB_NOTE_LENGTH from 400 to 500 and passes objectType and noteLength to the editor JS. Ref #2903 --- build/pre-publish-panel/block.json | 8 + build/pre-publish-panel/plugin.asset.php | 1 + build/pre-publish-panel/plugin.js | 2 + includes/class-blocks.php | 6 + includes/constants.php | 2 +- .../__tests__/plugin.test.js | 381 ++++++++++++++++++ src/pre-publish-panel/block.json | 8 + src/pre-publish-panel/plugin.js | 53 +++ src/pre-publish-panel/utils.js | 175 ++++++++ 9 files changed, 635 insertions(+), 1 deletion(-) create mode 100644 build/pre-publish-panel/block.json create mode 100644 build/pre-publish-panel/plugin.asset.php create mode 100644 build/pre-publish-panel/plugin.js create mode 100644 src/pre-publish-panel/__tests__/plugin.test.js create mode 100644 src/pre-publish-panel/block.json create mode 100644 src/pre-publish-panel/plugin.js create mode 100644 src/pre-publish-panel/utils.js diff --git a/build/pre-publish-panel/block.json b/build/pre-publish-panel/block.json new file mode 100644 index 0000000000..b95c083d4d --- /dev/null +++ b/build/pre-publish-panel/block.json @@ -0,0 +1,8 @@ +{ + "name": "pre-publish-panel", + "title": "Pre-Publish Panel: not a block, but block.json is very useful.", + "category": "widgets", + "icon": "admin-comments", + "keywords": [], + "editorScript": "file:./plugin.js" +} \ No newline at end of file diff --git a/build/pre-publish-panel/plugin.asset.php b/build/pre-publish-panel/plugin.asset.php new file mode 100644 index 0000000000..c1554a46e3 --- /dev/null +++ b/build/pre-publish-panel/plugin.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '202bcc97dd4ee989a5f0'); diff --git a/build/pre-publish-panel/plugin.js b/build/pre-publish-panel/plugin.js new file mode 100644 index 0000000000..5c27f6511b --- /dev/null +++ b/build/pre-publish-panel/plugin.js @@ -0,0 +1,2 @@ +(()=>{"use strict";const t=window.wp.editor,e=window.wp.blockEditor,o=window.wp.plugins,i=window.wp.components,a=window.wp.data,n=window.wp.element,s=window.wp.i18n,r=window._activityPubOptions?.noteLength||500,l=["youtube","vimeo","dailymotion","tiktok","videopress"],u=["spotify","soundcloud","mixcloud"],d=t=>{const e={imageCount:0,galleryCount:0,videoCount:0,audioCount:0,textLength:0,textBlockCount:0};if(!t||!t.length)return e;const o=["core/paragraph","core/heading","core/list-item","core/preformatted","core/verse","core/pullquote"],i=["core/gallery","jetpack/tiled-gallery","jetpack/slideshow"];for(const a of t){const{name:t,attributes:n,innerBlocks:s}=a;if("core/image"===t)e.imageCount++;else if(i.includes(t))e.galleryCount++;else if("core/video"===t)e.videoCount++;else if("core/audio"===t)e.audioCount++;else if("core/embed"===t){const t=(n?.providerNameSlug||"").toLowerCase();l.includes(t)?e.videoCount++:u.includes(t)&&e.audioCount++}if(o.includes(t)){const t=(n?.content||"").replace(/<[^>]*>/g,"");e.textLength+=t.length,e.textBlockCount++}if(s&&s.length){const t=d(s);e.imageCount+=t.imageCount,e.galleryCount+=t.galleryCount,e.videoCount+=t.videoCount,e.audioCount+=t.audioCount,e.textLength+=t.textLength,e.textBlockCount+=t.textBlockCount}}return e},c=window.ReactJSXRuntime;(0,o.registerPlugin)("activitypub-pre-publish",{render:()=>{const{blocks:o,postFormat:l}=(0,a.useSelect)(o=>({blocks:o(e.store).getBlocks(),postFormat:o(t.store).getEditedPostAttribute("format")}),[]),{editPost:u}=(0,a.useDispatch)(t.store),m=(0,n.useMemo)(()=>((t,e)=>{if(e&&"standard"!==e)return null;const o=d(t),i=o.imageCount>0||o.galleryCount>0||o.videoCount>0||o.audioCount>0;return(o.galleryCount>0||o.imageCount>1)&&o.textLength0&&o.textLength0&&o.textLength0&&o.textLength<280&&o.textBlockCount<=3?{format:"status",message:(0,s.__)("This is a short post with no media. Setting the format to Status will share it as a Note, which is the standard format on platforms like Mastodon.","activitypub")}:null})(o,l),[o,l]);return"wordpress-post-format"!==window._activityPubOptions?.objectType?null:m?(0,c.jsxs)(t.PluginPrePublishPanel,{title:(0,s.__)("Fediverse ⁂","activitypub"),initialOpen:!0,children:[(0,c.jsx)("p",{children:m.message}),(0,c.jsx)(i.Button,{variant:"secondary",onClick:()=>u({format:m.format}),children:(0,s.sprintf)(/* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ /* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ +(0,s.__)("Set format to %s","activitypub"),m.format.charAt(0).toUpperCase()+m.format.slice(1))})]}):null}})})(); \ No newline at end of file diff --git a/includes/class-blocks.php b/includes/class-blocks.php index 4bf0aa8b27..364c36ccba 100644 --- a/includes/class-blocks.php +++ b/includes/class-blocks.php @@ -50,6 +50,8 @@ public static function enqueue_editor_assets() { ), 'showAvatars' => (bool) \get_option( 'show_avatars' ), 'defaultQuotePolicy' => \get_option( 'activitypub_default_quote_policy', ACTIVITYPUB_INTERACTION_POLICY_ANYONE ), + 'objectType' => \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ), + 'noteLength' => ACTIVITYPUB_NOTE_LENGTH, ); wp_localize_script( 'wp-editor', '_activityPubOptions', $data ); @@ -63,6 +65,10 @@ public static function enqueue_editor_assets() { $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/editor-plugin/plugin.asset.php'; $plugin_url = plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); + + $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 ); } /** diff --git a/includes/constants.php b/includes/constants.php index 5c251d3d99..e01f661eb0 100644 --- a/includes/constants.php +++ b/includes/constants.php @@ -9,7 +9,7 @@ defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' ); defined( 'ACTIVITYPUB_EXCERPT_LENGTH' ) || define( 'ACTIVITYPUB_EXCERPT_LENGTH', 400 ); -defined( 'ACTIVITYPUB_NOTE_LENGTH' ) || define( 'ACTIVITYPUB_NOTE_LENGTH', 400 ); +defined( 'ACTIVITYPUB_NOTE_LENGTH' ) || define( 'ACTIVITYPUB_NOTE_LENGTH', 500 ); defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 4 ); defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=

)|(?<=
)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' ); defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); diff --git a/src/pre-publish-panel/__tests__/plugin.test.js b/src/pre-publish-panel/__tests__/plugin.test.js new file mode 100644 index 0000000000..92908581d6 --- /dev/null +++ b/src/pre-publish-panel/__tests__/plugin.test.js @@ -0,0 +1,381 @@ +/** + * @jest-environment jsdom + */ + +// Set up the global options before importing utils, so NOTE_LENGTH picks up the value. +window._activityPubOptions = { noteLength: 500 }; + +import { analyzeBlocks, getSuggestedPostFormat } from '../utils'; + +describe( 'analyzeBlocks', () => { + test( 'returns all zeros for empty blocks', () => { + expect( analyzeBlocks( [] ) ).toEqual( { + imageCount: 0, + galleryCount: 0, + videoCount: 0, + audioCount: 0, + textLength: 0, + textBlockCount: 0, + } ); + } ); + + test( 'returns all zeros for null/undefined', () => { + expect( analyzeBlocks( null ) ).toEqual( { + imageCount: 0, + galleryCount: 0, + videoCount: 0, + audioCount: 0, + textLength: 0, + textBlockCount: 0, + } ); + } ); + + test( 'counts text from a single paragraph', () => { + const blocks = [ + { + name: 'core/paragraph', + attributes: { content: 'Hello world' }, + innerBlocks: [], + }, + ]; + const result = analyzeBlocks( blocks ); + expect( result.textLength ).toBe( 11 ); + expect( result.textBlockCount ).toBe( 1 ); + } ); + + test( 'strips HTML from text content', () => { + const blocks = [ + { + name: 'core/paragraph', + attributes: { + content: 'Hello world', + }, + innerBlocks: [], + }, + ]; + const result = analyzeBlocks( blocks ); + expect( result.textLength ).toBe( 11 ); // "Hello world" + } ); + + test( 'counts image blocks', () => { + const blocks = [ + { name: 'core/image', attributes: {}, innerBlocks: [] }, + { name: 'core/image', attributes: {}, innerBlocks: [] }, + ]; + const result = analyzeBlocks( blocks ); + expect( result.imageCount ).toBe( 2 ); + } ); + + test( 'counts gallery blocks', () => { + const blocks = [ { name: 'core/gallery', attributes: {}, innerBlocks: [] } ]; + const result = analyzeBlocks( blocks ); + expect( result.galleryCount ).toBe( 1 ); + } ); + + test( 'counts jetpack gallery blocks', () => { + const blocks = [ + { + name: 'jetpack/tiled-gallery', + attributes: {}, + innerBlocks: [], + }, + { name: 'jetpack/slideshow', attributes: {}, innerBlocks: [] }, + ]; + const result = analyzeBlocks( blocks ); + expect( result.galleryCount ).toBe( 2 ); + } ); + + test( 'counts video blocks', () => { + const blocks = [ { name: 'core/video', attributes: {}, innerBlocks: [] } ]; + const result = analyzeBlocks( blocks ); + expect( result.videoCount ).toBe( 1 ); + } ); + + test( 'counts audio blocks', () => { + const blocks = [ { name: 'core/audio', attributes: {}, innerBlocks: [] } ]; + const result = analyzeBlocks( blocks ); + expect( result.audioCount ).toBe( 1 ); + } ); + + test( 'detects video embed providers', () => { + const blocks = [ + { + name: 'core/embed', + attributes: { providerNameSlug: 'youtube' }, + innerBlocks: [], + }, + { + name: 'core/embed', + attributes: { providerNameSlug: 'vimeo' }, + innerBlocks: [], + }, + ]; + const result = analyzeBlocks( blocks ); + expect( result.videoCount ).toBe( 2 ); + } ); + + test( 'detects audio embed providers', () => { + const blocks = [ + { + name: 'core/embed', + attributes: { providerNameSlug: 'spotify' }, + innerBlocks: [], + }, + { + name: 'core/embed', + attributes: { providerNameSlug: 'soundcloud' }, + innerBlocks: [], + }, + ]; + const result = analyzeBlocks( blocks ); + expect( result.audioCount ).toBe( 2 ); + } ); + + test( 'counts nested inner blocks recursively', () => { + const blocks = [ + { + name: 'core/group', + attributes: {}, + innerBlocks: [ + { + name: 'core/image', + attributes: {}, + innerBlocks: [], + }, + { + name: 'core/paragraph', + attributes: { content: 'Nested text' }, + innerBlocks: [], + }, + ], + }, + ]; + const result = analyzeBlocks( blocks ); + expect( result.imageCount ).toBe( 1 ); + expect( result.textLength ).toBe( 11 ); + expect( result.textBlockCount ).toBe( 1 ); + } ); + + test( 'counts multiple text block types', () => { + const blocks = [ + { + name: 'core/heading', + attributes: { content: 'Title' }, + innerBlocks: [], + }, + { + name: 'core/preformatted', + attributes: { content: 'Code' }, + innerBlocks: [], + }, + { + name: 'core/verse', + attributes: { content: 'Poem' }, + innerBlocks: [], + }, + { + name: 'core/pullquote', + attributes: { content: 'Quote' }, + innerBlocks: [], + }, + { + name: 'core/list-item', + attributes: { content: 'Item' }, + innerBlocks: [], + }, + ]; + const result = analyzeBlocks( blocks ); + expect( result.textBlockCount ).toBe( 5 ); + expect( result.textLength ).toBe( 22 ); // Title(5) + Code(4) + Poem(4) + Quote(5) + Item(4) + } ); +} ); + +describe( 'getSuggestedPostFormat', () => { + test( 'returns null when format is already set', () => { + const blocks = [ { name: 'core/image', attributes: {}, innerBlocks: [] } ]; + expect( getSuggestedPostFormat( blocks, 'image' ) ).toBeNull(); + expect( getSuggestedPostFormat( blocks, 'gallery' ) ).toBeNull(); + expect( getSuggestedPostFormat( blocks, 'aside' ) ).toBeNull(); + } ); + + test( 'suggests gallery for gallery blocks with short text', () => { + const blocks = [ + { name: 'core/gallery', attributes: {}, innerBlocks: [] }, + { + name: 'core/paragraph', + attributes: { content: 'Caption' }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, 'standard' ); + expect( result.format ).toBe( 'gallery' ); + } ); + + test( 'suggests gallery for multiple images with short text', () => { + const blocks = [ + { name: 'core/image', attributes: {}, innerBlocks: [] }, + { name: 'core/image', attributes: {}, innerBlocks: [] }, + { + name: 'core/paragraph', + attributes: { content: 'Photos' }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'gallery' ); + } ); + + test( 'suggests image for single image with short text', () => { + const blocks = [ + { name: 'core/image', attributes: {}, innerBlocks: [] }, + { + name: 'core/paragraph', + attributes: { content: 'My photo' }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'image' ); + } ); + + test( 'suggests video for video with short text', () => { + const blocks = [ + { name: 'core/video', attributes: {}, innerBlocks: [] }, + { + name: 'core/paragraph', + attributes: { content: 'Watch this' }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'video' ); + } ); + + test( 'suggests video for YouTube embed', () => { + const blocks = [ + { + name: 'core/embed', + attributes: { providerNameSlug: 'youtube' }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'video' ); + } ); + + test( 'suggests audio for audio with short text', () => { + const blocks = [ + { name: 'core/audio', attributes: {}, innerBlocks: [] }, + { + name: 'core/paragraph', + attributes: { content: 'Listen' }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'audio' ); + } ); + + test( 'suggests audio for Spotify embed', () => { + const blocks = [ + { + name: 'core/embed', + attributes: { providerNameSlug: 'spotify' }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'audio' ); + } ); + + test( 'suggests status for very short text-only posts', () => { + const blocks = [ + { + name: 'core/paragraph', + attributes: { content: 'Just a quick thought.' }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'status' ); + } ); + + test( 'does not suggest status when text is too long', () => { + const blocks = [ + { + name: 'core/paragraph', + attributes: { content: 'A'.repeat( 300 ) }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result ).toBeNull(); + } ); + + test( 'does not suggest status when there are too many text blocks', () => { + const blocks = [ + { + name: 'core/paragraph', + attributes: { content: 'One' }, + innerBlocks: [], + }, + { + name: 'core/paragraph', + attributes: { content: 'Two' }, + innerBlocks: [], + }, + { + name: 'core/paragraph', + attributes: { content: 'Three' }, + innerBlocks: [], + }, + { + name: 'core/paragraph', + attributes: { content: 'Four' }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result ).toBeNull(); + } ); + + test( 'returns null for long text articles', () => { + const blocks = [ + { + name: 'core/image', + attributes: {}, + innerBlocks: [], + }, + { + name: 'core/paragraph', + attributes: { content: 'A'.repeat( 600 ) }, + innerBlocks: [], + }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result ).toBeNull(); + } ); + + test( 'returns null when no blocks present', () => { + expect( getSuggestedPostFormat( [], '' ) ).toBeNull(); + } ); + + test( 'works with falsy currentFormat values', () => { + const blocks = [ { name: 'core/image', attributes: {}, innerBlocks: [] } ]; + // All falsy values should allow suggestion. + expect( getSuggestedPostFormat( blocks, '' ) ).not.toBeNull(); + expect( getSuggestedPostFormat( blocks, null ) ).not.toBeNull(); + expect( getSuggestedPostFormat( blocks, undefined ) ).not.toBeNull(); + expect( getSuggestedPostFormat( blocks, 'standard' ) ).not.toBeNull(); + } ); + + test( 'gallery has higher priority than image', () => { + const blocks = [ + { name: 'core/image', attributes: {}, innerBlocks: [] }, + { name: 'core/image', attributes: {}, innerBlocks: [] }, + ]; + // Multiple images should suggest gallery, not image. + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'gallery' ); + } ); +} ); diff --git a/src/pre-publish-panel/block.json b/src/pre-publish-panel/block.json new file mode 100644 index 0000000000..295ede929f --- /dev/null +++ b/src/pre-publish-panel/block.json @@ -0,0 +1,8 @@ +{ + "name": "pre-publish-panel", + "title": "Pre-Publish Panel: not a block, but block.json is very useful.", + "category": "widgets", + "icon": "admin-comments", + "keywords": [], + "editorScript": "file:./plugin.js" +} diff --git a/src/pre-publish-panel/plugin.js b/src/pre-publish-panel/plugin.js new file mode 100644 index 0000000000..06d04b16fe --- /dev/null +++ b/src/pre-publish-panel/plugin.js @@ -0,0 +1,53 @@ +import { PluginPrePublishPanel, store as editorStore } from '@wordpress/editor'; +import { store as blockEditorStore } from '@wordpress/block-editor'; +import { registerPlugin } from '@wordpress/plugins'; +import { Button } from '@wordpress/components'; +import { useSelect, useDispatch } from '@wordpress/data'; +import { useMemo } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import { getSuggestedPostFormat } from './utils'; + +/** + * Pre-publish panel that suggests a post format for better federation. + * + * Only renders when the object type setting is 'wordpress-post-format' and + * the post content suggests a better format than the default Article. + * + * @return {React.JSX.Element|null} The pre-publish panel or null. + */ +const PrePublishPanel = () => { + const { blocks, postFormat } = useSelect( ( selectFn ) => { + return { + blocks: selectFn( blockEditorStore ).getBlocks(), + postFormat: selectFn( editorStore ).getEditedPostAttribute( 'format' ), + }; + }, [] ); + + const { editPost } = useDispatch( editorStore ); + + const suggestion = useMemo( () => getSuggestedPostFormat( blocks, postFormat ), [ blocks, postFormat ] ); + + // Only show when object type is set to post format mapping. + if ( window._activityPubOptions?.objectType !== 'wordpress-post-format' ) { + return null; + } + + if ( ! suggestion ) { + return null; + } + + return ( + +

{ suggestion.message }

+ + + ); +}; + +registerPlugin( 'activitypub-pre-publish', { render: PrePublishPanel } ); diff --git a/src/pre-publish-panel/utils.js b/src/pre-publish-panel/utils.js new file mode 100644 index 0000000000..7f56ecd884 --- /dev/null +++ b/src/pre-publish-panel/utils.js @@ -0,0 +1,175 @@ +import { __ } from '@wordpress/i18n'; + +/** + * The maximum note length from the server-side ACTIVITYPUB_NOTE_LENGTH constant. + * + * @type {number} + */ +const NOTE_LENGTH = window._activityPubOptions?.noteLength || 500; + +/** + * Video embed providers for detecting video content in embed blocks. + * + * @type {string[]} + */ +const VIDEO_PROVIDERS = [ 'youtube', 'vimeo', 'dailymotion', 'tiktok', 'videopress' ]; + +/** + * Audio embed providers for detecting audio content in embed blocks. + * + * @type {string[]} + */ +const AUDIO_PROVIDERS = [ 'spotify', 'soundcloud', 'mixcloud' ]; + +/** + * Recursively analyzes blocks and returns content statistics. + * + * Walks all blocks (including inner blocks) and counts media elements + * and text content to help determine the best post format. + * + * @param {Array} blocks The blocks to analyze. + * + * @return {Object} Content statistics with imageCount, galleryCount, videoCount, audioCount, textLength, textBlockCount. + */ +export const analyzeBlocks = ( blocks ) => { + const result = { + imageCount: 0, + galleryCount: 0, + videoCount: 0, + audioCount: 0, + textLength: 0, + textBlockCount: 0, + }; + + if ( ! blocks || ! blocks.length ) { + return result; + } + + const textBlockNames = [ + 'core/paragraph', + 'core/heading', + 'core/list-item', + 'core/preformatted', + 'core/verse', + 'core/pullquote', + ]; + + const galleryBlockNames = [ 'core/gallery', 'jetpack/tiled-gallery', 'jetpack/slideshow' ]; + + for ( const block of blocks ) { + const { name, attributes, innerBlocks } = block; + + if ( name === 'core/image' ) { + result.imageCount++; + } else if ( galleryBlockNames.includes( name ) ) { + result.galleryCount++; + } else if ( name === 'core/video' ) { + result.videoCount++; + } else if ( name === 'core/audio' ) { + result.audioCount++; + } else if ( name === 'core/embed' ) { + const provider = ( attributes?.providerNameSlug || '' ).toLowerCase(); + if ( VIDEO_PROVIDERS.includes( provider ) ) { + result.videoCount++; + } else if ( AUDIO_PROVIDERS.includes( provider ) ) { + result.audioCount++; + } + } + + if ( textBlockNames.includes( name ) ) { + const content = ( attributes?.content || '' ).replace( /<[^>]*>/g, '' ); + result.textLength += content.length; + result.textBlockCount++; + } + + if ( innerBlocks && innerBlocks.length ) { + const inner = analyzeBlocks( innerBlocks ); + result.imageCount += inner.imageCount; + result.galleryCount += inner.galleryCount; + result.videoCount += inner.videoCount; + result.audioCount += inner.audioCount; + result.textLength += inner.textLength; + result.textBlockCount += inner.textBlockCount; + } + } + + return result; +}; + +/** + * Suggests a post format based on block content analysis. + * + * Returns a suggestion only when the current format is the default (standard). + * If the user has explicitly chosen a format, no suggestion is made. + * + * @param {Array} blocks The blocks to analyze. + * @param {string} currentFormat The current post format. + * + * @return {Object|null} Suggestion object with `format` and `message`, or null. + */ +export const getSuggestedPostFormat = ( blocks, currentFormat ) => { + // Don't suggest if user explicitly set a format. + if ( currentFormat && currentFormat !== 'standard' ) { + return null; + } + + const stats = analyzeBlocks( blocks ); + const hasMedia = stats.imageCount > 0 || stats.galleryCount > 0 || stats.videoCount > 0 || stats.audioCount > 0; + + // Gallery: gallery blocks or multiple images with short text. + if ( ( stats.galleryCount > 0 || stats.imageCount > 1 ) && stats.textLength < NOTE_LENGTH ) { + return { + format: 'gallery', + message: __( + 'This post contains multiple images. Setting the format to Gallery will share it as a media post, making it visible on platforms like Pixelfed.', + 'activitypub' + ), + }; + } + + // Image: single image with short text. + if ( stats.imageCount === 1 && stats.textLength < NOTE_LENGTH ) { + return { + format: 'image', + message: __( + 'This post contains an image. Setting the format to Image will share it as a media post, making it visible on platforms like Pixelfed.', + 'activitypub' + ), + }; + } + + // Video: video with short text. + if ( stats.videoCount > 0 && stats.textLength < NOTE_LENGTH ) { + return { + format: 'video', + message: __( + 'This post contains video. Setting the format to Video will share it as a media post, improving compatibility with video-focused platforms.', + 'activitypub' + ), + }; + } + + // Audio: audio with short text. + if ( stats.audioCount > 0 && stats.textLength < NOTE_LENGTH ) { + return { + format: 'audio', + message: __( + 'This post contains audio. Setting the format to Audio will share it as a media post, improving compatibility with audio-focused platforms.', + 'activitypub' + ), + }; + } + + // Status: very short text-only. + if ( ! hasMedia && stats.textLength > 0 && stats.textLength < 280 && stats.textBlockCount <= 3 ) { + return { + format: 'status', + message: __( + 'This is a short post with no media. Setting the format to Status will share it as a Note, which is the standard format on platforms like Mastodon.', + 'activitypub' + ), + }; + } + + return null; +}; From 36848c8dfa3ac2763d9d9505c8d33591100c749b Mon Sep 17 00:00:00 2001 From: Automattic Bot Date: Tue, 24 Feb 2026 14:46:48 +0200 Subject: [PATCH 2/5] Add changelog --- .github/changelog/2971-from-description | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 .github/changelog/2971-from-description diff --git a/.github/changelog/2971-from-description b/.github/changelog/2971-from-description new file mode 100644 index 0000000000..50418a10ed --- /dev/null +++ b/.github/changelog/2971-from-description @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Add a pre-publish suggestion that recommends a post format for better compatibility with media-focused Fediverse platforms. From 3d8209cf4564c43a115a36546d8c97c21c158812 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 24 Feb 2026 13:50:04 +0100 Subject: [PATCH 3/5] Use stripHTML from @wordpress/dom for tag stripping Replaces manual regex with the same utility used elsewhere in the codebase. Fixes CodeQL incomplete-sanitization warning. --- build/pre-publish-panel/plugin.asset.php | 2 +- build/pre-publish-panel/plugin.js | 4 ++-- src/pre-publish-panel/utils.js | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/build/pre-publish-panel/plugin.asset.php b/build/pre-publish-panel/plugin.asset.php index c1554a46e3..1fc2fe1162 100644 --- a/build/pre-publish-panel/plugin.asset.php +++ b/build/pre-publish-panel/plugin.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '202bcc97dd4ee989a5f0'); + array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-dom', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => 'f26fe9a63cb7c4d2f12e'); diff --git a/build/pre-publish-panel/plugin.js b/build/pre-publish-panel/plugin.js index 5c27f6511b..9bad09e2b8 100644 --- a/build/pre-publish-panel/plugin.js +++ b/build/pre-publish-panel/plugin.js @@ -1,2 +1,2 @@ -(()=>{"use strict";const t=window.wp.editor,e=window.wp.blockEditor,o=window.wp.plugins,i=window.wp.components,a=window.wp.data,n=window.wp.element,s=window.wp.i18n,r=window._activityPubOptions?.noteLength||500,l=["youtube","vimeo","dailymotion","tiktok","videopress"],u=["spotify","soundcloud","mixcloud"],d=t=>{const e={imageCount:0,galleryCount:0,videoCount:0,audioCount:0,textLength:0,textBlockCount:0};if(!t||!t.length)return e;const o=["core/paragraph","core/heading","core/list-item","core/preformatted","core/verse","core/pullquote"],i=["core/gallery","jetpack/tiled-gallery","jetpack/slideshow"];for(const a of t){const{name:t,attributes:n,innerBlocks:s}=a;if("core/image"===t)e.imageCount++;else if(i.includes(t))e.galleryCount++;else if("core/video"===t)e.videoCount++;else if("core/audio"===t)e.audioCount++;else if("core/embed"===t){const t=(n?.providerNameSlug||"").toLowerCase();l.includes(t)?e.videoCount++:u.includes(t)&&e.audioCount++}if(o.includes(t)){const t=(n?.content||"").replace(/<[^>]*>/g,"");e.textLength+=t.length,e.textBlockCount++}if(s&&s.length){const t=d(s);e.imageCount+=t.imageCount,e.galleryCount+=t.galleryCount,e.videoCount+=t.videoCount,e.audioCount+=t.audioCount,e.textLength+=t.textLength,e.textBlockCount+=t.textBlockCount}}return e},c=window.ReactJSXRuntime;(0,o.registerPlugin)("activitypub-pre-publish",{render:()=>{const{blocks:o,postFormat:l}=(0,a.useSelect)(o=>({blocks:o(e.store).getBlocks(),postFormat:o(t.store).getEditedPostAttribute("format")}),[]),{editPost:u}=(0,a.useDispatch)(t.store),m=(0,n.useMemo)(()=>((t,e)=>{if(e&&"standard"!==e)return null;const o=d(t),i=o.imageCount>0||o.galleryCount>0||o.videoCount>0||o.audioCount>0;return(o.galleryCount>0||o.imageCount>1)&&o.textLength0&&o.textLength0&&o.textLength0&&o.textLength<280&&o.textBlockCount<=3?{format:"status",message:(0,s.__)("This is a short post with no media. Setting the format to Status will share it as a Note, which is the standard format on platforms like Mastodon.","activitypub")}:null})(o,l),[o,l]);return"wordpress-post-format"!==window._activityPubOptions?.objectType?null:m?(0,c.jsxs)(t.PluginPrePublishPanel,{title:(0,s.__)("Fediverse ⁂","activitypub"),initialOpen:!0,children:[(0,c.jsx)("p",{children:m.message}),(0,c.jsx)(i.Button,{variant:"secondary",onClick:()=>u({format:m.format}),children:(0,s.sprintf)(/* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ /* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ -(0,s.__)("Set format to %s","activitypub"),m.format.charAt(0).toUpperCase()+m.format.slice(1))})]}):null}})})(); \ No newline at end of file +(()=>{"use strict";const t=window.wp.editor,e=window.wp.blockEditor,o=window.wp.plugins,i=window.wp.components,n=window.wp.data,a=window.wp.element,s=window.wp.i18n,r=window.wp.dom,l=window._activityPubOptions?.noteLength||500,u=["youtube","vimeo","dailymotion","tiktok","videopress"],d=["spotify","soundcloud","mixcloud"],c=t=>{const e={imageCount:0,galleryCount:0,videoCount:0,audioCount:0,textLength:0,textBlockCount:0};if(!t||!t.length)return e;const o=["core/paragraph","core/heading","core/list-item","core/preformatted","core/verse","core/pullquote"],i=["core/gallery","jetpack/tiled-gallery","jetpack/slideshow"];for(const n of t){const{name:t,attributes:a,innerBlocks:s}=n;if("core/image"===t)e.imageCount++;else if(i.includes(t))e.galleryCount++;else if("core/video"===t)e.videoCount++;else if("core/audio"===t)e.audioCount++;else if("core/embed"===t){const t=(a?.providerNameSlug||"").toLowerCase();u.includes(t)?e.videoCount++:d.includes(t)&&e.audioCount++}if(o.includes(t)){const t=(0,r.__unstableStripHTML)(a?.content||"");e.textLength+=t.length,e.textBlockCount++}if(s&&s.length){const t=c(s);e.imageCount+=t.imageCount,e.galleryCount+=t.galleryCount,e.videoCount+=t.videoCount,e.audioCount+=t.audioCount,e.textLength+=t.textLength,e.textBlockCount+=t.textBlockCount}}return e},m=window.ReactJSXRuntime;(0,o.registerPlugin)("activitypub-pre-publish",{render:()=>{const{blocks:o,postFormat:r}=(0,n.useSelect)(o=>({blocks:o(e.store).getBlocks(),postFormat:o(t.store).getEditedPostAttribute("format")}),[]),{editPost:u}=(0,n.useDispatch)(t.store),d=(0,a.useMemo)(()=>((t,e)=>{if(e&&"standard"!==e)return null;const o=c(t),i=o.imageCount>0||o.galleryCount>0||o.videoCount>0||o.audioCount>0;return(o.galleryCount>0||o.imageCount>1)&&o.textLength0&&o.textLength0&&o.textLength0&&o.textLength<280&&o.textBlockCount<=3?{format:"status",message:(0,s.__)("This is a short post with no media. Setting the format to Status will share it as a Note, which is the standard format on platforms like Mastodon.","activitypub")}:null})(o,r),[o,r]);return"wordpress-post-format"!==window._activityPubOptions?.objectType?null:d?(0,m.jsxs)(t.PluginPrePublishPanel,{title:(0,s.__)("Fediverse ⁂","activitypub"),initialOpen:!0,children:[(0,m.jsx)("p",{children:d.message}),(0,m.jsx)(i.Button,{variant:"secondary",onClick:()=>u({format:d.format}),children:(0,s.sprintf)(/* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ /* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ +(0,s.__)("Set format to %s","activitypub"),d.format.charAt(0).toUpperCase()+d.format.slice(1))})]}):null}})})(); \ No newline at end of file diff --git a/src/pre-publish-panel/utils.js b/src/pre-publish-panel/utils.js index 7f56ecd884..0440ca31c7 100644 --- a/src/pre-publish-panel/utils.js +++ b/src/pre-publish-panel/utils.js @@ -1,3 +1,4 @@ +import { __unstableStripHTML as stripHTML } from '@wordpress/dom'; import { __ } from '@wordpress/i18n'; /** @@ -77,8 +78,8 @@ export const analyzeBlocks = ( blocks ) => { } if ( textBlockNames.includes( name ) ) { - const content = ( attributes?.content || '' ).replace( /<[^>]*>/g, '' ); - result.textLength += content.length; + const text = stripHTML( attributes?.content || '' ); + result.textLength += text.length; result.textBlockCount++; } From 447fde54546e99f4ae4bb4b2553db95cd54f2a0e Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Tue, 24 Feb 2026 14:03:33 +0100 Subject: [PATCH 4/5] Address Copilot review feedback - Move textBlockNames/galleryBlockNames to module-level constants. - Fix core/pullquote to read from `value` attribute instead of `content`. - Reorder priority: video/audio now suggested before single image to avoid incorrect recommendations for mixed-media posts. - Fix grammar in video/audio suggestion messages. - Add tests for mixed-media priority (image+video, image+audio). --- build/pre-publish-panel/plugin.asset.php | 2 +- build/pre-publish-panel/plugin.js | 2 +- .../__tests__/plugin.test.js | 20 ++++- src/pre-publish-panel/utils.js | 86 +++++++++++++------ 4 files changed, 81 insertions(+), 29 deletions(-) diff --git a/build/pre-publish-panel/plugin.asset.php b/build/pre-publish-panel/plugin.asset.php index 1fc2fe1162..354b89bb2e 100644 --- a/build/pre-publish-panel/plugin.asset.php +++ b/build/pre-publish-panel/plugin.asset.php @@ -1 +1 @@ - array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-dom', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => 'f26fe9a63cb7c4d2f12e'); + array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-dom', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '465a213971cc6d890a65'); diff --git a/build/pre-publish-panel/plugin.js b/build/pre-publish-panel/plugin.js index 9bad09e2b8..be56f9c50a 100644 --- a/build/pre-publish-panel/plugin.js +++ b/build/pre-publish-panel/plugin.js @@ -1,2 +1,2 @@ -(()=>{"use strict";const t=window.wp.editor,e=window.wp.blockEditor,o=window.wp.plugins,i=window.wp.components,n=window.wp.data,a=window.wp.element,s=window.wp.i18n,r=window.wp.dom,l=window._activityPubOptions?.noteLength||500,u=["youtube","vimeo","dailymotion","tiktok","videopress"],d=["spotify","soundcloud","mixcloud"],c=t=>{const e={imageCount:0,galleryCount:0,videoCount:0,audioCount:0,textLength:0,textBlockCount:0};if(!t||!t.length)return e;const o=["core/paragraph","core/heading","core/list-item","core/preformatted","core/verse","core/pullquote"],i=["core/gallery","jetpack/tiled-gallery","jetpack/slideshow"];for(const n of t){const{name:t,attributes:a,innerBlocks:s}=n;if("core/image"===t)e.imageCount++;else if(i.includes(t))e.galleryCount++;else if("core/video"===t)e.videoCount++;else if("core/audio"===t)e.audioCount++;else if("core/embed"===t){const t=(a?.providerNameSlug||"").toLowerCase();u.includes(t)?e.videoCount++:d.includes(t)&&e.audioCount++}if(o.includes(t)){const t=(0,r.__unstableStripHTML)(a?.content||"");e.textLength+=t.length,e.textBlockCount++}if(s&&s.length){const t=c(s);e.imageCount+=t.imageCount,e.galleryCount+=t.galleryCount,e.videoCount+=t.videoCount,e.audioCount+=t.audioCount,e.textLength+=t.textLength,e.textBlockCount+=t.textBlockCount}}return e},m=window.ReactJSXRuntime;(0,o.registerPlugin)("activitypub-pre-publish",{render:()=>{const{blocks:o,postFormat:r}=(0,n.useSelect)(o=>({blocks:o(e.store).getBlocks(),postFormat:o(t.store).getEditedPostAttribute("format")}),[]),{editPost:u}=(0,n.useDispatch)(t.store),d=(0,a.useMemo)(()=>((t,e)=>{if(e&&"standard"!==e)return null;const o=c(t),i=o.imageCount>0||o.galleryCount>0||o.videoCount>0||o.audioCount>0;return(o.galleryCount>0||o.imageCount>1)&&o.textLength0&&o.textLength0&&o.textLength0&&o.textLength<280&&o.textBlockCount<=3?{format:"status",message:(0,s.__)("This is a short post with no media. Setting the format to Status will share it as a Note, which is the standard format on platforms like Mastodon.","activitypub")}:null})(o,r),[o,r]);return"wordpress-post-format"!==window._activityPubOptions?.objectType?null:d?(0,m.jsxs)(t.PluginPrePublishPanel,{title:(0,s.__)("Fediverse ⁂","activitypub"),initialOpen:!0,children:[(0,m.jsx)("p",{children:d.message}),(0,m.jsx)(i.Button,{variant:"secondary",onClick:()=>u({format:d.format}),children:(0,s.sprintf)(/* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ /* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ +(()=>{"use strict";const t=window.wp.editor,e=window.wp.blockEditor,o=window.wp.plugins,i=window.wp.components,n=window.wp.data,a=window.wp.element,s=window.wp.i18n,r=window.wp.dom,l=window._activityPubOptions?.noteLength||500,u=["youtube","vimeo","dailymotion","tiktok","videopress"],d=["spotify","soundcloud","mixcloud"],c=["core/paragraph","core/heading","core/list-item","core/preformatted","core/verse","core/pullquote"],m=["core/gallery","jetpack/tiled-gallery","jetpack/slideshow"],p=(t,e)=>"core/pullquote"===t?e?.value||"":e?.content||"",g=t=>{const e={imageCount:0,galleryCount:0,videoCount:0,audioCount:0,textLength:0,textBlockCount:0};if(!t||!t.length)return e;for(const o of t){const{name:t,attributes:i,innerBlocks:n}=o;if("core/image"===t)e.imageCount++;else if(m.includes(t))e.galleryCount++;else if("core/video"===t)e.videoCount++;else if("core/audio"===t)e.audioCount++;else if("core/embed"===t){const t=(i?.providerNameSlug||"").toLowerCase();u.includes(t)?e.videoCount++:d.includes(t)&&e.audioCount++}if(c.includes(t)){const o=(0,r.__unstableStripHTML)(p(t,i));e.textLength+=o.length,e.textBlockCount++}if(n&&n.length){const t=g(n);e.imageCount+=t.imageCount,e.galleryCount+=t.galleryCount,e.videoCount+=t.videoCount,e.audioCount+=t.audioCount,e.textLength+=t.textLength,e.textBlockCount+=t.textBlockCount}}return e},h=window.ReactJSXRuntime;(0,o.registerPlugin)("activitypub-pre-publish",{render:()=>{const{blocks:o,postFormat:r}=(0,n.useSelect)(o=>({blocks:o(e.store).getBlocks(),postFormat:o(t.store).getEditedPostAttribute("format")}),[]),{editPost:u}=(0,n.useDispatch)(t.store),d=(0,a.useMemo)(()=>((t,e)=>{if(e&&"standard"!==e)return null;const o=g(t),i=o.imageCount>0||o.galleryCount>0||o.videoCount>0||o.audioCount>0;return(o.galleryCount>0||o.imageCount>1)&&o.textLength0&&o.textLength0&&o.textLength0&&o.textLength<280&&o.textBlockCount<=3?{format:"status",message:(0,s.__)("This is a short post with no media. Setting the format to Status will share it as a Note, which is the standard format on platforms like Mastodon.","activitypub")}:null})(o,r),[o,r]);return"wordpress-post-format"!==window._activityPubOptions?.objectType?null:d?(0,h.jsxs)(t.PluginPrePublishPanel,{title:(0,s.__)("Fediverse ⁂","activitypub"),initialOpen:!0,children:[(0,h.jsx)("p",{children:d.message}),(0,h.jsx)(i.Button,{variant:"secondary",onClick:()=>u({format:d.format}),children:(0,s.sprintf)(/* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ /* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ (0,s.__)("Set format to %s","activitypub"),d.format.charAt(0).toUpperCase()+d.format.slice(1))})]}):null}})})(); \ No newline at end of file diff --git a/src/pre-publish-panel/__tests__/plugin.test.js b/src/pre-publish-panel/__tests__/plugin.test.js index 92908581d6..13802a874a 100644 --- a/src/pre-publish-panel/__tests__/plugin.test.js +++ b/src/pre-publish-panel/__tests__/plugin.test.js @@ -175,7 +175,7 @@ describe( 'analyzeBlocks', () => { }, { name: 'core/pullquote', - attributes: { content: 'Quote' }, + attributes: { value: 'Quote' }, innerBlocks: [], }, { @@ -369,6 +369,24 @@ describe( 'getSuggestedPostFormat', () => { expect( getSuggestedPostFormat( blocks, 'standard' ) ).not.toBeNull(); } ); + test( 'suggests video over image for mixed media', () => { + const blocks = [ + { name: 'core/image', attributes: {}, innerBlocks: [] }, + { name: 'core/video', attributes: {}, innerBlocks: [] }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'video' ); + } ); + + test( 'suggests audio over image for mixed media', () => { + const blocks = [ + { name: 'core/image', attributes: {}, innerBlocks: [] }, + { name: 'core/audio', attributes: {}, innerBlocks: [] }, + ]; + const result = getSuggestedPostFormat( blocks, '' ); + expect( result.format ).toBe( 'audio' ); + } ); + test( 'gallery has higher priority than image', () => { const blocks = [ { name: 'core/image', attributes: {}, innerBlocks: [] }, diff --git a/src/pre-publish-panel/utils.js b/src/pre-publish-panel/utils.js index 0440ca31c7..035dd2be6c 100644 --- a/src/pre-publish-panel/utils.js +++ b/src/pre-publish-panel/utils.js @@ -3,6 +3,7 @@ import { __ } from '@wordpress/i18n'; /** * The maximum note length from the server-side ACTIVITYPUB_NOTE_LENGTH constant. + * Fallback matches ACTIVITYPUB_NOTE_LENGTH default; keep in sync with includes/constants.php. * * @type {number} */ @@ -22,6 +23,45 @@ const VIDEO_PROVIDERS = [ 'youtube', 'vimeo', 'dailymotion', 'tiktok', 'videopre */ const AUDIO_PROVIDERS = [ 'spotify', 'soundcloud', 'mixcloud' ]; +/** + * Block names that contain text content. + * + * @type {string[]} + */ +const TEXT_BLOCK_NAMES = [ + 'core/paragraph', + 'core/heading', + 'core/list-item', + 'core/preformatted', + 'core/verse', + 'core/pullquote', +]; + +/** + * Block names that represent gallery content. + * + * @type {string[]} + */ +const GALLERY_BLOCK_NAMES = [ 'core/gallery', 'jetpack/tiled-gallery', 'jetpack/slideshow' ]; + +/** + * Returns the text content attribute for a given block name. + * + * Most text blocks store content in `attributes.content`, but some + * blocks use different attribute keys (e.g. `core/pullquote` uses `value`). + * + * @param {string} name The block name. + * @param {Object} attributes The block attributes. + * + * @return {string} The text content. + */ +const getBlockTextContent = ( name, attributes ) => { + if ( name === 'core/pullquote' ) { + return attributes?.value || ''; + } + return attributes?.content || ''; +}; + /** * Recursively analyzes blocks and returns content statistics. * @@ -46,23 +86,12 @@ export const analyzeBlocks = ( blocks ) => { return result; } - const textBlockNames = [ - 'core/paragraph', - 'core/heading', - 'core/list-item', - 'core/preformatted', - 'core/verse', - 'core/pullquote', - ]; - - const galleryBlockNames = [ 'core/gallery', 'jetpack/tiled-gallery', 'jetpack/slideshow' ]; - for ( const block of blocks ) { const { name, attributes, innerBlocks } = block; if ( name === 'core/image' ) { result.imageCount++; - } else if ( galleryBlockNames.includes( name ) ) { + } else if ( GALLERY_BLOCK_NAMES.includes( name ) ) { result.galleryCount++; } else if ( name === 'core/video' ) { result.videoCount++; @@ -77,8 +106,8 @@ export const analyzeBlocks = ( blocks ) => { } } - if ( textBlockNames.includes( name ) ) { - const text = stripHTML( attributes?.content || '' ); + if ( TEXT_BLOCK_NAMES.includes( name ) ) { + const text = stripHTML( getBlockTextContent( name, attributes ) ); result.textLength += text.length; result.textBlockCount++; } @@ -128,34 +157,39 @@ export const getSuggestedPostFormat = ( blocks, currentFormat ) => { }; } - // Image: single image with short text. - if ( stats.imageCount === 1 && stats.textLength < NOTE_LENGTH ) { + // Video: video with short text. + if ( stats.videoCount > 0 && stats.textLength < NOTE_LENGTH ) { return { - format: 'image', + format: 'video', message: __( - 'This post contains an image. Setting the format to Image will share it as a media post, making it visible on platforms like Pixelfed.', + 'This post contains a video. Setting the format to Video will share it as a media post, improving compatibility with video-focused platforms.', 'activitypub' ), }; } - // Video: video with short text. - if ( stats.videoCount > 0 && stats.textLength < NOTE_LENGTH ) { + // Audio: audio with short text. + if ( stats.audioCount > 0 && stats.textLength < NOTE_LENGTH ) { return { - format: 'video', + format: 'audio', message: __( - 'This post contains video. Setting the format to Video will share it as a media post, improving compatibility with video-focused platforms.', + 'This post contains audio content. Setting the format to Audio will share it as a media post, improving compatibility with audio-focused platforms.', 'activitypub' ), }; } - // Audio: audio with short text. - if ( stats.audioCount > 0 && stats.textLength < NOTE_LENGTH ) { + // Image: single image with short text (after video/audio to avoid wrong suggestion for mixed media). + if ( + stats.imageCount === 1 && + stats.videoCount === 0 && + stats.audioCount === 0 && + stats.textLength < NOTE_LENGTH + ) { return { - format: 'audio', + format: 'image', message: __( - 'This post contains audio. Setting the format to Audio will share it as a media post, improving compatibility with audio-focused platforms.', + 'This post contains an image. Setting the format to Image will share it as a media post, making it visible on platforms like Pixelfed.', 'activitypub' ), }; From c7d0f0df2cdefb280d47566b8174746e924035ac Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Thu, 26 Feb 2026 12:19:36 +0100 Subject: [PATCH 5/5] Improve pre-publish panel block.json metadata Address PR review feedback: namespace the block name to `activitypub/pre-publish-panel`, use a descriptive title, and add schema, description, and textdomain fields. --- build/pre-publish-panel/block.json | 11 +++++++---- src/pre-publish-panel/block.json | 11 +++++++---- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/build/pre-publish-panel/block.json b/build/pre-publish-panel/block.json index b95c083d4d..dd1d305d9c 100644 --- a/build/pre-publish-panel/block.json +++ b/build/pre-publish-panel/block.json @@ -1,8 +1,11 @@ { - "name": "pre-publish-panel", - "title": "Pre-Publish Panel: not a block, but block.json is very useful.", + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "activitypub/pre-publish-panel", + "title": "ActivityPub Post Format Suggestions", "category": "widgets", - "icon": "admin-comments", - "keywords": [], + "description": "Suggests optimal post formats for ActivityPub federation before publishing.", + "icon": "layout", + "textdomain": "activitypub", "editorScript": "file:./plugin.js" } \ No newline at end of file diff --git a/src/pre-publish-panel/block.json b/src/pre-publish-panel/block.json index 295ede929f..a98e6c270c 100644 --- a/src/pre-publish-panel/block.json +++ b/src/pre-publish-panel/block.json @@ -1,8 +1,11 @@ { - "name": "pre-publish-panel", - "title": "Pre-Publish Panel: not a block, but block.json is very useful.", + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "activitypub/pre-publish-panel", + "title": "ActivityPub Post Format Suggestions", "category": "widgets", - "icon": "admin-comments", - "keywords": [], + "description": "Suggests optimal post formats for ActivityPub federation before publishing.", + "icon": "layout", + "textdomain": "activitypub", "editorScript": "file:./plugin.js" }