From 6425df482684bcc6b38602132be429896f4732e5 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 12:09:21 +0100 Subject: [PATCH 01/28] RTC: Add branded editor notices and room-limit enforcement for real-time collaboration rollout --- .../src/class-jetpack-mu-wpcom.php | 1 + .../class-wp-rest-rtc-notices.php | 217 ++++++++++++++++++ .../gutenberg-rtc-notices.php | 75 ++++++ .../gutenberg-rtc-notices/images.d.ts | 20 ++ .../features/gutenberg-rtc-notices/index.tsx | 68 ++++++ .../rtc-admin-someone-waiting-notice.tsx | 107 +++++++++ .../rtc-connection-error-modal-filter.tsx | 131 +++++++++++ .../rtc-non-admin-post-upgrade-notice.tsx | 75 ++++++ .../notices/rtc-welcome-notice.tsx | 54 +++++ .../gutenberg-rtc-notices/room-limit.ts | 216 +++++++++++++++++ .../rtc-header-illustration.tsx | 21 ++ .../gutenberg-rtc-notices/rtc-hero.png | Bin 0 -> 118324 bytes .../rtc-notice-modal.scss | 114 +++++++++ .../rtc-notice-modal.tsx | 110 +++++++++ .../jetpack-mu-wpcom/webpack.config.js | 1 + 15 files changed, 1210 insertions(+) create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-non-admin-post-upgrade-notice.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/room-limit.ts create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-header-illustration.tsx create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-hero.png create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss create mode 100644 projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.tsx diff --git a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php index 52b3ebe24858..3cbacfb1aafa 100644 --- a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php +++ b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php @@ -435,6 +435,7 @@ public static function load_etk_features() { } require_once __DIR__ . '/features/gutenberg-rtc/gutenberg-rtc.php'; + require_once __DIR__ . '/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php'; require_once __DIR__ . '/features/jetpack-global-styles/class-global-styles.php'; require_once __DIR__ . '/features/mailerlite/subscriber-popup.php'; require_once __DIR__ . '/features/wpcom-fse/wpcom-fse.php'; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php new file mode 100644 index 000000000000..2c22bbc9a81a --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php @@ -0,0 +1,217 @@ + WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'dismiss_notice' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + + register_rest_route( + 'wpcom/v2', + '/rtc-notices/status', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_status' ), + 'permission_callback' => array( $this, 'check_permission' ), + ) + ); + + register_rest_route( + 'wpcom/v2', + '/rtc-notices/join-request', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'record_join_request' ), + 'permission_callback' => array( $this, 'check_permission' ), + 'args' => array( + 'post_id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + ), + ) + ); + + register_rest_route( + 'wpcom/v2', + '/rtc-notices/join-requests', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_join_requests' ), + 'permission_callback' => array( $this, 'check_admin_permission' ), + 'args' => array( + 'post_id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + ), + ) + ); + + register_rest_route( + 'wpcom/v2', + '/rtc-notices/join-requests/clear', + array( + 'methods' => WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'clear_join_requests' ), + 'permission_callback' => array( $this, 'check_admin_permission' ), + 'args' => array( + 'post_id' => array( + 'required' => true, + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ), + ), + ) + ); + } + + /** + * Check if the current user is logged in. + * + * @return bool + */ + public function check_permission() { + return is_user_logged_in(); + } + + /** + * Check if the current user is an admin. + * + * @return bool + */ + public function check_admin_permission() { + return current_user_can( 'manage_options' ); + } + + /** + * Dismiss the welcome notice. + * + * @return WP_REST_Response + */ + public function dismiss_notice() { + update_user_meta( get_current_user_id(), self::META_KEY, 'dismissed' ); + return rest_ensure_response( array( 'success' => true ) ); + } + + /** + * Get the dismissed status. + * + * @return WP_REST_Response + */ + public function get_status() { + return rest_ensure_response( array( 'dismissed' => self::is_dismissed() ) ); + } + + /** + * Check if the welcome notice is dismissed. + * + * @return bool + */ + public static function is_dismissed() { + $user_id = get_current_user_id(); + + if ( ! metadata_exists( 'user', $user_id, self::META_KEY ) ) { + return false; + } + + return 'dismissed' === get_user_meta( $user_id, self::META_KEY, true ); + } + + /** + * Record a join request from a blocked user. + * Stored as a transient per post so it auto-expires. + * + * @param WP_REST_Request $request The request. + * @return WP_REST_Response + */ + public function record_join_request( $request ) { + $post_id = $request->get_param( 'post_id' ); + $user = wp_get_current_user(); + + $key = self::JOIN_REQUEST_OPTION . '_' . $post_id; + $requests = get_transient( $key ); + if ( ! is_array( $requests ) ) { + $requests = array(); + } + + $requests[ $user->ID ] = array( + 'userName' => $user->display_name, + 'userId' => $user->ID, + 'time' => time(), + ); + + // Expire after 2 minutes. + set_transient( $key, $requests, 2 * MINUTE_IN_SECONDS ); + + return rest_ensure_response( array( 'success' => true ) ); + } + + /** + * Get pending join requests for a post. Admin only. + * + * @param WP_REST_Request $request The request. + * @return WP_REST_Response + */ + public function get_join_requests( $request ) { + $post_id = $request->get_param( 'post_id' ); + $key = self::JOIN_REQUEST_OPTION . '_' . $post_id; + $requests = get_transient( $key ); + + if ( ! is_array( $requests ) ) { + $requests = array(); + } + + // Filter out requests older than 60 seconds. + $now = time(); + $recent = array(); + foreach ( $requests as $uid => $req ) { + if ( $now - $req['time'] < 60 ) { + $recent[ $uid ] = $req; + } + } + + return rest_ensure_response( array( 'requests' => array_values( $recent ) ) ); + } + + /** + * Clear join requests for a post. + * + * @param WP_REST_Request $request The request. + * @return WP_REST_Response + */ + public function clear_join_requests( $request ) { + $post_id = $request->get_param( 'post_id' ); + delete_transient( self::JOIN_REQUEST_OPTION . '_' . $post_id ); + return rest_ensure_response( array( 'success' => true ) ); + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php new file mode 100644 index 000000000000..74210be84ebc --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -0,0 +1,75 @@ +register_routes(); + } +); + +/** + * Get the maximum number of peers allowed per room. + * + * @return int Max peers per room. + */ +function wpcom_get_rtc_max_peers_per_room() { + return (int) apply_filters( 'wpcom_rtc_max_peers_per_room', 2 ); +} + +/** + * Get the maximum number of clients (tabs) allowed per user in a room. + * + * @return int Max clients per user. + */ +function wpcom_get_rtc_max_clients_per_user() { + return (int) apply_filters( 'wpcom_rtc_max_clients_per_user', 2 ); +} + +/** + * Enqueue block editor assets for RTC notices and limits. + */ +function wpcom_enqueue_rtc_notices_assets() { + if ( ! wpcom_is_gutenberg_rtc_enabled() ) { + return; + } + + $handle = jetpack_mu_wpcom_enqueue_assets( 'gutenberg-rtc-notices', array( 'js', 'css' ) ); + wp_set_script_translations( $handle, 'jetpack-mu-wpcom' ); + + require_once __DIR__ . '/class-wp-rest-rtc-notices.php'; + + $is_admin_user = current_user_can( 'manage_options' ); + + $data = wp_json_encode( + array( + 'isAdmin' => $is_admin_user, + 'welcomeDismissed' => WP_REST_RTC_Notices::is_dismissed(), + 'postId' => get_the_ID(), + 'postTitle' => get_the_title(), + 'postEditUrl' => get_edit_post_link( get_the_ID(), 'raw' ), + 'postsListUrl' => admin_url( 'edit.php' ), + 'siteSlug' => wpcom_get_site_slug(), + 'maxPeersPerRoom' => wpcom_get_rtc_max_peers_per_room(), + 'maxClientsPerUser' => wpcom_get_rtc_max_clients_per_user(), + ), + JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP + ); + + wp_add_inline_script( + $handle, + "var wpcomRtcNotices = $data;", + 'before' + ); +} +add_action( 'enqueue_block_editor_assets', 'wpcom_enqueue_rtc_notices_assets' ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts new file mode 100644 index 000000000000..edc1d7c2c8af --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts @@ -0,0 +1,20 @@ +declare module '*.png' { + const src: string; + export default src; +} + +interface WpcomRtcNoticesConfig { + isAdmin: boolean; + welcomeDismissed: boolean; + postId: number; + postTitle: string; + postEditUrl: string; + postsListUrl: string; + siteSlug: string; + maxPeersPerRoom?: number; + maxClientsPerUser?: number; +} + +interface Window { + wpcomRtcNotices?: WpcomRtcNoticesConfig; +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx new file mode 100644 index 000000000000..acee52656bde --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx @@ -0,0 +1,68 @@ +/** + * Gutenberg RTC Notices & Limits + * + * WP.com-specific entry point for real-time collaboration in the block editor. + * Registers a sync.providers filter (priority 20) that wraps providers with + * room-limit enforcement, a filter on editor.SyncConnectionErrorModal to + * replace Gutenberg's default connection error modal with branded WP.com + * notices, and a block editor plugin for the welcome notice and admin polling. + */ + +import { addFilter } from '@wordpress/hooks'; +import { registerPlugin } from '@wordpress/plugins'; +import RtcAdminSomeoneWaitingNotice from './notices/rtc-admin-someone-waiting-notice'; +import { registerConnectionErrorModalFilter } from './notices/rtc-connection-error-modal-filter'; +import RtcNonAdminPostUpgradeNotice from './notices/rtc-non-admin-post-upgrade-notice'; +import RtcWelcomeNotice from './notices/rtc-welcome-notice'; +import { withRoomLimit } from './room-limit'; +import type { ProviderCreator } from '@wordpress/sync'; + +/** + * Wrap all sync providers with room-limit enforcement. + * Runs at priority 20 so it wraps providers registered by the rtc package (priority 10). + */ +function registerRoomLimitFilter(): void { + const config = window.wpcomRtcNotices; + if ( ! config?.maxPeersPerRoom && ! config?.maxClientsPerUser ) { + return; + } + + addFilter( + 'sync.providers', + 'wpcom/rtc-room-limits', + ( providers: ProviderCreator[] ) => { + return providers.map( creator => + withRoomLimit( creator, config.maxPeersPerRoom, config.maxClientsPerUser ) + ); + }, + 20 + ); + + // Disable Gutenberg's built-in connection limit check so our + // room-limit.ts handles it with branded notices instead. + addFilter( + 'sync.pollingProvider.maxClientsPerRoom', + 'wpcom/rtc-disable-builtin-limit', + () => Number.MAX_SAFE_INTEGER + ); +} + +// Register limit enforcement. +registerRoomLimitFilter(); + +// Replace Gutenberg's default "Connection lost" modal with branded notices. +registerConnectionErrorModalFilter(); + +const RtcNoticesPlugin = () => { + return ( + <> + + + + + ); +}; + +registerPlugin( 'wpcom-rtc-notices', { + render: RtcNoticesPlugin, +} ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx new file mode 100644 index 000000000000..d99fa75073d3 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx @@ -0,0 +1,107 @@ +/** + * RTC Admin Someone Waiting Notice + * + * Polls for pending join requests from non-admin users who were blocked + * by the collaborator limit. Shows a dismissible notice to the admin + * with the waiting user's name. + * + * Title: "Someone's waiting to join" + * CTA: "Upgrade to invite your entire team" + */ + +import apiFetch from '@wordpress/api-fetch'; +import { useCallback, useEffect, useRef, useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import RtcNoticeModal from '../rtc-notice-modal'; + +interface JoinRequest { + userName: string; + userId: number; + time: number; +} + +const POLL_INTERVAL = 5000; + +const RtcAdminSomeoneWaitingNotice = () => { + const config = window.wpcomRtcNotices; + const [ waitingUser, setWaitingUser ] = useState< JoinRequest | null >( null ); + const dismissed = useRef( false ); + + const pollForRequests = useCallback( () => { + if ( ! config?.isAdmin || ! config.postId || dismissed.current ) { + return; + } + + apiFetch< { requests: JoinRequest[] } >( { + path: `/wpcom/v2/rtc-notices/join-requests?post_id=${ config.postId }`, + } ) + .then( response => { + if ( response.requests.length > 0 && ! dismissed.current ) { + setWaitingUser( response.requests[ 0 ] ); + } + } ) + .catch( () => {} ); + }, [ config ] ); + + useEffect( () => { + if ( ! config?.isAdmin || ! config.postId ) { + return; + } + + const interval = setInterval( pollForRequests, POLL_INTERVAL ); + return () => clearInterval( interval ); + }, [ config, pollForRequests ] ); + + const handleDismiss = useCallback( () => { + dismissed.current = true; + setWaitingUser( null ); + + if ( config?.postId ) { + apiFetch( { + path: '/wpcom/v2/rtc-notices/join-requests/clear', + method: 'POST', + data: { post_id: config.postId }, + } ).catch( () => {} ); + } + }, [ config ] ); + + if ( ! waitingUser || ! config?.isAdmin ) { + return null; + } + + const description = sprintf( + /* translators: 1: user name, 2: post title */ + __( + '%1$s wants to join \u201c%2$s\u201d while your team is already working on it, but you\u2019ve hit your plan\u2019s collaborator limit.', + 'jetpack-mu-wpcom' + ), + waitingUser.userName, + config.postTitle + ); + + return ( + { + const slug = config.siteSlug || ''; + const upgradeUrl = `https://wordpress.com/setup/plan-upgrade/plans?${ new URLSearchParams( + { + siteSlug: slug, + feature: 'wpcom-ws-rtc', + cancel_to: `https://wordpress.com/post/${ slug }/${ config.postId }`, + } + ).toString() }`; + window.location.href = upgradeUrl; + }, + } } + onRequestClose={ handleDismiss } + className="rtc-notice-modal--someone-waiting" + /> + ); +}; + +export default RtcAdminSomeoneWaitingNotice; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx new file mode 100644 index 000000000000..dc072b3d5e5f --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx @@ -0,0 +1,131 @@ +/** + * RTC Connection Error Modal Filter + * + * Replaces Gutenberg's default `SyncConnectionErrorModal` with a branded + * WP.com RTC notice using the `editor.SyncConnectionErrorModal` filter + * (added in Gutenberg PR #76554). + * + * Detects limit-related disconnections via: + * 1. The `connection-limit-exceeded` error code (from Gutenberg's built-in check). + * 2. The `isRoomLimitBreached()` flag (from our room-limit.ts wrapper). + * + * For non-admin users hitting the limit, also records a join request + * so the admin can be notified. + */ + +import apiFetch from '@wordpress/api-fetch'; +import { useRef } from '@wordpress/element'; +import { addFilter } from '@wordpress/hooks'; +import { __ } from '@wordpress/i18n'; +import { isRoomLimitBreached } from '../room-limit'; +import RtcNoticeModal from '../rtc-notice-modal'; +import type { FC } from 'react'; + +interface SyncConnectionErrorModalProps { + description: string; + error?: { code?: string }; + manualRetry?: () => void; + postType?: { slug?: string; labels?: { name?: string } } | null; + secondsRemainingUntilAutoRetry?: number; + title: string; +} + +const WpcomSyncConnectionErrorModal: FC< SyncConnectionErrorModalProps > = () => { + const config = window.wpcomRtcNotices; + const joinRequestSent = useRef( false ); + + if ( ! config ) { + return null; + } + + // Non-admin: record a join request so the admin gets notified. + if ( ! config.isAdmin && config.postId && ! joinRequestSent.current ) { + joinRequestSent.current = true; + apiFetch( { + path: '/wpcom/v2/rtc-notices/join-request', + method: 'POST', + data: { post_id: config.postId }, + } ).catch( () => {} ); + } + + const isAdmin = config.isAdmin; + + const title = isAdmin + ? __( 'Allow your team to collaborate', 'jetpack-mu-wpcom' ) + : __( 'You can\u2019t edit this together yet', 'jetpack-mu-wpcom' ); + + const description = isAdmin + ? __( + 'Your plan\u2019s collaborator limit has been reached. No room for anyone else right now.', + 'jetpack-mu-wpcom' + ) + : __( + 'Your team is already editing this post and the collaborator limit has been reached. We\u2019ve notified your admin so they can make room.', + 'jetpack-mu-wpcom' + ); + + const siteSlug = config.siteSlug || ''; + const upgradeUrl = `https://wordpress.com/setup/plan-upgrade/plans?${ new URLSearchParams( { + siteSlug, + feature: 'wpcom-ws-rtc', + cancel_to: `https://wordpress.com/post/${ siteSlug }/${ config.postId }`, + } ).toString() }`; + + const primaryAction = isAdmin + ? { + label: __( 'Upgrade to invite your entire team', 'jetpack-mu-wpcom' ), + onClick: () => { + window.location.href = upgradeUrl; + }, + } + : { + label: __( 'Back to posts', 'jetpack-mu-wpcom' ), + onClick: () => { + window.location.href = config.postsListUrl; + }, + }; + + const secondaryAction = isAdmin + ? { + label: __( 'Back to posts', 'jetpack-mu-wpcom' ), + onClick: () => { + window.location.href = config.postsListUrl; + }, + } + : undefined; + + return ( + {} } + isDismissible={ false } + shouldCloseOnClickOutside={ false } + className="rtc-notice-modal--limit-reached" + /> + ); +}; + +/** + * Register the filter to replace Gutenberg's default sync connection error modal. + */ +export function registerConnectionErrorModalFilter(): void { + addFilter( + 'editor.SyncConnectionErrorModal', + 'wpcom/rtc-connection-error-modal', + ( OriginalComponent: FC< SyncConnectionErrorModalProps > ) => + ( props: SyncConnectionErrorModalProps ) => { + const isLimitError = + props.error?.code === 'connection-limit-exceeded' || isRoomLimitBreached(); + + if ( isLimitError ) { + return ; + } + + return ; + } + ); +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-non-admin-post-upgrade-notice.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-non-admin-post-upgrade-notice.tsx new file mode 100644 index 000000000000..188e9d521ce7 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-non-admin-post-upgrade-notice.tsx @@ -0,0 +1,75 @@ +/** + * RTC Non-Admin Post Upgrade Notice + * + * Shown to non-admin users after the admin has upgraded the plan + * and there is now room for more collaborators on a post they + * were previously blocked from. + */ + +import { useEffect, useState } from '@wordpress/element'; +import { __, sprintf } from '@wordpress/i18n'; +import RtcNoticeModal from '../rtc-notice-modal'; + +/** + * Props passed via a custom event from the RTC sync layer when + * a non-admin can now join after the admin upgraded. + */ +interface PostUpgradeData { + postTitle: string; + postEditUrl: string; +} + +const RtcNonAdminPostUpgradeNotice = () => { + const config = window.wpcomRtcNotices; + const [ noticeData, setNoticeData ] = useState< PostUpgradeData | null >( null ); + + // Listen for a custom event dispatched by the RTC sync layer. + useEffect( () => { + const handler = ( event: CustomEvent< PostUpgradeData > ) => { + if ( config && ! config.isAdmin ) { + setNoticeData( event.detail ); + } + }; + window.addEventListener( 'wpcom-rtc-post-upgrade', handler as EventListener ); + return () => { + window.removeEventListener( 'wpcom-rtc-post-upgrade', handler as EventListener ); + }; + }, [ config ] ); + + if ( ! noticeData || config?.isAdmin ) { + return null; + } + + const description = sprintf( + /* translators: %s: post title */ + __( + 'Your admin made room for more editors. Head back to \u201c%s\u201d and pick up where you left off.', + 'jetpack-mu-wpcom' + ), + noticeData.postTitle + ); + + const ctaLabel = sprintf( + /* translators: %s: post title */ + __( 'Edit \u201c%s\u201d', 'jetpack-mu-wpcom' ), + noticeData.postTitle + ); + + return ( + { + window.location.href = noticeData.postEditUrl; + }, + } } + onRequestClose={ () => setNoticeData( null ) } + className="rtc-notice-modal--non-admin-post-upgrade" + /> + ); +}; + +export default RtcNonAdminPostUpgradeNotice; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx new file mode 100644 index 000000000000..03b67acfbd65 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx @@ -0,0 +1,54 @@ +/** + * RTC Welcome Notice + * + * One-time dismissible notice shown when users first enter the editor + * on sites with real-time collaboration enabled. Dismissed per-site per-user. + * + * Title: "Edit posts together, in real time" + * Body: Explains RTC feature and invites feedback via support. + * CTA: "Start editing together" + */ + +import apiFetch from '@wordpress/api-fetch'; +import { useState, useCallback } from '@wordpress/element'; +import { __ } from '@wordpress/i18n'; +import RtcNoticeModal from '../rtc-notice-modal'; + +const RtcWelcomeNotice = () => { + const config = window.wpcomRtcNotices; + + const [ isDismissed, setIsDismissed ] = useState( config?.welcomeDismissed ?? true ); + + const dismissNotice = useCallback( () => { + setIsDismissed( true ); + apiFetch( { + path: '/wpcom/v2/rtc-notices/dismiss', + method: 'POST', + } ).catch( () => { + // Silently fail - the notice will show again next time if the API call fails. + } ); + }, [] ); + + if ( ! config || isDismissed ) { + return null; + } + + return ( + + ); +}; + +export default RtcWelcomeNotice; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/room-limit.ts b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/room-limit.ts new file mode 100644 index 000000000000..35049a8f0d0e --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/room-limit.ts @@ -0,0 +1,216 @@ +import type { ProviderCreator, ProviderCreatorResult } from '@wordpress/sync'; +import type { Awareness } from 'y-protocols/awareness'; + +/* + * Module-level state shared across all `withRoomLimit` wrappers. + * + * The polling manager in `@wordpress/sync` is a singleton that sends a single + * HTTP request for every registered room. To fully stop polling when the + * room limit is reached we must destroy *all* wrapped providers — entity + * rooms AND collection rooms — in one shot. We only trigger that global + * teardown when the current client is in the overflow set (newest by enteredAt). + */ +let breached = false; + +/** + * Returns true if the room limit has been breached in this window. + * Uses a window global so the value is accessible across webpack bundles + * (the RTC bundle sets it, the notices bundle reads it). + * + * @return Whether the room limit has been breached. + */ +export function isRoomLimitBreached(): boolean { + return ( window as Record< string, unknown > ).__wpcomRtcRoomLimitBreached === true; +} +const teardowns: Array< () => void > = []; + +const NOOP_RESULT: ProviderCreatorResult = { + destroy: () => {}, + on: () => {}, +}; + +/** + * Build a sorted list of unique collaborators (by WordPress user ID), ordered + * by the time they entered the room. + * + * The earliest `enteredAt` value for each user is used so that multiple tabs + * for the same user share a single, stable "join time". + * + * @param awareness - The Yjs awareness instance. + * @return Array of user descriptors sorted by enter time. + */ +function getSortedCollaborators( + awareness: Awareness +): Array< { id: number; enteredAt: number } > { + const byId = new Map< number, number >(); + + for ( const [ , state ] of awareness.getStates() ) { + const info = state?.collaboratorInfo as { id?: unknown; enteredAt?: unknown } | undefined; + const id = info?.id; + const enteredAt = info?.enteredAt; + if ( typeof id !== 'number' || typeof enteredAt !== 'number' ) { + continue; + } + const prev = byId.get( id ); + if ( prev === undefined || enteredAt < prev ) { + byId.set( id, enteredAt ); + } + } + + return Array.from( byId.entries() ) + .map( ( [ id, enteredAt ] ) => ( { id, enteredAt } ) ) + .sort( ( a, b ) => { + if ( a.enteredAt !== b.enteredAt ) { + return a.enteredAt - b.enteredAt; + } + return a.id - b.id; + } ); +} + +/** + * Build a sorted list of clients (tabs) for the local WordPress user, ordered + * by when they entered the room (earliest enteredAt per clientId). + * + * @param awareness - The Yjs awareness instance. + * @param localUserId - The WordPress user ID to filter by. + * @return Array of client descriptors sorted by enter time. + */ +function getSortedClientsForLocalUser( + awareness: Awareness, + localUserId: number +): Array< { clientId: number; enteredAt: number } > { + const byClientId = new Map< number, number >(); + + for ( const [ clientId, state ] of awareness.getStates() ) { + const info = state?.collaboratorInfo as { id?: unknown; enteredAt?: unknown } | undefined; + if ( info?.id !== localUserId || typeof info?.enteredAt !== 'number' ) { + continue; + } + const enteredAt = info.enteredAt as number; + const prev = byClientId.get( clientId ); + if ( prev === undefined || enteredAt < prev ) { + byClientId.set( clientId, enteredAt ); + } + } + + return Array.from( byClientId.entries() ) + .map( ( [ clientId, enteredAt ] ) => ( { clientId, enteredAt } ) ) + .sort( ( a, b ) => { + if ( a.enteredAt !== b.enteredAt ) { + return a.enteredAt - b.enteredAt; + } + return a.clientId - b.clientId; + } ); +} + +/** + * Wraps a provider creator to enforce a per-room user limit. + * + * Collaborators are ordered by `enteredAt`. When the total number of unique + * users (or clients for the same user) exceeds the allowed maximum, only the + * current client is considered "overflow" if it is among the newest (by enteredAt). + * In that case all wrapped providers in this window are destroyed. + * + * @param creator - The provider creator to wrap. + * @param maxPeersPerRoom - Max other unique users allowed. Undefined or <= 0 disables peer enforcement. + * @param maxClientsPerUser - Max additional clients (tabs) for the same user. Undefined or <= 0 disables per-user enforcement. + * @return Wrapped provider creator. + */ +export function withRoomLimit( + creator: ProviderCreator, + maxPeersPerRoom?: number, + maxClientsPerUser?: number +): ProviderCreator { + const hasPeerLimit = Boolean( maxPeersPerRoom && maxPeersPerRoom > 0 ); + const hasClientLimit = Boolean( maxClientsPerUser && maxClientsPerUser > 0 ); + if ( ! hasPeerLimit && ! hasClientLimit ) { + return creator; + } + + return async ( options ): Promise< ProviderCreatorResult > => { + if ( breached ) { + return NOOP_RESULT; + } + + const { awareness } = options; + const innerProvider = await creator( options ); + let destroyed = false; + + /** Trigger a global teardown for all wrapped providers in this window. */ + function destroyAll(): void { + breached = true; + ( window as Record< string, unknown > ).__wpcomRtcRoomLimitBreached = true; + for ( const fn of teardowns ) { + fn(); + } + teardowns.length = 0; + } + + /** Tear down this single provider and detach the awareness listener. */ + function destroy(): void { + if ( destroyed ) { + return; + } + destroyed = true; + awareness?.off( 'change', onAwarenessChange ); + innerProvider.destroy(); + } + + /** React to awareness changes; trigger global teardown only when this client is in the overflow. */ + function onAwarenessChange(): void { + if ( destroyed || ! awareness ) { + return; + } + + const localUserId = awareness.getLocalState()?.collaboratorInfo?.id; + if ( typeof localUserId !== 'number' ) { + return; + } + + // Peer limit: too many unique users; only newest users (by enteredAt) are overflow. + if ( hasPeerLimit ) { + const collaborators = getSortedCollaborators( awareness ); + if ( collaborators.length > maxPeersPerRoom! ) { + const overflow = collaborators.slice( maxPeersPerRoom ); + if ( overflow.some( user => user.id === localUserId ) ) { + destroyAll(); + return; + } + } + } + + // Client limit: too many tabs for this user; only newest clients (by enteredAt) are overflow. + if ( hasClientLimit ) { + const localClientId = ( awareness as Awareness & { clientID?: unknown } ).clientID; + if ( typeof localClientId !== 'number' ) { + return; + } + const clientsForUser = getSortedClientsForLocalUser( awareness, localUserId ); + if ( clientsForUser.length > maxClientsPerUser! ) { + const overflow = clientsForUser.slice( maxClientsPerUser ); + if ( overflow.some( c => c.clientId === localClientId ) ) { + destroyAll(); + } + } + } + } + + teardowns.push( destroy ); + + if ( awareness ) { + awareness.on( 'change', onAwarenessChange ); + onAwarenessChange(); + } + + return { + destroy: () => { + const idx = teardowns.indexOf( destroy ); + if ( idx >= 0 ) { + teardowns.splice( idx, 1 ); + } + destroy(); + }, + on: innerProvider.on.bind( innerProvider ), + }; + }; +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-header-illustration.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-header-illustration.tsx new file mode 100644 index 000000000000..ffb0b49d45f3 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-header-illustration.tsx @@ -0,0 +1,21 @@ +/** + * RTC Header Illustration + * + * Blue decorative header for the RTC notice modals, featuring + * the "Real-time Collaboration" text with avatars and cursors. + */ + +import heroImage from './rtc-hero.png'; + +const RtcHeaderIllustration = () => { + return ( + + ); +}; + +export default RtcHeaderIllustration; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-hero.png b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-hero.png new file mode 100644 index 0000000000000000000000000000000000000000..fd360bc5d4ed66ee36cd164cbddb521ab4b7eeba GIT binary patch literal 118324 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 literal 0 HcmV?d00001 diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss new file mode 100644 index 000000000000..f1f4190a1d83 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss @@ -0,0 +1,114 @@ +@use "@wordpress/base-styles/mixins"; + +.rtc-notice-modal { + max-width: 480px; + border-radius: 8px; + overflow: hidden; + + .components-modal__header { + height: auto; + padding: 8px; + border-bottom: 0; + position: absolute; + inset-inline-start: 0; + inset-inline-end: 0; + margin: 0; + background-color: transparent; + z-index: 1; + + button { + inset-inline-start: unset; + + svg { + fill: #fff; + } + } + } + + .components-modal__content { + padding: 0; + margin-block-start: 0; + + &::before { + margin: 0; + } + } + + .rtc-notice-modal__header { + display: flex; + justify-content: center; + width: 100%; + line-height: 0; + } + + .rtc-notice-modal__header-illustration { + width: 100%; + height: auto; + display: block; + border-radius: 8px 8px 0 0; + } + + .rtc-notice-modal__body { + padding: 24px 32px 32px; + } + + .rtc-notice-modal__title { + margin: 0; + font-size: 24px; + font-weight: 600; + line-height: 1.3; + color: #1e1e1e; + + @include mixins.break-mobile { + font-size: 28px; + } + } + + .rtc-notice-modal__description { + margin: 12px 0 0; + font-size: 14px; + line-height: 1.6; + color: #1e1e1e; + + @include mixins.break-mobile { + font-size: 16px; + } + } + + .rtc-notice-modal__actions { + display: flex; + flex-direction: column; + align-items: stretch; + margin-block-start: 24px; + gap: 12px; + } + + .rtc-notice-modal__primary-action.components-button { + justify-content: center; + padding: 12px 24px; + height: auto; + font-size: 14px; + font-weight: 500; + border-radius: 4px; + background-color: #3858e9; + + &:hover { + background-color: #2c48c7; + } + + &:focus { + box-shadow: 0 0 0 2px #3858e9; + } + } + + .rtc-notice-modal__secondary-action.components-button { + justify-content: center; + font-size: 14px; + color: #1e1e1e; + text-decoration: none; + + &:hover { + color: #3858e9; + } + } +} diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.tsx new file mode 100644 index 000000000000..6559dd1c1ee1 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.tsx @@ -0,0 +1,110 @@ +/** + * RTC Notice Modal + * + * A generalized modal component for real-time collaboration notices. + * Used across different RTC notice scenarios (welcome, upgrade, blocked, etc.). + */ + +import { Button, Modal } from '@wordpress/components'; +import { useEffect, useRef } from '@wordpress/element'; +import clsx from 'clsx'; +import RtcHeaderIllustration from './rtc-header-illustration'; +import './rtc-notice-modal.scss'; +import type { FC, ReactNode } from 'react'; + +interface RtcNoticeAction { + /** Button label */ + label: string; + /** Click handler */ + onClick: () => void; + /** Button variant - defaults to 'primary' */ + variant?: 'primary' | 'secondary' | 'tertiary' | 'link'; +} + +interface RtcNoticeModalProps { + /** Whether the modal is open */ + isOpen: boolean; + /** Modal title */ + title: string; + /** Modal description / body text */ + description: string | ReactNode; + /** Primary action button */ + primaryAction: RtcNoticeAction; + /** Optional secondary action (rendered as a link below the primary button) */ + secondaryAction?: RtcNoticeAction; + /** Handler for closing the modal (X button or overlay click) */ + onRequestClose: () => void; + /** Optional callback when modal opens */ + onOpen?: () => void; + /** Optional extra CSS class */ + className?: string; + /** Whether the modal shows the X close button. Defaults to true. */ + isDismissible?: boolean; + /** Whether clicking outside closes the modal. Defaults to true. */ + shouldCloseOnClickOutside?: boolean; +} + +const RtcNoticeModal: FC< RtcNoticeModalProps > = ( { + isOpen, + title, + description, + primaryAction, + secondaryAction, + onRequestClose, + onOpen, + className, + isDismissible = true, + shouldCloseOnClickOutside = true, +} ) => { + const prevIsOpen = useRef< boolean | null >( null ); + + useEffect( () => { + if ( ! prevIsOpen.current && isOpen ) { + onOpen?.(); + } + prevIsOpen.current = isOpen; + }, [ prevIsOpen, isOpen, onOpen ] ); + + if ( ! isOpen ) { + return null; + } + + return ( + +
+ +
+
+

{ title }

+

{ description }

+
+ + { secondaryAction && ( + + ) } +
+
+
+ ); +}; + +export default RtcNoticeModal; +export type { RtcNoticeAction, RtcNoticeModalProps }; diff --git a/projects/packages/jetpack-mu-wpcom/webpack.config.js b/projects/packages/jetpack-mu-wpcom/webpack.config.js index 16b75cdecb7d..16f907fb2afa 100644 --- a/projects/packages/jetpack-mu-wpcom/webpack.config.js +++ b/projects/packages/jetpack-mu-wpcom/webpack.config.js @@ -19,6 +19,7 @@ module.exports = async () => { 'customizer-control': './src/features/custom-css/custom-css/css/customizer-control.css', 'error-reporting': './src/features/error-reporting/index.js', 'gutenberg-rtc': './src/features/gutenberg-rtc/gutenberg-rtc.ts', + 'gutenberg-rtc-notices': './src/features/gutenberg-rtc-notices/index.tsx', 'holiday-snow': './src/features/holiday-snow/holiday-snow.scss', 'html-block-restricted-tags': './src/features/html-block-restricted-tags/html-block-restricted-tags.tsx', From e8e2454d9bf82892e0ea2d453133d84114cb46f6 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 12:10:56 +0100 Subject: [PATCH 02/28] Add changelog entry --- .../dotcom-16395-add-one-time-editor-notice-for-rtc-rollout | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/jetpack-mu-wpcom/changelog/dotcom-16395-add-one-time-editor-notice-for-rtc-rollout diff --git a/projects/packages/jetpack-mu-wpcom/changelog/dotcom-16395-add-one-time-editor-notice-for-rtc-rollout b/projects/packages/jetpack-mu-wpcom/changelog/dotcom-16395-add-one-time-editor-notice-for-rtc-rollout new file mode 100644 index 000000000000..54eea051919c --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/dotcom-16395-add-one-time-editor-notice-for-rtc-rollout @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +RTC: Add branded editor notices and room-limit enforcement for real-time collaboration. From 4e2a5ccbef6a003ae935106125449eb787b38e47 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 13:05:41 +0100 Subject: [PATCH 03/28] Fix i18n string literals broken by production minifier --- .../rtc-connection-error-modal-filter.tsx | 79 +++++++++---------- 1 file changed, 38 insertions(+), 41 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx index dc072b3d5e5f..c556dd2fabb5 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx @@ -48,22 +48,6 @@ const WpcomSyncConnectionErrorModal: FC< SyncConnectionErrorModalProps > = () => } ).catch( () => {} ); } - const isAdmin = config.isAdmin; - - const title = isAdmin - ? __( 'Allow your team to collaborate', 'jetpack-mu-wpcom' ) - : __( 'You can\u2019t edit this together yet', 'jetpack-mu-wpcom' ); - - const description = isAdmin - ? __( - 'Your plan\u2019s collaborator limit has been reached. No room for anyone else right now.', - 'jetpack-mu-wpcom' - ) - : __( - 'Your team is already editing this post and the collaborator limit has been reached. We\u2019ve notified your admin so they can make room.', - 'jetpack-mu-wpcom' - ); - const siteSlug = config.siteSlug || ''; const upgradeUrl = `https://wordpress.com/setup/plan-upgrade/plans?${ new URLSearchParams( { siteSlug, @@ -71,36 +55,49 @@ const WpcomSyncConnectionErrorModal: FC< SyncConnectionErrorModalProps > = () => cancel_to: `https://wordpress.com/post/${ siteSlug }/${ config.postId }`, } ).toString() }`; - const primaryAction = isAdmin - ? { - label: __( 'Upgrade to invite your entire team', 'jetpack-mu-wpcom' ), - onClick: () => { - window.location.href = upgradeUrl; - }, - } - : { - label: __( 'Back to posts', 'jetpack-mu-wpcom' ), - onClick: () => { - window.location.href = config.postsListUrl; - }, - }; + if ( config.isAdmin ) { + return ( + { + window.location.href = upgradeUrl; + }, + } } + secondaryAction={ { + label: __( 'Back to posts', 'jetpack-mu-wpcom' ), + onClick: () => { + window.location.href = config.postsListUrl; + }, + } } + onRequestClose={ () => {} } + isDismissible={ false } + shouldCloseOnClickOutside={ false } + className="rtc-notice-modal--limit-reached" + /> + ); + } - const secondaryAction = isAdmin - ? { + return ( + { window.location.href = config.postsListUrl; }, - } - : undefined; - - return ( - {} } isDismissible={ false } shouldCloseOnClickOutside={ false } From 70903b5887946e0071b281455008095e959a528a Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 13:27:49 +0100 Subject: [PATCH 04/28] Add PHP tests for RTC notices and limits --- .../RTC_Notices_Test.php | 261 ++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php new file mode 100644 index 000000000000..12d4f2b186a4 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php @@ -0,0 +1,261 @@ +user_id = wp_insert_user( + array( + 'user_login' => 'rtc_test_user', + 'user_pass' => 'password', + 'role' => 'administrator', + ) + ); + wp_set_current_user( $this->user_id ); + } + + /** + * Clean up after each test. + */ + public function tear_down(): void { + wp_delete_user( $this->user_id ); + remove_all_filters( 'wpcom_rtc_max_peers_per_room' ); + remove_all_filters( 'wpcom_rtc_max_clients_per_user' ); + parent::tear_down(); + } + + /** + * Tests that default max peers per room is 2. + */ + public function test_default_max_peers_per_room() { + $this->assertSame( 2, wpcom_get_rtc_max_peers_per_room() ); + } + + /** + * Tests that max peers per room is filterable. + */ + public function test_filterable_max_peers_per_room() { + add_filter( + 'wpcom_rtc_max_peers_per_room', + function () { + return 5; + } + ); + + $this->assertSame( 5, wpcom_get_rtc_max_peers_per_room() ); + } + + /** + * Tests that default max clients per user is 2. + */ + public function test_default_max_clients_per_user() { + $this->assertSame( 2, wpcom_get_rtc_max_clients_per_user() ); + } + + /** + * Tests that max clients per user is filterable. + */ + public function test_filterable_max_clients_per_user() { + add_filter( + 'wpcom_rtc_max_clients_per_user', + function () { + return 3; + } + ); + + $this->assertSame( 3, wpcom_get_rtc_max_clients_per_user() ); + } + + /** + * Tests that welcome notice is not dismissed by default. + */ + public function test_welcome_notice_not_dismissed_by_default() { + $this->assertFalse( WP_REST_RTC_Notices::is_dismissed() ); + } + + /** + * Tests that welcome notice can be dismissed via user meta. + */ + public function test_welcome_notice_dismiss_persists() { + update_user_meta( $this->user_id, WP_REST_RTC_Notices::META_KEY, 'dismissed' ); + + $this->assertTrue( WP_REST_RTC_Notices::is_dismissed() ); + } + + /** + * Tests that welcome notice dismiss is per-user. + */ + public function test_welcome_notice_dismiss_is_per_user() { + $other_user_id = wp_insert_user( + array( + 'user_login' => 'rtc_test_user_2', + 'user_pass' => 'password', + 'role' => 'editor', + ) + ); + + update_user_meta( $this->user_id, WP_REST_RTC_Notices::META_KEY, 'dismissed' ); + + wp_set_current_user( $other_user_id ); + $this->assertFalse( WP_REST_RTC_Notices::is_dismissed() ); + + wp_set_current_user( $this->user_id ); + $this->assertTrue( WP_REST_RTC_Notices::is_dismissed() ); + + wp_delete_user( $other_user_id ); + } + + /** + * Tests the dismiss REST endpoint. + */ + public function test_dismiss_rest_endpoint() { + $controller = new WP_REST_RTC_Notices(); + $response = $controller->dismiss_notice(); + $data = $response->get_data(); + + $this->assertTrue( $data['success'] ); + $this->assertTrue( WP_REST_RTC_Notices::is_dismissed() ); + } + + /** + * Tests the status REST endpoint. + */ + public function test_status_rest_endpoint() { + $controller = new WP_REST_RTC_Notices(); + + $response = $controller->get_status(); + $this->assertFalse( $response->get_data()['dismissed'] ); + + $controller->dismiss_notice(); + + $response = $controller->get_status(); + $this->assertTrue( $response->get_data()['dismissed'] ); + } + + /** + * Tests that join request can be recorded and retrieved. + */ + public function test_join_request_record_and_retrieve() { + $post_id = wp_insert_post( + array( + 'post_title' => 'Test Post', + 'post_status' => 'publish', + ) + ); + + $controller = new WP_REST_RTC_Notices(); + + $request = new WP_REST_Request( 'POST', '/wpcom/v2/rtc-notices/join-request' ); + $request->set_param( 'post_id', $post_id ); + $response = $controller->record_join_request( $request ); + $this->assertTrue( $response->get_data()['success'] ); + + $request = new WP_REST_Request( 'GET', '/wpcom/v2/rtc-notices/join-requests' ); + $request->set_param( 'post_id', $post_id ); + $response = $controller->get_join_requests( $request ); + $requests = $response->get_data()['requests']; + + $this->assertCount( 1, $requests ); + $this->assertSame( $this->user_id, $requests[0]['userId'] ); + + wp_delete_post( $post_id, true ); + } + + /** + * Tests that join requests can be cleared. + */ + public function test_clear_join_requests() { + $post_id = wp_insert_post( + array( + 'post_title' => 'Test Post', + 'post_status' => 'publish', + ) + ); + + $controller = new WP_REST_RTC_Notices(); + + $request = new WP_REST_Request( 'POST', '/wpcom/v2/rtc-notices/join-request' ); + $request->set_param( 'post_id', $post_id ); + $controller->record_join_request( $request ); + + $request = new WP_REST_Request( 'POST', '/wpcom/v2/rtc-notices/join-requests/clear' ); + $request->set_param( 'post_id', $post_id ); + $controller->clear_join_requests( $request ); + + $request = new WP_REST_Request( 'GET', '/wpcom/v2/rtc-notices/join-requests' ); + $request->set_param( 'post_id', $post_id ); + $response = $controller->get_join_requests( $request ); + $this->assertCount( 0, $response->get_data()['requests'] ); + + wp_delete_post( $post_id, true ); + } + + /** + * Tests that permission check requires login. + */ + public function test_permission_requires_login() { + wp_set_current_user( 0 ); + $controller = new WP_REST_RTC_Notices(); + + $this->assertFalse( $controller->check_permission() ); + } + + /** + * Tests that admin permission requires manage_options. + */ + public function test_admin_permission_requires_manage_options() { + $editor_id = wp_insert_user( + array( + 'user_login' => 'rtc_editor', + 'user_pass' => 'password', + 'role' => 'editor', + ) + ); + + $controller = new WP_REST_RTC_Notices(); + + wp_set_current_user( $editor_id ); + $this->assertFalse( $controller->check_admin_permission() ); + + wp_set_current_user( $this->user_id ); + $this->assertTrue( $controller->check_admin_permission() ); + + wp_delete_user( $editor_id ); + } +} From 5917b1ccabeb331e1963ad475b6c6e43e72d5d4a Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 13:35:38 +0100 Subject: [PATCH 05/28] Only show RTC notices when collaboration is enabled in Writing Settings --- .../features/gutenberg-rtc-notices/gutenberg-rtc-notices.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php index 74210be84ebc..669ee4316452 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -44,6 +44,11 @@ function wpcom_enqueue_rtc_notices_assets() { return; } + // Don't show notices if the user has turned off RTC in Writing Settings. + if ( ! get_option( 'wp_enable_real_time_collaboration', false ) ) { + return; + } + $handle = jetpack_mu_wpcom_enqueue_assets( 'gutenberg-rtc-notices', array( 'js', 'css' ) ); wp_set_script_translations( $handle, 'jetpack-mu-wpcom' ); From 5c919516828ebccabf0c064b6f61da82a8c0e18c Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 13:38:19 +0100 Subject: [PATCH 06/28] Match modal sizing to Figma specs: 409px max-width, 20px title --- .../features/gutenberg-rtc-notices/rtc-notice-modal.scss | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss index f1f4190a1d83..14792948b1cf 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss @@ -1,7 +1,7 @@ @use "@wordpress/base-styles/mixins"; .rtc-notice-modal { - max-width: 480px; + max-width: 409px; border-radius: 8px; overflow: hidden; @@ -54,14 +54,10 @@ .rtc-notice-modal__title { margin: 0; - font-size: 24px; + font-size: 20px; font-weight: 600; line-height: 1.3; color: #1e1e1e; - - @include mixins.break-mobile { - font-size: 28px; - } } .rtc-notice-modal__description { From ef46e526bc3cfbf3a524eb7cdf56d4a386bb406c Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 13:41:07 +0100 Subject: [PATCH 07/28] Hide welcome notice when sync connection is lost --- .../notices/rtc-welcome-notice.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx index 03b67acfbd65..ae56879e540b 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx @@ -10,6 +10,7 @@ */ import apiFetch from '@wordpress/api-fetch'; +import { useSelect } from '@wordpress/data'; import { useState, useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import RtcNoticeModal from '../rtc-notice-modal'; @@ -19,6 +20,14 @@ const RtcWelcomeNotice = () => { const [ isDismissed, setIsDismissed ] = useState( config?.welcomeDismissed ?? true ); + // Don't show the welcome notice if the sync connection is lost (e.g. room + // limit reached). The limit-reached modal should take priority. + const isDisconnected = useSelect( select => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const status = ( select( 'core' ) as any ).getSyncConnectionStatus?.(); + return status?.status === 'disconnected'; + }, [] ); + const dismissNotice = useCallback( () => { setIsDismissed( true ); apiFetch( { @@ -29,7 +38,7 @@ const RtcWelcomeNotice = () => { } ); }, [] ); - if ( ! config || isDismissed ) { + if ( ! config || isDismissed || isDisconnected ) { return null; } From ae97c82926c4ab59a072fd340831063e653a154a Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 13:45:36 +0100 Subject: [PATCH 08/28] Use typed interface instead of any for getSyncConnectionStatus --- .../notices/rtc-welcome-notice.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx index ae56879e540b..bc9ca24464ce 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx @@ -15,6 +15,10 @@ import { useState, useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; import RtcNoticeModal from '../rtc-notice-modal'; +interface SyncConnectionStatus { + status: string; +} + const RtcWelcomeNotice = () => { const config = window.wpcomRtcNotices; @@ -23,9 +27,10 @@ const RtcWelcomeNotice = () => { // Don't show the welcome notice if the sync connection is lost (e.g. room // limit reached). The limit-reached modal should take priority. const isDisconnected = useSelect( select => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const status = ( select( 'core' ) as any ).getSyncConnectionStatus?.(); - return status?.status === 'disconnected'; + const coreStore = select( 'core' ) as { + getSyncConnectionStatus?: () => SyncConnectionStatus | undefined; + }; + return coreStore.getSyncConnectionStatus?.()?.status === 'disconnected'; }, [] ); const dismissNotice = useCallback( () => { From d93d50880eacccf3dd5d8d85e309c0eac2428111 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 13:54:28 +0100 Subject: [PATCH 09/28] Check both RTC option names and fix apostrophes in copy --- .../gutenberg-rtc-notices/gutenberg-rtc-notices.php | 5 ++++- .../notices/rtc-admin-someone-waiting-notice.tsx | 4 ++-- .../notices/rtc-connection-error-modal-filter.tsx | 6 +++--- .../gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php index 669ee4316452..16a2cca16583 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -45,7 +45,10 @@ function wpcom_enqueue_rtc_notices_assets() { } // Don't show notices if the user has turned off RTC in Writing Settings. - if ( ! get_option( 'wp_enable_real_time_collaboration', false ) ) { + // Both the old and new option names must be false to consider it disabled. + $old_option = get_option( 'wp_enable_real_time_collaboration', false ); + $new_option = get_option( 'wp_collaboration_enabled', false ); + if ( ! $old_option && ! $new_option ) { return; } diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx index d99fa75073d3..3f87a5d21219 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx @@ -72,7 +72,7 @@ const RtcAdminSomeoneWaitingNotice = () => { const description = sprintf( /* translators: 1: user name, 2: post title */ __( - '%1$s wants to join \u201c%2$s\u201d while your team is already working on it, but you\u2019ve hit your plan\u2019s collaborator limit.', + "%1$s wants to join \u201c%2$s\u201d while your team is already working on it, but you've hit your plan's collaborator limit.", 'jetpack-mu-wpcom' ), waitingUser.userName, @@ -82,7 +82,7 @@ const RtcAdminSomeoneWaitingNotice = () => { return ( = () => isOpen={ true } title={ __( 'Allow your team to collaborate', 'jetpack-mu-wpcom' ) } description={ __( - 'Your plan\u2019s collaborator limit has been reached. No room for anyone else right now.', + "Your plan's collaborator limit has been reached. No room for anyone else right now.", 'jetpack-mu-wpcom' ) } primaryAction={ { @@ -87,9 +87,9 @@ const WpcomSyncConnectionErrorModal: FC< SyncConnectionErrorModalProps > = () => return ( { isOpen={ ! isDismissed } title={ __( 'Edit posts together, in real time', 'jetpack-mu-wpcom' ) } description={ __( - 'You can now edit posts with others at the same time. See each other\u2019s changes instantly, right in the WordPress editor. Have fun, and if you have questions or feedback reach out to our support team.', + "You can now edit posts with others at the same time. See each other's changes instantly, right in the WordPress editor. Have fun, and if you have questions or feedback reach out to our support team.", 'jetpack-mu-wpcom' ) } primaryAction={ { From 765105cd04d8d88491d82a36f1ffd745eb035b6d Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 14:01:55 +0100 Subject: [PATCH 10/28] Add edit_post permission checks and switch dismiss to user options for per-site persistence --- .../class-wp-rest-rtc-notices.php | 40 ++++++++++++-- .../RTC_Notices_Test.php | 54 +++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php index 2c22bbc9a81a..b50ba073b372 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php @@ -49,7 +49,7 @@ public function register_routes() { array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'record_join_request' ), - 'permission_callback' => array( $this, 'check_permission' ), + 'permission_callback' => array( $this, 'check_edit_post_permission' ), 'args' => array( 'post_id' => array( 'required' => true, @@ -66,7 +66,7 @@ public function register_routes() { array( 'methods' => WP_REST_Server::READABLE, 'callback' => array( $this, 'get_join_requests' ), - 'permission_callback' => array( $this, 'check_admin_permission' ), + 'permission_callback' => array( $this, 'check_admin_edit_post_permission' ), 'args' => array( 'post_id' => array( 'required' => true, @@ -83,7 +83,7 @@ public function register_routes() { array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'clear_join_requests' ), - 'permission_callback' => array( $this, 'check_admin_permission' ), + 'permission_callback' => array( $this, 'check_admin_edit_post_permission' ), 'args' => array( 'post_id' => array( 'required' => true, @@ -113,6 +113,40 @@ public function check_admin_permission() { return current_user_can( 'manage_options' ); } + /** + * Check if the current user can edit the post specified in the request. + * + * @param WP_REST_Request $request The request. + * @return bool|WP_Error + */ + public function check_edit_post_permission( $request ) { + $post_id = $request->get_param( 'post_id' ); + + if ( ! get_post( $post_id ) ) { + return new \WP_Error( 'rest_post_invalid_id', __( 'Invalid post ID.', 'jetpack-mu-wpcom' ), array( 'status' => 404 ) ); + } + + if ( ! current_user_can( 'edit_post', $post_id ) ) { + return new \WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to edit this post.', 'jetpack-mu-wpcom' ), array( 'status' => 403 ) ); + } + + return true; + } + + /** + * Check if the current user is an admin who can edit the post. + * + * @param WP_REST_Request $request The request. + * @return bool|WP_Error + */ + public function check_admin_edit_post_permission( $request ) { + if ( ! current_user_can( 'manage_options' ) ) { + return false; + } + + return $this->check_edit_post_permission( $request ); + } + /** * Dismiss the welcome notice. * diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php index 12d4f2b186a4..ce34e361de06 100644 --- a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php @@ -258,4 +258,58 @@ public function test_admin_permission_requires_manage_options() { wp_delete_user( $editor_id ); } + + /** + * Tests that join request rejects invalid post IDs. + */ + public function test_join_request_rejects_invalid_post_id() { + $controller = new WP_REST_RTC_Notices(); + + $request = new WP_REST_Request( 'POST', '/wpcom/v2/rtc-notices/join-request' ); + $request->set_param( 'post_id', 999999 ); + $result = $controller->check_edit_post_permission( $request ); + + $this->assertInstanceOf( 'WP_Error', $result ); + $this->assertSame( 'rest_post_invalid_id', $result->get_error_code() ); + } + + /** + * Tests that join request requires edit_post capability. + */ + public function test_join_request_requires_edit_post() { + $post_id = wp_insert_post( + array( + 'post_title' => 'Test Post', + 'post_status' => 'publish', + 'post_author' => $this->user_id, + ) + ); + + $subscriber_id = wp_insert_user( + array( + 'user_login' => 'rtc_subscriber', + 'user_pass' => 'password', + 'role' => 'subscriber', + ) + ); + + $controller = new WP_REST_RTC_Notices(); + + $request = new WP_REST_Request( 'POST', '/wpcom/v2/rtc-notices/join-request' ); + $request->set_param( 'post_id', $post_id ); + + // Subscriber cannot edit posts. + wp_set_current_user( $subscriber_id ); + $result = $controller->check_edit_post_permission( $request ); + $this->assertInstanceOf( 'WP_Error', $result ); + $this->assertSame( 'rest_forbidden', $result->get_error_code() ); + + // Admin can edit posts. + wp_set_current_user( $this->user_id ); + $result = $controller->check_edit_post_permission( $request ); + $this->assertTrue( $result ); + + wp_delete_user( $subscriber_id ); + wp_delete_post( $post_id, true ); + } } From 177745e5d3042bd57134dbd47cf05cafb9004217 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 14:12:45 +0100 Subject: [PATCH 11/28] Use user options for per-site dismiss and move apiFetch to useEffect --- .../class-wp-rest-rtc-notices.php | 13 ++++--------- .../notices/rtc-connection-error-modal-filter.tsx | 15 +++++++++------ .../gutenberg-rtc-notices/RTC_Notices_Test.php | 4 ++-- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php index b50ba073b372..400f8e850a3c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php @@ -16,7 +16,7 @@ */ class WP_REST_RTC_Notices extends WP_REST_Controller { - const META_KEY = 'wpcom_rtc_welcome_notice_dismissed'; + const OPTION_KEY = 'wpcom_rtc_welcome_notice_dismissed'; const JOIN_REQUEST_OPTION = 'rtc_pending_join_requests'; /** @@ -153,7 +153,7 @@ public function check_admin_edit_post_permission( $request ) { * @return WP_REST_Response */ public function dismiss_notice() { - update_user_meta( get_current_user_id(), self::META_KEY, 'dismissed' ); + update_user_option( get_current_user_id(), self::OPTION_KEY, 'dismissed' ); return rest_ensure_response( array( 'success' => true ) ); } @@ -168,17 +168,12 @@ public function get_status() { /** * Check if the welcome notice is dismissed. + * Uses user options (not user meta) so dismissal is per-site on multisite/Simple. * * @return bool */ public static function is_dismissed() { - $user_id = get_current_user_id(); - - if ( ! metadata_exists( 'user', $user_id, self::META_KEY ) ) { - return false; - } - - return 'dismissed' === get_user_meta( $user_id, self::META_KEY, true ); + return 'dismissed' === get_user_option( self::OPTION_KEY, get_current_user_id() ); } /** diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx index 1c06e03be02c..0a4abc943ceb 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx @@ -14,7 +14,7 @@ */ import apiFetch from '@wordpress/api-fetch'; -import { useRef } from '@wordpress/element'; +import { useEffect, useRef } from '@wordpress/element'; import { addFilter } from '@wordpress/hooks'; import { __ } from '@wordpress/i18n'; import { isRoomLimitBreached } from '../room-limit'; @@ -34,18 +34,21 @@ const WpcomSyncConnectionErrorModal: FC< SyncConnectionErrorModalProps > = () => const config = window.wpcomRtcNotices; const joinRequestSent = useRef( false ); - if ( ! config ) { - return null; - } - // Non-admin: record a join request so the admin gets notified. - if ( ! config.isAdmin && config.postId && ! joinRequestSent.current ) { + useEffect( () => { + if ( ! config || config.isAdmin || ! config.postId || joinRequestSent.current ) { + return; + } joinRequestSent.current = true; apiFetch( { path: '/wpcom/v2/rtc-notices/join-request', method: 'POST', data: { post_id: config.postId }, } ).catch( () => {} ); + }, [ config ] ); + + if ( ! config ) { + return null; } const siteSlug = config.siteSlug || ''; diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php index ce34e361de06..dcdacab5b2c2 100644 --- a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php @@ -113,7 +113,7 @@ public function test_welcome_notice_not_dismissed_by_default() { * Tests that welcome notice can be dismissed via user meta. */ public function test_welcome_notice_dismiss_persists() { - update_user_meta( $this->user_id, WP_REST_RTC_Notices::META_KEY, 'dismissed' ); + update_user_option( $this->user_id, WP_REST_RTC_Notices::OPTION_KEY, 'dismissed' ); $this->assertTrue( WP_REST_RTC_Notices::is_dismissed() ); } @@ -130,7 +130,7 @@ public function test_welcome_notice_dismiss_is_per_user() { ) ); - update_user_meta( $this->user_id, WP_REST_RTC_Notices::META_KEY, 'dismissed' ); + update_user_option( $this->user_id, WP_REST_RTC_Notices::OPTION_KEY, 'dismissed' ); wp_set_current_user( $other_user_id ); $this->assertFalse( WP_REST_RTC_Notices::is_dismissed() ); From 19ef7ba03e2560f5a51d04881313290af49421e0 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 14:48:09 +0100 Subject: [PATCH 12/28] Add tests for enqueue, route registration, and admin edit post permission --- .../RTC_Notices_Test.php | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php index dcdacab5b2c2..b59513ce3717 100644 --- a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php @@ -13,6 +13,7 @@ require_once Jetpack_Mu_Wpcom::PKG_DIR . 'src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php'; // phpcs:ignore WordPressVIPMinimum.Files.IncludingFile.NotAbsolutePath require_once Jetpack_Mu_Wpcom::PKG_DIR . 'src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php'; +use PHPUnit\Framework\Attributes\Before; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\CoversFunction; @@ -50,13 +51,45 @@ public function set_up(): void { wp_set_current_user( $this->user_id ); } + /** + * Original WP_Scripts instance. + * + * @var \WP_Scripts|null + */ + private $original_wp_scripts; + + /** + * Original WP_Styles instance. + * + * @var \WP_Styles|null + */ + private $original_wp_styles; + + /** + * Set up before each test. + * + * @before + */ + #[Before] + public function save_globals(): void { + global $wp_scripts, $wp_styles; + $this->original_wp_scripts = $wp_scripts; + $this->original_wp_styles = $wp_styles; + } + /** * Clean up after each test. */ public function tear_down(): void { + global $wp_scripts, $wp_styles; + $wp_scripts = $this->original_wp_scripts; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wp_styles = $this->original_wp_styles; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited wp_delete_user( $this->user_id ); remove_all_filters( 'wpcom_rtc_max_peers_per_room' ); remove_all_filters( 'wpcom_rtc_max_clients_per_user' ); + remove_all_filters( 'wpcom_is_gutenberg_rtc_enabled' ); + delete_option( 'wp_enable_real_time_collaboration' ); + delete_option( 'wp_collaboration_enabled' ); parent::tear_down(); } @@ -312,4 +345,139 @@ public function test_join_request_requires_edit_post() { wp_delete_user( $subscriber_id ); wp_delete_post( $post_id, true ); } + + /** + * Tests admin edit post permission denies non-admins. + */ + public function test_admin_edit_post_permission_denies_non_admin() { + $post_id = wp_insert_post( + array( + 'post_title' => 'Test Post', + 'post_status' => 'publish', + ) + ); + + $editor_id = wp_insert_user( + array( + 'user_login' => 'rtc_editor_perm', + 'user_pass' => 'password', + 'role' => 'editor', + ) + ); + + $controller = new WP_REST_RTC_Notices(); + $request = new WP_REST_Request( 'GET', '/wpcom/v2/rtc-notices/join-requests' ); + $request->set_param( 'post_id', $post_id ); + + wp_set_current_user( $editor_id ); + $this->assertFalse( $controller->check_admin_edit_post_permission( $request ) ); + + wp_set_current_user( $this->user_id ); + $this->assertTrue( $controller->check_admin_edit_post_permission( $request ) ); + + wp_delete_user( $editor_id ); + wp_delete_post( $post_id, true ); + } + + /** + * Reset scripts/styles to a fresh state for enqueue tests. + */ + private function reset_scripts(): void { + global $wp_scripts, $wp_styles; + $wp_scripts = new \WP_Scripts(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $wp_styles = new \WP_Styles(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + } + + /** + * Tests that enqueue skips when RTC is not enabled. + */ + public function test_enqueue_skips_when_rtc_disabled() { + $this->reset_scripts(); + add_filter( 'wpcom_is_gutenberg_rtc_enabled', '__return_false' ); + + wpcom_enqueue_rtc_notices_assets(); + + $this->assertFalse( wp_script_is( 'jetpack-mu-wpcom-gutenberg-rtc-notices', 'enqueued' ) ); + } + + /** + * Tests that enqueue skips when RTC setting is off. + */ + public function test_enqueue_skips_when_rtc_setting_off() { + $this->reset_scripts(); + add_filter( 'wpcom_is_gutenberg_rtc_enabled', '__return_true' ); + // Explicitly set to '0' — deleting won't work because gutenberg-rtc.php + // has a default_option filter that returns '1' when providers exist. + update_option( 'wp_enable_real_time_collaboration', '0' ); + update_option( 'wp_collaboration_enabled', '0' ); + + wpcom_enqueue_rtc_notices_assets(); + + $this->assertFalse( wp_script_is( 'jetpack-mu-wpcom-gutenberg-rtc-notices', 'enqueued' ) ); + } + + /** + * Tests that enqueue works when RTC is enabled and old setting is on. + */ + public function test_enqueue_works_with_old_rtc_option() { + $this->reset_scripts(); + add_filter( 'wpcom_is_gutenberg_rtc_enabled', '__return_true' ); + update_option( 'wp_enable_real_time_collaboration', '1' ); + + wpcom_enqueue_rtc_notices_assets(); + + $this->assertTrue( wp_script_is( 'jetpack-mu-wpcom-gutenberg-rtc-notices', 'enqueued' ) ); + } + + /** + * Tests that enqueue works when RTC is enabled and new setting is on. + */ + public function test_enqueue_works_with_new_rtc_option() { + $this->reset_scripts(); + add_filter( 'wpcom_is_gutenberg_rtc_enabled', '__return_true' ); + update_option( 'wp_collaboration_enabled', '1' ); + + wpcom_enqueue_rtc_notices_assets(); + + $this->assertTrue( wp_script_is( 'jetpack-mu-wpcom-gutenberg-rtc-notices', 'enqueued' ) ); + } + + /** + * Tests that the inline script includes expected config keys. + */ + public function test_enqueue_includes_inline_config() { + $this->reset_scripts(); + add_filter( 'wpcom_is_gutenberg_rtc_enabled', '__return_true' ); + update_option( 'wp_enable_real_time_collaboration', '1' ); + + wpcom_enqueue_rtc_notices_assets(); + + global $wp_scripts; + $handle = 'jetpack-mu-wpcom-gutenberg-rtc-notices'; + $extra = $wp_scripts->registered[ $handle ]->extra['before'] ?? array(); + $inline = implode( "\n", array_filter( $extra ) ); + + $this->assertStringContainsString( 'wpcomRtcNotices', $inline ); + $this->assertStringContainsString( '"isAdmin"', $inline ); + $this->assertStringContainsString( '"welcomeDismissed"', $inline ); + $this->assertStringContainsString( '"maxPeersPerRoom"', $inline ); + $this->assertStringContainsString( '"maxClientsPerUser"', $inline ); + $this->assertStringContainsString( '"siteSlug"', $inline ); + } + + /** + * Tests that REST routes are registered. + */ + public function test_rest_routes_registered() { + $controller = new WP_REST_RTC_Notices(); + $controller->register_routes(); + + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey( '/wpcom/v2/rtc-notices/dismiss', $routes ); + $this->assertArrayHasKey( '/wpcom/v2/rtc-notices/status', $routes ); + $this->assertArrayHasKey( '/wpcom/v2/rtc-notices/join-request', $routes ); + $this->assertArrayHasKey( '/wpcom/v2/rtc-notices/join-requests', $routes ); + $this->assertArrayHasKey( '/wpcom/v2/rtc-notices/join-requests/clear', $routes ); + } } From bd37990cc754271c3042645fec15910955f89c5d Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 15:57:31 +0100 Subject: [PATCH 13/28] Remove unused check_admin_permission method --- .../class-wp-rest-rtc-notices.php | 9 -------- .../RTC_Notices_Test.php | 23 ------------------- 2 files changed, 32 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php index 400f8e850a3c..692b3754f466 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/class-wp-rest-rtc-notices.php @@ -104,15 +104,6 @@ public function check_permission() { return is_user_logged_in(); } - /** - * Check if the current user is an admin. - * - * @return bool - */ - public function check_admin_permission() { - return current_user_can( 'manage_options' ); - } - /** * Check if the current user can edit the post specified in the request. * diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php index b59513ce3717..143e95bfdc87 100644 --- a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php @@ -269,29 +269,6 @@ public function test_permission_requires_login() { $this->assertFalse( $controller->check_permission() ); } - /** - * Tests that admin permission requires manage_options. - */ - public function test_admin_permission_requires_manage_options() { - $editor_id = wp_insert_user( - array( - 'user_login' => 'rtc_editor', - 'user_pass' => 'password', - 'role' => 'editor', - ) - ); - - $controller = new WP_REST_RTC_Notices(); - - wp_set_current_user( $editor_id ); - $this->assertFalse( $controller->check_admin_permission() ); - - wp_set_current_user( $this->user_id ); - $this->assertTrue( $controller->check_admin_permission() ); - - wp_delete_user( $editor_id ); - } - /** * Tests that join request rejects invalid post IDs. */ From ed7fe0d6397237fce7bb824d02f570bb7e07af1a Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 16:18:45 +0100 Subject: [PATCH 14/28] Default maxPeersPerRoom to 3 to align with Core --- .../features/gutenberg-rtc-notices/gutenberg-rtc-notices.php | 2 +- .../php/features/gutenberg-rtc-notices/RTC_Notices_Test.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php index 16a2cca16583..ffaa8f5ca72a 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -24,7 +24,7 @@ function () { * @return int Max peers per room. */ function wpcom_get_rtc_max_peers_per_room() { - return (int) apply_filters( 'wpcom_rtc_max_peers_per_room', 2 ); + return (int) apply_filters( 'wpcom_rtc_max_peers_per_room', 3 ); } /** diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php index 143e95bfdc87..09e242dee867 100644 --- a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php @@ -97,7 +97,7 @@ public function tear_down(): void { * Tests that default max peers per room is 2. */ public function test_default_max_peers_per_room() { - $this->assertSame( 2, wpcom_get_rtc_max_peers_per_room() ); + $this->assertSame( 3, wpcom_get_rtc_max_peers_per_room() ); } /** From 121f5fd8f5bbc35c9bffc5e52f72bea9092ed696 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 16:46:42 +0100 Subject: [PATCH 15/28] Gate limit notices behind enableLimitNotices flag, off by default --- .../gutenberg-rtc-notices.php | 19 ++++++++++--------- .../gutenberg-rtc-notices/images.d.ts | 1 + .../features/gutenberg-rtc-notices/index.tsx | 15 ++++++++------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php index ffaa8f5ca72a..1b581aee8701 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -61,15 +61,16 @@ function wpcom_enqueue_rtc_notices_assets() { $data = wp_json_encode( array( - 'isAdmin' => $is_admin_user, - 'welcomeDismissed' => WP_REST_RTC_Notices::is_dismissed(), - 'postId' => get_the_ID(), - 'postTitle' => get_the_title(), - 'postEditUrl' => get_edit_post_link( get_the_ID(), 'raw' ), - 'postsListUrl' => admin_url( 'edit.php' ), - 'siteSlug' => wpcom_get_site_slug(), - 'maxPeersPerRoom' => wpcom_get_rtc_max_peers_per_room(), - 'maxClientsPerUser' => wpcom_get_rtc_max_clients_per_user(), + 'isAdmin' => $is_admin_user, + 'welcomeDismissed' => WP_REST_RTC_Notices::is_dismissed(), + 'postId' => get_the_ID(), + 'postTitle' => get_the_title(), + 'postEditUrl' => get_edit_post_link( get_the_ID(), 'raw' ), + 'postsListUrl' => admin_url( 'edit.php' ), + 'siteSlug' => wpcom_get_site_slug(), + 'maxPeersPerRoom' => wpcom_get_rtc_max_peers_per_room(), + 'maxClientsPerUser' => wpcom_get_rtc_max_clients_per_user(), + 'enableLimitNotices' => apply_filters( 'wpcom_rtc_enable_limit_notices', false ), ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts index edc1d7c2c8af..7251bf20eca9 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts @@ -13,6 +13,7 @@ interface WpcomRtcNoticesConfig { siteSlug: string; maxPeersPerRoom?: number; maxClientsPerUser?: number; + enableLimitNotices?: boolean; } interface Window { diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx index acee52656bde..12389ffbaa71 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx @@ -17,6 +17,8 @@ import RtcWelcomeNotice from './notices/rtc-welcome-notice'; import { withRoomLimit } from './room-limit'; import type { ProviderCreator } from '@wordpress/sync'; +const enableLimitNotices = window.wpcomRtcNotices?.enableLimitNotices ?? false; + /** * Wrap all sync providers with room-limit enforcement. * Runs at priority 20 so it wraps providers registered by the rtc package (priority 10). @@ -47,18 +49,17 @@ function registerRoomLimitFilter(): void { ); } -// Register limit enforcement. -registerRoomLimitFilter(); - -// Replace Gutenberg's default "Connection lost" modal with branded notices. -registerConnectionErrorModalFilter(); +if ( enableLimitNotices ) { + registerRoomLimitFilter(); + registerConnectionErrorModalFilter(); +} const RtcNoticesPlugin = () => { return ( <> - - + { enableLimitNotices && } + { enableLimitNotices && } ); }; From df116d19c05ef61ce579f30df91a0e00af0529db Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 17:05:01 +0100 Subject: [PATCH 16/28] Show upgrade CTA only to plan owner using cross-platform detection --- .../gutenberg-rtc-notices.php | 30 +++++++++++++++++++ .../gutenberg-rtc-notices/images.d.ts | 1 + .../rtc-admin-someone-waiting-notice.tsx | 6 ++-- .../rtc-connection-error-modal-filter.tsx | 2 +- 4 files changed, 35 insertions(+), 4 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php index 1b581aee8701..326671148826 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -36,6 +36,34 @@ function wpcom_get_rtc_max_clients_per_user() { return (int) apply_filters( 'wpcom_rtc_max_clients_per_user', 2 ); } +/** + * Check if the current user is the plan owner for this site. + * Works on Simple sites (via wpcom_get_blog_owner) and Atomic sites + * (via Jetpack connection master_user). Returns false on self-hosted + * since there is no WP.com plan to upgrade. + * + * @return bool + */ +function wpcom_rtc_is_plan_owner() { + $current_user_id = get_current_user_id(); + + // Simple sites: wpcom_get_blog_owner is the canonical source. + if ( function_exists( 'wpcom_get_blog_owner' ) ) { + $owner_id = wpcom_get_blog_owner( get_wpcom_blog_id() ); + return (int) $current_user_id === (int) $owner_id; + } + + // Atomic sites: the Jetpack connection master_user is the plan owner. + if ( class_exists( 'Jetpack_Options' ) ) { + $master_user = \Jetpack_Options::get_option( 'master_user' ); + if ( $master_user ) { + return (int) $current_user_id === (int) $master_user; + } + } + + return false; +} + /** * Enqueue block editor assets for RTC notices and limits. */ @@ -58,10 +86,12 @@ function wpcom_enqueue_rtc_notices_assets() { require_once __DIR__ . '/class-wp-rest-rtc-notices.php'; $is_admin_user = current_user_can( 'manage_options' ); + $is_plan_owner = wpcom_rtc_is_plan_owner(); $data = wp_json_encode( array( 'isAdmin' => $is_admin_user, + 'isPlanOwner' => $is_plan_owner, 'welcomeDismissed' => WP_REST_RTC_Notices::is_dismissed(), 'postId' => get_the_ID(), 'postTitle' => get_the_title(), diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts index 7251bf20eca9..cd4da83ca0d2 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/images.d.ts @@ -5,6 +5,7 @@ declare module '*.png' { interface WpcomRtcNoticesConfig { isAdmin: boolean; + isPlanOwner: boolean; welcomeDismissed: boolean; postId: number; postTitle: string; diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx index 3f87a5d21219..5b654899f569 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-admin-someone-waiting-notice.tsx @@ -28,7 +28,7 @@ const RtcAdminSomeoneWaitingNotice = () => { const dismissed = useRef( false ); const pollForRequests = useCallback( () => { - if ( ! config?.isAdmin || ! config.postId || dismissed.current ) { + if ( ! config?.isPlanOwner || ! config.postId || dismissed.current ) { return; } @@ -44,7 +44,7 @@ const RtcAdminSomeoneWaitingNotice = () => { }, [ config ] ); useEffect( () => { - if ( ! config?.isAdmin || ! config.postId ) { + if ( ! config?.isPlanOwner || ! config.postId ) { return; } @@ -65,7 +65,7 @@ const RtcAdminSomeoneWaitingNotice = () => { } }, [ config ] ); - if ( ! waitingUser || ! config?.isAdmin ) { + if ( ! waitingUser || ! config?.isPlanOwner ) { return null; } diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx index 0a4abc943ceb..b99788972f34 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-connection-error-modal-filter.tsx @@ -58,7 +58,7 @@ const WpcomSyncConnectionErrorModal: FC< SyncConnectionErrorModalProps > = () => cancel_to: `https://wordpress.com/post/${ siteSlug }/${ config.postId }`, } ).toString() }`; - if ( config.isAdmin ) { + if ( config.isPlanOwner ) { return ( Date: Thu, 19 Mar 2026 17:25:18 +0100 Subject: [PATCH 17/28] Fire join request from room-limit.ts so it works without the modal filter --- .../features/gutenberg-rtc-notices/room-limit.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/room-limit.ts b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/room-limit.ts index 35049a8f0d0e..cd8a36cef5c7 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/room-limit.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/room-limit.ts @@ -140,6 +140,22 @@ export function withRoomLimit( function destroyAll(): void { breached = true; ( window as Record< string, unknown > ).__wpcomRtcRoomLimitBreached = true; + + // Non-admin: record a join request so the admin gets notified. + const config = window.wpcomRtcNotices; + if ( config && ! config.isAdmin && config.postId ) { + const wp = ( window as Record< string, unknown > ).wp as + | { apiFetch?: ( opts: Record< string, unknown > ) => Promise< unknown > } + | undefined; + wp + ?.apiFetch?.( { + path: '/wpcom/v2/rtc-notices/join-request', + method: 'POST', + data: { post_id: config.postId }, + } ) + ?.catch?.( () => {} ); + } + for ( const fn of teardowns ) { fn(); } From d2b5301cea2530a35817f51fdcec5ea11b199043 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 17:44:21 +0100 Subject: [PATCH 18/28] Match description font-size to Figma (13px) --- .../features/gutenberg-rtc-notices/rtc-notice-modal.scss | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss index 14792948b1cf..93d8f15a897c 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/rtc-notice-modal.scss @@ -62,13 +62,9 @@ .rtc-notice-modal__description { margin: 12px 0 0; - font-size: 14px; + font-size: 13px; line-height: 1.6; color: #1e1e1e; - - @include mixins.break-mobile { - font-size: 16px; - } } .rtc-notice-modal__actions { From f60d1e30c8c2a170c36f2bb2e138ad12c6a89791 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 17:44:36 +0100 Subject: [PATCH 19/28] Always register room-limit filter, gate only branded modals behind enableLimitNotices --- .../src/features/gutenberg-rtc-notices/index.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx index 12389ffbaa71..4ff147b671f7 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/index.tsx @@ -49,8 +49,11 @@ function registerRoomLimitFilter(): void { ); } +// Room-limit enforcement always runs (it stops polling, sends join requests). +// The branded modals are gated behind enableLimitNotices. +registerRoomLimitFilter(); + if ( enableLimitNotices ) { - registerRoomLimitFilter(); registerConnectionErrorModalFilter(); } From 7a42182af5bd19f8abb68939be19114c0028d364 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 17:49:38 +0100 Subject: [PATCH 20/28] Delay welcome notice 6s to avoid flash before room-limit disconnect --- .../notices/rtc-welcome-notice.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx index 0c5f7c854f96..93409ec22686 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/notices/rtc-welcome-notice.tsx @@ -11,10 +11,15 @@ import apiFetch from '@wordpress/api-fetch'; import { useSelect } from '@wordpress/data'; -import { useState, useCallback } from '@wordpress/element'; +import { useState, useCallback, useEffect } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; +import { isRoomLimitBreached } from '../room-limit'; import RtcNoticeModal from '../rtc-notice-modal'; +// Wait before showing the welcome notice to give the sync connection +// time to establish and potentially hit the room limit first. +const WELCOME_NOTICE_DELAY_MS = 6000; + interface SyncConnectionStatus { status: string; } @@ -23,6 +28,13 @@ const RtcWelcomeNotice = () => { const config = window.wpcomRtcNotices; const [ isDismissed, setIsDismissed ] = useState( config?.welcomeDismissed ?? true ); + const [ isReady, setIsReady ] = useState( false ); + + // Delay showing the notice to avoid flashing it before a limit disconnect. + useEffect( () => { + const timeout = setTimeout( () => setIsReady( true ), WELCOME_NOTICE_DELAY_MS ); + return () => clearTimeout( timeout ); + }, [] ); // Don't show the welcome notice if the sync connection is lost (e.g. room // limit reached). The limit-reached modal should take priority. @@ -30,7 +42,9 @@ const RtcWelcomeNotice = () => { const coreStore = select( 'core' ) as { getSyncConnectionStatus?: () => SyncConnectionStatus | undefined; }; - return coreStore.getSyncConnectionStatus?.()?.status === 'disconnected'; + return ( + coreStore.getSyncConnectionStatus?.()?.status === 'disconnected' || isRoomLimitBreached() + ); }, [] ); const dismissNotice = useCallback( () => { @@ -43,7 +57,7 @@ const RtcWelcomeNotice = () => { } ); }, [] ); - if ( ! config || isDismissed || isDisconnected ) { + if ( ! config || isDismissed || ! isReady || isDisconnected ) { return null; } From d0e59e5da089adbc8dccc5369c3f81626e16c65a Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Thu, 19 Mar 2026 19:09:53 +0100 Subject: [PATCH 21/28] Register RTC notices REST routes at priority 20 to avoid interfering with Launchpad schema --- .../features/gutenberg-rtc-notices/gutenberg-rtc-notices.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php index 326671148826..9ead896f0864 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -15,7 +15,8 @@ function () { require_once __DIR__ . '/class-wp-rest-rtc-notices.php'; ( new WP_REST_RTC_Notices() )->register_routes(); - } + }, + 20 ); /** From 62c523a7a1dfb475e4efed54afe5ed6596585c9d Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 20 Mar 2026 09:24:10 +0100 Subject: [PATCH 22/28] Gate REST route registration behind RTC feature check to avoid test interference --- .../features/gutenberg-rtc-notices/gutenberg-rtc-notices.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php index 9ead896f0864..8d9a306097ba 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -13,6 +13,9 @@ add_action( 'rest_api_init', function () { + if ( ! wpcom_is_gutenberg_rtc_enabled() ) { + return; + } require_once __DIR__ . '/class-wp-rest-rtc-notices.php'; ( new WP_REST_RTC_Notices() )->register_routes(); }, From 312f4b3d0204a0ff235ad51a680169614979c976 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 20 Mar 2026 09:52:16 +0100 Subject: [PATCH 23/28] Load RTC notices only on editor pages to avoid REST API test interference --- .../packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php index 3cbacfb1aafa..e51b22d9622d 100644 --- a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php +++ b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php @@ -435,7 +435,6 @@ public static function load_etk_features() { } require_once __DIR__ . '/features/gutenberg-rtc/gutenberg-rtc.php'; - require_once __DIR__ . '/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php'; require_once __DIR__ . '/features/jetpack-global-styles/class-global-styles.php'; require_once __DIR__ . '/features/mailerlite/subscriber-popup.php'; require_once __DIR__ . '/features/wpcom-fse/wpcom-fse.php'; @@ -454,6 +453,7 @@ public static function load_etk_features() { require_once __DIR__ . '/features/tags-education/tags-education.php'; require_once __DIR__ . '/features/wpcom-block-description-links/wpcom-block-description-links.php'; require_once __DIR__ . '/features/wpcom-block-editor-nux/class-wpcom-block-editor-nux.php'; + require_once __DIR__ . '/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php'; require_once __DIR__ . '/features/wpcom-blocks/a8c-posts-list/a8c-posts-list.php'; require_once __DIR__ . '/features/wpcom-blocks/event-countdown/event-countdown.php'; require_once __DIR__ . '/features/wpcom-blocks/timeline/timeline.php'; From 530a577e03831f12b0470399038b81fe59752ecf Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 20 Mar 2026 10:21:20 +0100 Subject: [PATCH 24/28] Fix test tear_down: reset current user, clean up options and transients --- .../gutenberg-rtc-notices/RTC_Notices_Test.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php index 09e242dee867..5a12cc47c1a0 100644 --- a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php @@ -84,12 +84,22 @@ public function tear_down(): void { global $wp_scripts, $wp_styles; $wp_scripts = $this->original_wp_scripts; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited $wp_styles = $this->original_wp_styles; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + + wp_set_current_user( 0 ); wp_delete_user( $this->user_id ); + remove_all_filters( 'wpcom_rtc_max_peers_per_room' ); remove_all_filters( 'wpcom_rtc_max_clients_per_user' ); remove_all_filters( 'wpcom_is_gutenberg_rtc_enabled' ); + remove_all_filters( 'wpcom_rtc_enable_limit_notices' ); + delete_option( 'wp_enable_real_time_collaboration' ); delete_option( 'wp_collaboration_enabled' ); + + // Clean up any user options and transients left by tests. + delete_user_option( $this->user_id, WP_REST_RTC_Notices::OPTION_KEY ); + delete_transient( WP_REST_RTC_Notices::JOIN_REQUEST_OPTION . '_' . 999999 ); + parent::tear_down(); } From 189806db90181df4bf69cf11adbe3c3c0596e8f5 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 20 Mar 2026 10:26:57 +0100 Subject: [PATCH 25/28] Reset wp_rest_server in tear_down to prevent stale route leaks into Launchpad tests --- .../php/features/gutenberg-rtc-notices/RTC_Notices_Test.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php index 5a12cc47c1a0..1961aafa59ac 100644 --- a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php @@ -100,6 +100,11 @@ public function tear_down(): void { delete_user_option( $this->user_id, WP_REST_RTC_Notices::OPTION_KEY ); delete_transient( WP_REST_RTC_Notices::JOIN_REQUEST_OPTION . '_' . 999999 ); + // Reset the REST server so stale route registrations don't leak + // into other test classes (e.g. Launchpad endpoint tests). + global $wp_rest_server; + $wp_rest_server = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + parent::tear_down(); } From 7e2ed813f2cb991d2ca6fd5e67c6c570931ed6a7 Mon Sep 17 00:00:00 2001 From: Richard Ortiz Date: Fri, 20 Mar 2026 10:44:52 +0100 Subject: [PATCH 26/28] Add tests for wpcom_rtc_is_plan_owner and improve enqueue coverage --- .../RTC_Notices_Test.php | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php index 1961aafa59ac..394054545dfd 100644 --- a/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/gutenberg-rtc-notices/RTC_Notices_Test.php @@ -22,10 +22,14 @@ * * @covers ::wpcom_get_rtc_max_peers_per_room * @covers ::wpcom_get_rtc_max_clients_per_user + * @covers ::wpcom_rtc_is_plan_owner + * @covers ::wpcom_enqueue_rtc_notices_assets * @covers WP_REST_RTC_Notices */ #[CoversFunction( 'wpcom_get_rtc_max_peers_per_room' )] #[CoversFunction( 'wpcom_get_rtc_max_clients_per_user' )] +#[CoversFunction( 'wpcom_rtc_is_plan_owner' )] +#[CoversFunction( 'wpcom_enqueue_rtc_notices_assets' )] #[CoversClass( WP_REST_RTC_Notices::class )] class RTC_Notices_Test extends \WorDBless\BaseTestCase { @@ -472,4 +476,63 @@ public function test_rest_routes_registered() { $this->assertArrayHasKey( '/wpcom/v2/rtc-notices/join-requests', $routes ); $this->assertArrayHasKey( '/wpcom/v2/rtc-notices/join-requests/clear', $routes ); } + + /** + * Tests that plan owner returns false when no owner detection is available. + */ + public function test_plan_owner_returns_false_without_detection() { + $this->assertFalse( wpcom_rtc_is_plan_owner() ); + } + + /** + * Tests that plan owner uses Jetpack master_user when available. + */ + public function test_plan_owner_via_jetpack_master_user() { + if ( ! class_exists( 'Jetpack_Options' ) ) { + $this->markTestSkipped( 'Jetpack_Options not available' ); + } + + // Set master_user to current user. + \Jetpack_Options::update_option( 'master_user', $this->user_id ); + $this->assertTrue( wpcom_rtc_is_plan_owner() ); + + // Set master_user to a different user. + \Jetpack_Options::update_option( 'master_user', 999999 ); + $this->assertFalse( wpcom_rtc_is_plan_owner() ); + + \Jetpack_Options::delete_option( 'master_user' ); + } + + /** + * Tests that inline config includes isPlanOwner. + */ + public function test_enqueue_includes_is_plan_owner() { + $this->reset_scripts(); + add_filter( 'wpcom_is_gutenberg_rtc_enabled', '__return_true' ); + update_option( 'wp_enable_real_time_collaboration', '1' ); + + wpcom_enqueue_rtc_notices_assets(); + + global $wp_scripts; + $handle = 'jetpack-mu-wpcom-gutenberg-rtc-notices'; + $extra = $wp_scripts->registered[ $handle ]->extra['before'] ?? array(); + $inline = implode( "\n", array_filter( $extra ) ); + + $this->assertStringContainsString( '"isPlanOwner"', $inline ); + $this->assertStringContainsString( '"enableLimitNotices"', $inline ); + } + + /** + * Tests that enqueue skips when both RTC options are explicitly off. + */ + public function test_enqueue_skips_when_both_options_explicitly_off() { + $this->reset_scripts(); + add_filter( 'wpcom_is_gutenberg_rtc_enabled', '__return_true' ); + update_option( 'wp_enable_real_time_collaboration', '0' ); + update_option( 'wp_collaboration_enabled', '0' ); + + wpcom_enqueue_rtc_notices_assets(); + + $this->assertFalse( wp_script_is( 'jetpack-mu-wpcom-gutenberg-rtc-notices', 'enqueued' ) ); + } } From 94cf33162f9877ceca91c95b28d21c4e5f9dbd79 Mon Sep 17 00:00:00 2001 From: mmtr <1233880+mmtr@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:34:13 +0100 Subject: [PATCH 27/28] Disable RTC notices in site editor --- .../gutenberg-rtc-notices/gutenberg-rtc-notices.php | 10 ++++++++++ ...om-16395-add-one-time-editor-notice-for-rtc-rollout | 4 ++++ projects/packages/rtc/src/class-rtc.php | 10 ++++++++++ 3 files changed, 24 insertions(+) create mode 100644 projects/packages/rtc/changelog/dotcom-16395-add-one-time-editor-notice-for-rtc-rollout diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php index 4049794b32e6..bcdb72dd2a30 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -72,10 +72,20 @@ function wpcom_rtc_is_plan_owner() { * Enqueue block editor assets for RTC notices and limits. */ function wpcom_enqueue_rtc_notices_assets() { + global $pagenow; + if ( ! \Automattic\Jetpack\RTC::is_enabled() ) { return; } + // Real-time collaboration is not enabled in the site editor. + if ( + 'site-editor.php' === $pagenow || + ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'site-editor-v2' === sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ) { + return; + } + // Don't show notices if the user has turned off RTC in Writing Settings. // Both the old and new option names must be false to consider it disabled. $old_option = get_option( 'wp_enable_real_time_collaboration', false ); diff --git a/projects/packages/rtc/changelog/dotcom-16395-add-one-time-editor-notice-for-rtc-rollout b/projects/packages/rtc/changelog/dotcom-16395-add-one-time-editor-notice-for-rtc-rollout new file mode 100644 index 000000000000..3697c735bd29 --- /dev/null +++ b/projects/packages/rtc/changelog/dotcom-16395-add-one-time-editor-notice-for-rtc-rollout @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Disable RTC in site editor diff --git a/projects/packages/rtc/src/class-rtc.php b/projects/packages/rtc/src/class-rtc.php index 8d4fc825f8ae..15c3983d7f79 100644 --- a/projects/packages/rtc/src/class-rtc.php +++ b/projects/packages/rtc/src/class-rtc.php @@ -121,6 +121,16 @@ public static function register_rest_routes() { * @return void */ public static function enqueue_assets() { + global $pagenow; + + // Real-time collaboration is not enabled in the site editor. + if ( + 'site-editor.php' === $pagenow || + ( 'admin.php' === $pagenow && isset( $_GET['page'] ) && 'site-editor-v2' === sanitize_text_field( wp_unslash( $_GET['page'] ) ) ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended + ) { + return; + } + $providers = self::get_providers(); // If HTTP polling (Gutenberg's built-in default provider when this script isn't enqueued) From 89800efbc5a46fd9fc594b412b0267039c2be47a Mon Sep 17 00:00:00 2001 From: mmtr <1233880+mmtr@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:45:35 +0100 Subject: [PATCH 28/28] Hide notices on P2 sites --- .../features/gutenberg-rtc-notices/gutenberg-rtc-notices.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php index bcdb72dd2a30..82cb897b3139 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/gutenberg-rtc-notices/gutenberg-rtc-notices.php @@ -94,6 +94,11 @@ function wpcom_enqueue_rtc_notices_assets() { return; } + // Notices are not relevant for P2 sites. + if ( function_exists( '\WPForTeams\is_wpforteams_site' ) && \WPForTeams\is_wpforteams_site( get_current_blog_id() ) ) { + return; + } + $handle = jetpack_mu_wpcom_enqueue_assets( 'gutenberg-rtc-notices', array( 'js', 'css' ) ); wp_set_script_translations( $handle, 'jetpack-mu-wpcom' );