Tutorial

Phoenix Presence with Phoenix LiveView

presence liveview

A lot of apps have some sort of notification on if users are online or not. Phoenix makes it easy to build that with the built-in Phoenix Presence. In this tutorial, I will combine the presence feature with Phoenix LiveView.

The realtime nature of LiveView makes it easy to display the current online users. Let me show you how to implement this in our LiveView Tutorial project.

STEP 1 - Set Up the Presence State

First, we need a way to store our presence state. While we could use Presence directly, the recommended approach is to use a GenServer to manage our presence state. This gives us more control and better testability.

# lib/tutorial/presence/server.ex

defmodule Tutorial.Presence.Server do
  use GenServer
  
  alias Tutorial.Presence
  
  def start_link(_) do
    GenServer.start_link(__MODULE__, %{}, name: __MODULE__)
  end
  
  def init(state) do
    {:ok, state}
  end
  
  def track(user_id, user_data) do
    GenServer.cast(__MODULE__, {:track, user_id, user_data})
  end
  
  def get_presence do
    GenServer.call(__MODULE__, :get_presence)
  end
  
  def handle_cast({:track, user_id, user_data}, state) do
    {:noreply, Map.put(state, user_id, user_data)}
  end
  
  def handle_call(:get_presence, _from, state) do
    {:reply, state, state}
  end
end

STEP 2 - Add the Presence Module

Next I need to define a presence module and make sure it's started in the application.

# lib/tutorial/presence.ex

defmodule Tutorial.Presence do
  use Phoenix.Presence,
    otp_app: :tutorial,
    pubsub_server: Tutorial.PubSub
end

Add it in the start function:

# lib/tutorial/application.ex

def start(_type, _args) do
  children = [
    # ...other processes
    Tutorial.Presence,
    Tutorial.Presence.Server
  ]
  
  opts = [strategy: :one_for_one, name: Tutorial.Supervisor]
  Supervisor.start_link(children, opts)
end

STEP 3 - Add Presence to LiveView

The idea is that when a visitor visits a page, I need to add that visit in a specific channel. In this case, I have a route that points to a specific LiveView so I can add the logic in the mount function.

I have already set up so the currentuser is assigned in the user session in a plug. That means, I can get it as session["currentuser"]. In this tutorial, I use the user id as the unique identifier in the presence channel.

So, in the LiveView that is responsible for showing the online users, I need to add:

# lib/tutorial_web/live/page_live.ex

defmodule TutorialWeb.PageLive do
  use TutorialWeb, :live_view

  alias Tutorial.Presence
  alias Tutorial.Presence.Server
  alias Tutorial.PubSub

  @presence "tutorial:presence"

  @impl true
  def mount(_params, session, socket) do
    if connected?(socket) do
      user = session["current_user"]

      {:ok, _} = Presence.track(self(), @presence, user.id, %{
        name: user.name,
        joined_at: :os.system_time(:seconds)
      })

      # Track in our server as well
      Server.track(user.id, %{name: user.name})

      Phoenix.PubSub.subscribe(PubSub, @presence)
    end

    users = Server.get_presence()

    {:ok,
     socket
     |> assign(:current_user, session["current_user"])
     |> assign(:users, users)}
  end

  @impl true
  def handle_info(%Phoenix.Socket.Broadcast{event: "presence_diff", payload: diff}, socket) do
    {:noreply,
     socket
     |> handle_leaves(diff.leaves)
     |> handle_joins(diff.joins)}
  end

  defp handle_joins(socket, joins) do
    Enum.reduce(joins, socket, fn {user, %{metas: [meta | _]}}, socket ->
      assign(socket, :users, Map.put(socket.assigns.users, user, meta))
    end)
  end

  defp handle_leaves(socket, leaves) do
    Enum.reduce(leaves, socket, fn {user, _}, socket ->
      assign(socket, :users, Map.delete(socket.assigns.users, user))
    end)
  end
end

Note that I use phoenix pubsub to notify all other PageLive-processes that are spawned for other page visitors.

STEP 4 - Create a Function Component

I want to display the online users in a LiveView component called online_users.

# lib/tutorial_web/components/online_users_component.ex

defmodule TutorialWeb.OnlineUsersComponent do
  use TutorialWeb, :html

  attr :current_user, :map, required: true
  attr :users, :map, required: true

  def online_users(assigns) do
    ~H"""
    <div class="space-y-4">
      <.header>Users Online</.header>
      <div class="space-x-2">
        <%= for {user_id, user} <- @users do %>
          <%= if user_id == @current_user.id do %>
            <.badge variant="success"><%= user.name %> (me)</.badge>
          <% else %>
            <.badge variant="info"><%= user.name %></.badge>
          <% end %>
        <% end %>
      </div>
    </div>
    """
  end
end

Note that I assign both current_user and all users.

STEP 5 - Update the LiveView Template

Finally, let's update our LiveView template to use the new component:

# lib/tutorial_web/live/page_live.html.heex

<div class="max-w-2xl mx-auto py-8">
  <.online_users current_user={@current_user} users={@users} />
</div>

This implementation gives you a solid foundation for handling online presence in your Phoenix LiveView application. The GenServer addition makes it more maintainable and testable, while the modern LiveView patterns make the code cleaner and more idiomatic.

That's it! A simple but powerful way to add online presence to your LiveView app. Remember, this is just a starting point - you can extend it with user statuses, last seen timestamps, or whatever else your app needs

Related Tutorials

Published 31 May - 2023
Phoenix 1.7

CSV Import file upload with preview in LiveView

In this tutorial, I will go through how to upload and import a CSV file with Phoenix LiveView and, show how easy it is to preview the imports before..

Published 03 Apr - 2024

Set session values from LiveView

Working with session data can significantly improve the feel of web applications, making interactions feel more connected and dynamic. However, Phoe..