Skip to main content

Members on-line Widget (PHP snippet)

Yesterday I talked with one community member here and we agreed that on-line status widget would be great as widget in right feed sidebar. I created snippet which create it and render well. If you want use it, feel free to copy it. Make sure you copy whole snippet, recommend you copy it in notepad and then to editor in your site (code snippets, fluent snippets etc..)

Snippet:

<?php
/**

  • Online Members Widget for FluentCommunity right sidebar.
  • Shows online member count + avatars, with live refresh (no reload).
  • "Online" = last_activity within N minutes (same logic FluentCommunity's
  • own frontend uses). No public is_online field/endpoint exists, so this
  • reads fcom_xprofile directly.
  • Notes:
    • last_activity is written in WP's local timezone (current_time('mysql')),
  • not UTC - so the threshold below is computed the same way, not via
  • MySQL's NOW().
    • Script runs via portal_footer, not inside the after_contents filter -
  • that filter renders through Vue innerHTML, which never executes
  • embedded tags.
    • Repositioning uses a light interval poll (not a subtree MutationObserver,
  • which hurt performance in testing), and waits for the native widgets'
  • own skeleton loader to clear first.
    • Live refresh polls a small REST route every 60s; keep that interval in
  • sync with $cacheSeconds or refreshes just re-serve the same cache.
    */

if (!function_exists('fcw_online_widget_get_data')) {
function fcw_online_widget_get_data($onlineThresholdMinutes = 5, $maxAvatarsToShow = 14, $cacheSeconds = 60)
{
$cacheKey = 'fcw_online_members_widget_v1';
$data = get_transient($cacheKey);

    if (is_array($data) && isset($data['total'], $data['avatars'])) {
        return $data;
    }

    global $wpdb;

    $table = $wpdb->prefix . 'fcom_xprofile';

    // Local WP time, not MySQL NOW() - see timezone note above.
    $thresholdTimestamp = current_time('timestamp') - ($onlineThresholdMinutes * 60);
    $thresholdMysql     = date('Y-m-d H:i:s', $thresholdTimestamp);

    $totalOnline = (int) $wpdb->get_var($wpdb->prepare(
        "SELECT COUNT(*) FROM {$table} WHERE status = %s AND last_activity >= %s",
        'active',
        $thresholdMysql
    ));

    $userIds = $wpdb->get_col($wpdb->prepare(
        "SELECT user_id FROM {$table}
         WHERE status = %s AND last_activity >= %s
         ORDER BY last_activity DESC
         LIMIT %d",
        'active',
        $thresholdMysql,
        $maxAvatarsToShow
    ));

    $avatars = [];

    if (!empty($userIds) && class_exists('\FluentCommunity\App\Services\ProfileHelper')) {
        $portalSlug = defined('FLUENT_COMMUNITY_PORTAL_SLUG') ? FLUENT_COMMUNITY_PORTAL_SLUG : 'portal';
        $portalSlug = trim($portalSlug, '/');

        foreach ($userIds as $userId) {
            $profile = \FluentCommunity\App\Services\ProfileHelper::getProfile((int) $userId);

            if (!$profile || empty($profile->username)) {
                continue;
            }

            $avatarUrl = !empty($profile->avatar)
                ? $profile->avatar
                : get_avatar_url($userId, ['size' => 80]);

            $profileUrl = home_url('/' . $portalSlug . '/u/' . $profile->username . '/');

            $avatars[] = [
                'url'  => $avatarUrl,
                'link' => $profileUrl,
                'name' => $profile->display_name ?: $profile->username,
            ];
        }
    }

    $data = [
        'total'   => $totalOnline,
        'avatars' => $avatars,
    ];

    set_transient($cacheKey, $data, $cacheSeconds);

    return $data;
}

}

