Возможности Как это работает Тарифы Документация Cookbook Партнёрам Личный кабинет
Основы
👥

Онлайн-статус (Presence)

10 мин чтения Средне

Список онлайн-пользователей в реальном времени. Кто сейчас в чате или документе.

Presence JavaScript Vue

📋 Что получится

Любой бэкенд: Presence-каналы работают с любым языком. Примеры на PHP, но концепция универсальна.
Сейчас онлайн 5
Алиса Иванова
Борис Петров
Вера Сидорова (отошла)
Геннадий Козлов
Дарья Новикова
  • Список онлайн-пользователей в реальном времени
  • Мгновенное обновление при входе/выходе
  • Информация о пользователях (имя, аватар, статус)
  • Счётчик онлайн

🏗️ Как работают Presence-каналы

1
Пользователь подписывается на presence-канал
2
Сервер авторизует и возвращает данные пользователя
3
Pushler добавляет пользователя в список присутствия
4
Всем участникам приходит событие member_added
5
При отключении — событие member_removed
Ключевые события:
  • pushler:subscription_succeeded — успешная подписка (содержит список всех)
  • pushler:member_added — новый участник вошёл
  • pushler:member_removed — участник вышел

💻 Код сервера

Сервер авторизует пользователей для presence-каналов. Примеры на PHP — принцип одинаков для всех языков.

Установка PHP SDK

Bash
composer require pushler/php-sdk

Авторизация Presence-канала

PHP
<?php
// auth.php — авторизация каналов Pushler

require_once 'vendor/autoload.php';

use PushlerRu\PushlerClient;

// Инициализация Pushler
$pushler = new PushlerClient(
    'key_ваш_ключ',      // App Key
    'secret_ваш_секрет'  // App Secret
);

header('Content-Type: application/json');

$channelName = $_POST['channel_name'];
$socketId = $_POST['socket_id'];
$userId = getCurrentUserId(); // Ваша функция авторизации

// Проверяем авторизацию пользователя
if (!$userId) {
    http_response_code(401);
    echo json_encode(['error' => 'Unauthorized']);
    exit;
}

// Проверяем доступ к каналу
if (!canAccessChannel($userId, $channelName)) {
    http_response_code(403);
    echo json_encode(['error' => 'Forbidden']);
    exit;
}

// Для presence-каналов передаём данные пользователя
if (str_starts_with($channelName, 'presence-')) {
    $user = getUserById($userId);
    
    $auth = $pushler->authorizePresenceChannel(
        $channelName,
        $socketId,
        [
            'user_id' => (string) $userId, // Обязательно строка!
            'user_info' => [
                'name' => $user['name'],
                'avatar' => $user['avatar'],
                'email' => $user['email'],
                'role' => $user['role'] ?? 'user'
            ]
        ]
    );
    
    echo json_encode($auth);
    exit;
}

// Для приватных каналов
$auth = $pushler->authorizeChannel($channelName, $socketId);
echo json_encode($auth);

// ============================================
// Функция проверки доступа к каналу
// ============================================
function canAccessChannel(int $userId, string $channelName): bool
{
    // Presence-канал чата — проверяем участие
    if (preg_match('/^presence-chat-(\d+)$/', $channelName, $matches)) {
        $chatId = (int) $matches[1];
        return userHasAccessToChat($userId, $chatId);
    }

    // Presence-канал документа — проверяем права
    if (preg_match('/^presence-document-(\d+)$/', $channelName, $matches)) {
        $documentId = (int) $matches[1];
        return userCanViewDocument($userId, $documentId);
    }

    // Общий канал приложения — всем авторизованным
    if ($channelName === 'presence-app') {
        return true;
    }

    // Приватный канал пользователя — только владельцу
    if (preg_match('/^private-user-(\d+)$/', $channelName, $matches)) {
        return (int) $matches[1] === $userId;
    }

    return false;
}

Отправка событий в Presence-канал

PHP
<?php
// notify_room.php — отправить уведомление всем в комнате

require_once 'vendor/autoload.php';

use PushlerRu\PushlerClient;

$pushler = new PushlerClient('key_ваш_ключ', 'secret_ваш_секрет');

$roomId = (int) $_POST['room_id'];
$message = $_POST['message'];

// Отправляем в presence-канал комнаты
$pushler->trigger(
    "presence-room-{$roomId}",
    'announcement',
    [
        'message' => $message,
        'timestamp' => date('c')
    ]
);

