Not all web apps need chat, but if you want to communicate with your customers, or you want them to communicate with each other, you’re going to run into this problem quickly. CometChat Pro is a fantastic alternative to rolling your own chat service. You’ll see that you can easily insert chat into your application, without it getting in the way of other content you may be delivering.

Prerequisites

This tutorial doesn’t assume much knowledge at all. Ruby on Rails is an incredibly easy framework to get started with. If you’ve worked with other web technologies, you’ll probably find it very clear and very opinionated.

CometChat Pro itself needs to run in Javascript, so you’ll see a marriage between a Ruby-based framework and view manipulation through Javascript. There are many more complicated ways to handle this in Rails. For this tutorial, I kept everything in basic Javascript and the templating engine Rails ships with: erb. Rails enthusiasts will be able to easily translate these foundational tools into their specialized setups.

I assume you are able to run Rails applications - which means you already have Ruby and bundler installed. This application uses Ruby 2.5.3 and Rails 5.2.2 but nothing happening here is special for latest versions - you could plug these tools into any modern version of Rails.

Introduction

One of the hardest parts of building chat is dealing with concurrency - users need to see new messages without refreshing their browsers. CometChat Pro achieves this with very little code, as you will see.

CometChat Pro example running in two windows

If you’ve ever tried to use Action Cable to achieve similar results, you will be very pleased with how quickly you can copy-paste your way to real-time updates in the browser.

You can jump right into the code on Github, or take this guy for a spin yourself. Let’s get it!

Setting up

Initialization

Grab yourself a terminal and fire up a new Rails app: rails new cometchatpro --skip-active-record -T. We don’t need a database for this, and the -T prevents Rails from creating test files for us. If you’ve already got an application, you can skip this step - it should be easy to pop this code into an existing project.

Environmental variables and CometChat credentails

Next, you’re going to need an API key and an App ID from CometChat Pro. Once you’ve logged in, head to the dashboard, create a new app, then generate an API key. You’ll need fullAccess, not just authOnly. Grab those credentials and add them to your .env file. You’ll need your file to look something like this:

COMETCHAT_APP_ID=23n2f2n3p2y3
COMETCHAT_API_KEY=av22g24ll

Next, add dotenv-rails to your Gemfile and bundle. Now your application can read your environmental variables. These are private credentials for your CometChat Pro account; do not commit this file to your version control!

Chat pages

Next we’ll add some boilerplate Rails views and routing so we can chat as a user with other users. Generate a new controller: rails generate controller Users. This will create a number of files, which we’ll get into shortly. You’ll also need to add a route to your new view in config/routes.rb. Here’s what my routes.rb looks like:

Rails.application.routes.draw do
  resources :users, only: %i[index show]

  root 'users#index'
end

If you jump into the GitHub code or pop over to the hosted app, you’ll see that I have added user creation. I’ve left it in place because I already built it, but CometChat Pro comes with sample users baked into every account, so you can start testing right away without needing to set up user creation first. I’ve omitted discussion of user creation for this tutorial in the sake of brevity, but you can see the code needed for that in the Github repo.

You’ll also need to create a /views/users/show.html.erb file. We’ll fill it will content in a moment.

CometChat Service

Let’s get into the meat of our application - interacting with the CometChat API. I organized interactions with CometChat Pro into a service. Here’s what it looks like:

class CometChatService
  include HTTParty
  BASE_URI = 'https://api.cometchat.com/v1'.freeze

  def fetch_users
    response = HTTParty.get("#{BASE_URI}/users", headers: headers)
    response.dig('data')
      &.map { |user| {name: user['name'], id: user['uid']} }
  end

  private

  def headers
    {
      apikey: ENV['COMETCHAT_API_KEY'],
      appid: ENV['COMETCHAT_APP_ID']
    }
  end
end

