Back to search

Build a group chat app with AdonisJs

  • Olayinka Omole
March 8th, 2018
To follow this tutorial, basic knowledge of the following will be helpful: JavaScript (ES6 syntax), Node.js, npm and MySQL.

Group chatting in messenger applications such as WhatsApp, Telegram and Facebook messenger makes it easier than ever to keep in touch with a group of people at the same time. In this tutorial we will build our own custom group chat application, aptly named “Chatly”.

We will build Chatly using AdonisJS (a Node.js framework) and Pusher’s Chatkit Service, which makes it super easy to integrate 1-1 messaging and group chats into your applications.

Requirements

The requirements to follow this tutorial successfully are:

  1. Basic understanding of JavaScript (ES6 syntax)
  2. Node. (>= 8.0)
  3. npm (>= 3)
  4. MySQL

Setting up Pusher Chatkit

As at the time of writing this article, Chatkit is still in public beta which means we can try it out totally free of charge. Go to the Chatkit to sign up.

Create a new Chatkit application and name it “Chatly”. On the next interface, select the Keys tab and you will be presented with your instance locator and secret key. We will need these later. The Chatkit documentation can be found here.

ChatKit keys

Setting up the AdonisJs application

Installing AdonisJs

AdonisJS is a simplicity first Node JS framework that has been garnering a lot of attention recently. AdonisJS comes packaged with a very helpful command-line tool that helps ease our development process. We will be making use of its latest version at the time of writing (4.0) to build our app. View the AdonisJS documentation and guide here.

Install AdonisJS cli:

    > npm i -g @adonisjs/cli

The above command will install AdonisJS command-line tools and make it available globally as adonis.

    > adonis new chatly

The command will create a new folder called chatly in our current working directory. You should get a response similar to the image below. Execute the command below to start serving the application at 127.0.0.1:3333.

    > cd chatly && adonis serve --dev

AdonisJs running

Configuring our development environment

Now that we have a shiny new AdonisJS app, let us configure it to suit what we want to do. Open the chatly folder in your favourite text editor and edit the .env file.

Remember the Chatkit credentials we created earlier? We need to create new keys for them in our .env file. Add the environment variables below using the keys from your Pusher Chatkit dashboard:

    CHATKIT_INSTANCE_LOCATOR=v1:us1:xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx
    CHATKIT_KEY=xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxx:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Registering and authenticating users

Database setup

Create a database using your preferred relational database backend. We will use MySQL in this tutorial. Open up your .env file and edit the database credentials to fit your specific needs.

Migrations

Adonis comes pre-packaged with a couple of default database migrations located at ./database/migrations that are intended to help speed up development. As it turns out, they work perfectly for Chatly, so we do not need to create new migrations. In your terminal, run the code below:

    > adonis migration:run

You can find the documentation on migrations in AdonisJS here.

If you are using the MySQL database connection as we are in this article, make sure you install the Node.js MySQL package. You will need to do this before your migrations can work:

    > npm install mysql --save

Configuring Pusher Chatkit Server

Pusher has provided a package for our server to communicate with the Chatkit API in an easy and structured manner. Install the pusher-chatkit-server package in our project and add it as a dependency to our package.json file:

    > npm i --save pusher-chatkit-server 

Create a Services folder in ./app to house the code for the various services in the our application. Create a ChatkitService.js file in the ./app/Services folder. The beauty of the app folder in AdonisJS is that it is namespaced so when we need to use the ChatkitService module we can simply make use of the use function (use(App\Services\ChatkitService)) instead of dealing with relative paths. Edit the ChatkitService.js file:

    // ./app/Services/ChatkitService.js
    const Helpers = use('Helpers')
    const Env = use('Env')
    const Chatkit = require('pusher-chatkit-server')

    /**
     * Initialize the chatkit service using the instanceLocator 
     * and secret key gotten from the chatkit dashboard
     */
    const chatkit = new Chatkit.default({
      instanceLocator: Env.get('CHATKIT_INSTANCE_LOCATOR'),
      key: Env.get('CHATKIT_KEY')
    })
    /**
     * Chatkit Service Class
     */
    class ChatkitService {
      /**
       * Register a new user
       * @param {String|Int} id - User unique ID
       * @param {String} name - User's Screen name
       * @param {String} avatar - Url to user's profile picture
       */
      async registerUser (id, name, avatar = null) {
        return await chatkit.createUser(String(id), name, avatar)
      }
      /**
       * Authenticate the user via Chatkit to get a JWT token
       * @param {Object} request - Request object sent from the Frontend
       * @param {String|Int} id - User unique ID 
       */
      async authenticateUser (request, id) {
        return await chatkit.authenticate(request, String(id))
      }
    }
    module.exports = new ChatkitService()

