Elixir/Phoenix — Build a simple chat room

With Coherence, Channels and Presence

Stephan Bakkelund Valois

--

Hello, how are you?
In this blog post I want to discuss how to write a very simple chat room with a list of online users. The user will need to register an account before being able to enter the chat room. We will use both elixir and JavaScript, as well as a brand new authentication package I’ve been dabbling a bit with.

Modules and dependencies

Coherence

Coherence is a full featured, configurable authentication
system for Phoenix”
For those of you who are coming from Ruby on Rails, this package will look familiar to the lovely Devise gem. Be aware that Coherence is in the early release stages, and the project is under active development. Things may change! Read more at https://github.com/smpallen99/coherence

Phoenix Channels

For client-server communication over web sockets.

Phoenix Presence

Provides tracking for processes and channels.

Setting up the project

We’ll start off by creating our new project, I will call mine Chatourious:

~/$ mix phoenix.new chatourius
* creating chatourius/config/config.exs
* creating chatourius/config/dev.exs
Fetch and install dependencies? [Yn] y
...
~/$ cd chatourius/
~/chatourius$ mix ecto.create
...
The database for Chatourius.Repo has been created

Next, we’ll add Coherence, run mix deps.get and restart our server:

#mix.exs
......
def application do
[mod: {Chatourius, []},
applications: [:phoenix, :phoenix_pubsub, :phoenix_html, :cowboy, :logger, :gettext,
:phoenix_ecto, :postgrex, :coherence]]
end
......defp deps do
[{:phoenix, “~> 1.2.1”},
{:phoenix_pubsub, “~> 1.0”},
{:phoenix_ecto, “~> 3.0”},
{:postgrex, “>= 0.0.0”},
{:phoenix_html, “~> 2.6”},
{:phoenix_live_reload, “~> 1.0”, only: :dev},
{:gettext, “~> 0.11”},
{:cowboy, “~> 1.0”},
{:coherence, “~> 0.3”}]
end
......

Coherence comes with different modules just like Devise- Authenticatable, Invitable, Registerable, Confirmable and so on. We’ll use the built in Coherence installer to generate some boilerplate files. We’ll run the installer without the confirmable option:

~/chatourius$ mix coherence.install — full-invitable
Your config/config.exs file was updated.
Compiling 12 files (.ex)

The installer also gave us some instructions on updating some files manually.

This is my updated routes:

# web/router.exdefmodule Chatourius.Router do
use Chatourius.Web, :router
use Coherence.Router
pipeline :browser do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug Coherence.Authentication.Session # Add this
end

# Add this block
pipeline :protected do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug Coherence.Authentication.Session, protected: true # Add this
end
pipeline :api do
plug :accepts, ["json"]
end
scope "/" do
pipe_through :browser
coherence_routes
end
# Add this block
scope "/" do
pipe_through :protected
coherence_routes :protected
end
scope "/", Chatourius do
pipe_through :browser # Use the default browser stack

end
# Add this block
scope "/", Chatourius do
pipe_through :protected
# Add protected routes below
get "/", PageController, :index
end
end

I’ve also moved our page_controller route to the protected scope, so that the users need to log in to see it. Adding and editing model fields are beyond the scope of this blog, however, I just want to make a tiny change to the registration field name label:

# web/templates/coherence/registration/form.html.eex
......
<div class="form-group">
<%= required_label f, :username, class: "control-label" %>
<%= text_input f, :name, class: "form-control", required: ""%>
<%= error_tag f, :name %>
</div>
......

Also, we’ll add the jQuery CDN url at the bottom our app.html.eex file:

# web/templates/page/index.html.eex
......
<script src="https://cdnjs.cloudflare.com/ajax/
libs/jquery/2.2.4/jquery.min.js"></script>
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>

We’ll run mix ecto.migrate, and restart our application. Now, after we’ve admired the amazing boilerplate templates for a while, we create a new account and sign into our application.

Building a simple interface

For simplicity, we’ll just stash our chat inside our Page Controller’s index template. First, delete everything inside the template located in web/templates/page/index.html.eex.
We’ll also delete the customized container css in web/static/css/phoenix.css:

This block of css code needs to go...@media (min-width: 768px) {
.container {
max-width: 730px;
}
}
After removing the content of index template and removing the container block

Perfect. A nice, white and spotless canvas for us to fill with amazing design! Jokes aside, let’s start writing some html. We will use minimal css, and stick with the building blocks Twitter Bootstrap already provides us with:

