Skip to content
Merged
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
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Posts to Podcast: Add an editor modal inviting eligible sites to create a podcast episode after publishing a post.
2 changes: 2 additions & 0 deletions projects/packages/podcast/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,15 @@
"@wordpress/dataviews": "14.1.0",
"@wordpress/date": "5.44.0",
"@wordpress/dependency-extraction-webpack-plugin": "6.44.0",
"@wordpress/editor": "14.44.0",
"@wordpress/element": "6.44.0",
"@wordpress/hooks": "4.44.0",
"@wordpress/html-entities": "4.44.0",
"@wordpress/i18n": "6.17.0",
"@wordpress/icons": "12.2.0",
"@wordpress/media-utils": "5.44.0",
"@wordpress/notices": "5.44.0",
"@wordpress/plugins": "7.44.0",
"@wordpress/primitives": "4.44.0",
"@wordpress/route": "0.10.0",
"@wordpress/ui": "0.11.0",
Expand Down
218 changes: 218 additions & 0 deletions projects/packages/podcast/src/class-create-ai-podcast-page.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@

require_once __DIR__ . '/admin-pages/create-ai-podcast/presets.php';

use Automattic\Jetpack\Assets;
use Automattic\Jetpack\Status\Host;
use function Automattic\Jetpack\Podcast\Admin_Pages\Create_AI_Podcast\length_presets;
use function Automattic\Jetpack\Podcast\Admin_Pages\Create_AI_Podcast\voice_presets;
use function Automattic\Jetpack\Podcast\Admin_Pages\Create_AI_Podcast\window_presets;
Expand All @@ -31,6 +33,11 @@ class Create_AI_Podcast_Page {
const STYLE_HANDLE = 'jetpack-create-ai-podcast';
const EPISODES_PER_PAGE = 5;

const POST_PUBLISH_PROMO_SCRIPT_HANDLE = 'jetpack-post-publish-podcast-promo';
const POST_PUBLISH_PROMO_DISMISSED_OPTION = 'jetpack_posts_to_podcast_post_publish_promo_dismissed';
const POST_PUBLISH_PROMO_MIN_POSTS = 5;
const POST_PUBLISH_PROMO_MIN_VISITORS = 50;

/**
* Whether `init()` has wired its hooks.
*
Expand All @@ -48,6 +55,7 @@ public static function init() {
self::$initialized = true;

add_action( 'admin_menu', array( __CLASS__, 'register_menu' ) );
add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'enqueue_post_publish_promo_assets' ) );
}

/**
Expand Down Expand Up @@ -118,6 +126,216 @@ public static function enqueue_assets() {
);
}

/**
* Enqueue the post-publish modal in the post block editor for eligible sites.
*/
public static function enqueue_post_publish_promo_assets() {
if (
! self::is_post_block_editor()
|| self::is_current_post_published_for_post_publish_promo()
|| ! self::is_post_publish_promo_site_eligible()
) {
return;
}

Assets::register_script(
self::POST_PUBLISH_PROMO_SCRIPT_HANDLE,
'../dist/blocks/post-publish-podcast-promo/editor.js',
__FILE__,
array(
'enqueue' => true,
'in_footer' => true,
'textdomain' => 'jetpack-podcast',
)
);

wp_add_inline_script(
self::POST_PUBLISH_PROMO_SCRIPT_HANDLE,
'window.jetpackPostPublishPodcastPromo = ' . wp_json_encode(
array(
'createUrl' => admin_url( 'upload.php?page=' . self::PAGE_SLUG ),
'dismissPath' => Posts_To_Podcast_Endpoint::get_post_publish_promo_dismiss_rest_path(),
),
JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT
) . ';',
'before'
);
}

/**
* Whether the current editor post has already been published.
*/
private static function is_current_post_published_for_post_publish_promo(): bool {
$post = get_post();

return $post instanceof \WP_Post
&& 'post' === $post->post_type
&& 'publish' === $post->post_status;
}

/**
* Whether the current screen is the post block editor.
*/
private static function is_post_block_editor(): bool {
if ( ! function_exists( 'get_current_screen' ) ) {
return false;
}

$screen = get_current_screen();
return ! empty( $screen )
&& 'post' === $screen->base
&& 'post' === $screen->post_type
&& $screen->is_block_editor();
}

/**
* Whether the site has enough published posts to generate a better episode.
*/
private static function has_enough_recent_posts_for_post_publish_promo(): bool {
/**
* Filters the minimum posts published in the last month needed for the Posts to Podcast post-publish promo.
*
* @since $$next-version$$
*
* @param int $minimum Minimum number of published posts.
*/
$minimum = (int) apply_filters(
'jetpack_posts_to_podcast_post_publish_promo_min_published_posts',
self::POST_PUBLISH_PROMO_MIN_POSTS
);
$minimum = max( 1, $minimum );

$published_posts = get_posts(
array(
'fields' => 'ids',
'no_found_rows' => true,
'post_status' => 'publish',
'post_type' => 'post',
'posts_per_page' => $minimum,
'suppress_filters' => false,
'date_query' => array(
array(
'after' => '1 month ago',
'inclusive' => true,
),
),
)
);
$total = count( $published_posts );

$post = get_post();
if ( $post && 'post' === $post->post_type && 'publish' !== $post->post_status ) {
++$total;
}

return $total >= $minimum;
}

/**
* Whether the site has visitors who could benefit from a podcast episode.
*/
private static function has_visitors_for_post_publish_promo(): bool {
$visitors = self::get_post_publish_promo_visitor_count();

/**
* Filters the minimum visitors in the last week needed for the Posts to Podcast post-publish promo.
*
* @since $$next-version$$
*
* @param int $minimum Minimum number of visitors.
*/
$minimum = (int) apply_filters(
'jetpack_posts_to_podcast_post_publish_promo_min_visitors',
self::POST_PUBLISH_PROMO_MIN_VISITORS
);

return $visitors >= max( 1, $minimum );
}

/**
* Fetch the last week's visitor count from Jetpack Stats when available.
*/
private static function get_post_publish_promo_visitor_count(): int {
$host = new Host();
if ( $host->is_wpcom_simple() ) {
return self::get_wpcom_simple_post_publish_promo_visitor_count();
}

if ( class_exists( '\Automattic\Jetpack\Stats\WPCOM_Stats' ) ) {
$wpcom_stats = new \Automattic\Jetpack\Stats\WPCOM_Stats();
$stats = $wpcom_stats->get_visits(
array(
'unit' => 'day',
'quantity' => 7,
'stat_fields' => 'visitors',
)
);

if ( ! is_wp_error( $stats ) && is_array( $stats ) ) {
return self::sum_visits_field( $stats, 'visitors' );
}
}

return 0;
}

/**
* Fetch the last week's visitor count directly on WordPress.com Simple.
*/
private static function get_wpcom_simple_post_publish_promo_visitor_count(): int {
if ( ! function_exists( 'stats_get_visitors' ) ) {
return 0;
}

$visitors = stats_get_visitors( get_current_blog_id(), gmdate( 'Y-m-d' ), 7, 1 );

return is_array( $visitors ) ? (int) array_sum( $visitors ) : 0;
}

/**
* Sum a metric from the Stats visits response.
*
* @param array $stats Stats visits response.
* @param string $field Field to sum.
* @return int
*/
private static function sum_visits_field( array $stats, string $field ): int {
if ( ! isset( $stats['data'] ) || ! is_array( $stats['data'] ) ) {
return 0;
}

$fields = isset( $stats['fields'] ) && is_array( $stats['fields'] ) ? $stats['fields'] : array();
$index = array_search( $field, $fields, true );
if ( false === $index ) {
return 0;
}

$total = 0;
foreach ( $stats['data'] as $row ) {
if ( is_array( $row ) && isset( $row[ $index ] ) ) {
$total += (int) $row[ $index ];
}
}

return $total;
}

/**
* Whether the site is relevant for the post-publish promo.
*/
public static function is_post_publish_promo_site_eligible(): bool {
$host = new Host();
if ( $host->is_p2_site() ) {
return false;
}

if ( get_user_option( self::POST_PUBLISH_PROMO_DISMISSED_OPTION, get_current_user_id() ) ) {
return false;
}

return self::has_enough_recent_posts_for_post_publish_promo() && self::has_visitors_for_post_publish_promo();
}

/**
* Build the data bundle passed to the JS island via wp_localize_script.
*
Expand Down
42 changes: 7 additions & 35 deletions projects/packages/podcast/src/class-podcast.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ public static function init() {
// late-registered filter callback still takes effect.
Podcast_Episode_Block::register_hooks();

// Register the local REST routes before request-local rollout gates.
// Requests from public-api.wordpress.com may not satisfy those gates,
// but the wpcom/v2 routes still need to exist so permission and
// callback checks can handle the request.
Posts_To_Podcast_Endpoint::init();

if ( ! self::is_enabled() ) {
return;
}
Expand All @@ -76,20 +82,11 @@ public static function init() {
}

// Posts to Podcast lives behind its own filter so the Create AI
// Podcast page and its REST proxy can ship together but ramp
// independently of the broader untangle. Only register code in the
// context that actually needs it: the page in wp-admin, the REST
// routes during REST request processing.
// Podcast page can ship independently of the broader untangle.
if ( self::is_posts_to_podcast_enabled() ) {
if ( is_admin() ) {
Create_AI_Podcast_Page::init();
}
// Register the local proxy in REST and admin contexts: the admin
// page bootstraps its initial quota + episodes via rest_do_request
// so the first paint doesn't pay the wpcom-proxy iframe round-trip.
if ( is_admin() || self::is_rest_request() ) {
Posts_To_Podcast_Endpoint::init();
}
}
}

Expand All @@ -112,31 +109,6 @@ public static function is_posts_to_podcast_enabled() {
return (bool) apply_filters( 'jetpack_posts_to_podcast', self::is_proxied_request() );
}

/**
* Whether the current request is a WP REST API request.
*
* `Podcast::init()` typically fires before `REST_REQUEST` is defined
* and before `wp_is_serving_rest_request()` is reliable, so fall back
* to a URL prefix check.
*/
private static function is_rest_request() {
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return true;
}
if ( function_exists( 'wp_is_serving_rest_request' ) && wp_is_serving_rest_request() ) {
return true;
}
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
return false;
}
$path = wp_parse_url( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ), PHP_URL_PATH );
if ( ! is_string( $path ) ) {
return false;
}
$prefix = function_exists( 'rest_get_url_prefix' ) ? rest_get_url_prefix() : 'wp-json';
return false !== strpos( $path, '/' . trim( $prefix, '/' ) . '/' );
}

/**
* Whether the Podcast untangle is enabled for the current request.
*
Expand Down
Loading
Loading