What we do in the code above is initialize Chatkit using the instance locator and secret key we stored in our .env file earlier, then we create a ChatkitService class with methods for registering and authenticating a user.

The registerUser() method accepts the following parameters: a unique ID representing a user, the user’s name and an optional avatar URL, while the authenticateUser() method accepts a request object (generated by the pusher-chatkit-client) and the ID of the user to be authenticated.

Views, Routes and Controllers

Views

To create controllers and views, we can make use of the adonis command. Run the following code in your terminal:

    > adonis make:controller Auth
    > adonis make:view login
    > adonis make:view register 

Select For HTTP request when creating the controller.

AdonisJS utilizes Edge as its templating engine to create dynamic views and they have a .edge extension. Create a layout folder in the ./resources/views folder, then create a main.edge file in the ./resources/views/layout folder and paste in the code block below:

    <!-- ./resources/views/layout/main.edge -->
    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
        <title>
          <!-- Section with default content -->
          @section('title') 
            Welcome 
          @endsection
           - Chatly
        </title>
        <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/css/bootstrap.min.css" integrity="sha384-Zug+QiDoJOrZ5t4lssLdxGhVrurbmBWopoEl+M6BdEfwnCJZtKxi1KgxUyJq13dy" crossorigin="anonymous">
        {{ css('style') }}
      </head>
      <body>
        <div class="container-fluid h-100">
          <!-- Self-closing template with no default content -->
          @!section('content') 
        </div>
      </body>
      <script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
      <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
      <script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta.3/js/bootstrap.min.js" integrity="sha384-a5N7Y/aK3qNeh15eJKGWxsqtnX/wWdSZSKp+81YjTmS15nvnvxKHuzaWwXHDli+4" crossorigin="anonymous"></script>
      @!section('scripts') 
    </html>

The Edge templating engine comes pre-packaged with support for layouts so we have created the main.edge file to serve as the default layout for our application. Layouts in Edge contain sections where templates extending the layout can insert content. The first block title has default content “Welcome” Inside it. The second is self-closing without default content.

Tip: AdonisJS ships with certain Edge globals thats are specific to it alone. One of these is the css`()` global function which imports CSS files based on the relative path from the public folder. Here is an example of how the function works:

    // Imports style.css from public/style.css
    {{ css('style') }}

Next, paste the following code into the ./resources/views/login.edge file.

    @layout('layout.main')
    @section('title') 
      Login 
    @endsection
    @section('content')
      <div class="row justify-content-center h-100">
        <div class="col-12 col-md-auto" style="margin-top: auto; margin-bottom: auto;">
          <h4 class="text-center">Login</h4>
          @if(flashMessage('error'))
            <span class="session-error"> {{ flashMessage('error') }} </span>
          @endif
          <form action="/login" method="post">
            {{ csrfField() }}
            <div class="form-group">
              <label for="email">Email address</label>
              <input name="email" type="email" class="form-control" id="email" aria-describedby="emailHelp" placeholder="Enter email">
              <small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
            </div>
            <div class="form-group">
              <label for="password">Password</label>
              <input name="password" type="password" class="form-control" id="password" placeholder="Password">
            </div>
            <button type="submit" class="btn btn-primary">Login</button>
          </form>
        </div>
      </div>
    @endsection

In the code snippet above, we are extending the main.edge file we created earlier so the Login template follows the structure we have defined in the main layout. Based on the sections defined, we can insert our HTML code specific to each section.

