Introduction
At Part 1, we explored how to implement the database for our chat service. In this article, we'll dive into how to implement chat functionality using LiveView, LiveComponent, PubSub, and Channels.
Channel
In Endpoint.ex
, we will define the socket handler that will manage connections on a specified URL.
socket "/live", ChatServiceWeb.UserSocket, websocket: true
socket "/api/socket", ChatServiceWeb.UserSocket,
websocket: true,
longpoll: false
On the client side, you will establish a socket connection to the route above, the url of application will be like this: http://localhost:8080/chat_room?user_id=jack&channel_id=game_1
with user_id
is your id and channel_id
is name of room.
const userId = params.get('user_id');
const channelId = params.get('channel_id');
let channel = liveSocket.channel("channel:" + channelId, {params: {user_id: userId}})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
On the server, Phoenix will call ChatServiceWeb.UserSocket.connect/2
, passing your parameters along with the initial socket state. Within this function, you can authenticate and identify the socket connection, as well as set default socket assigns. The socket is also where you define your channel routes.
The asterisk * serves as a wildcard matcher. For example, in the following channel route, requests for channel:channel_id will all be directed to the game_channel.ex
. In your UserSocket, you would have:
defmodule ChatServiceWeb.UserSocket do
use Phoenix.LiveView.Socket
channel "lv:*", Phoenix.LiveView.Channel
channel "channel:*", ChatServiceWeb.GameChannel
@impl true
def connect(_params, socket, _connect_info) do
{:ok, socket}
end
@impl true
def id(_socket), do: nil
end
In game_channel.ex
, we will implement the join/3
callback, allowing anyone to join the room using the channel_id
. After joining the channel, the user will be redirected to the LiveView page, where we'll now discuss LiveView and LiveComponent.
defmodule ChatServiceWeb.GameChannel do
use Phoenix.Channel
alias Phoenix.PubSub
alias ChatService.Room.RoomActions
alias Phoenix.LiveView.Socket
use Phoenix.LiveView
@pubsub_chat_room Application.compile_env(:chat_service,
[:pubsub, :chat_room])
@impl true
def join("channel:" <> channel_id,
%{"params" => payload},
%Phoenix.Socket{transport_pid: transport_pid} = socket) do
user_id = Map.get(payload, "user_id", :not_found)
is_embedded = Map.get(payload, "is_embedded", :not_found)
if authorized?(user_id, payload) do
topic = "channel:" <> channel_id
if connected?(%Socket{transport_pid: transport_pid}), do:
PubSub.broadcast(@pubsub_chat_room, topic, {:join_room,
%{channel_id: channel_id,
is_embedded: is_embedded,
user_id: user_id}})
{:ok, socket}
else
{:error, %{reason: "unauthorized"}}
end
end
However, when game_channel.ex
communicates with LiveView, it needs to use PubSub to transfer messages between them. In the ChatRoomLiveView
module, you’ll need to subscribe to the PubSub topic within the mount
function.
def mount(params, session, socket) do
{channel_id, user_id} =
{Map.get(params, "channel_id", :not_found),
Map.get(params, "user_id", :not_found)}
topic = "channel:" <> channel_id
:ok = PubSub.subscribe(@pubsub_chat_room, topic)
...
end
Using this Pubsub, whenever we need to send message from channel to liveview, we just use PubSub.broadcast/3
.
We will design simple chat service page like this, this is using liveview page.
There are two main components: the discussion area on the left side and the list of current users online for the room on the right side. Everything else is part of the LiveView page.
Whenever a user joins the room or sends a message, the LiveView page receives the message from the channel via PubSub, updates the message history in the message component, and refreshes the list of online users.
For example, when a user sends a new message, we handle the keypress
event and use channel.push
to send the message from JavaScript (app.js) to the channel:
let chatInput = document.querySelector("#chat-input")
chatInput.addEventListener("keypress", event => {
if(event.key === 'Enter' && chatInput.value != ""){
channel.push("new_msg", {body: chatInput.value, user_id: userId})
chatInput.value = ""
}
})
In game_channel.ex
, we will implement the handle_in/3
callback to receive new messages from the client side, along with the channel_id
as the topic. The new message will be saved, associating it with the user, channel_id, and message body. We use the spawn function to run the save_message
function asynchronously, as we don't need to wait for it to complete.
@impl true
def handle_in("new_msg", %{"body" => body, "user_id" => user_id},
%Phoenix.Socket{transport_pid: transport_pid,
topic: "channel:" <> channel_id} = socket) do
if connected?(%Socket{transport_pid: transport_pid}) do
topic = "channel:" <> channel_id
spawn(fn -> save_message(channel_id, body, user_id) end)
PubSub.broadcast(@pubsub_chat_room, topic, {:send_msg,
%{body: body,
user_id: user_id,
channel_id: channel_id}})
end
{:noreply, socket}
end
# save message into database.
defp save_message(channel_id, body, user_id) do
[room_id] = RoomActions.get_room_id_of_channel(channel_id)
{:ok, _} = RoomActions.add_chat(%{message: body, user_id: user_id,
room_id: room_id})
end
On the LiveView page, we will use the handle_info/2
callback to capture messages from the channel and then update the message history accordingly.
@impl true
def handle_info({:send_msg,
%{body: body, user_id: user_id}},
%{assigns: %{message: old_msg}} = socket) do
new_msg = old_msg ++ [%{messages: body, user_name: user_id}]
# it will send update message to ChatServiceWeb.MessagesComponent
# because we assign new :message for socket (socket is changed)
{:noreply, assign(socket, :message, new_msg)}
end
At message live componnent page, we just need to assign message to socket.
@impl true
def update(%{message: message}, socket) do
{:ok, assign(socket, :message, message)}
end
Finally, when we run command iex -S mix phx.server
, and we can access url http://localhost:8080/chat_room?user_id=jack&channel_id=game_1
in browser, and chat something with your friend.
Next part, we will discover how to run the application in EC2 Amazon AWS (Part 3).
Top comments (0)