Presence room

Open this page in multiple tabs to see visitors join, leave, and send collaborative room pings through Phoenix Presence and Dstar patches.

Demo

Lobby

Who is here right now?

Keep this tab open, then open another. Each tab becomes a distinct presence and receives live roster patches.

Online now

0

active browser tabs

Connecting to the room stream…

How it works

  • Phoenix Presence tracks each browser tab as a visitor.
  • handle_connect/2 and handle_info/2 drive the room stream.
  • Roster, online count, and activity are patched independently.
  • Renaming yourself repaints every open tab's roster.
  • The ping button broadcasts a tiny collaborative interaction.

handle_connect/2 subscribes to the room topic and tracks this tab with Phoenix.Presence, then patches the roster. handle_info/2 reacts to presence_diff broadcasts, and handle_disconnect/1 untracks on every exit from the receive loop — bookkeeping that a per-connection process would do for you.

lib/dstar_demo_web/pages/presence_page.ex
Phoenix.PubSub.subscribe(DstarDemo.PubSub, @topic)
remember_presence_instance(registry_tab, presence_instance)

{:ok, _} =
  Presence.track(self(), @topic, tab_id, %{
    name: name,
    color: color,
    status: "active",
    presence_instance: presence_instance,
    last_seen: System.system_time(:second)
  })

If this were LiveView

Both sides track each tab with Phoenix.Presence, react to presence_diff, and broadcast a room ping. They differ in what has to happen when a visitor leaves.

Dstar

lib/dstar_demo_web/pages/presence_page.ex
Phoenix.PubSub.subscribe(DstarDemo.PubSub, @topic)
remember_presence_instance(registry_tab, presence_instance)

{:ok, _} =
  Presence.track(self(), @topic, tab_id, %{
    name: name,
    color: color,
    status: "active",
    presence_instance: presence_instance,
    last_seen: System.system_time(:second)
  })

LiveView

illustrative
# Illustrative LiveView equivalent — not compiled or run.
def mount(_params, _session, socket) do
  socket =
    assign(socket,
      visitor_name: random_name(),
      visitor_color: random_color(),
      activity: []
    )

  if connected?(socket) do
    Phoenix.PubSub.subscribe(DstarDemo.PubSub, @topic)

    {:ok, _} =
      Presence.track(self(), @topic, socket.id, %{
        name: socket.assigns.visitor_name,
        color: socket.assigns.visitor_color,
        status: "active",
        last_seen: System.system_time(:second)
      })
  end

  {:ok, assign(socket, roster: Presence.list(@topic))}
end

def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff"}, socket) do
  {:noreply, assign(socket, roster: Presence.list(@topic))}
end

def handle_info({:ping, name}, socket) do
  {:noreply, assign(socket, activity: [name | socket.assigns.activity])}
end

def handle_event("ping", _params, socket) do
  Phoenix.PubSub.broadcast(
    DstarDemo.PubSub,
    @topic,
    {:ping, socket.assigns.visitor_name}
  )

  {:noreply, socket}
end

# No untrack is written anywhere. Presence is bound to self(), and the
# LiveView process dying on disconnect removes the entry — including when
# it dies by crashing.