The same template procedure applies to the registration page. Paste this in the ./resources/views/register.edge file:

    @layout('layout.main')
    @section('title') 
      Register 
    @endsection
    @section('content')
      <div class="row justify-content-center h-100">
        <div class="col-12 col-md-auto" style="margin-top: auto; margin-bottom: auto;">
          <h4 class="text-center">Register</h4>
          @if(flashMessage('error'))
            <span class="session-error"> {{ flashMessage('error') }} </span>
          @endif
          <form action="/register" method="post">
            {{ csrfField() }}
            <div class="form-group">
              <label for="username">Username</label>
              <input required name="username" type="text" class="form-control" id="username" aria-describedby="emailHelp" placeholder="Enter username">
            </div>
            <div class="form-group">
              <label for="email">Email address</label>
              <input required name="email" type="email" class="form-control" id="email" aria-describedby="emailHelp" placeholder="Enter email">
              <small id="emailHelp" class="form-text text-muted">We'll never share your email with anyone else.</small>
            </div>
            <div class="form-group">
              <label for="password">Password</label>
              <input required name="password" type="password" class="form-control" id="password" placeholder="Password">
            </div>
            <button type="submit" class="btn btn-primary">Register</button>
          </form>
        </div>
      </div>
    @endsection

Now for a dash of styling, replace the code in ./public/style.css with the snippet below:

    /* ./public/styles.css */
    @import url("https://fonts.googleapis.com/css?family=Montserrat:300");
    html,
    body {
      height: 100%;
      width: 100%;
    }
    .chat-list {
      border-right: 1px solid grey;
      padding-right: 0;
      padding-left: 0;
    }
    .chat-details {
      padding-right: 0;
      padding-left: 0;
    }
    .head {
      background-color: #1781c5;
      color: aliceblue;
      height: 7vh;
      padding-bottom: 4px;
      border-bottom: 1px solid blue;
    }
    .message {
      max-width: 60%;
      margin-top: 10px;
      margin-bottom: 10px;
    }
    .message img {
      width: 50px;
      height: 50px;
      margin: 4px;
    }
    .message-sent {
      margin-left: 40%;
    }
    .chat {
      padding: 7px;
      height: 93vh;
    }
    .rooms-list {
      height: 93vh;
    }
    #messages-list {
      width: 100%;
      padding: 7px;
      height: 86vh;
      max-width: 100%;
      overflow-y: scroll;
      position: relative;
    }
    .hide {
      display: none;
    }
    #message-input-group {
      position: absolute;
      bottom: 0;
    }
    .btns {
      padding: 15px;
    }

Routes

Now, we will be creating our routes for the application. Open the routes.js file located in the ./start folder and overwrite with the following block of code:

    // ./start/routes.js
    'use strict'

    const Route = use('Route')
    // Auth
    // Login
    Route.get('login', 'AuthController.showLogin')
    Route.post('login', 'AuthController.login')
    // Register
    Route.get('register', 'AuthController.showRegister')
    Route.post('register', 'AuthController.register')

    // Main
    Route.group(() => {
      Route.get('/', async ({ view }) => {   
        // Render the chat.edge file
        return view.render('chat')
      })
      // Chatkit Authorization token
      Route.post('token', 'AuthController.token')
      // Logout
      Route.post('logout', 'AuthController.logout')
    }).middleware(['auth'])

The routes file defines mappings from routes in our web application to the underlying functions that execute them. As you can see, we are handing over the actions that should be executed on each route to the controller functions since we don’t want to write all our code in a single file.

Middlewares in AdonisJS are a set of functions that act on requests that are sent to the application. It comes in very handy for authentication. In the routes file above, we passed an array containing the auth element into the middleware function that encloses some of our routes. This tells the application to execute the auth middleware for any of the enclosed routes, which means that unauthenticated users will not be allowed access to the specified resources. They will instead be presented with an error page.

We would need to prevent the regular CSRF on the '/token' route by AdonisJS as Chatkit will make a request to that route to authenticate our application. Open up the shield.js file located in the ./config folder, scroll to the csrf key and add '*/token*' to the filterUris array as seen in the code block below:

    // ./config/shield.js
    // ...
    csrf: {
      // ...
      filterUris: [
        '/token'
      ],
      // ...
    }
    // ...

Controllers