if (!function_exists('fcw_online_widget_render_body')) {
function fcw_online_widget_render_body($totalOnline, $avatars)
{
$totalOnline = (int) $totalOnline;
$extraCount = max(0, $totalOnline - count($avatars));

    $memberWord = $totalOnline === 1 ? 'member' : 'members';
    $verb       = $totalOnline === 1 ? 'is' : 'are';

    $avatarsHtml = '';
    foreach ($avatars as $avatar) {
        $avatarsHtml .= '<a class="fcw-online-avatar-link" href="' . esc_url($avatar['link']) . '" title="' . esc_attr($avatar['name']) . '">'
            . '<img class="fcw-online-avatar-img" src="' . esc_url($avatar['url']) . '" alt="' . esc_attr($avatar['name']) . '" loading="lazy">'
            . '</a>';
    }
    if ($extraCount > 0) {
        $avatarsHtml .= '<span class="fcw-online-extra">+' . (int) $extraCount . '</span>';
    }

    if (!empty($avatars)) {
        return '<div class="fcw-online-avatars">' . $avatarsHtml . '</div>'
            . '<p class="fcw-online-caption">' . $totalOnline . ' ' . $memberWord . ' ' . $verb . ' currently online</p>';
    }

    return '<div class="fcw-online-empty">No one is online right now</div>';
}

}

add_filter('fluent_community/activity/after_contents', function ($content, $spaceId) {

if (is_array($content)) {
    return $content;
}
if (!is_string($content)) {
    $content = (string) $content;
}

// ==== Settings ====
$onlineThresholdMinutes = 5;
$maxAvatarsToShow       = 14;
$cacheSeconds           = 60;

$data        = fcw_online_widget_get_data($onlineThresholdMinutes, $maxAvatarsToShow, $cacheSeconds);
$totalOnline = (int) $data['total'];
$avatars     = $data['avatars'];

$bodyHtml = fcw_online_widget_render_body($totalOnline, $avatars);

return '
<style>
    .fcw-online-box{
        margin-bottom:20px;
        border:1px solid #e4e7eb;
        border-radius:10px;
        padding:16px;
        background:#fff;
        opacity:0;
        transform:translateY(-6px);
        transition:opacity .25s ease, transform .25s ease;
    }
    .fcw-online-box.fcw-online-visible{
        opacity:1;
        transform:translateY(0);
    }
    .fcw-online-header{
        display:flex;
        align-items:center;
        justify-content:space-between;
        margin-bottom:12px;
    }
    .fcw-online-header-left{
        display:flex;
        align-items:center;
        gap:8px;
    }
    .fcw-online-dot{
        width:9px;
        height:9px;
        border-radius:50%;
        background:#22c55e;
        box-shadow:0 0 0 3px rgba(34,197,94,.15);
        flex-shrink:0;
    }
    .fcw-online-title{
        font-weight:500;
        color:#333;
    }
    .fcw-online-avatars{
        display:flex;
        align-items:center;
    }
    .fcw-online-avatar-link{
        position:relative;
        display:block;
        width:34px;
        height:34px;
        border-radius:50%;
        margin-right:-10px;
        transition:transform .15s ease;
    }
    .fcw-online-avatar-link:hover{
        transform:translateY(-2px);
        z-index:1;
    }
    .fcw-online-avatar-img{
        width:100%;
        height:100%;
        border-radius:50%;
        object-fit:cover;
        display:block;
        border:2px solid #fff;
    }
    .fcw-online-extra{
        width:34px;
        height:34px;
        border-radius:50%;
        background:#f1f3f5;
        display:flex;
        align-items:center;
        justify-content:center;
        font-size:11px;
        font-weight:500;
        color:#666;
        border:2px solid #fff;
    }
    .fcw-online-caption{
        font-size:12px;
        color:#888;
        margin:10px 0 0;
    }
    .fcw-online-empty{
        font-size:13px;
        color:#888;
    }
    html.dark .fcw-online-box{
        background:#2b2e33;
        border-color:rgba(255,255,255,.08);
    }
    html.dark .fcw-online-title{
        color:#fff;
    }
    html.dark .fcw-online-avatar-img,
    html.dark .fcw-online-extra{
        border-color:#2b2e33;
    }
    html.dark .fcw-online-extra{
        background:#3a3d43;
        color:#ccc;
    }
    html.dark .fcw-online-caption,
    html.dark .fcw-online-empty{
        color:#999;
    }
</style>
<div class="fcw-online-box">
    <div class="fcw-online-header">
        <div class="fcw-online-header-left">
            <span class="fcw-online-title">Who\'s online</span>
            <span class="fcw-online-dot"></span>
        </div>
    </div>
    <div class="fcw-online-body" id="fcw-online-body">' . $bodyHtml . '</div>
</div>' . $content;

}, 10, 2);

