Let’s Build |> a Slack Clone with Elixir, Phoenix, and React (part 4— Creating Chat Rooms)

Live Demo — GitHub Repo
Part 1 — Part 2 — Part 3 — Part 4 — Part 5 — Part 6 — Part 7

So far, we’ve created a simple app that allows users to login and access a protected Home page. In this section, we will be adding forms to that Home page which allow the user to create or join a room. Then we’ll create some basic views similar to Slack, with a sidebar that lists of all of the user’s joined rooms, and a chat room view.

We need to start by creating a new Room model which we can save in our database. We’ll want to have a name and a topic field on each room, so we can begin by running a generator

mix phoenix.gen.json Room rooms name:string topic:string

Then update the generated migration file like this

sling/api/priv/repo/migrations/timestamp_create_room.exs

In our application, we want users to be able to save a list of rooms. To do this, we will need to create a join table, called user_rooms. Before we go any further, let’s create that model with a Phoenix generator

mix phoenix.gen.model UserRoom user_rooms user_id:references:users room_id:references:rooms
sling/api/priv/repo/migrations/timestamp_create_user_room.exs

The last index, index(:user_rooms, [:user_id, :room_id], unique: true) will allow us to create a unique_constraint scoped to the user and the room, meaning a user will not be able to join the same room twice. Here is the implementation of the UserRoom model.

sling/api/web/models/user_room.ex

And here is our new Room model

sling/api/web/models/room.ex

Notice in the Room model, we defined a many_to_many relationship to users through the user_room table. We need to add a similar line to the User model — many_to_many :rooms, Sling.Room, join_through: “user_rooms”.

Run the migrations now if you have not (comment out the room_controller if it gives you errors)

mix ecto.migrate

Now we can begin implementing the controller actions. Let’s first make the routes in router.ex. I’ll be adding these routes to the bottom of the /api scope.

sling/api/web/router.ex

You can move the generated room_controller.ex to the /api folder next to the other controllers.

sling/api/web/controllers/api/room_controller.ex

This is the first time we’re using a Guardian plug in a controller. The EnsureAuthenticated plug will ensure we have a valid user because we implemented the VerifyHeader plug in the router. If that fails, it will reply with the unauthenticated function that is in our SessionController.

In the create action, you will also see we are accessing the current_user with Guardian. When a room is created, we also want the user to join the room they just created, so for now I have that happening with the assoc_changeset after the room is created. It feel dirty to put that there…in Rails I may have put that in an ActiveRecord::Base.transaction, but for now it will do and I’ll leave that as an opportunity to refactor.

The join action is creating a UserRoom, based on the current_user.id and the room_id passed in params. I was thinking about putting this in a UserRoom controller to handle joining/leaving rooms, but this will work for now.

Let’s add one action to the User controller to get a list of their rooms. We’ll also add the Guardian plug there, only for the rooms action.

sling/api/web/controllers/api/user_controller.ex
Here’s a git recap of the backend work for Room api endpoints

With our backend updates in place, we should be able to create rooms from the frontend. Before we create the forms to do this, let’s set up some actions which will load the current users rooms when they log in, and create a sidebar similar to Slack that shows an icon for each room.

We’ll want to fetch these rooms after a user logs in, so let’s dispatch this function in our authentication flow’s setCurrentUser function.

sling/web/src/actions/session.js

And create a new rooms.js action file

sling/web/src/actions/rooms.js

This file has all the actions we need to fetch all rooms, fetch the current user’s rooms, create a room, and join a room. Now we need to create the reducer to update the redux store.

sling/web/src/reducers/rooms.js

Don’t forget to add the new rooms reducer to our root reducer

sling/web/src/reducers/index.js

We will want our sidebar to be present on our Home page and our Room page, so let’s add this new component to the App container. I also want to move the logout button to the sidebar, so we will be moving the handleLogout function to App. And with our current react-router setup, we need to pass in the router context so we can dispatch redirects.

I also added a Room container, which will be our chat room view, but for now let’s just show the id passed in the url

Onwards with the Sidebar component

sling/web/src/components/Sidebar/index.js

We now have the sidebar in place that logged in users will see, and they can tap the button on bottom to logout. But your room list will probably be empty, so let’s add a form to create some, and a list of all room on the Home page.

First I am going to check in with a git commit of our working Sidebar

Here is our new Home page:

sling/web/src/containers/Home/index.js

This new Home component is rendering each Room as a RoomListItem, let’s create that component in a separate file.

The RoomListItems receives a list of ids for each of the current user’s rooms, and that way we can disable the join button if they have already joined.

Next is the NewRoomForm:

This form is pretty similar to the others we have created like the login form, and just need a name to be submitted.

Go ahead and submit the form, and see the new room created as an icon in the left sidebar!

Here is the last git commit with all the work in part 4

Now we have the ability to create new chat rooms, and see a list of our rooms in the sidebar. But we don’t have any of the cool real-time features that make Phoenix fun. In part 5 we will be changing that by connecting to sockets and channels to get our chat room up and running.

Read part 5 or view the live demo