diff --git a/projects/packages/my-jetpack/_inc/utils/async-notification-bubble.ts b/projects/packages/my-jetpack/_inc/utils/async-notification-bubble.ts new file mode 100644 index 0000000000000..7ecc6b664773e --- /dev/null +++ b/projects/packages/my-jetpack/_inc/utils/async-notification-bubble.ts @@ -0,0 +1,46 @@ +/** + * My Jetpack Notification Bubble async loader. + * Fetches fresh alert data via REST API without blocking page load. + */ +import apiFetch from '@wordpress/api-fetch'; + +// Minimal type for counting non-silent alerts. +type Alert = { + is_silent?: boolean; +}; + +type AlertsResponse = Record< string, Alert >; + +apiFetch< AlertsResponse >( { + path: 'my-jetpack/v1/red-bubble-notifications', + method: 'POST', +} ) + .then( alerts => { + const count = Object.values( alerts ).filter( a => ! a.is_silent ).length; + const menuItem = document.querySelector( '#toplevel_page_jetpack .wp-menu-name' ); + + if ( ! menuItem ) { + return; + } + + const bubble = menuItem.querySelector( '.awaiting-mod' ); + + if ( count > 0 ) { + if ( bubble ) { + bubble.className = 'awaiting-mod'; + bubble.textContent = String( count ); + } else { + const span = document.createElement( 'span' ); + span.className = 'awaiting-mod'; + span.textContent = String( count ); + menuItem.appendChild( document.createTextNode( ' ' ) ); + menuItem.appendChild( span ); + } + } else if ( bubble ) { + bubble.remove(); + } + } ) + .catch( ( error: Error ) => { + // eslint-disable-next-line no-console + console.error( '[My Jetpack] Failed to fetch notification alerts:', error ); + } ); diff --git a/projects/packages/my-jetpack/changelog/MYJP-268-bubble-async b/projects/packages/my-jetpack/changelog/MYJP-268-bubble-async new file mode 100644 index 0000000000000..795ebd3ab3ee5 --- /dev/null +++ b/projects/packages/my-jetpack/changelog/MYJP-268-bubble-async @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Check red bubble notification async when cache is not available. diff --git a/projects/packages/my-jetpack/src/class-initializer.php b/projects/packages/my-jetpack/src/class-initializer.php index fdeb897d30f97..e64d3106d7d74 100644 --- a/projects/packages/my-jetpack/src/class-initializer.php +++ b/projects/packages/my-jetpack/src/class-initializer.php @@ -688,12 +688,15 @@ public static function get_idc_container_id() { } /** - * Conditionally append the red bubble notification to the "Jetpack" menu item if there are alerts to show + * Conditionally append the red bubble notification to the "Jetpack" menu item if there are alerts to show. + * + * On My Jetpack page: Uses blocking behavior to fetch fresh data. + * On other admin pages: Uses cached data only to avoid blocking, with async JS fetch if cache is empty. * * @return void */ public static function maybe_show_red_bubble() { - global $menu; + global $menu, $pagenow; // Don't show red bubble alerts for non-admin users // These alerts are generally only actionable for admins @@ -708,14 +711,32 @@ public static function maybe_show_red_bubble() { return; } - $rbn = new Red_Bubble_Notifications(); + // Check if we're on the My Jetpack page + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; + $is_my_jetpack_page = $pagenow === 'admin.php' && $page === 'my-jetpack'; + + if ( $is_my_jetpack_page ) { + // On My Jetpack page: use blocking behavior for fresh data. + add_filter( 'my_jetpack_red_bubble_notification_slugs', array( Red_Bubble_Notifications::class, 'add_red_bubble_alerts' ) ); + $red_bubble_alerts = Red_Bubble_Notifications::get_red_bubble_alerts(); + } else { + // On other pages: use cached data only to avoid blocking. + $cached_alerts = Red_Bubble_Notifications::get_cached_alerts(); + + if ( false === $cached_alerts ) { + // No cache - fetch asynchronously via JS. + add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_red_bubble_script' ) ); + return; + } + + $red_bubble_alerts = $cached_alerts; + } - // filters for the items in this file - add_filter( 'my_jetpack_red_bubble_notification_slugs', array( $rbn, 'add_red_bubble_alerts' ) ); + // Filter out silent alerts $red_bubble_alerts = array_filter( - $rbn::get_red_bubble_alerts(), + $red_bubble_alerts, function ( $alert ) { - // We don't want to show the red bubble for silent alerts return empty( $alert['is_silent'] ); } ); @@ -731,6 +752,24 @@ function ( $alert ) { } } + /** + * Enqueue the notification bubble script. + * Fetches fresh alert data via REST API without blocking page load. + * + * @return void + */ + public static function enqueue_red_bubble_script() { + Assets::register_script( + 'my_jetpack_notification_bubble', + '../build/async-notification-bubble.js', + __FILE__, + array( + 'enqueue' => true, + 'in_footer' => true, + ) + ); + } + /** * Get list of module names sorted by their recommendation score * diff --git a/projects/packages/my-jetpack/src/class-red-bubble-notifications.php b/projects/packages/my-jetpack/src/class-red-bubble-notifications.php index 06eea0b29fd68..74da7f992c255 100644 --- a/projects/packages/my-jetpack/src/class-red-bubble-notifications.php +++ b/projects/packages/my-jetpack/src/class-red-bubble-notifications.php @@ -368,6 +368,17 @@ public static function add_red_bubble_alerts( array $red_bubble_slugs ) { } } + /** + * Get cached red bubble alerts without triggering expensive computation. + * Returns the cached transient value or an empty array if not cached. + * + * @return array Cached alerts or empty array. + */ + public static function get_cached_alerts() { + $stored_alerts = get_transient( self::MY_JETPACK_RED_BUBBLE_TRANSIENT_KEY ); + return $stored_alerts !== false ? $stored_alerts : array(); + } + /** * Collect all possible alerts that we might use a red bubble notification for * diff --git a/projects/packages/my-jetpack/webpack.config.js b/projects/packages/my-jetpack/webpack.config.js index 1555ba45ffafe..f7c9016438df2 100644 --- a/projects/packages/my-jetpack/webpack.config.js +++ b/projects/packages/my-jetpack/webpack.config.js @@ -5,6 +5,7 @@ module.exports = [ { entry: { index: './_inc/admin.jsx', + 'async-notification-bubble': './_inc/utils/async-notification-bubble.ts', }, mode: jetpackWebpackConfig.mode, devtool: jetpackWebpackConfig.devtool, diff --git a/projects/plugins/backup/changelog/MYJP-268-bubble-async b/projects/plugins/backup/changelog/MYJP-268-bubble-async new file mode 100644 index 0000000000000..795ebd3ab3ee5 --- /dev/null +++ b/projects/plugins/backup/changelog/MYJP-268-bubble-async @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Check red bubble notification async when cache is not available. diff --git a/projects/plugins/boost/changelog/MYJP-268-bubble-async b/projects/plugins/boost/changelog/MYJP-268-bubble-async new file mode 100644 index 0000000000000..795ebd3ab3ee5 --- /dev/null +++ b/projects/plugins/boost/changelog/MYJP-268-bubble-async @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Check red bubble notification async when cache is not available. diff --git a/projects/plugins/jetpack/changelog/MYJP-268-bubble-async b/projects/plugins/jetpack/changelog/MYJP-268-bubble-async new file mode 100644 index 0000000000000..cc73c077fc0c1 --- /dev/null +++ b/projects/plugins/jetpack/changelog/MYJP-268-bubble-async @@ -0,0 +1,4 @@ +Significance: patch +Type: enhancement + +My Jetpack: Check red bubble notification async when cache is not available. diff --git a/projects/plugins/protect/changelog/MYJP-268-bubble-async b/projects/plugins/protect/changelog/MYJP-268-bubble-async new file mode 100644 index 0000000000000..795ebd3ab3ee5 --- /dev/null +++ b/projects/plugins/protect/changelog/MYJP-268-bubble-async @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Check red bubble notification async when cache is not available. diff --git a/projects/plugins/search/changelog/MYJP-268-bubble-async b/projects/plugins/search/changelog/MYJP-268-bubble-async new file mode 100644 index 0000000000000..795ebd3ab3ee5 --- /dev/null +++ b/projects/plugins/search/changelog/MYJP-268-bubble-async @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Check red bubble notification async when cache is not available. diff --git a/projects/plugins/social/changelog/MYJP-268-bubble-async b/projects/plugins/social/changelog/MYJP-268-bubble-async new file mode 100644 index 0000000000000..795ebd3ab3ee5 --- /dev/null +++ b/projects/plugins/social/changelog/MYJP-268-bubble-async @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Check red bubble notification async when cache is not available. diff --git a/projects/plugins/starter-plugin/changelog/MYJP-268-bubble-async b/projects/plugins/starter-plugin/changelog/MYJP-268-bubble-async new file mode 100644 index 0000000000000..795ebd3ab3ee5 --- /dev/null +++ b/projects/plugins/starter-plugin/changelog/MYJP-268-bubble-async @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Check red bubble notification async when cache is not available. diff --git a/projects/plugins/videopress/changelog/MYJP-268-bubble-async b/projects/plugins/videopress/changelog/MYJP-268-bubble-async new file mode 100644 index 0000000000000..795ebd3ab3ee5 --- /dev/null +++ b/projects/plugins/videopress/changelog/MYJP-268-bubble-async @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +My Jetpack: Check red bubble notification async when cache is not available.