CometChat Pro does a lot of things, and as we grow our app, we might want to fill this service with further interactions with their API. Here, all we need to do is fetch a list of users we can chat with. Note that no gem is needed to interact with CCP, you simply send your requests however you like (I used HTTParty) with the proper headers (from our .env file) and CCP sends back the information you requested.

I map the JSON response from CCP into the information my application needs - the user name and the user ID.

Working with the service in the users controller

Now that we have a protocol established for speaking with the CCP API, we can use it in our UsersController. Here’s what mine looks like:

class UsersController < ApplicationController
  def index
    @users = CometChatService.new.fetch_users
  end

  def show
    users = CometChatService.new.fetch_users
    @current_user = users.find { |user| user[:id] == params[:id] }
    @users = users.reject { |user| user[:id] == @user[:id] }
  end
end

I’ve skipped over the index page for brevity, but it’s just a list of users who we may “chat as”. You can think of it as the most insecure, ridiculous login page imaginable. On both views, we fetch the users registered with our CometChat Pro app. We’ve already mapped the JSON response in the service, so on the index and show pages we loop through users to display them. On the show page, we are chatting “as” a user, so we don’t want him to show up in the list of possible people to chat with (line 9).

 List of users in a CometChat Pro app

The user show view

Let’s walk through the sections of our user show page in turn. In a minute, we’ll discuss the Javascript necessary to make them run. I’ve also omitted discussion of styling. You can see full code for styling in the GitHub repo, or of course you could implement your own (surely much-better) styling.

Who is talking?

To make information about our “logged-in” user (the one whose show page we are on) available to the CometChat service, we’ll want to include an invisible div with the user name and id. You can put this anywhere on your page:

The user list

To display the list of users as shown above, here’s the erb code:

&ltul class="list-group list-group-flush">
  &lt% @users.each do |user| %>
    &ltli class="list-group-item user-select bg-light" id="&lt%= user[:id] %>" name='user-select'>
      &ltdiv class="d-flex w-100 justify-content-between">
        &lth5 class="mb-1">&lt%= user[:name] %>&lt/h5>
      &lt/div>
    &lt/li>
  &lt% end %>
&lt/ul>

There’s nothing special happening here - the only thing to note is that we are setting the user id as an id on the list-item. We’ll need it later to reference which user we are speaking to. Remember that our @users has come from the controller, where we gathered the list of users from the CometChat service.

Receiving and displaying messages

Next, let’s look at how we will display incoming messages. Our code starts as this:

&ltdiv id="messages" class="messages">
    Messages loading...
&lt/div>

We need to supply a div for CometChat Pro to load messages into. I’ve called mine messages but you could call yours anything you like.

Sending messages

We’ll use Ruby’s form helpers to create our message form, but we don’t let Ruby handle any of the functionality here:

&lt%= form_for :message, remote: true do |f| %>
    &ltdiv class="col-xs-9">
      Message : &lt%= f.text_area :text, class: "form-control" %>&ltbr/>
    &lt/div>
    &ltdiv class="col-xs-3 capitalize">
      &lt%= button_tag "Send Message", type: 'button', onclick: "javascript:sendMessage()", class: "btn btn-info btn-block" %>
    &lt/div>
&lt% end %>

When we click the “Send Message” button, we’ll be using Javascript to work our magic. The Rails form will just sit there, happily waiting for more input.

The heart of our application: Javascript methods

The CometChat Pro service runs on Javascript and we’ll be using their clear sample code to make the pieces of our app work.

We’ll need to take the following steps:

1. Initialization - set up a connection to the CometChat service.

2. Log in a current user.

3. Select a user to chat with and fetch message history with that person.

4. Add an event listener to pick up new messages from the other person.

5. Set up an action to send messages.

Initialization

First, we’ll need to get our app talking to the CometChat Pro service and log in. In application.html.erb, add this line between the <head> tags:

&ltscript type="text/javascript" src="https://unpkg.com/@cometchat-pro/chat/CometChat.js">&lt/script>

Add these lines to the bottom of /views/users/show.html.erb:

