Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 64 additions & 18 deletions alerts/class-alert-trigger-author.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,10 @@ class Alert_Trigger_Author extends Alert_Trigger {
* @return bool False on failure, otherwise should return original value of $success.
*/
public function check_record( $success, $record_id, $recordarr, $alert ) {
if ( ! empty( $alert->alert_meta['trigger_author'] ) && intval( $alert->alert_meta['trigger_author'] ) !== intval( $recordarr['user_id'] ) ) {
// The stored value may be '0' (WP-CLI), which empty() would drop and
// turn the trigger into "any author".
$trigger_author = isset( $alert->alert_meta['trigger_author'] ) ? (string) $alert->alert_meta['trigger_author'] : '';
if ( '' !== $trigger_author && (int) $trigger_author !== (int) $recordarr['user_id'] ) {
return false;
}

Expand All @@ -60,12 +63,30 @@ public function add_fields( $form, $alert = array() ) {
$value = $alert->alert_meta['trigger_author'];
}

$picker = $this->plugin->user_picker->get( $this->plugin->admin->get_preload_users_max() );

// Over the preload cap: Ajax user combobox instead of a preloaded select.
if ( $picker['ajax'] ) {
$form->add_field(
'user_combobox',
array(
'name' => esc_attr( $this->field_key ),
'value' => esc_attr( $value ),
'selected_label' => $this->plugin->user_picker->label_for_value( $value ),
'data' => array(
'placeholder' => __( 'Any Author', 'stream' ),
),
)
);
return;
}

$form->add_field(
'grouped_select',
array(
'name' => esc_attr( $this->field_key ),
'value' => esc_attr( $value ),
'options' => $this->get_values(),
'options' => $this->append_stored_value_option( $this->get_values(), $value ),
'data' => array(
'placeholder' => __( 'Any Author', 'stream' ),
),
Expand Down Expand Up @@ -113,7 +134,7 @@ function ( $login ) {
$all_records[] = array(
'id' => $user->id,
'value' => $user->id,
'text' => $user->get_display_name(),
'text' => $this->plugin->user_picker->label( $user->id ),
);
}

Expand All @@ -131,11 +152,37 @@ function ( $login ) {
*/
public function save_fields( $alert ) {
$input = wp_stream_filter_input( INPUT_POST, $this->field_key );
if ( array_key_exists( $input, $this->get_values( $alert ) ) ) {
$alert->alert_meta['trigger_author'] = $input;
} else {
$alert->alert_meta['trigger_author'] = '';
$input = is_scalar( $input ) ? (string) $input : '';

// Only a user ID (or 0 for WP-CLI) is stored; anything else clears the
// trigger. Membership checks are impossible in combobox (Ajax) mode,
// where the option list is not rendered server-side.
$alert->alert_meta['trigger_author'] = ctype_digit( $input ) ? $input : '';
}

/**
* Append the stored author to the option list when missing (e.g. deleted user).
*
* @param array $options Picker options.
* @param string $current Stored author id.
* @return array
*/
private function append_stored_value_option( array $options, $current ) {
if ( ! ctype_digit( (string) $current ) ) {
return $options;
}

$values = array_map( 'strval', array_column( $options, 'value' ) );
if ( in_array( (string) $current, $values, true ) ) {
return $options;
}

$options[] = array(
'value' => (string) $current,
'text' => $this->plugin->user_picker->label( (int) $current ),
);

return $options;
}

/**
Expand All @@ -149,18 +196,17 @@ public function save_fields( $alert ) {
* @return string
*/
public function get_display_value( $context = 'normal', $alert = null ) {
$author = ( ! empty( $alert->alert_meta['trigger_author'] ) ) ? $alert->alert_meta['trigger_author'] : null;
if ( empty( $author ) ) {
$author = __( 'Any User', 'stream' );
} elseif ( is_numeric( $author ) ) {
$author_data = get_userdata( $author );
if ( $author_data ) {
$author = $author_data->display_name;
} else {
$author = __( 'Unknown User', 'stream' );
}
// Note: the stored value may be '0' (WP-CLI), which is falsy — use isset().
$trigger_author = $alert?->alert_meta['trigger_author'] ?? '';

if ( '' === $trigger_author ) {
return __( 'Any User', 'stream' );
}

if ( ctype_digit( $trigger_author ) ) {
return $this->plugin->user_picker->label( (int) $trigger_author );
}

return ucfirst( $author );
return ucfirst( $trigger_author );
}
}
2 changes: 1 addition & 1 deletion changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
### Enhancements

- Add an Outgoing Webhook alert (HTTP POST or PUT, optional headers, JSON body with record-field placeholders). IFTTT is no longer offered when creating a new alert; existing IFTTT alerts still fire. To keep using Maker, configure a webhook whose URL is `https://maker.ifttt.com/trigger/{event}/with/key/{key}`.
- Replace Select2 with native `<select>` elements across the admin (records filters, settings exclude rules, alert triggers). Author/role filters offer every user in grouped native selects; IP exclusion rules accept a comma-separated list in a plain text field. Relative timestamps now use `Intl.RelativeTimeFormat` (locale-aware) with the same bold relative + absolute date presentation as before. The `select2` and `jquery-timeago` dependencies and their bundled vendor copies are gone, and the unused `stream_get_users` and `stream_get_ips` Ajax actions were removed. Editing an alert now also regenerates its list title ("Author > Context > Action") so it matches the saved triggers instead of keeping the creation-time summary.
- Replace Select2 with native `<select>` elements across the admin (records filters, settings exclude rules, alert triggers). Author/role filters offer every user in grouped native selects; IP exclusion rules accept a comma-separated list in a plain text field. Relative timestamps now use `Intl.RelativeTimeFormat` (locale-aware) with the same bold relative + absolute date presentation as before. The `select2` and `jquery-timeago` dependencies and their bundled vendor copies are gone, and the unused `stream_get_users` and `stream_get_ips` Ajax actions were removed. User pickers preload every user in a native `<select>` while the site stays under the new `wp_stream_preload_users_max` filter (default 50); larger sites get an accessible Ajax combobox (keyboard navigation, `wp.a11y` announcements) backed by the same `wp_stream_filters` endpoint. Quick-editing an alert no longer drops its author trigger; `WP-CLI` shows as itself instead of "N/A" in pickers and alert summaries; and a stored `WP-CLI` author trigger now actually matches only WP-CLI records (the numeric string `"0"` was previously treated as unset). Editing an alert now also regenerates its list title ("Author > Context > Action") so it matches the saved triggers instead of keeping the creation-time summary.
- Stop tracking Jetpack modules that no longer exist (Google+ authorship, Mobile theme, Custom CSS). Remaining Jetpack module logging was audited against Jetpack 15.5 on 2026-09-07.

### Development
Expand Down
69 changes: 7 additions & 62 deletions classes/class-admin-ajax.php
Original file line number Diff line number Diff line change
Expand Up @@ -137,34 +137,15 @@ public function ajax_filters() {
check_ajax_referer( 'stream_filters_user_search_nonce', 'nonce' );

switch ( wp_stream_filter_input( INPUT_GET, 'filter' ) ) {
// User picker search: shared by the records filter, settings exclude
// rules, and alert author trigger comboboxes. See the DB user picker
// section for the option-source contract.
case 'user_id':
$users = array_merge(
array(
0 => (object) array(
'display_name' => 'WP-CLI',
),
),
get_users()
);

$search = wp_stream_filter_input( INPUT_GET, 'q' );
if ( is_string( $search ) && '' !== $search ) {
// `search` arg for get_users() is not enough.
$filtered = array();
foreach ( $users as $key => $user ) {
if ( self::user_display_name_contains( $user, $search ) ) {
$filtered[ $key ] = $user;
}
}
$users = $filtered;
}

if ( count( $users ) > $this->admin->preload_users_max ) {
$users = array_slice( $users, 0, $this->admin->preload_users_max );
}

// Get gravatar / roles for final result set.
$results = $this->get_users_record_meta( $users );
$search = is_string( $search ) ? $search : '';
$limit = max( 20, $this->admin->get_preload_users_max() );

$results = $this->admin->plugin->user_picker->search( $search, $limit );

break;
}
Expand All @@ -175,31 +156,6 @@ public function ajax_filters() {

die();
}

/**
* Return relevant user meta data for Ajax filter results.
*
* @param array $authors Author data keyed by user ID.
* @return array
*/
public function get_users_record_meta( $authors ) {
$authors_records = array();

foreach ( $authors as $user_id => $args ) {
$author = new Author( $args->ID );

$authors_records[ $user_id ] = array(
'text' => $author->get_display_name(),
'id' => $author->id,
'label' => $author->get_display_name(),
'icon' => $author->get_avatar_src( 32 ),
'title' => '',
);
}

return $authors_records;
}

/**
* Render confirmation notices keyed by the wp_stream_message query arg.
*
Expand Down Expand Up @@ -230,15 +186,4 @@ public function maybe_display_message() {
esc_html( $notices[ $message ] )
);
}

/**
* Whether a user display name contains the search needle.
*
* @param object $user User-like object with display_name.
* @param string $search Search needle.
* @return bool
*/
private static function user_display_name_contains( $user, string $search ): bool {
return false !== mb_strpos( mb_strtolower( $user->display_name ), mb_strtolower( $search ) );
}
}
49 changes: 41 additions & 8 deletions classes/class-admin-assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,29 @@ private function register_hooks(): void {
add_action( 'admin_enqueue_scripts', array( $this, 'admin_menu_css' ) );
}

/**
* Script data shared by every screen that renders the user combobox.
*
* @return array{userSearchNonce: string, userSearchI18n: array<string, string>}
*/
public function user_combobox_l10n(): array {
return array(
'userSearchNonce' => wp_create_nonce( 'stream_filters_user_search_nonce' ),
'userSearchI18n' => array(
'noUsers' => __( 'No users found.', 'stream' ),
'searchError' => __( 'Unable to search users.', 'stream' ),
'minChars' => __( 'Type at least 2 characters to search users.', 'stream' ),
/* translators: %d: number of matching users */
'foundSingular' => _n( '%d user found.', '%d users found.', 1, 'stream' ),
/* translators: %d: number of matching users */
'foundPlural' => _n( '%d user found.', '%d users found.', 2, 'stream' ),
'rolesHeader' => __( 'Roles', 'stream' ),
'usersHeader' => __( 'Users', 'stream' ),
'searchUsersHint' => __( 'Start typing to search users.', 'stream' ),
),
);
}

/**
* Enqueue scripts/styles for admin screen
*
Expand All @@ -43,22 +66,32 @@ public function admin_enqueue_scripts( $hook ) {
if ( in_array( $hook, $this->admin->menu->screen_id, true ) ) {
$this->admin->plugin->enqueue_asset(
'admin',
array(),
array(
'i18n' => array(
'confirm_purge' => __( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
'confirm_defaults' => __( 'Are you sure you want to reset all site settings to default? This cannot be undone.', 'stream' ),
'wp-a11y',
),
array_merge(
array(
'i18n' => array(
'confirm_purge' => __( 'Are you sure you want to delete all Stream activity records from the database? This cannot be undone.', 'stream' ),
'confirm_defaults' => __( 'Are you sure you want to reset all site settings to default? This cannot be undone.', 'stream' ),
),
'locale' => strtolower( substr( get_locale(), 0, 2 ) ),
'gmt_offset' => get_option( 'gmt_offset' ),
),
'locale' => strtolower( substr( get_locale(), 0, 2 ) ),
'gmt_offset' => get_option( 'gmt_offset' ),
$this->user_combobox_l10n()
)
);

$this->admin->plugin->enqueue_asset(
'admin-exclude',
array(),
array(
'getActionsNonce' => wp_create_nonce( 'stream_get_actions' ),
'wp-a11y',
),
array_merge(
array(
'getActionsNonce' => wp_create_nonce( 'stream_get_actions' ),
),
$this->user_combobox_l10n()
)
);

Expand Down
20 changes: 20 additions & 0 deletions classes/class-admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,26 @@ class Admin {
*/
public int $preload_users_max = 50;

/**
* Filtered preload cap for user pickers.
*
* @return int
*/
public function get_preload_users_max(): int {
/**
* Filters the maximum number of users preloaded into a picker.
*
* Above this cap the picker switches to Ajax search. Zero forces Ajax.
*
* @since 5.0.0
*
* @param int $preload_users_max Default cap (50).
*/
$max = apply_filters( 'wp_stream_preload_users_max', $this->preload_users_max );

return max( 0, (int) $max );
}

/**
* Admin notices, collected and displayed on proper action
*/
Expand Down
10 changes: 7 additions & 3 deletions classes/class-alert.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,9 @@ class Alert {
/**
* Alert meta data
*
* @var int
* @var array
*/
public $alert_meta;
public array $alert_meta;

/**
* Class constructor
Expand All @@ -71,7 +71,11 @@ public function __construct( $item, public $plugin ) {
$this->author = isset( $item->author ) ? $item->author : null;

$this->alert_type = isset( $item->alert_type ) ? $item->alert_type : null;
$this->alert_meta = isset( $item->alert_meta ) ? $item->alert_meta : array();
// get_post_meta() returns an empty string for posts with no stored
// meta; only accept real arrays so the typed property stays honest.
$this->alert_meta = isset( $item->alert_meta ) && is_array( $item->alert_meta )
? $item->alert_meta
: array();
}

/**
Expand Down
12 changes: 8 additions & 4 deletions classes/class-alerts-admin-ui.php
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,15 @@ public function register_scripts() {
'alerts',
array(
'inline-edit-post',
'wp-a11y',
),
array(
'any' => __( 'Any', 'stream' ),
'anyContext' => __( 'Any Context', 'stream' ),
'getActionsNonce' => wp_create_nonce( 'stream_get_actions' ),
array_merge(
array(
'any' => __( 'Any', 'stream' ),
'anyContext' => __( 'Any Context', 'stream' ),
'getActionsNonce' => wp_create_nonce( 'stream_get_actions' ),
),
$this->plugin->admin->assets->user_combobox_l10n()
)
);
}
Expand Down
7 changes: 7 additions & 0 deletions classes/class-alerts-list.php
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,13 @@ public function column_data( $column_name, $post_id ) {
<input type="hidden" name="wp_stream_trigger_context" value="<?php echo esc_attr( $trigger_context ); ?>" />
<input type="hidden" name="wp_stream_trigger_action" value="<?php echo esc_attr( $trigger_action ); ?>" />
<?php
$trigger_author = isset( $alert->alert_meta['trigger_author'] )
? (string) $alert->alert_meta['trigger_author']
: '';
?>
<input type="hidden" name="wp_stream_trigger_author" value="<?php echo esc_attr( $trigger_author ); ?>" />
<input type="hidden" name="wp_stream_trigger_author_label" value="<?php echo esc_attr( $this->plugin->user_picker->label_for_value( $trigger_author ) ); ?>" />
<?php
echo wp_kses_post( $this->custom_column_actions( $post_id ) );
break;
case 'alert_type':
Expand Down
Loading
Loading