'aafm/menu-pointer-not-flagged', 'status' => 'error', 'event_type' => 'setting_changed', 'detail' => sprintf( /* translators: %s: the name of the setting, for example "Read-only mode". */ __( '%s could not be changed: the site\'s persistent object cache is still returning the old value. Flush the object cache (Redis, Memcached, or your host\'s cache) and save again.', 'agent-abilities-for-mcp' ), __( 'The first-activation pointer', 'agent-abilities-for-mcp' ) ), ) ); } } /** * Whether the current user has already dismissed the menu pointer. * * @return bool */ function aafm_quickconnect_pointer_dismissed_for_user(): bool { $dismissed = (string) aafm_meta_get( 'user', get_current_user_id(), 'dismissed_wp_pointers', true ); $list = array_filter( array_map( 'trim', explode( ',', $dismissed ) ) ); return in_array( aafm_quickconnect_pointer_id(), $list, true ); } /** * Record the menu pointer as dismissed for the current user. * * Appends the pointer id to the core `dismissed_wp_pointers` meta - the same store core's * dismiss-wp-pointer AJAX writes to - so opening the plugin page dismisses the pointer server-side * without waiting for a click. * * @return void */ function aafm_quickconnect_mark_pointer_dismissed_for_user(): void { $user_id = get_current_user_id(); if ( $user_id <= 0 ) { return; } // The stored list is read failure-aware: rewriting it after a failed read would drop every // pointer this user dismissed before. $baseline = aafm_meta_row( 'user', $user_id, 'dismissed_wp_pointers' ); if ( ! $baseline['ok'] ) { return; } $dismissed = (string) $baseline['value']; $list = array_filter( array_map( 'trim', explode( ',', $dismissed ) ) ); if ( in_array( aafm_quickconnect_pointer_id(), $list, true ) ) { return; } $list[] = aafm_quickconnect_pointer_id(); aafm_meta_set( 'user', $user_id, 'dismissed_wp_pointers', implode( ',', $list ) ); } /** * Whether the first-activation menu pointer should be enqueued for this admin request. * * True only when the install is flagged new, the user can manage options and has not dismissed the * pointer, and the current screen is NOT the plugin's own page (opening that page dismisses the * pointer instead - see aafm_maybe_enqueue_menu_pointer()). * * @param string $hook Current admin page hook suffix. * @return bool */ function aafm_quickconnect_pointer_should_show( string $hook ): bool { return '1' === (string) get_option( 'aafm_menu_pointer_active', '' ) && current_user_can( 'manage_options' ) && 'toplevel_page_agent-abilities-for-mcp' !== $hook && ! aafm_quickconnect_pointer_dismissed_for_user(); } /** * Enqueue and configure the first-activation admin-menu pointer. * * Hooked on admin_enqueue_scripts for every admin screen. On the plugin's own page it silently marks * the pointer dismissed for this user (opening the page counts as "seen"). On any other admin screen, * when the pointer is still due, it enqueues core's wp-pointer script/style and hands the pointer * copy and target to a small inline script that anchors the callout to the plugin's menu item. The * pointer dismisses through core's own dismiss-wp-pointer AJAX, which stores the id in user meta. * * @param string $hook Current admin page hook suffix. * @return void */ function aafm_maybe_enqueue_menu_pointer( string $hook ): void { // Opening the plugin page dismisses the pointer for this user, so it never nags afterwards. if ( 'toplevel_page_agent-abilities-for-mcp' === $hook ) { if ( '1' === (string) get_option( 'aafm_menu_pointer_active', '' ) && current_user_can( 'manage_options' ) && ! aafm_quickconnect_pointer_dismissed_for_user() ) { aafm_quickconnect_mark_pointer_dismissed_for_user(); } return; } if ( ! aafm_quickconnect_pointer_should_show( $hook ) ) { return; } wp_enqueue_style( 'wp-pointer' ); wp_enqueue_script( 'wp-pointer' ); $page_url = admin_url( 'admin.php?page=agent-abilities-for-mcp' ); $data = array( 'id' => aafm_quickconnect_pointer_id(), 'target' => '#toplevel_page_agent-abilities-for-mcp', 'ajaxUrl' => admin_url( 'admin-ajax.php' ), 'pageUrl' => $page_url, 'heading' => __( 'Agent Abilities for MCP', 'agent-abilities-for-mcp' ), 'body' => __( 'Connect an AI agent to your site. It takes about two minutes.', 'agent-abilities-for-mcp' ), 'start' => __( 'Start setup', 'agent-abilities-for-mcp' ), 'dismiss' => __( 'Dismiss', 'agent-abilities-for-mcp' ), ); // Attach the config and behaviour to the core wp-pointer handle. The inline script builds the // pointer content with jQuery text nodes (no innerHTML), so nothing is parsed as raw markup. wp_add_inline_script( 'wp-pointer', 'window.aafmMenuPointer = ' . wp_json_encode( $data ) . ';' . aafm_quickconnect_pointer_inline_js() ); } /** * The inline jQuery that opens the first-activation menu pointer. * * Kept as a small self-contained IIFE so it needs no separate asset file. Every string comes from * the localized aafmMenuPointer object and reaches the DOM through jQuery text()/attr only. The * "Start setup" button links to the plugin page; both buttons close the pointer, and closing posts * to core's dismiss-wp-pointer action so the id is recorded in user meta. * * @return string */ function aafm_quickconnect_pointer_inline_js(): string { return <<<'JS' ( function ( $ ) { $( function () { var cfg = window.aafmMenuPointer || {}; var $target = $( cfg.target ); if ( ! $target.length || ! $.fn.pointer ) { return; } var $content = $( '
' ); $( '

' ).text( cfg.heading ).appendTo( $content ); $( '

' ).text( cfg.body ).appendTo( $content ); var dismiss = function () { $.post( cfg.ajaxUrl, { action: 'dismiss-wp-pointer', pointer: cfg.id } ); }; $target.pointer( { content: $content.html(), position: { edge: 'left', align: 'center' }, pointerClass: 'wp-pointer aafm-menu-pointer', buttons: function ( event, t ) { var $box = $( '' ); $( '' ).text( cfg.dismiss ).on( 'click', function ( e ) { e.preventDefault(); t.element.pointer( 'close' ); } ).appendTo( $box ); $( '' ) .text( cfg.start ).attr( 'href', cfg.pageUrl ).appendTo( $box ); return $box; }, close: dismiss } ).pointer( 'open' ); } ); }( jQuery ) ); JS; }