&lt% javascript_include_tag 'show', cache: 'myfiles' %>
&ltscript type="text/javascript">
    setUserListeners();
    document.addEventListener('turbolinks:load', initializeChat);
&lt/script>

We’ll look at the setUserListeners method in a second. Finally, we’re referencing a show.js.erb, which we’ll need to create and add to app/assets/javascripts.

Here’s our first Javascript method:

const initializeChat = () => {
    CometChat.init('').then(
      hasInitialized => {
        loginUser()
      },
      error => {
        console.log("Initialization failed with error:", error);
      }
  )};

Because we loaded the CometChat Pro Javascript in our application.html.erb, we can now use CometChat methods in our Javascript file. The first one we’ll use is .init, which requires us to send in our APP ID as a credential. We’re calling the loginUser method once our chat is initialized.

User login

After initialization, we need to log in the current user. This application includes no security whatsoever - anyone can chat as anyone else. Obviously you’d want to have something just a tiny bit more customized in a production app. Here’s how we log in our user on CometChat Pro:

const loginUser = () => {
    const userDiv = document.getElementById('user-id');
    if (!userDiv) { return true }

    const id = userDiv.dataset.id;
    CometChat.login(id, '').then(
        User => {
          const messageDiv = document.getElementById('messages');
          messageDiv.innerHTML = `<div class="whisper">Choose a  user to start chatting</div>`;
        },
        error => {
            console.log("Login failed with exception:", {error});
        })
}

Remember that weird tag('div') that held our user information from show.html.erb? Here we use it to find out what user needs to log in. If something has gone wrong with that div, we just bail from this operation.

Next, we call our next CometChat method: .login. We take the id we got from the user div, and use our API credentials. If CometChat Pro sends back a User, we update the message div to say “Choose a user to start chatting”.

setUserListeners

Let’s hop back to the show.html.erb page where we called setUserListeners() at the bottom of the view. In order to know who we want to chat with, we’ll need to listen for clicks on the user list. Here’s what that code looks like:

const setUserListeners = () => {
    const userDivs = document.getElementsByName('user-select');
    userDivs.forEach(user => user.addEventListener("click", e => {
        const id = e.target.id || e.target.offsetParent.id
        setUser(id);
        fetchMessages(id);
    }))
}

userDivs is a node list of divs, one for each of the users we can chat with. We loop through them and add event listeners on each. If we click on a user, we want to first call setUser to tell our application who we want to talk to, and then fetchMessages for that user.

setUser and addMessageListener

Here’s where we really get some magic and CometChat Pro takes care of the heavy lifting for us. Once we log our user in and select someone to chat with, we want to see any new messages from that person in real time. With CometChat Pro, we don’t have to worry about keeping any websockets open or dealing with refreshing - the messages just appear as you would expect.

The setUser function is only concerned with making the active user blue in the list, so I’ve skipped it here. But it also calls the addMessageListener class, which is critical for receiving new messages as they come in:

const addMessageListener = id => {
    CometChat.addMessageListener(
        'listener_id',
        new CometChat.MessageListener({
            onTextMessageReceived: textMessage => displayNewMessage(id, textMessage)
        })
)}

This is taken almost directly from the CometChat Pro documentation. We call the CometChat addMessageListener method, then using incoming messages to update our message div. Fire up two different browsers to see this in action (or just check out the gif above).

displayNewMessage

When that new message comes in, we’ll need to update our view. Here’s the code:

const displayNewMessage = (currentChatterId, msg) => {
    const userDiv = document.getElementById('user-id');
    const id = userDiv.dataset.id;
    if (![currentChatterId, id].includes(msg.sender.uid)) { return; }

    const newNode = document.createElement("div")
    newNode.innerHTML = newMessage(msg, id)
    const messageDiv = document.getElementById('messages')
    messageDiv.appendChild(newNode)
    messageDiv.scrollTop = messageDiv.scrollHeight
}

