Chat web-app using Phoenix and Vue.js — Part 3

In part 2 we added support for entering your name before chatting, in this part we’ll make it possible to see who else is online in the chat room, so you don’t just talk to yourself (there’s no fun in that, right? Right? Hellloooo?).

Here’s all the parts in this series:
Part 1 — Introduction and getting a basic web-app with chat functionality going
Part 2 — Make it possible for a user to identify themselves by name before joining the chat
Part 3 (this article) — See who’s online in the chat with you
Part 4 — Prettier design + fun css transitions

The code we’ll be adding in this tutorial can be seen here:

We’ll make use of a great new feature of the Phoenix framework that was introduced in Phoenix 1.2 — and that is Phoenix.Presence.

Phoenix comes with a handy little generator that we can run using mix so go ahead and run mix phoenix.gen.presence . The instructions will tell you to add this newly generated module to the supervision tree of your application, so open up lib/vuechat.ex and add this line:

supervisor(Vuechat.Presence, []),

To the childens: [ ... ] array that’s already in there. If you open this new module web/channels/presence.ex there’s a pretty self explanatory piece of documentation in there on how to use it.

First we’ll open web/channels/room_channel.ex and in join("room:lobby", payload, socket) we’ll add a new line, so it’ll look like this:

def join("room:lobby", payload, socket) do
send(self, :after_join)
{:ok, socket}

Now we just need to add a corresponding handler, so let’s add the following at the bottom of web/channels/room_channel.ex:

def handle_info(:after_join, socket) do
{:ok, _} = Presence.track(socket, socket.assigns.username, %{
online_at: inspect(System.system_time(:seconds))
push socket, "presence_state", Presence.list(socket)
{:noreply, socket}

We track the username that we assigned to the socket in part 2, together with some metadata of when the user was online. That information is then pushed on to the socket as “presence_state”. Before this will work, we need to add alias Vuechat.Presence at the top below use Vuechat.Web, :channel

That probably means we have to go to our my-app.vue and add a listener for that event.

In the connectToChat() function in web/static/components/my-app.vue at the bottom just before …, we’ll add:"presence_state", state => {
})"presence_diff", diff => {

If you restart your server mix phoenix.server and open up the development console — you should see things happening after you’ve entered your name and click the next button:

We can see that we receive two events, the diff and the state event. The diff event contains a list of joins and leaves — and the state event contains a list of all users.

If you open up a new browser window and navigate to localhost:4000 enter a name and enter the chat. You can see that your first window receives a new diff event that contains the new user in the joins object. If you close the second browser window — yet another diff event arrives, and this time the user is in the leaves object. Pretty neat!

Let’s actually do something with this data, and render the users on the screen.

In our my-app.vue component, import Presence from “phoenix” as well:

import {Socket, Presence} from "phoenix"

Add a new data() attribute named users: [] :

data() {
return {
socket: null,
channel: null,
messages: [],
message: "",
username: "",
enterName: true,
users: []

Let’s hook that up in the view, so we can see when users join. As the first thing in the <div class="messages"> div, add the following:

Online users:
<ul v-for="user in users">
{{user.user}} ({{user.online_at}})

This should seem familiar, as it’s the same we’re doing with our messages list.

As the first thing in our connectToChat method, we’ll add a local variable named presences — that’ll keep track of the presence object that Phoenix returns.

So add let presences = {} at the top of connectToChat . We’ll then go ahead and change our presence_state handler to:"presence_state", state => {
presences = Presence.syncState(presences, state)

and we’ll do the same for the presence_diff handler:"presence_diff", diff => {
presences = Presence.syncDiff(presences, diff)

Presence.syncState and Presence.syncDiff does the heavy lifting based on what we get back on the socket. We then just need to write our assignUsers function, which will take the data from presences and assign it to our users array.

So let’s add assignUsers(presences) under methods:

assignUsers(presences) {
this.users = Presence.list(presences, (user, {metas: metas}) => {
return { user: user, online_at: metas[0].online_at }

If we restart our server, and visit localhost:4000 we should now see your name in the list of connected users. You’ll also notice that the timestamp doesn’t look so nice — so let’s write a quick formatter for that:

Add this to the top of the assignUsers function:

let formatTimestamp = (timestamp) => {
timestamp = parseInt(timestamp)
let date = new Date(timestamp)
return date.toLocaleTimeString()

Wrap the metas[0].online_at in a formatTimestamp call:

return { user: user, online_at: formatTimestamp(metas[0].online_at) }

Almost there! Let’s wrap the user’s name and the message input in a <p> tag so things are a little bit separated. Let’s also add a placeholder to the input field with the content: “What do you have to say?” so it’ll look like this:

<input type="text" placeholder="What do you have to say?" v-model="message" v-on:keyup.13="sendMessage">

Let’s open up web/templates/layout/app.html.eex and delete the <header>..</header> — we should now have something that looks and behaves like this:

The code can as always be found on github:

In the next part, we’ll look into how to actually make this look a bit better — and explore some of the great transition stuff that Vue.js has to offer. It’ll be fun! :) Stay tuned!

I did go ahead and deploy it to Heroku as well. I’ll cover how to do that in a later part, but come say hi here:

Like what you read? Give Jesper Christiansen a round of applause.

From a quick cheer to a standing ovation, clap to show how much you enjoyed this story.