echo json_encode(['success' => true]);

🌐 Код клиента (JavaScript)

Vanilla JavaScript — класс OnlineUsers

JavaScript
/**
 * Класс для отслеживания онлайн-пользователей через Presence-канал
 */
class OnlineUsers {
    constructor(pushler, channelName) {
        this.pushler = pushler;
        this.channelName = channelName;
        this.members = new Map();
        this.channel = null;
        
        // Callbacks
        this.onMembersChange = null;
        this.onMemberJoined = null;
        this.onMemberLeft = null;
    }

    async connect() {
        return new Promise((resolve, reject) => {
            this.channel = this.pushler.subscribe(this.channelName);

            // Получили список всех участников при подписке
            this.channel.on('pushler:subscription_succeeded', (data) => {
                this.members.clear();
                
                if (data.members) {
                    Object.entries(data.members).forEach(([id, info]) => {
                        this.members.set(id, info);
                    });
                }
                
                this.notifyChange();
                resolve(this.getMembersList());
            });

            // Новый участник вошёл
            this.channel.on('pushler:member_added', (member) => {
                this.members.set(member.id, member.info);
                this.notifyChange();
                
                if (this.onMemberJoined) {
                    this.onMemberJoined(member);
                }
            });

            // Участник вышел
            this.channel.on('pushler:member_removed', (member) => {
                this.members.delete(member.id);
                this.notifyChange();
                
                if (this.onMemberLeft) {
                    this.onMemberLeft(member);
                }
            });

            // Ошибка подписки
            this.channel.on('pushler:subscription_error', (error) => {
                reject(error);
            });
        });
    }

    getMembersList() {
        return Array.from(this.members.entries()).map(([id, info]) => ({
            id,
            ...info
        }));
    }

    getMembersCount() {
        return this.members.size;
    }

    isMember(userId) {
        return this.members.has(String(userId));
    }

    notifyChange() {
        if (this.onMembersChange) {
            this.onMembersChange(this.getMembersList());
        }
    }

    disconnect() {
        if (this.channel) {
            this.channel.unsubscribe();
            this.channel = null;
        }
        this.members.clear();
    }
}

// === Использование ===

const pushler = new PushlerClient({
    appKey: 'key_ваш_ключ',
    authEndpoint: '/auth.php'
});

pushler.on('connected', async () => {
    const onlineUsers = new OnlineUsers(pushler, 'presence-chat-123');

    onlineUsers.onMembersChange = (members) => {
        updateOnlineList(members);
        updateOnlineCount(members.length);
    };

    onlineUsers.onMemberJoined = (member) => {
        showNotification(`${member.info.name} присоединился`);
    };

    onlineUsers.onMemberLeft = (member) => {
        showNotification(`${member.info.name} вышел`);
    };

    await onlineUsers.connect();
});

function updateOnlineList(members) {
    const container = document.getElementById('online-list');
    container.innerHTML = members.map(member => `
        <div class="online-user">
            <img src="${member.avatar || '/default-avatar.png'}" alt="" />
            <span class="name">${escapeHtml(member.name)}</span>
            <span class="status-dot online"></span>
        </div>
    `).join('');
}

function updateOnlineCount(count) {
    document.getElementById('online-count').textContent = count;
}

Vue 3 Composable

JavaScript
// composables/usePresence.js
import { ref, computed, onMounted, onUnmounted, shallowRef } from 'vue';

export function usePresence(pushler, channelName) {
    const members = shallowRef(new Map());
    const isConnected = ref(false);
    const error = ref(null);
    
    let channel = null;

    const membersList = computed(() => 
        Array.from(members.value.entries()).map(([id, info]) => ({
            id,
            ...info
        }))
    );

    const membersCount = computed(() => members.value.size);

    const isMember = (userId) => members.value.has(String(userId));

    function connect() {
        return new Promise((resolve, reject) => {
            channel = pushler.subscribe(channelName);

            channel.on('pushler:subscription_succeeded', (data) => {
                const newMembers = new Map();
                
                if (data.members) {
                    Object.entries(data.members).forEach(([id, info]) => {
                        newMembers.set(id, info);
                    });
                }
                
                members.value = newMembers;
                isConnected.value = true;
                resolve(membersList.value);
            });

            channel.on('pushler:member_added', (member) => {
                const newMembers = new Map(members.value);
                newMembers.set(member.id, member.info);
                members.value = newMembers;
            });

            channel.on('pushler:member_removed', (member) => {
                const newMembers = new Map(members.value);
                newMembers.delete(member.id);
                members.value = newMembers;
            });

            channel.on('pushler:subscription_error', (err) => {
                error.value = err;
                reject(err);
            });
        });
    }

    function disconnect() {
        channel?.unsubscribe();
        members.value = new Map();
        isConnected.value = false;
    }

    onMounted(() => {
        if (pushler.connectionState === 'connected') {
            connect();
        } else {
            pushler.on('connected', connect);
        }
    });

    onUnmounted(disconnect);

    return { members: membersList, count: membersCount, isConnected, error, isMember };
}

