Shows which Ultimate Member users are online β as a block, a widget or the [whos_online] shortcode, as a status dot on profiles and member directories, and as an Online/Offline directory filter.
This is an independent fork of Ultimate Member - Online (v2.2.2) with the presence layer rebuilt. It is not affiliated with, endorsed by, or supported by Ultimate Member.
- Online users list as a block, a sidebar widget, or
[whos_online max="11" roles="all"] - Status dot beside a member's name on their profile and in every member directory view
- Online Status profile field and an Online / Offline member directory filter
- Per-user opt-out on the account page's Privacy tab, honoured everywhere including the Private Messages integration
- Online count in Ultimate Member's REST stats; integrates with the Private Messages and Friends extensions
- Configurable offline timeout, plus an optional ping for sites behind full-page caching
- WordPress personal-data exporter and eraser, and suggested privacy policy text
- Overridable templates,
screen-reader-textlabels on every status marker
- Copy the
um-online-usersfolder into/wp-content/plugins/ - Activate it β Ultimate Member 2.7.0+ must be active
- Configure at Ultimate Member β Settings β Extensions β Online
| Setting | Default | What it does |
|---|---|---|
| Show online stats in member directory | off | Enables the status dot in member directories |
| Minutes until a user counts as offline | 15 | How long after a page view a user stops being shown as online |
| Enable cache-compatible presence ping | off | Turn on for sites with full-page caching (see below) |
The original extension kept every online user in a single autoloaded option (um_online_users) and rewrote the whole option on every authenticated page view. That meant:
- one
update_option()write per page view per logged-in user; - a read-modify-write race β two concurrent requests each wrote back their own stale copy of the array, so one user's presence was silently lost;
- the full online-user map unserialized on every request, including admin, AJAX and REST.
This fork stores one integer per user in usermeta:
- writes are per-row, so there is no contention and nothing is lost;
- writes are throttled to at most one per user per minute (
um_online_users_write_throttle); - reads come out of WordPress's existing user-meta cache;
- list queries run through
WP_User_Querybehind a 60-second transient (um_online_users_cache_ttl); - an hourly cron trims expired rows β housekeeping only, since
is_online()checks the timestamp itself.
Timestamps are always GMT. The original mixed time() and current_time( 'timestamp' ), so changing the site's timezone either flushed everyone offline or pinned them online permanently.
Presence is recorded on both template_redirect and admin_init, so a member working in the dashboard stays online. The original bailed on is_admin() and silently timed them out.
Presence is stored in usermeta, which is shared across a network, but the visible list is filtered by WP_User_Query and therefore scoped to the users of the current site. On a network with shared users, someone active on site A is not listed as online on site B unless they are a member of site B as well. There is no cross-network aggregate view.
Presence is normally recorded on template_redirect. A cached page never reaches PHP, so on sites using WP Rocket, LiteSpeed, Varnish or Cloudflare APO nobody would ever appear online. Turning on Enable cache-compatible presence ping loads a ~1 KB script that reports presence with one admin-ajax request per logged-in visitor per interval (a third of the timeout), throttled through sessionStorage and paused while the tab is hidden.
um_online_interval // int Offline timeout in minutes (default: the setting, 15)
um_online_users_write_throttle // int Seconds between presence writes per user (default 60)
um_online_users_cache_ttl // int List-query cache lifetime in seconds (default 60)
um_online_users_max_results // int Hard ceiling on users returned per query (default 500)
um_online_users_query_args // array WP_User_Query arguments for the online list
um_online_users_is_hidden // bool Whether a user's status is hidden
um_online_users_can_view // bool Whether a user may appear in the list
um_online_users_avatar // string Avatar markup used in the list
um_online_users_purge_batch // int Rows the cleanup cron trims per run (default 1000)um_online_users_touched // ( int $user_id, int $timestamp ) after presence is recorded
um_online_users_cleanup // hourly cron eventUM()->Online()->is_online( $user_id ); // bool, false for users who opted out
UM()->Online()->count(); // int
UM()->Online()->get_user_ids( array( // int[], most recently active first
'roles' => array( 'um_member' ),
'number' => 20,
) );
UM()->Online()->store()->last_seen( $user_id ); // int GMT timestamp, 0 if never seenUM()->Online()->get_users() and UM()->Online()->users still return the original id => last seen array (or false) for backward compatibility, but they cannot be paginated β prefer get_user_ids().
[whos_online max="11" roles="all"] the documented tag
[um_whos_online] prefixed, guaranteed collision-free
[ultimatemember_online] the original extension's tag, still honoured
All three render identically. max is how many members show before the "+N" toggle; roles is a comma-separated list of role slugs, or all.
A tag is only claimed if nothing else has registered it β the check runs on init at priority 20, after other plugins have registered theirs. If another plugin already owns whos_online or ultimatemember_online, this plugin leaves it alone rather than silently breaking it, and um_whos_online still works.
Copy any of these into yourtheme/ultimate-member/um-online-users/:
| Template | Purpose |
|---|---|
online.php |
List wrapper and the "+N" toggle |
online-users.php |
The user tiles β also rendered on its own for the "+N" AJAX response |
online-marker.php |
The status dot |
online-text.php |
The status as text |
nobody.php |
Shown when nobody is online |
Overrides in the original yourtheme/ultimate-member/um-online/ path are still honoured.
Deactivate the original, activate this one. Nothing else is required.
- Presence data is migrated out of
um_online_usersinto usermeta on the first admin page load, in batches of 500. Nobody is logged out or shown as offline as a side effect. - The
online_show_statssetting, the_hide_online_statususer preference, the[ultimatemember_online]shortcode, theum_online_userswidget and its stored instances,UM()->Online(), and theum_online_intervalfilter all carry over unchanged. - Theme overrides in
ultimate-member/um-online/keep resolving. online.phpwas rewritten andonline-users.phpis new. If you overrideonline.php, re-copy it β the old one expects an$onlinearray ofid => timestampand no longer receives it.
- Presence moved from one autoloaded option, rewritten on every page view, to per-user throttled usermeta (see above).
is_online()never compared the timestamp against the timeout β it only checked that a key existed in the option. Between the 15-minute purges, every user who had ever loaded a page reported as online.clear_online_user()resetum_online_users_last_updatedto "now", so on a site where anyone logs out every 15 minutes the stale-user purge could never run and users stayed "online" indefinitely.- The list rendered every online user server-side β one
get_userdata()call and up to fifteenfile_exists()probes each β then used JavaScript to hide everything pastmax. Onlymaxusers are fetched now; the rest load on demand. get_plugin_data()ran at file scope, pullingwp-admin/includes/plugin.phpinto every front-end request just to read the version out of the plugin header.- Presence logging ran on
init, so it also fired for AJAX, cron and REST requests. It runs ontemplate_redirectnow.
- Status leak: the Private Messages integration reported a user's real online status without checking whether they had opted out.
- Inverted preference: for the new-UI boolean field, the checked value (
1) β meaning "show my online status" β was treated as hidden. The same comparison ('yes' == 1) istrueon PHP 7, so opting in could opt you out. - Filtering a member directory by Online when nobody was online applied no filter at all and listed every member. Same for the UM-metadata backend.
- The directory filter assigned
query_args['include']/['exclude']outright, discarding anything another filter had already set. Both are merged now. - Filter values arriving from the query string were compared with
in_array( 1, $value ), which resolves differently on PHP 7 and PHP 8. - Avatars were built by probing
wp-content/uploads/ultimatemember/<id>/with up to fifteenfile_exists()calls, which broke on offloaded media (S3, Cloudinary, CDN rewrites) and ignored every avatar filter. Ultimate Member's own avatar handling is used instead. - The list ignored UM profile privacy, exposing members whose profiles are private or role-restricted.
- A user changing their visibility preference stayed listed until the next purge.
roles="a, b"silently never matchedbβ the role list was never trimmed.roles="does-not-exist"fell through to "all roles" instead of returning nobody.- The per-directory "Hide online stats" logic could never evaluate to "show", and read a key the directory never set.
- Timestamps mixed local and GMT time.
- Widget values were interpolated into a
[ultimatemember_online max="β¦" roles="β¦"]string, so a stored value containing a quote broke out of the attribute. The renderer is called directly now, andmax/roleare cast and whitelisted. - Unescaped output: the status text, role labels in the widget form, and
_e()whereesc_html_e()belonged. - Role slugs from the shortcode and AJAX are checked against the roles that actually exist.
- Text domain loading moved off
plugins_loaded(the WordPress 6.7 "just in time" notice, which 2.2.2's changelog claimed to have fixed).
- The header declared
Requires PHP: 5.6while the code usedUM()->frontend()->enqueue()::get_suffix(), which needs PHP 7.0. The suffix is computed locally now and the floor is honestly stated as 7.4. - The widget read
$instance['title'],['max']and['role']with noisset()guard β undefined-index warnings on PHP 8 for a freshly dropped widget or a legacy-widget block preview. function um_online_dependencies()was declared three times inside different branches of one function β a redeclare fatal waiting for two branches to be reachable.&$thispassed by reference toadd_action().
- Online Users block (
um-online-users/online-users) with a server-side render and an inspector panel. No build step: the editor script is plain ES5 usingwp.element.createElement. - Offline timeout as a setting instead of filter-only.
- Cache-compatible presence ping.
- WordPress personal-data exporter, eraser and suggested privacy policy text.
screen-reader-textlabels on every status marker, which previously conveyed state through a coloured icon and atitlealone.- Stylesheet rebuilt on flexbox with CSS custom properties, plus selectors for Ultimate Member 2.9's directory markup β the original only knew the older
uimob*classes. index.phpguards,composer.json,phpcs.xml.dist,.editorconfigand a regenerated.pot.- Deactivation now clears the cron event; uninstall removes presence metadata too.
- WordPress 5.6+
- PHP 7.4+
- Ultimate Member 2.7.0+
GPL-2.0-or-later. See LICENSE.
Forked from Ultimate Member - Online by Ultimate Member, distributed under the GPL-2.0-or-later; the original copyright is retained. "Ultimate Member" is a trademark of its owner and is used here only to describe compatibility.