`. * Single-row keeps autoload happy and avoids per-user options. * * **Public surface.** PHP helpers: * * - `desktop_mode_presence_record( $user_id, $active )` * - `desktop_mode_presence_status_for_user( $user_id )` * - `desktop_mode_presence_get_all()` * - `desktop_mode_presence_snapshot( $user_ids = null )` * * Filters: * * - `desktop_mode_presence_inactive_after` — int seconds. Default 300. * - `desktop_mode_presence_offline_after` — int seconds. Default 120. * - `desktop_mode_presence_can_track` — bool, $user_id. Veto. * - `desktop_mode_presence_visible_users` — int[], $viewer_id. * Privacy gate for who's * surfaced to a given user. * * Actions: * * - `desktop_mode_presence_recorded( $user_id, $record )` — on every * bump. * - `desktop_mode_presence_changed( $user_id, $new, $old )` — on * state transitions only. * * REST: `/desktop-mode/v1/presence` (GET snapshot, POST mark active / * inactive). * * @package WPDesktopMode * @since 0.5.5 */ const DESKTOP_MODE_PRESENCE_OPTION = '_desktop_mode_presence'; /** * Read the entire presence map. Single autoload=false option. * * @since 0.5.5 * * @return array */ function desktop_mode_presence_get_all() { $raw = get_option( DESKTOP_MODE_PRESENCE_OPTION, array() ); if ( ! is_array( $raw ) ) { return array(); } $out = array(); foreach ( $raw as $uid => $record ) { $uid = (int) $uid; if ( $uid <= 0 || ! is_array( $record ) ) { continue; } $out[ $uid ] = array( 'last_seen_ms' => isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0, 'last_active_ms' => isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0, ); } return $out; } /** * Record a "user is alive" heartbeat. Bumps `last_seen_ms`. If * `$active` is true, also bumps `last_active_ms` (the user just * interacted, not just held a tab open). * * Pass-through to the option; cheap enough to call every Heartbeat * tick. Fires `desktop_mode_presence_recorded` on every call and * `desktop_mode_presence_changed` only when the computed status moves * between `online | inactive | offline`. * * The `desktop_mode_presence_can_track` filter is the per-user opt-out: * a plugin that hides specific accounts (compliance, "set yourself * invisible", etc.) returns false to skip the bump entirely. * * @since 0.5.5 * * @param int $user_id User to record. * @param bool $active Pass `true` when the heartbeat is paired with * explicit user activity (mousedown, keydown). * @return bool True if recorded; false if vetoed by filter or invalid id. */ function desktop_mode_presence_record( $user_id, $active = true ) { $user_id = (int) $user_id; if ( $user_id <= 0 ) { return false; } /** * Per-user veto on presence tracking. Return false to skip the * bump entirely — useful for "appear offline" toggles, audit * exemptions for sensitive accounts, or forcing a non-admin * never-tracked policy. * * @since 0.5.5 * * @param bool $can Default true. * @param int $user_id The user being tracked. */ $can = (bool) apply_filters( 'desktop_mode_presence_can_track', true, $user_id ); if ( ! $can ) { return false; } $now_ms = (int) round( microtime( true ) * 1000 ); $all = desktop_mode_presence_get_all(); $prev = isset( $all[ $user_id ] ) ? $all[ $user_id ] : array( 'last_seen_ms' => 0, 'last_active_ms' => 0, ); $prev_status = desktop_mode_presence_status_from_record( $prev ); $next = array( 'last_seen_ms' => $now_ms, 'last_active_ms' => $active ? $now_ms : (int) $prev['last_active_ms'], ); $all[ $user_id ] = $next; update_option( DESKTOP_MODE_PRESENCE_OPTION, $all, false ); $next_status = desktop_mode_presence_status_from_record( $next ); /** * Fires on every recorded heartbeat — useful for audit logging * or third-party "who's around right now" dashboards. Fires * regardless of whether the computed status changed. * * @since 0.5.5 * * @param int $user_id * @param array $record { last_seen_ms, last_active_ms } */ do_action( 'desktop_mode_presence_recorded', $user_id, $next ); if ( $next_status !== $prev_status ) { /** * Fires when a user's computed presence status transitions. * Plugins driving "user came online / went offline" UI hook * here — the recorded action above fires every tick whether * the state changed or not, which would be too noisy. * * @since 0.5.5 * * @param int $user_id * @param string $new_status One of `online | inactive | offline`. * @param string $old_status One of `online | inactive | offline`. */ do_action( 'desktop_mode_presence_changed', $user_id, $next_status, $prev_status ); } return true; } /** * Compute presence status from a record. * * Pure function — given the same `(record, now)`, always returns * the same answer. Filters override the thresholds, not the logic * order; an `online` user transitions through `inactive` to * `offline` if they're idle long enough. * * @since 0.5.5 * * @param array $record { last_seen_ms?: int, last_active_ms?: int } * @return string `online | inactive | offline` */ function desktop_mode_presence_status_from_record( $record ) { $now_ms = (int) round( microtime( true ) * 1000 ); $last_seen = isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0; $last_active = isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0; /** * Inactive threshold (default 300s = 5 min). Online users * transition to `inactive` when they haven't moused / typed * for this long, even if Heartbeat keeps firing. * * @since 0.5.5 * * @param int $seconds */ $inactive_after = (int) apply_filters( 'desktop_mode_presence_inactive_after', 300 ); /** * Offline threshold (default 120s = 2 min). Inactive / online * users transition to `offline` when the last Heartbeat is * older than this. * * @since 0.5.5 * * @param int $seconds */ $offline_after = (int) apply_filters( 'desktop_mode_presence_offline_after', 120 ); if ( $now_ms - $last_seen > $offline_after * 1000 ) { return 'offline'; } if ( $now_ms - $last_active > $inactive_after * 1000 ) { return 'inactive'; } return 'online'; } /** * Look up presence status for a single user. * * @since 0.5.5 * * @param int $user_id * @return string `online | inactive | offline` */ function desktop_mode_presence_status_for_user( $user_id ) { $all = desktop_mode_presence_get_all(); $record = isset( $all[ (int) $user_id ] ) ? $all[ (int) $user_id ] : array(); return desktop_mode_presence_status_from_record( (array) $record ); } /** * Build a presence snapshot. With `$user_ids = null` returns every * tracked user; with a list returns only those ids (useful for * "users I care about" filtering — e.g., a plugin that surfaces * the subset of users relevant to the viewer). * * Output shape uses string keys so the JSON encoder produces an * object (not a sparse array) when the smallest id isn't 1. * * @since 0.5.5 * * @param int[]|null $user_ids Restrict to these ids. `null` = all. * @return array */ function desktop_mode_presence_snapshot( $user_ids = null ) { $all = desktop_mode_presence_get_all(); $out = array(); if ( null === $user_ids ) { $ids = array_keys( $all ); } else { $ids = array(); foreach ( (array) $user_ids as $uid ) { $uid = (int) $uid; if ( $uid > 0 ) { $ids[] = $uid; } } } foreach ( $ids as $uid ) { $record = isset( $all[ $uid ] ) ? $all[ $uid ] : array(); $out[ (string) $uid ] = array( 'status' => desktop_mode_presence_status_from_record( $record ), 'lastSeenMs' => isset( $record['last_seen_ms'] ) ? (int) $record['last_seen_ms'] : 0, 'lastActiveMs' => isset( $record['last_active_ms'] ) ? (int) $record['last_active_ms'] : 0, ); } return $out; } /** * Filter a list of candidate user ids down to those a given viewer * is allowed to see presence for. Defaults to passing the list * through unchanged — plugins implementing per-team / per-role * privacy boundaries hook `desktop_mode_presence_visible_users` * (e.g., "subscribers can only see other subscribers' presence"). * * @since 0.5.5 * * @param int[] $candidate_user_ids * @param int $viewer_id Defaults to the current user. * @return int[] */ function desktop_mode_presence_visible_users( $candidate_user_ids, $viewer_id = 0 ) { $viewer_id = (int) $viewer_id ?: get_current_user_id(); $ids = array(); foreach ( (array) $candidate_user_ids as $uid ) { $uid = (int) $uid; if ( $uid > 0 ) { $ids[] = $uid; } } $ids = array_values( array_unique( $ids ) ); /** * Filter the list of user ids whose presence is visible to * `$viewer_id`. Default behaviour: all candidates pass. Hook * to enforce privacy — e.g., subscribers only see other * subscribers; admins see everyone; an opt-out list never shows. * * @since 0.5.5 * * @param int[] $ids Candidate user ids. * @param int $viewer_id The user requesting visibility. */ return (array) apply_filters( 'desktop_mode_presence_visible_users', $ids, $viewer_id ); } /** * Daily cron: prune presence entries for users idle >14 days. * Keeps the option compact even on long-running sites. * * @since 0.5.5 */ function desktop_mode_presence_cron_prune() { $all = desktop_mode_presence_get_all(); if ( empty( $all ) ) { return; } $threshold = (int) round( microtime( true ) * 1000 ) - ( 14 * DAY_IN_SECONDS * 1000 ); $pruned = array(); foreach ( $all as $uid => $record ) { if ( ( (int) $record['last_seen_ms'] ) < $threshold ) { continue; } $pruned[ (int) $uid ] = $record; } if ( count( $pruned ) !== count( $all ) ) { update_option( DESKTOP_MODE_PRESENCE_OPTION, $pruned, false ); } } add_action( 'desktop_mode_presence_daily_prune', 'desktop_mode_presence_cron_prune' ); /** * Schedule the daily cron once. Idempotent. * * @since 0.5.5 */ function desktop_mode_presence_schedule_cron() { if ( ! wp_next_scheduled( 'desktop_mode_presence_daily_prune' ) ) { wp_schedule_event( time() + DAY_IN_SECONDS, 'daily', 'desktop_mode_presence_daily_prune' ); } } add_action( 'init', 'desktop_mode_presence_schedule_cron', 50 ); /* ------------------------------------------------------------------------- * Heartbeat integration * ----------------------------------------------------------------------- */ /** * Heartbeat handler — bumps presence on every tick a desktop-mode * user is on the page. Returns the visible-presence snapshot in * the response so the client store can update without a separate * REST round-trip. * * Triggered by the client opting in via `desktop_mode_presence_active: * true` in the heartbeat-send payload, with optional * `desktop_mode_user_active` (mousedown / keydown within the * inactive-threshold window). * * @since 0.5.5 * * @param array $response Pre-filtered response. * @param array $data Client-sent payload. * @return array */ function desktop_mode_presence_heartbeat_received( $response, $data ) { if ( ! is_array( $response ) ) { $response = array(); } if ( empty( $data['desktop_mode_presence_active'] ) ) { return $response; } if ( ! function_exists( 'desktop_mode_is_enabled' ) || ! desktop_mode_is_enabled() ) { return $response; } $user_id = (int) get_current_user_id(); $user_active = ! empty( $data['desktop_mode_user_active'] ); desktop_mode_presence_record( $user_id, $user_active ); // Snapshot the users this viewer is allowed to see — by default // all tracked users; plugins can narrow via the // `desktop_mode_presence_visible_users` filter. $all_ids = array_keys( desktop_mode_presence_get_all() ); $visible = desktop_mode_presence_visible_users( $all_ids, $user_id ); $response['desktop_mode_presence'] = array( 'snapshot' => desktop_mode_presence_snapshot( $visible ), 'serverTimeMs' => (int) round( microtime( true ) * 1000 ), ); return $response; } add_filter( 'heartbeat_received', 'desktop_mode_presence_heartbeat_received', 5, 2 ); /* ------------------------------------------------------------------------- * REST endpoints * ----------------------------------------------------------------------- */ /** * Permission gate for presence endpoints — login required + * desktop mode enabled. Delegates to the shared * {@see desktop_mode_rest_require_enabled()} gate. * * @since 0.5.5 * * @return true|WP_Error */ function desktop_mode_presence_rest_permission() { return desktop_mode_rest_require_enabled(); } /** * Register `/desktop-mode/v1/presence` routes. * * @since 0.5.5 */ function desktop_mode_presence_register_rest_routes() { register_rest_route( 'desktop-mode/v1', '/presence', array( array( 'methods' => WP_REST_Server::READABLE, 'permission_callback' => 'desktop_mode_presence_rest_permission', 'callback' => 'desktop_mode_presence_rest_get', ), array( 'methods' => WP_REST_Server::CREATABLE, 'permission_callback' => 'desktop_mode_presence_rest_permission', 'callback' => 'desktop_mode_presence_rest_post', 'args' => array( 'active' => array( 'type' => 'boolean' ), 'inactive' => array( 'type' => 'boolean' ), ), ), ) ); } add_action( 'rest_api_init', 'desktop_mode_presence_register_rest_routes' ); /** * GET /desktop-mode/v1/presence — current snapshot, narrowed by the * visibility filter. */ function desktop_mode_presence_rest_get() { $viewer_id = (int) get_current_user_id(); $all_ids = array_keys( desktop_mode_presence_get_all() ); $visible = desktop_mode_presence_visible_users( $all_ids, $viewer_id ); return rest_ensure_response( array( 'snapshot' => desktop_mode_presence_snapshot( $visible ), 'serverTimeMs' => (int) round( microtime( true ) * 1000 ), ) ); } /** * POST /desktop-mode/v1/presence — explicit bump. Body shape: * * - `{ active: true }` → bump both seen + active timestamps. * - `{ active: false }` → bump seen only (window in background). * - `{ inactive: true }` → bump seen only AND zero active so the * user lands on `inactive` immediately * (the "set yourself away" UI hook). * * Defaults to `{ active: true }` when neither flag is supplied — * the simplest "I'm here" call. */ function desktop_mode_presence_rest_post( WP_REST_Request $request ) { $user_id = (int) get_current_user_id(); $active = $request->get_param( 'active' ); $inactive = (bool) $request->get_param( 'inactive' ); if ( $inactive ) { // Set the user immediately to `inactive`: bump last_seen // (still alive) but force last_active to zero (no recent // interaction). $all = desktop_mode_presence_get_all(); $rec = isset( $all[ $user_id ] ) ? $all[ $user_id ] : array( 'last_seen_ms' => 0, 'last_active_ms' => 0, ); $prev_status = desktop_mode_presence_status_from_record( $rec ); $rec['last_seen_ms'] = (int) round( microtime( true ) * 1000 ); $rec['last_active_ms'] = 0; $all[ $user_id ] = $rec; update_option( DESKTOP_MODE_PRESENCE_OPTION, $all, false ); $next_status = desktop_mode_presence_status_from_record( $rec ); do_action( 'desktop_mode_presence_recorded', $user_id, $rec ); if ( $next_status !== $prev_status ) { do_action( 'desktop_mode_presence_changed', $user_id, $next_status, $prev_status ); } } else { $flag = ( null === $active ) ? true : (bool) $active; desktop_mode_presence_record( $user_id, $flag ); } return rest_ensure_response( array( 'ok' => true ) ); }