Next, let us define methods in our AuthController. Replace the content of the AuthController with this code snippet:

    // ./app/Controllers/Http/AuthController
    'use strict'
    const ChatkitService = use('App/Services/ChatkitService')
    const User = use('App/Models/User')
    /**
     * AuthController
     */
    class AuthController {
      /**
       * Show login view
       * @param {*}  
       */
      async showLogin({ view }) {
        return view.render('login')
      }
      /**
       * Show Register view
       * @param {*}  
       */
      async showRegister({ view }) {
        return view.render('register')
      }
      /**
       * POST - Login
       * @param {*}  
       */
      async login({request, auth, response, session}) {
        const {email, password} = request.all()
        try {
          await auth.remember(true).attempt(email, password)      
        } catch (error) {
          session.flash({ error: 'Unable to Login!'})
          return response.redirect('back') 
        }
        response.redirect('/')
      }
      /**
       * POST - Register
       * @param {*} 
       */
      async register({ request, auth, response, session }) {
        // Create User in the Database
        let user;
        try {
          // Pick out the username, email and password from the request object
          user = await User.create(request.only(['username', 'email', 'password']))     
        } catch (error) {
          session.flash({ error: 'Unable to register!'})
          return response.redirect('back')  
        }
        // Register on chatkit
        try {
          // Generate Random User Picture using randomuser.me api
          let gender = ['men', 'women']
          let random_number = Math.floor(Math.random() * 100);
          let random_gender = gender[Math.floor(Math.random()*gender.length)];
          let avatarUrl = `https://randomuser.me/api/portraits/${random_gender}/${random_number}.jpg`
          let resp = await ChatkitService.registerUser(user.id, user.username, avatarUrl)            
        } catch (error) {
          session.flash({ error: 'Unable to register on Chatkit.'})
          return response.redirect('back')      
        }
        // Attempt Login 
        try {
          const {email, password} = request.all()
          await auth.remember(true).attempt(email, password)      
        } catch (error) {
          session.flash({ error: 'Unable to Login!'})
          return response.redirect('back') 
        }
        response.redirect('/')
      }
      /**
       * POST - Logout user
       * @param {*} 
       */
      async logout({ auth, response }) {
        try {
          await auth.logout()
          response.redirect('/login')
        } catch (error) {
          response.redirect('/')
        }    
      }
      /**
       * POST - get user token
       * @param {*}  
       */
      async token({ request, response, auth }){
        let data = request.all()

        const user = await auth.getUser()
        const token = await ChatkitService.authenticateUser(data, user.id)

        response.json(token)
      }
    }
    module.exports = AuthController

At the top of the module, we are importing the ChatkitService, which we created earlier, and our User model. The showLogin and showRegister methods render login.edge and register.edge views respectively.

The object deconstructing syntax in the login() method is used to extract the required values from the context supplied by AdonisJS to all routes handlers. The login() methods requires an email and a password, which we extract from the request object. We attempt to login using the auth module.

The register() method creates a user with the supplied email, username and password, then proceeds to generate a random avatar using a nifty little snippet of code. The user ID, username and avatar are passed to the ChatkitService registerUser method which creates a user in our Chatkit instance.

The token method verifies that the user is logged in then authenticates the user using the ChatkitService authenticateUser method. The result of this is a JWT Token that we would use in our frontend client to make requests to the Pusher API.

Frontend Client

To create the view for the chats:

    > adonis make:view chat