# web/templates/page/index.html.eex
<div class="chat container">
<div class="col-md-9">
<div class="panel panel-default chat-room">
<div class="panel-heading">
Hello <%= Coherence.current_user(@conn).name %>
<%= link "Sign out", to: session_path(@conn, :delete),
method: :delete %>
</div>
<div id="chat-messages" class="panel-body panel-messages">
</div>
<input type="text" id="message-input" class="form-control"
placeholder="Type a message…">
</div>
</div>
<div class="col-md-3">
<div class="panel panel-default chat-room">
<div class="panel-heading">
Online Users
</div>
<div class="panel-body panel-users" id="online-users">
</div>
</div>
</div>
</div>

Notice that we can fetch our current user with the help of a built in Coherence helper, Coherence.current_user(@conn).name .

Our css goes into our app.css file:

.chat {
margin-top: 0em;
}
.chat-room {
margin-top: 1em;
}
.panel-messages, .panel-users {
height: 400px;
}
#chat-messages {
min-height: 400px;
overflow-y: scroll;
}

And this is what we’ll end up with:

Two windows for chat messages and online users. An input field for typing messages.

Connecting to our room with channels

We’ll start of by making our server ready to receive connections from the client through web sockets.

Phoenix has a built in tool for creating boilerplate channel files.
mix phoenix.gen.channel name_of_channel. This creates two files, a file which contains our channel code, and a file which contains test code for our channel. However, we’ll just write everything by hand.

The first thing we’ll do, is adding a room_channel.ex file with the following content:

# web/channels/room_channel.exdefmodule Chatourius.RoomChannel do
use Phoenix.Channel
def join("room", _payload, socket) do
{:ok, socket}
end
end

We also need to add our room channel to user_socket.ex:

# web/channels/user_socket.exdefmodule Chatourius.UserSocket do 
use Phoenix.Socket
......
channel "room", Chatourius.RoomChannel
......
end

In our room channel, we write a function called join/3. This function will handle all client authentication for the the given topic. In this case, we only have a “room” channel, and at this moment we’ll allow anyone to join, and we’ll return a {:ok, socket} tuple to authorize the socket.

Now, obviously if you look in your development console, you won’t find anything there, since we haven’t added the client side code yet.
Let’s tacle that now.

In our app.js file, we have to uncomment import socket from “./socket”:

# web/static/js/app.jsimport “phoenix_html”import socket from “./socket”

Now, if you take a look in your development console, you’ll notice an error:

Unable to join Object {reason: “unmatched topic”}

This is a good error! It means our client is trying to join our room.

For simplicity, we are just going to use our socket.js file for all our client code. If we take a look in this file (web/static/js/socket.js), we’ll notice that our client is trying to connect to “topic:subtopic”. We’ll change this to our “room”, which we wrote server side:

# web/static/js/socket.jsimport {Socket} from "phoenix"let socket = new Socket("/socket", {
params: {token: window.userToken}})
socket.connect()let channel = socket.channel("room", {}) # Edit this linechannel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket

Sweet, if we refresh our application, we get a better looking message:

Joined successfully Object {}

This means our client and server are talking to each other over web sockets.

Now, let’s figure out how to dish out messages in our chat application. Since we don’t have a send button, we’ll just rely on sending the message when we press enter. We then push the message to our channel, and sending them to the server:

# web/static/js/socket.jsimport {Socket} from "phoenix"let socket = new Socket("/socket", {
params: {token: window.userToken}})
socket.connect()
let channel = socket.channel("room", {})
let message = $('#message-input')
let nickName = "Nickname"
let chatMessages = document.getElementById("chat-messages")
message.focus();message.on('keypress', event => {
if(event.keyCode == 13) {
channel.push('message:new', {message: message.val(),
user: nickName})
message.val("")
}
});
channel.on('message:new', payload => {
let template = document.createElement("div");
template.innerHTML = `<b>${payload.user}</b>:
${payload.message}<br>`
chatMessages.appendChild(template);
chatMessages.scrollTop = chatMessages.scrollHeight;
})
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket

First, we create some variables for our DOM elements (input field and the window for our chat messages), and add “message.focus()”. This function will focus the input field so that the user doesn’t have to physically click it each time they want to send a message. We also write a listener function, where we listen for a keypress on key code 13. Key code 13 is the enter key, which means we’ll do something whenever the enter key is pressed.