// REST route for live refresh, shares the same data function/cache above.
add_action('rest_api_init', function () {
register_rest_route('fcw-online-widget/v1', '/data', [
'methods' => 'GET',
'callback' => function () {
$data = fcw_online_widget_get_data(5, 14, 60);
return [
'total' => (int) $data['total'],
'avatars' => $data['avatars'],
'html' => fcw_online_widget_render_body((int) $data['total'], $data['avatars']),
];
},
'permission_callback' => '__return_true',
]);
});

// Repositions above native widgets + polls the REST route for live refresh.
add_action('fluent_community/portal_footer', function () {
$restUrl = esc_url_raw(rest_url('fcw-online-widget/v1/data'));
echo '
(function () {
var restUrl = ' . wp_json_encode($restUrl) . ';
var refreshIntervalMs = 60000; // keep in sync with $cacheSeconds

    var firstSeenAt = null;
    var maxWaitMs = 6000;

    function findSidebarColumn() {
        var card = document.querySelector(\'.app_side_widget\');
        return card ? card.parentElement : null;
    }

    function nativeContentReady() {
        var widgets = document.querySelectorAll(\'.app_side_widget\');
        if (!widgets.length) {
            return false;
        }
        for (var i = 0; i < widgets.length; i++) {
            if (widgets[i].querySelector(\'.el-skeleton\')) {
                return false;
            }
        }
        return true;
    }

    // Keeps running all session - FC is an SPA, widget re-renders on navigation.
    function tryPlace() {
        var box = document.querySelector(\'.fcw-online-box:not(.fcw-online-visible)\');
        if (!box) {
            firstSeenAt = null;
            return;
        }

        if (firstSeenAt === null) {
            firstSeenAt = Date.now();
        }

        var waitedTooLong = (Date.now() - firstSeenAt) > maxWaitMs;

        if (!nativeContentReady() && !waitedTooLong) {
            return;
        }

        var column = findSidebarColumn();
        if (column) {
            if (column.firstElementChild !== box) {
                column.insertBefore(box, column.firstElementChild);
            }
            box.classList.add(\'fcw-online-visible\');
            firstSeenAt = null;
        }
    }

    tryPlace();
    setInterval(tryPlace, 700);

    // Live refresh
    function refreshData() {
        var body = document.getElementById(\'fcw-online-body\');
        if (!body) {
            return;
        }

        fetch(restUrl, { credentials: \'omit\', cache: \'no-store\' })
            .then(function (res) { return res.ok ? res.json() : null; })
            .then(function (json) {
                if (!json || typeof json.html !== \'string\') {
                    return;
                }
                var freshBody = document.getElementById(\'fcw-online-body\');
                if (freshBody) {
                    freshBody.innerHTML = json.html;
                }
            })
            .catch(function () {
                // Silent fail - keeps last known state.
            });
    }

    setInterval(refreshData, refreshIntervalMs);
})();
</script>';

});

Members on-line Widget (PHP snippet)

Drive Zone

If someone will use it, I can also provide version with on-page status refresh without need of refreshing page to update members online status.

Cointacter

Drive ZoneΒ i will use it in my plugin. i believe fcom native user status is improved from how it was earlier, it was not updating statuses for minutes, and i havent tried again since months so i guess its improved. so if u can give me the code of version with on-page status refresh without need of refreshing page to update members online status, this would be great and would save me some time.

Another question, i see youre managing automotive community. I plan to start one too, automotiwe.com will be the name and itll sell plugins for car checks for the start, maybe there would be room for more cooperation on that. I have another guy in the UK hes got cars moving business and is getting access to UK's car registry apis for the start.

Drive Zone

CointacterΒ Snippet updated in post, it is my latest version I use now. It was too long for comment. Hope it will work well for you. Refresh time, cache time and database call for fetching online status you can edit as you want.

Cointacter

Drive ZoneΒ i will let you know thanks a lot πŸ₯³

Helmar Rudolph

That snippet didn't come out right. A download link would be better, methinks. Great idea and work, btw!