Execute the above command on the terminal to create the chat.edge file. It will contain the following code:

    {{-- ./resources/views/chat.egde --}}
    @layout('layout.main')
    @section('title') 
      Chat 
    @endsection
    @section('content')
      <input type="hidden" name="user_id" id="user_id" value="{{ auth.user.id }}">
      <div class="row h-100">
        <div class="col-4 chat-list">
          <div class="head">
            <h5 class="vertical-align text-center">Chatly</h5>
          </div>
          <div id="rooms-list" class="list-group">
            <p class="text-muted text-center">Loading chat rooms...</p>
          </div>
          <div class="btns row">
            <div class="col-sm-6">
              <button class="btn btn-primary" type="button" data-toggle="modal" data-target="#createRoomModal">Create Room</button>           
            </div>
            <div class="col-sm-6">
              <form action="/logout" method="post">
                {{ csrfField() }}
                <button class="btn btn-danger" type="submit">Logout</button>              
              </form>
            </div>
          </div>
        </div>
        <div class="col-8 chat-details">
          <div class="head">
            <p id="group-name" class="align-middle">Chat</p>
          </div>
          <div class="chat">
            <div id="messages-list">

            </div>
            <div class="hide input-group mb-3" id="message-input-group">
              <input id="messageInputText" type="text" class="form-control" placeholder="What do you want to say?" aria-label="What do you want to say?" aria-describedby="basic-addon2">
              <div class="input-group-append">
                <button class="btn btn-outline-primary" id="sendMessageBtn" type="button">Send</button>
              </div>
            </div>
          </div>
        </div>
      </div>
      <!-- Create Room Modal -->
      <div class="modal fade" id="createRoomModal" tabindex="-1" role="dialog" aria-labelledby="createRoomModal" aria-hidden="true">
        <div class="modal-dialog" role="document">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title" id="exampleModalLongTitle">Create Room</h5>
              <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">&times;</span>
              </button>
            </div>
            <div class="modal-body">
              <form action="" method="post">
                <div class="form-group">
                  <label for="roomNameInput">Room name</label>
                  <input type="email" class="form-control" id="roomNameInput" aria-describedby="emailHelp" placeholder="Enter room name">
                  <small id="emailHelp" class="form-text text-muted">Should not be longer than 60 characters.</small>
                </div>
                <div class="form-check">
                  <input type="checkbox" class="form-check-input" id="isPrivateCheck">
                  <label class="form-check-label" for="isPrivateCheck">Private Room?</label>
                </div>
              </form>
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
              <button id="createNewRoomBtn" type="button" class="btn btn-primary">Create Room</button>
            </div>
          </div>
        </div>
      </div>
        <!-- Join Room Modal -->
      <div class="modal fade" id="joinRoomModal" tabindex="-1" role="dialog" aria-labelledby="joinRoomModal" aria-hidden="true">
        <div class="modal-dialog" role="document">
          <div class="modal-content">
            <div class="modal-header">
              <h5 class="modal-title" id="exampleModalLongTitle">Join Room</h5>
              <button type="button" class="close" data-dismiss="modal" aria-label="Close">
                <span aria-hidden="true">&times;</span>
              </button>
            </div>
            <div class="modal-body">
              <p>You are currenty NOT a member of this room, join?</p>
            </div>
            <div class="modal-footer">
              <button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
              <button id="joinRoomBtn" type="button" class="btn btn-primary">Join Room</button>
            </div>
          </div>
        </div>
      </div>
    @endsection
    @section('scripts')
      <script src="https://unpkg.com/pusher-chatkit-client"></script>
      {{ script('chat') }}
    @endsection

As usual, we are extending the main layout template we created earlier by inserting HTML into the content and scripts sections.

In the scripts section, we are importing the Pusher Chatkit client library and our custom chat.js file located in the public folder using the script global function from AdonisJS.

Create a chat.js file in the public folder. Open the chat.js file and let us start integrating our app view with some Pusher Chatkit magic.

Instantiate ChatManager

To initiate the ChatManager, we have to initialize the TokenProvider instance in order to fetch our access token. The TokenProvider constructor requires the url for authentication. We created this url earlier in our authController, aptly named the token function. We initialize the TokenProvider like this:

    var tokenProvider = new Chatkit.TokenProvider({
      url: "/token"
    });

The ChatManager constructor requires an object with the following attributes:

  • instanceLocator (Get this from chatkit dashboard )
  • userId (ID of the user you want to connect as)
  • tokenProvider (Instance of the TokenProvider object)
    var chatManager = new Chatkit.ChatManager({
      instanceLocator: "v1:xxx:xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxx",
      userId: $("#user_id").val(),
      tokenProvider: tokenProvider
    });

Replace the value of the instanceLocator with the one we created earlier. The userId value is obtained from a hidden input in our frontend code (refer to the chat.edge file) that holds the user ID of the user currently logged in.

Get User Details

To get the user details, we need to connect to the Chatkit service using the connect() method on the chatManager. Before that though, let us create variables to hold the user, rooms and the selected room.

Add the block of code below to the very top of the chat.js file:

    var user,
      rooms = [],
      selected_room = null;

Next, we need to connect to Chatkit servers to get our user details. Put the code block below after the initialisation of chatManager:

    /**
     * Activate connection to chatKit
     */
    chatManager.connect({
      onSuccess(currentUser) {
        user = currentUser;
        // Load chat rooms user belongs to
        loadUserChatRooms();
      },
      onError(error) {
        console.log('Error on connection');
      }
    });

The code above connects to Chatkit using the chatManager and assigns the result from the onSuccess callback to the user variable and proceeds to call the loadUserChatRooms() function, which we will define in a bit.

User Chatrooms

We will define a function to get the rooms the user belongs to and all the rooms he is eligible to join. The loadUserChatRoom() function puts all the available rooms from the current user into the rooms variable we created earlier while avoiding duplicates. It also subscribes the user to all the rooms he is a member of using the subscribeUserToRoom() function, which is a wrapper around the call to the subscribeToRoom() method on the user object. Subscribing to a room means that the client will receive new messages as they are sent.