First, if the incoming message isn’t between our logged-in user and the user we’re chatting with, we ignore it. Then, we need to process the new message - we construct a new div with the message, then we append it to the messages div, again scrolling to the bottom so we can see it.

newMessage

Our newMessage method will simply format the message - it’s a new bubble in our chat app. We check if the sender has the same id as the logged-in user so we can apply the self class to the div - making it pink for “self” and blue for everybody else.

const newMessage = (msg, id) => {
    return (
        `&ltdiv class='message ${msg.sender.uid === id && 'self'}'>
          &ltdiv class='message-text'>${msg.text}&lt/div>
          &ltdiv class='message-sender'>- ${msg.sender.name}&lt/div>
        &lt/div>`
    )
}

fetchMessages

Before we start chatting, we need to see the messages that have come before and anything that was exchanged when we weren’t chatting with that particular user. Our fetchMessages method will grab the messages between our logged-in user and our selected user.

const fetchMessages = id => {
    if (!id) { return; }

    const limit = 30;
    const messagesRequest = new CometChat.MessagesRequestBuilder().setUID(id).setLimit(limit).build();
    messagesRequest.fetchPrevious().then(
        messages => {
            const messageDiv = document.getElementById('messages');
            messageDiv.innerHTML = messages.length > 0 ?
              messages.map(msg => newMessage(msg, id)).join('') :
              `&ltdiv class="whisper">Start of message history&lt/div>`;
            messageDiv.scrollTop = messageDiv.scrollHeight
        },
        error => {
            console.log("Message fetching failed with error:", error);
        }
    );
}

If for some reason we don’t have an id, we’ll return so as not to get errors. First we construct our request, setting the ID of the user we want to talk to, and the limit of messages we want to receive. More complicated applications would want to automatically fetch results from further back in time as the user scrolls.

Once our message requester is set up, we can call fetchPrevious to grab a list of messages. This returns messages, which we can use to populate our messages div. Each message is a JSON object with a sender.uid, sender.name and a text. We map through the messages and return them as HTML objects and replace whatever is inside messages with our new message node list. Remember to .join('') them or you’ll see a bunch of commas between divs. If there were no messages, we want to show the user that this is the “start of message history”.

Last, we want to scroll to the bottom of the messages div to show the user the latest messages.

Sending new messages

We’re logging in, selecting a user to chat with, fetching all the old message history between these two, and receiving any new messages sent to us from that user. Finally, we want to be able to send messages. Here’s how we do it:

const sendMessage = () => {
    const recipient_id = document.getElementsByClassName('bg-info')[0].id;
    const message_text = document.getElementsByName('message[text]')[0].value;
    document.getElementsByName('message[text]')[0].value = ''

    const messageType = CometChat.MESSAGE_TYPE.TEXT;
    const receiverType = CometChat.RECEIVER_TYPE.USER;
    const textMessage = new CometChat.TextMessage(recipient_id, message_text, messageType, receiverType);

    CometChat.sendMessage(textMessage).then(
        message => displayNewMessage(recipient_id, message),
        error => {
            console.log("Message sending failed with error:", error);
        }
    );
}

First, we determine which user we are chatting with, based on the highlighted user in the list. Next, we read the message from the form, and clear the form so it’s ready for the next message.

Our textMessage object will call CCP’s TextMessage method, constructing a message to send based on the logged-in user, receiver id, message text, and the types of message and recipient. Then we just sendMessage with our textMessage object, wait for a message response, and render the message in the same way we would an incoming message from another user.

And that, my friends, is everything we need to install chat in our Rails app!

Conclusion

If you’ve been following along, you now have a fully-functional one-to-one chat app in your project. You can start exploring the other functionalities available through CometChat Pro, or you can start refactoring with jQuery, Slim, or other tools you may wish to incorporate to pare down the code. Hopefully seeing everything in plain Javascript made it clear what we are accomplishing, even if you want to have more efficient code in your own application.

Best of luck installing chat in your own app and have fun!