Hello everyone,
Long time lurker, first time poster. I’ve learned a lot from this community already so let me begin by giving my thanks for this. My question is quite simple and high level: What is the basic architecture of a chat room using a LiveView? Why or why not should a Channel be used?
I’ve read the first two parts of Stephen Bussey’s “Real-Time Phoenix”, which greatly clarified the function and role of Socket and Channel in Phoenix, and their individual responsibilities. Having a chat application in mind, and finding the book quite motivating I decided to begin writing a prototype before finishing the book.
But I quickly became stuck due to some confusion about a few things. I have a LiveView of my Chat resource that has a name and room number (two users should be able to connect to the same example.com/chat/:room_number
and chat), a User resource generated with mix.gen.auth
, and a join table such that there is a many-many relationship between the two tables. According to the Phoenix docs and “Real-Time Phoenix” the best practice would be to create a Channel for the individual chat clients to connect to. But why should a Channel be created and have clients connect to it when there is already a stateful WebSocket connection to the server via the LiveView?
Should a new Channel be used in this circumstance, even though it would require adding some JS to the client? If not, then how can a subscription to the shared broadcast be maintained in the LiveView “controller” (I’m not sure if this is the right terminology)? I tried creating a new Channel, and subscribing to it within the LiveView, thus avoiding a new Socket connection.
This is a sketch of the approach I had in mind:
defmodule MyAppWeb.ChatLive.Show do
use MyAppWeb, :live_view
alias MyApp.Chats
@impl true
def mount(%{"room_number" => room_number}, _session, socket) do
if connected?(socket) do
# Is this the right approach or a foul hack?
# Assume there is a basic Channel that simply passes along all messages to all who have joined, like in the tutorial.
MyAppWeb.Endpoint.subscribe("chat:#{room_number}")
end
{:ok, assign(socket, :room_number, room_number, :messages, [])}
end
@impl true
def handle_event("send_message", %{"message" => message}, socket) do
MyAppWeb.Endpoint.broadcast("chat:#{socket.assigns.room_number}", "new_message", %{body: message})
{:noreply, socket}
end
@impl true
def handle_info(%{event: "new_message", payload: %{body: body}}, socket) do
{:noreply, update(socket, :messages, fn messages -> [body | messages] end)}
end
end
This seemed like a code smell, which is when I decided to come here for advice. I feel quite confident that with some sage guidance on high-level architecture I can avoid any major footguns and hack through the rest of the details.
Thanks,
Ipnon