The second parameter passed into the subscribeToRoom() method on the user object is known as the roomDelegate and lets us respond to events that happens in that particular room. As we can see below we have the newMessage and it is being handled by the handleNewMessage() function, which will be defined shortly.

The function also pushes the list of available rooms to the view in the addRoomToList() method.

    function loadUserChatRooms() {
      // put rooms created by the user into the rooms global variable
      rooms = user.rooms;
      // getAllRooms that the user is a member of and can join.
      user.getAllRooms(
        rms => {
          // Push into existing rooms while avoiding duplicates
          rms.forEach(rm => {
            if (!rooms.find(room => room.id == rm.id)) {
              rooms.push(rm);
            }
          });
          // Clear existing data
          $("#rooms-list").html("");
          rooms.forEach(room => {
            // Subscribe to rooms that the user is a member of
            subscribeUserToRoom(room);
            // Add Room to list sidebar
            addRoomToList(room);
          });
        },
        error => {
          console.log(`Error getting rooms: ${error}`);
        }
      );
    }

    function subscribeUserToRoom(room) {
      if (room.userIds.find(user_id => user_id == user.id)) {
        room.user_is_a_member = true;
        user.subscribeToRoom(
          room,
          {
            newMessage: handleNewMessage
          },
          100
        );
      } else {
        room.user_is_a_member = false;
      }
    }

    function addRoomToList(room) {
      var $a = $("<a>", {
        "data-roomid": room.id,
        href: "#",
        class:
          "single-room list-group-item list-group-item-action flex-column align-items-start"
      });
      var $div = $("<div>", { class: "d-flex w-100 justify-content-between" });
      var $h5 = $("<h5>", { class: "mb-1", html: room.name });
      var $small = $("<small>", { html: (room.isPrivate) ? 'Private' : 'Public' });
      var $p = $("<p>", { class: "mb-1", html: (room.user_is_a_member) ? 'Member' : 'Not a member - Click to join' });
      $a.click(loadChatRoom);
      $div.append($h5).append($small);
      $a.append($div).append($p);
      $("#rooms-list").append($a);
    } 

Creating and Joining a Chatroom

Users of Chatkit have the ability to create chatrooms and can choose to make it public or private. The user.createRoom() method gives us the ability to create rooms for a particular user. If the room is public, we must call the joinRoom() function to add the user to the room. Here is the code below:

    $("#createNewRoomBtn").on("click", event => {
      $(this).attr("disabled");
      var room_name = $("#roomNameInput").val();
      var is_private = $("#isPrivateCheck").is(":checked") ? true : false;
      createChatRoom(room_name, is_private);
      $("#createRoomModal").modal("hide");
    });

    function createChatRoom(room_name, private = false) {
      var options = {
        name: room_name,
        private: private
      };
      user.createRoom(
        options,
        room => {
          var room_type = private == true ? "private" : "public";
          var message = `Created ${room_type} room called ${room.name}`;
          alert(message);
          rooms.push(room);
          addRoomToList(room);
          selected_room = room;
          // If it's public, we need to manually join the group
          if (!private){
            joinRoom(selected_room, false)
          }
          // Subscribe user to Room
          subscribeUserToRoom(room);
        },
        error => {
          console.log(`Error creating room ${error}`);
        }
      );
    }
    $("#joinRoomBtn").on("click", event => {
      joinRoom(selected_room, true)
      $("#joinRoomModal").modal("hide");
    });
    function joinRoom(room, load_messages = false) {
      user.joinRoom(
        room.id,
        room => {
          alert(`Joined room: "${room.name}"`);
          if (load_messages) {
            loadRoomMessages(room, messages => subscribeUserToRoom(room));        
          }
        },
        error => {
          alert(`Error joining room ${room.name}`);
          console.log(`Error joining room ${room.name}: ${error}`);
        });
    }

In the above code block, we are responding to the user clicking the createNewRoomBtn() on the Create Room Modal which invokes the createRoom() method, adds the room to list of rooms on the sidebar, joins the room if it’s public and subscribes to receive messages from that room.

Note: You can only join public rooms, you need to be invited in order to join private rooms.

Load a Single Chatroom