Once the enter key is pressed, we push the value of the input field in “message”, and our hard coded nick name in “user” to the channel. Next,
we reset the input field so that it’s empty and ready for a new message.

We write another listener function, where we listen for a ‘message:new’ on our channel. Our channel will pick this up every time we push a ‘message:new’ from our key press function.

When we receive a new message, we put it into a new div element, and append it to our chat window.

This should be all we need for now. We are sending a message to our server, addressing it as ‘message:new’. We are ready to pick it up server side.

Conversations between the client and server

# web/channels/room_channel.ex
......
def handle_in("message:new", payload, socket) do
broadcast! socket, "message:new", %{user: payload["user"],
message: payload["message"]}
{:noreply, socket}
end

The handle_in/3 function handles every incoming event to the server. We are handling everything addressed as “message:new”, and send it out to every client connected to our room. The payload parameter holds the information about the user (where we stored our nickname from the client side) and the message (where we stored the chat message).

We are now able to dish out messages. From the client, to the server, and back to all connected clients

If you open up another browser and create a new account, you’ll be able to send messages back and forth between the browsers. The nickname is hardcoded, and won’t change even though we are logged into two different accounts. However the messages are broadcasted correctly from the server.

Now, let’s try and figure out how to display our user name instead of the hard coded nickname. We are already authenticated. Can we send this information over the channel? What we’ll do, is adding a user token with the user’s id in it, when the user signs in.

We’ll add a plug to our router, which generates a token
when the user signs in:

# web/router.ex
......
defp put_user_token(conn, _) do
current_user = Coherence.current_user(conn).id
user_id_token = Phoenix.Token.sign(conn, "user_id",
Coherence.current_user(conn).id)
conn
|> assign(:user_id, user_id_token)
IO.inspect user_id_token
end
......

We create a token and add the current user’s id in it, before storing it in the connection as :user_id.

We also have to add the plug to our protected pipeline:

# web/router.expipeline :protected do
plug :accepts, ["html"]
plug :fetch_session
plug :fetch_flash
plug :protect_from_forgery
plug :put_secure_browser_headers
plug Coherence.Authentication.Session, protected: true
plug :put_user_token
end

If we refresh our app, it’s going to crash, however if you look in your server console, you’ll find the generated user token:

“SFMyNTY.g3QAAAACZAAEZGF0YWEBZAAGc2lnbmVkbgYA8XevCFkB.o3buJ7JW9IvDhW4Zslp2wiyuO8JgMiGw1upGrm8XaV0”

Perfect. Now, remove the IO.inspect user_id_token.

I don’t know if you noticed, but in socket.js, we already have the following:

let socket = new Socket("/socket", {
params: {token: window.userToken}})

Our client is expecting a window.userToken. Let’s add that now. Add this line at the end of our app.html.eex file:

<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script>window.userToken = "<%= assigns[:user_id] %>"</script>
<script src="<%= static_path(@conn, "/js/app.js") %>"></script>
</body>
</html>

we put our user token that we added to the connection as :user_id in window.userToken. If you look at the source code in your development tools, you’ll find that our user token is available client side.

We need to authenticate the token when the client connects to our chat:

# web/channels/user_socket.ex
......
def connect(%{"token" => user_id_token}, socket) do
case Phoenix.Token.verify(socket,
"user_id",
user_id_token,
max_age: 1000000) do
{:ok, user_id} ->
{:ok, assign(socket, :user_id, user_id)}
{:error, _reason} ->
:error
end
end
......

We receive the token in our connect function, we verify it, and store it in our socket.

# web/channels/room_channel.exdefmodule Chatourius.RoomChannel do
use Phoenix.Channel
alias Chatourius.Repo
alias Chatourius.User
...... def handle_in("message:new", payload, socket) do
user = Repo.get(User, socket.assigns.user_id)
broadcast! socket, "message:new", %{user: user.name,
message: payload["message"]}
{:noreply, socket}
end
end

Now that we have the user id stored in the socket, we can query the database and collect the correct user. We then broadcast the user.name instead of the nickname we hardcoded in JavaScript.

Each user’s username is correctly displayed in the chat room.

Awesome! If you try it in both your browsers you can see the correct username in the chat. Try it out!

Feeding online users to the client

We have come far. We have a very simple chat, but we have no way to track who’s online. We will use a Phoenix’s built in channel tracker for this.

