Реалізація Presence-індикатора (хто зараз онлайн) на сайті

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація Presence-індикатора (хто зараз онлайн) на сайті
Проста
від 1 робочого дня до 3 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація Presence-індикатора (хто зараз онлайн) на веб-сайті

Зелена точка рядом з аватаром — простий елемент зовні, але за ним стоїть конкретна інфраструктурна задача: сервер повинен знати, хто підключений прямо зараз, та сповіщати інших користувачів при зміні цього стану. Застосування: чат підтримки, профілі користувачів, списки учасників курсу, спільне редагування.

Як визначається «онлайн»

Три підходи, різні за точністю:

Heartbeat через WebSocket/SSE — найточніший. Поки з'єднання активне, користувач онлайн. При розриві — подія disconnect. Затримка визначення офлайн: секунди.

Heartbeat через HTTP — клієнт відправляє POST /api/presence/ping кожні 30 секунд. Якщо пінга не було більше 60 секунд — користувач вважається офлайном. Затримка визначення: до 60 секунд. Простіше у реалізації, не потребує постійного з'єднання.

Last seen — м'який варіант. Не «онлайн/офлайн», а «був 3 хвилини тому». Оновлюється при будь-якому запиту до API. Корисно для приватності (користувач сам вибирає показувати ли точний статус).

Для більшості вебсайтів достатньо heartbeat через HTTP — не потрібно утримувати WebSocket-з'єднання ради однієї точки.

Redis-сховище присутності

Присутність зберігається в Redis, не в PostgreSQL. Причина: часті записи (кожні 30 секунд на користувача), TTL-логіка, немає смислу в персистентності.

class PresenceService
{
    private const TTL = 90; // секунди без пінга = офлайн

    public function markOnline(int $userId, string $context = 'global'): void
    {
        Redis::setex("presence:{$context}:{$userId}", self::TTL, now()->timestamp);

        // Сповістити канал, якщо це перша появ
        $wasOnline = Redis::exists("presence_flag:{$context}:{$userId}");
        if (!$wasOnline) {
            Redis::setex("presence_flag:{$context}:{$userId}", self::TTL + 10, 1);
            broadcast(new UserCameOnline($userId, $context));
        }
    }

    public function markOffline(int $userId, string $context = 'global'): void
    {
        Redis::del("presence:{$context}:{$userId}");
        Redis::del("presence_flag:{$context}:{$userId}");
        broadcast(new UserWentOffline($userId, $context));
    }

    public function getOnlineUsers(string $context = 'global'): array
    {
        $keys = Redis::keys("presence:{$context}:*");
        return array_map(fn($k) => (int) last(explode(':', $k)), $keys);
    }

    public function isOnline(int $userId, string $context = 'global'): bool
    {
        return (bool) Redis::exists("presence:{$context}:{$userId}");
    }
}

Параметр $context дозволяє розділити присутність по розділам: chat_room:42, course:17, global.

Heartbeat-ендпоінт

Route::middleware('auth:sanctum')->post('/api/presence/ping', function (Request $request) {
    app(PresenceService::class)->markOnline(
        $request->user()->id,
        $request->input('context', 'global')
    );
    return response()->json(['ok' => true]);
});

Клієнт викликає цей ендпоінт при завантаженні сторінки та далі кожні 30 секунд:

const ping = () => fetch('/api/presence/ping', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': csrfToken },
    body: JSON.stringify({ context: 'global' }),
});

ping();
const interval = setInterval(ping, 30_000);

// Очистка при закритті вкладки
window.addEventListener('beforeunload', () => {
    clearInterval(interval);
    navigator.sendBeacon('/api/presence/offline'); // fire-and-forget
});

navigator.sendBeacon — єдиний надійний спосіб відправити запит при закритті вкладки. Звичайний fetch в beforeunload браузер може перервати.

Broadcast подій

class UserCameOnline implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    public function __construct(
        public readonly int    $userId,
        public readonly string $context,
    ) {}

    public function broadcastOn(): Channel
    {
        return new Channel("presence.{$this->context}");
    }
}

Індикатор в інтерфейсі

// Початковий стан завантажується разом зі сторінкою
const onlineUsers = new Set(initialOnlineUserIds);

function updateDot(userId, isOnline) {
    const dot = document.querySelector(`[data-user-id="${userId}"] .presence-dot`);
    if (!dot) return;
    dot.classList.toggle('bg-green-500', isOnline);
    dot.classList.toggle('bg-gray-300', !isOnline);
    dot.title = isOnline ? 'Онлайн' : 'Офлайн';
}

Echo.channel('presence.global')
    .listen('UserCameOnline', ({ userId }) => {
        onlineUsers.add(userId);
        updateDot(userId, true);
    })
    .listen('UserWentOffline', ({ userId }) => {
        onlineUsers.delete(userId);
        updateDot(userId, false);
    });

Присутність через Laravel Presence Channels

Якщо використовується Laravel Echo + Pusher/Reverb, можна використовувати вбудований механізм Presence Channels — він автоматично управляє списком підключених користувачів:

Echo.join('room.42')
    .here((users) => { /* початковий список */ })
    .joining((user) => updateDot(user.id, true))
    .leaving((user) => updateDot(user.id, false));

Backend просто авторизує канал та повертає дані користувача:

Broadcast::channel('room.{roomId}', function (User $user, int $roomId) {
    if ($user->canAccessRoom($roomId)) {
        return ['id' => $user->id, 'name' => $user->name, 'avatar' => $user->avatar_url];
    }
});

Терміни

  • Heartbeat-пінг + Redis TTL + індикатор: 1–2 дні
  • Broadcast при зміні статусу (UserCameOnline / Offline): 1 день
  • Presence Channels через Laravel Echo: 1 день
  • Last seen замість онлайн/офлайн: 0.5 дня
  • Налаштування приватності (приховати статус): +0.5 дня