Vue компонент онлайн-пользователей

Vue
<!-- OnlineUsers.vue -->
<template>
  <div class="online-users">
    <div class="header">
      <h3>Сейчас онлайн</h3>
      <span class="count">{{ presence.count.value }}</span>
    </div>
    
    <TransitionGroup name="list" tag="div" class="users-list">
      <div 
        v-for="member in presence.members.value" 
        :key="member.id"
        class="user"
      >
        <div class="avatar-wrapper">
          <img :src="member.avatar || '/default-avatar.png'" :alt="member.name" />
          <span class="status-dot" :class="member.status || 'online'"></span>
        </div>
        <div class="info">
          <div class="name">{{ member.name }}</div>
          <div v-if="member.role" class="role">{{ member.role }}</div>
        </div>
      </div>
    </TransitionGroup>

    <div v-if="presence.members.value.length === 0" class="empty">
      Никого нет онлайн
    </div>
  </div>
</template>

<script setup>
import { usePresence } from './composables/usePresence';

const props = defineProps({
  pushler: { type: Object, required: true },
  channelName: { type: String, required: true }
});

const presence = usePresence(props.pushler, props.channelName);
</script>

🎨 Продвинутые сценарии

Статус пользователя (away/busy)

JavaScript
// Клиент отправляет обновление статуса
async function updateStatus(status) {
    await fetch('/update_status.php', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
        body: `status=${status}` // 'online' | 'away' | 'busy'
    });
}

// Автоматическое определение "отошёл"
let idleTimeout;

document.addEventListener('mousemove', resetIdle);
document.addEventListener('keydown', resetIdle);

function resetIdle() {
    clearTimeout(idleTimeout);
    updateStatus('online');
    
    idleTimeout = setTimeout(() => {
        updateStatus('away');
    }, 5 * 60 * 1000); // 5 минут
}

Подсчёт просмотров страницы

JavaScript
// Сколько человек сейчас смотрят страницу
const pushler = new PushlerClient({
    appKey: 'key_ваш_ключ',
    authEndpoint: '/auth.php'
});

pushler.on('connected', () => {
    const channel = pushler.subscribe(`presence-page-${pageId}`);
    
    channel.on('pushler:subscription_succeeded', (data) => {
        const count = Object.keys(data.members || {}).length;
        document.getElementById('viewers').textContent = `👁️ ${count} смотрят`;
    });
    
    channel.on('pushler:member_added', () => updateViewersCount(1));
    channel.on('pushler:member_removed', () => updateViewersCount(-1));
});

⚠️ Типичные ошибки

1. Неверное имя канала

JavaScript
// ❌ Неправильно — без префикса presence-
pushler.subscribe('chat-room-123');

// ✅ Правильно — с префиксом
pushler.subscribe('presence-chat-room-123');

2. Не передаём user_info при авторизации

PHP
// ❌ Неправильно — нет данных пользователя
$auth = $pushler->authorizePresenceChannel($channelName, $socketId, [
    'user_id' => $userId
    // user_info отсутствует!
]);

// ✅ Правильно — передаём информацию
$auth = $pushler->authorizePresenceChannel($channelName, $socketId, [
    'user_id' => (string) $userId,
    'user_info' => [
        'name' => $user['name'],
        'avatar' => $user['avatar']
    ]
]);

3. user_id должен быть строкой

PHP
// ❌ Может вызвать проблемы
'user_id' => $userId  // integer

// ✅ Правильно — явно приводим к строке
'user_id' => (string) $userId

Готовы попробовать?

Создайте бесплатный аккаунт и начните интеграцию за пару минут