Skip to content

Commit 56d9da1

Browse files
jdevalkclaude
andcommitted
Replace wp_dropdown_users with AJAX-powered user search
Replace the comment notification recipient dropdown with an AJAX search field to prevent high memory usage on sites with many users. Fixes #190 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 6d925b0 commit 56d9da1

2 files changed

Lines changed: 170 additions & 9 deletions

File tree

admin/admin.php

Lines changed: 95 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public function __construct() {
6868

6969
\add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_discussion_settings_script' ] );
7070

71+
\add_action( 'wp_ajax_ch_search_users', [ $this, 'ajax_search_users' ] );
72+
7173
new Comment_Parent();
7274
}
7375

@@ -240,17 +242,71 @@ public function register_meta_boxes(): void {
240242
* @return void
241243
*/
242244
public function meta_box_callback( $post ): void {
245+
$selected_id = (int) \get_post_meta( $post->ID, self::NOTIFICATION_RECIPIENT_KEY, true );
246+
$selected_name = '';
247+
248+
if ( $selected_id > 0 ) {
249+
$user = \get_userdata( $selected_id );
250+
if ( $user ) {
251+
$selected_name = $user->display_name;
252+
}
253+
}
254+
243255
?>
244256
<input
245257
type="hidden"
246258
name="comment_notification_recipient_nonce"
247259
value="<?php echo \esc_attr( \wp_create_nonce( 'comment_notification_recipient_nonce' ) ); ?>"
248260
/>
249-
<label for="comment_notification_recipient">
261+
<label for="comment_notification_recipient_search">
250262
<?php \esc_html_e( 'Comment notification recipients:', 'yoast-comment-hacks' ); ?>
251263
</label>
252264
<br/>
265+
<input
266+
type="hidden"
267+
name="comment_notification_recipient"
268+
id="comment_notification_recipient"
269+
value="<?php echo \esc_attr( (string) $selected_id ); ?>"
270+
/>
271+
<input
272+
type="text"
273+
id="comment_notification_recipient_search"
274+
value="<?php echo \esc_attr( $selected_name ); ?>"
275+
placeholder="<?php \esc_attr_e( 'Search for a user...', 'yoast-comment-hacks' ); ?>"
276+
class="widefat"
277+
autocomplete="off"
278+
/>
279+
<ul id="comment_notification_recipient_results" style="display:none;margin:0;padding:0;list-style:none;border:1px solid #ddd;background:#fff;max-height:150px;overflow-y:auto;"></ul>
280+
<?php if ( $selected_id > 0 ) : ?>
281+
<a href="#" id="comment_notification_recipient_clear" style="display:inline;">
282+
<?php \esc_html_e( 'Reset to post author', 'yoast-comment-hacks' ); ?>
283+
</a>
284+
<?php else : ?>
285+
<a href="#" id="comment_notification_recipient_clear" style="display:none;">
286+
<?php \esc_html_e( 'Reset to post author', 'yoast-comment-hacks' ); ?>
287+
</a>
288+
<?php endif; ?>
289+
<p class="description"><?php \esc_html_e( 'Leave empty to use post author.', 'yoast-comment-hacks' ); ?></p>
253290
<?php
291+
}
292+
293+
/**
294+
* AJAX handler for searching users.
295+
*
296+
* @return void
297+
*/
298+
public function ajax_search_users(): void {
299+
\check_ajax_referer( 'ch_search_users_nonce', 'nonce' );
300+
301+
if ( ! \current_user_can( 'edit_posts' ) ) {
302+
\wp_send_json_error( 'Unauthorized' );
303+
}
304+
305+
$search = isset( $_POST['search'] ) ? \sanitize_text_field( \wp_unslash( $_POST['search'] ) ) : '';
306+
307+
if ( \strlen( $search ) < 2 ) {
308+
\wp_send_json_success( [] );
309+
}
254310

255311
/**
256312
* This filter allows filtering which roles should be shown in the dropdown for notifications.
@@ -265,16 +321,25 @@ public function meta_box_callback( $post ): void {
265321
[ 'contributor', 'author', 'editor', 'administrator', 'super-admin' ]
266322
);
267323

268-
\wp_dropdown_users(
324+
$users = \get_users(
269325
[
270-
'selected' => \get_post_meta( $post->ID, self::NOTIFICATION_RECIPIENT_KEY, true ),
271-
'show_option_none' => 'Post author',
272-
'name' => 'comment_notification_recipient',
273-
'id' => 'comment_notification_recipient',
274-
'role__in' => $roles,
275-
'option_none_value' => 0,
326+
'search' => '*' . $search . '*',
327+
'role__in' => $roles,
328+
'number' => 20,
329+
'orderby' => 'display_name',
330+
'order' => 'ASC',
276331
]
277332
);
333+
334+
$results = [];
335+
foreach ( $users as $user ) {
336+
$results[] = [
337+
'id' => $user->ID,
338+
'name' => $user->display_name,
339+
];
340+
}
341+
342+
\wp_send_json_success( $results );
278343
}
279344

280345
/**
@@ -297,9 +362,11 @@ public function init(): void {
297362
/**
298363
* Enqueue our admin script.
299364
*
365+
* @param string $hook_suffix The current admin page.
366+
*
300367
* @return void
301368
*/
302-
public function enqueue(): void {
369+
public function enqueue( $hook_suffix ): void {
303370
$page = \filter_input( \INPUT_GET, 'page' );
304371

305372
if ( $page === 'comment-experience' ) {
@@ -318,6 +385,25 @@ public function enqueue(): void {
318385
true
319386
);
320387
}
388+
389+
if ( $hook_suffix === 'post.php' || $hook_suffix === 'post-new.php' ) {
390+
\wp_enqueue_script(
391+
'emiliaprojects-comment-hacks-user-search',
392+
\plugins_url( 'admin/assets/js/user-search.js', \EMILIA_COMMENT_HACKS_FILE ),
393+
[ 'jquery' ],
394+
\EMILIA_COMMENT_HACKS_VERSION,
395+
true
396+
);
397+
398+
\wp_localize_script(
399+
'emiliaprojects-comment-hacks-user-search',
400+
'chUserSearch',
401+
[
402+
'ajax_url' => \admin_url( 'admin-ajax.php' ),
403+
'nonce' => \wp_create_nonce( 'ch_search_users_nonce' ),
404+
]
405+
);
406+
}
321407
}
322408

323409
/**

admin/assets/js/user-search.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/* global chUserSearch */
2+
3+
jQuery( document ).ready( function( $ ) {
4+
var $search = $( "#comment_notification_recipient_search" );
5+
var $hidden = $( "#comment_notification_recipient" );
6+
var $results = $( "#comment_notification_recipient_results" );
7+
var $clear = $( "#comment_notification_recipient_clear" );
8+
var searchTimer = null;
9+
10+
$search.on( "input", function() {
11+
clearTimeout( searchTimer );
12+
var query = $search.val();
13+
14+
if ( query.length < 2 ) {
15+
$results.empty().hide();
16+
return;
17+
}
18+
19+
searchTimer = setTimeout( function() {
20+
$.ajax( {
21+
url: chUserSearch.ajax_url,
22+
type: "POST",
23+
data: {
24+
action: "ch_search_users",
25+
search: query,
26+
nonce: chUserSearch.nonce,
27+
},
28+
/**
29+
* Handle the AJAX response.
30+
*
31+
* @param {Object} response The response object.
32+
* @param {boolean} response.success Indicates if the request was successful.
33+
* @param {Array} response.data The response data.
34+
*
35+
* @returns {void}
36+
*/
37+
success: function( response ) {
38+
if ( response.success && response.data.length ) {
39+
$results.empty();
40+
$.each( response.data, function( i, user ) {
41+
$results.append(
42+
$( "<li>" )
43+
.text( user.name )
44+
.attr( "data-id", user.id )
45+
.on( "click", function() {
46+
$hidden.val( user.id );
47+
$search.val( user.name );
48+
$results.empty().hide();
49+
$clear.show();
50+
} )
51+
);
52+
} );
53+
$results.show();
54+
} else {
55+
$results.empty().hide();
56+
}
57+
},
58+
} );
59+
}, 300 );
60+
} );
61+
62+
$clear.on( "click", function( e ) {
63+
e.preventDefault();
64+
$hidden.val( "0" );
65+
$search.val( "" );
66+
$clear.hide();
67+
} );
68+
69+
// Hide results when clicking outside.
70+
$( document ).on( "click", function( e ) {
71+
if ( ! $( e.target ).closest( "#comment_notification_recipient_search, #comment_notification_recipient_results" ).length ) {
72+
$results.hide();
73+
}
74+
} );
75+
} );

0 commit comments

Comments
 (0)