Tutorial
Phoenix Presence with Phoenix 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.
STEP 1 - Add a Presence Module
First 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
# Start Presence process
PhoenixFeatures.Presence,
]
end
STEP 2 - 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.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)
})
Phoenix.PubSub.subscribe(PubSub, @presence)
end
{
:ok,
socket
|> assign(:current_user, session["current_user"])
|> assign(:users, %{})
|> handle_joins(Presence.list(@presence))
}
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.
I want to display the online users in a LiveView component called OnlineUsers
. I mount the component in the template like:
# lib/tutorial_web/live/page_live.html.leex
<%= live_component @socket, TutorialWeb.Components.OnlineUsers, current_user: @current_user, users: @users %>
Note that I assign both current_user and all users.
In the component, I don't need to set up anything specific. Just enough to render the template.
# lib/tutorial_web/live/components/online_users.ex
defmodule TutorialWeb.Components.OnlineUsers do
use TutorialWeb, :live_component
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
}
end
end
And lastly, in the template, rendering the online users is pretty straightforward.
<div>
<h2>Users online:</h2>
<%= for {user_id, user} <- @users do %>
<%= if user_id == @current_user[:id] do %>
<span class="text-xs bg-green-200 rounded px-2 py-1"><%= user[:name] %> (me)</span>
<% else %>
<span class="text-xs bg-blue-200 rounded px-2 py-1"><%= user[:name] %></span>
<% end %>
<% end %>
</div>