We start off by using a built in generator:

~chatourius$ mix phoenix.gen.presence
* creating web/channels/presence.ex
Add your new module to your supervision tree,in lib/chatourius.ex:children = [...supervisor(Chatourius.Presence, []),]You're all set! See the Phoenix.Presence docs for more details:http://hexdocs.pm/phoenix/Phoenix.Presence.html

We do as told, and add presence to our supervision tree:

# lib/chatourius.ex
......
def start(_type, _args) do
......
children = [
supervisor(Chatourius.Repo, []),
supervisor(Chatourius.Endpoint, []),
supervisor(Chatourius.Presence, []), #Added line
]
......
end

Now, let’s set up the server side first. We need to add some changes to our room channel:

# web/channels/room_channel.exdefmodule Chatourius.RoomChannel do
use Phoenix.Channel
alias Chatourius.Presence #Added alias
alias Chatourius.Repo
alias Chatourius.User
def join("room", _payload, socket) do
send(self, :after_join) #Added
{:ok, socket}
end
def handle_info(:after_join, socket) do
user = Repo.get(User, socket.assigns.user_id)
{:ok, _} = Presence.track(socket, user.name, %{
online_at: inspect(System.system_time(:seconds))
})
push socket, "presence_state", Presence.list(socket)
{:noreply, socket}
end
......

We added a new function, handle_info/2. This function will handle changes when the user joins and leaves the channel. It gets triggered by the newly added line in join/3. When a user joins the channel, handle_info/2 gets invoked. We then use Phoenix.Presence to track the user, and push the state to the socket. We can now pick it up in the client.

Fetching online users from the server

It’s time to write some JavaScript. The Presence state is sent over the channel, and our client can pick it up. Let’s write some more code in our socket.js file:

import {Socket, Presence} from "phoenix" // import Presencelet socket = new Socket("/socket", {
params: {token: window.userToken}})
socket.connect()
let channel = socket.channel("room", {})
let message = $('#message-input')
let chatMessages = document.getElementById("chat-messages")
// Added variables
let presences = {}
let onlineUsers = document.getElementById("online-users")
// Added block
let listUsers = (user) => {
return {
user: user
}
}
// Added block
let renderUsers = (presences) => {
onlineUsers.innerHTML = Presence.list(presences, listUsers)
.map(presence => `
<li>${presence.user}</li>`).join("")
}
message.focus();message.on('keypress', event => {
if(event.keyCode == 13) {
channel.push('message:new', {message: message.val()})
message.val("")
}
});
channel.on('message:new', payload => {
let template = document.createElement("div");
template.innerHTML = `<b>${payload.user}</b>: ${payload.message}<br>`
chatMessages.appendChild(template);
chatMessages.scrollTop = chatMessages.scrollHeight;
});
// Added block
channel.on('presence_state', state => {
presences = Presence.syncState(presences, state)
renderUsers(presences)
});
// Added block
channel.on('presence_diff', diff => {
presences = Presence.syncDiff(presences, diff)
renderUsers(presences)
});
channel.join()
.receive("ok", resp => { console.log("Joined successfully", resp) })
.receive("error", resp => { console.log("Unable to join", resp) })
export default socket

First, we added two variables. An empty object to hold our presence data, and a variable to contain our window which displays online users.

We added a block, listUsers, which will pluck out the user from the presence data. We then added another block, renderUsers, which will use the presence object containing our online users, and listUsers, to render the username within some li tags.

We also added two more listener functions. The first function is listening on “presence_state”. Presence state contains a map of the presence information sent from the server. It sends the information to renderUsers, which then displays the users in our chat room. If you try and log in and out, the list of users online won’t change. If you refresh your application, the users online will be displayed. So, how can we display the online users automatically without refreshing our application? “presence_diff”, also sent to the channel from the handle_info/2 function, holds join and leave event information.

The second listener function listens for a “presence_diff”. If it receives one, it sends it to the renderUsers function, which then updates the list of online users realtime.

Online users are displayed in real time.

Awesome. It didn’t take that long, or that much code to write this simple chat app. Phoenix provides of with amazing tools to help developers create functioning applications.

I hope you learned something from this post, that it may help you build awesome real time applications. Channels can be used to create a whole lot more than just online chats. One example could be to display real time inventory in e-commerce application.

That’s it for now.
Until next time!
Stephan Bakkelund Valois

--

--