In the code above, while we were generating the HTML code, we attached a function to the click event of the <a> tags surrounding each room, so when they are clicked we can perform certain actions like load the messages in the selected room and allow the user send messages to the group. You can see the loadChatRoom() method below:

    function loadChatRoom(event) {
      var roomid = $(this).data("roomid");
      // Find Chat room from array
      selected_room = rooms.find(room => room.id == roomid);
      if (!selected_room) {
        alert("You selected an Unavailable Room");
        return;
      }
      if (!selected_room.user_is_a_member) {
        // Ask user to join
        $("#joinRoomModal").modal("show");
        return;
      }
      loadRoomMessages(selected_room);
      $('#group-name').text(selected_room.name);
    }

    function loadRoomMessages(room, cb = null) {
      user.fetchMessagesFromRoom(
        room,
        {
          limit: 20
        },
        messages => {
          room.messages = messages ? messages : [];
          // Clear Existing
          $("#messages-list").html("");
          room.messages.forEach(message => {
            // Push Messages to view
            addMessageToCurrentRoomView(message);
          });
          updateScroll();
          $("#message-input-group").removeClass("hide");
          if (cb) {
            cb(null, messages);
          }
        },
        error => {
          console.log(`Error fetching messages from ${room.name}: ${error}`);
        }
      );
    }

    function addMessageToCurrentRoomView(message) {
      var userIsSender = message.sender.id == user.id ? true : false;
      var card_div_class = `card message ${(userIsSender) ? "message-sent" : "message-recieved"}`;

      var $card_div = $("<div>", { class: card_div_class });
      var $card_body_div = $("<div>", { class: "card-body" });
      var $img = $("<img>", {
        src: message.sender.avatarURL
          ? message.sender.avatarURL
          : "/profile-picture.png",
        class: "rounded float-right",
        alt: "Image"
      });
      var $h6 = $("<h6>", {
        class: "card-subtitle mb-2 text-muted",
        html: message.sender.name
      });
      var $p = $("<p>", { html: message.text });
      $card_body_div
        .append($img)
        .append($h6)
        .append($p);
      $card_div.append($card_body_div);
      $("#messages-list").append($card_div);
    }

    /**
     * Scrolls to the bottom of the Messages div
     */
    function updateScroll(){
      var chat = document.getElementById("messages-list");
      chat.scrollTop = chat.scrollHeight;
    }

The loadChatRoom() method updates the selected_room variable with the room that was selected by the user, then proceeds to fetch the last 20 messages in that room from chatkit servers and renders it using the addMessageToCurrentRoomView() function for each message.

Sending and Receiving Messages

Send messages using Chatkit is very straight forward. using the sendMessage() function on the user object we can send messages to a particular room:

    $("#sendMessageBtn").on("click", event => {
      var message = $("#messageInputText").val();
      sendMessage(message);
      $("#messageInputText").val("");
    });

    function sendMessage(text) {
      user.sendMessage(
        {
          text: text,
          roomId: selected_room.id
        },
        messageId => {
          console.log(`Added message to ${selected_room.name}`);
        },
        error => {
          console.log(`Error adding message to ${selected_room.name}: ${error}`);
        }
      );
    }

    function handleNewMessage(message) {
      // Push message to the appropriate room
      rm = rooms.find(room => room.id == message.room.id);
      if (rm.messages) {
        rm.messages.push(message);
      } else {
        rm.messages = [message];
      }
      // If the user is currently in that room, push it to the view
      if (selected_room && selected_room.id == message.room.id) {
        addMessageToCurrentRoomView(message);
        updateScroll();
      }
    }

Earlier, we had subscribed to the newMessage delegate when listing the rooms, the handleMessage() function above defines what happens when we receive a new message, including the ones we sent.

If your app isn’t running already, execute the command below to start it:

    > adonis serve --dev

It serves at port 3333 by default, so you can visit 127.0.0.1:3333/register to register a new user and start chatting.

Note: If you visit the homepage without being registered or logged in, you will see an error from AdonisJS. You can see how to fix that here.

Here is the final version of the app in action:

Completed Chatly chat view

Conclusion

In this tutorial we have seen how to build a very basic group chat application using AdonisJS and Pusher Chatkit. The Chatkit service makes it super easy to set up a group chat application, as we have seen in this tutorial. The code for the entire tutorial can be found on Github.

  • Chatkit

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 28 Scrutton Street, London EC2A 4RP.