How to use a LiveComponent to handle Presence

I use a vanilla (+ Presence) Phoenix 1.5.8 application to run a LiveView which lists all current visitors:

lib/example_web/live/page_live.ex

defmodule ExampleWeb.PageLive do
  use ExampleWeb, :live_view

  alias Example.Presence
  alias Phoenix.Socket.Broadcast

  @impl true
  def mount(_params, _session, socket) do
    Phoenix.PubSub.subscribe(Example.PubSub, "room:lobby")
    {:ok, _} = Presence.track(self(), "room:lobby", UUID.uuid4(), %{status: "user"})

    socket =
      assign(socket,
        current_users: current_user_ids()
      )

    {:ok, socket}
  end

  @impl true
  def handle_info(%Broadcast{event: "presence_diff"}, socket) do
    socket =
      assign(socket,
        current_users: current_user_ids()
      )

    {:noreply, socket}
  end

  @impl true
  def render(assigns) do
    ~L"""
    <%= inspect(@current_users) %>
    """
  end

  defp current_user_ids() do
    Presence.list("room:lobby")
    |> Enum.filter(fn {_id, x} -> hd(x.metas).status == "user" end)
    |> Enum.map(fn {k, _} -> k end)
  end
end

Works without a problem. It displays a unique ID for all current visitors.

But I would like to move this to a LiveComponent. So here is my attempt to do so:

lib/example_web/live/page_live.ex

defmodule ExampleWeb.PageLive do
  use ExampleWeb, :live_view

  alias ExampleWeb.ChatComponent

  @impl true
  def render(assigns) do
    ~L"""
    <%= live_component @socket, ChatComponent, id: 1 %>
    """
  end
end

lib/example_web/live/components/chat_component.ex

defmodule ExampleWeb.ChatComponent do
  use ExampleWeb, :live_component
  alias Example.Presence
  alias Phoenix.Socket.Broadcast

  @impl true
  def mount(socket) do
    Phoenix.PubSub.subscribe(Example.PubSub, "room:lobby")
    {:ok, _} = Presence.track(self(), "room:lobby", UUID.uuid4(), %{status: "user"})

    {:ok, assign(socket, current_users: current_user_ids())}
  end

  @impl true
  def update(assigns, socket) do
    socket =
      assign(socket,
        current_users: current_user_ids()
      )
    {:ok, socket}
  end

  @impl true
  def handle_info(%Broadcast{event: "presence_diff"}, socket) do
    socket =
      assign(socket,
        current_users: current_user_ids()
      )

    {:noreply, socket}
  end

  def render(assigns) do
    ~L"""
    <%= inspect(@current_users) %>
    """
  end

  defp current_user_ids() do
    Presence.list("room:lobby")
    |> Enum.filter(fn {_id, x} -> hd(x.metas).status == "user" end)
    |> Enum.map(fn {k, _} -> k end)
  end
end

It doesn’t work. Only one ID is displayed and that self updates every half a second or so.

Can this be done in a LiveComponent? What do I have to change?

LiveComponents don’t have their own backing process - it’s shared with the containing LiveView. This means they can’t directly receive messages from other processes. Have you tried putting an IO.inspect in ChatComponent's handle_info? I would guess that it isn’t being called.

I’d change the ChatComponent to be a standalone LiveView and embed that (like a component) wherever it’s needed using live_render. If it’s on every page you could embed it in your template.

3 Likes