Track which users are online and show presence indicators.
Presence Channel
ruby# app/channels/presence_channel.rb class PresenceChannel < ApplicationCable::Channel def subscribed stream_from "presence" add_user_to_presence broadcast_presence end def unsubscribed remove_user_from_presence broadcast_presence end private def add_user_to_presence presence_list << current_user.id end def remove_user_from_presence presence_list.delete(current_user.id) end def presence_list @presence_list ||= Rails.cache.fetch("presence_users") { Set.new } end def broadcast_presence users = User.where(id: presence_list.to_a).pluck(:id, :name) ActionCable.server.broadcast("presence", { type: "presence_update", users: users.map { |id, name| { id: id, name: name } }, count: users.size }) end end
Using Redis for Presence
More reliable with multiple servers:
ruby# app/channels/presence_channel.rb class PresenceChannel < ApplicationCable::Channel def subscribed stream_from "presence" add_user_presence broadcast_presence_list end def unsubscribed remove_user_presence broadcast_presence_list end def heartbeat # Client sends this periodically to confirm still connected refresh_user_presence end private def redis @redis ||= Redis.new end def presence_key "presence:online_users" end def add_user_presence redis.hset(presence_key, current_user.id, Time.current.to_i) end def remove_user_presence redis.hdel(presence_key, current_user.id) end def refresh_user_presence redis.hset(presence_key, current_user.id, Time.current.to_i) cleanup_stale_users end def cleanup_stale_users # Remove users not seen in 30 seconds threshold = 30.seconds.ago.to_i redis.hgetall(presence_key).each do |user_id, timestamp| if timestamp.to_i < threshold redis.hdel(presence_key, user_id) end end end def online_user_ids redis.hkeys(presence_key).map(&:to_i) end def broadcast_presence_list users = User.where(id: online_user_ids) .select(:id, :name, :avatar_url) ActionCable.server.broadcast("presence", { users: users.as_json, count: users.size }) end end
Client-Side Presence
javascript// app/javascript/channels/presence_channel.js import consumer from "./consumer" consumer.subscriptions.create("PresenceChannel", { connected() { // Start heartbeat this.heartbeatInterval = setInterval(() => { this.perform("heartbeat") }, 15000) // Every 15 seconds }, disconnected() { clearInterval(this.heartbeatInterval) }, received(data) { this.updateOnlineUsers(data.users, data.count) }, updateOnlineUsers(users, count) { const container = document.getElementById("online-users") if (!container) return container.innerHTML = ` <h4>Online (${count})</h4> <ul> ${users.map(user => ` <li class="online-user" data-user-id="${user.id}"> <span class="status-dot online"></span> ${user.name} </li> `).join("")} </ul> ` } })
Room-Specific Presence
rubyclass RoomPresenceChannel < ApplicationCable::Channel def subscribed @room = Room.find(params[:room_id]) stream_from "room_presence_#{@room.id}" add_to_room end def unsubscribed remove_from_room if @room end private def add_to_room redis.sadd("room:#{@room.id}:users", current_user.id) broadcast_room_presence end def remove_from_room redis.srem("room:#{@room.id}:users", current_user.id) broadcast_room_presence end end