Track online presence in a Ruby on Rails app

  • Christian Nwamba
May 27th, 2018
To follow this tutorial, you will need Ruby and Rails installed on your machine. A basic understanding of Ruby, PostgreSQL and CoffeeScript will help you get the most out of this tutorial.

Introduction

We live in a social age, an age where people meet and form relationships online. On whatever online platform people meet, one important feature to have is the ability for users to know when their friends are online or offline. In this post we’ll build a simple app where we can monitor the online presence of users in realtime. When we’re done, we’d have built something that looks like this:

Prerequisites

A basic understanding of Ruby, CoffeeScript and PostgreSQL will help you get the best out of this tutorial. You can check the PostgreSQL, Ruby and Rails documentation for installation steps.

Before we start building our app, let’s ensure we have Ruby and Rails installed. Run the following command in your terminal to confirm you have both Ruby and Rails installed:

    $ ruby -v      // 2.1 or above
    $ rails -v   // 4.2 or above

Setting up the application

Open your terminal and run the following Rails commands to create our demo application:

    # create a new Rails application
    $ rails new pusher-online-presence -T --database=postgresql

Go ahead and change directory into the newly created pusher-online-presence folder:

    # change directory
    $ cd pusher-online-presence

In the root of your pusher-online-presence directory, open your Gemfile and add the following gems:

    # Gemfile

    gem 'bootstrap', '~> 4.1.0'
    gem 'jquery-rails'
    gem 'pusher'
    gem 'figaro'
    gem 'devise'

In your terminal, ensure you are in the pusher-online-presence project directory and install the gems by running:

    $ bundle install

Database setup

To get our app up and running, we’ll create a database for it to work with. You can check out this article on how to create a Postgres database and an associated user and password.

Once you have your database details, in your database.yml file, under the development key, add the following code:

    # config/database.yml

    ...
    development:
      <<: *default
      database: pusher-online-presence_development // add this line if it isn't already there
      username: database_user // add this line
      password: user_password // add this line
    ...

Ensure that the username and password entered in the code above has access to the pusher-online-presence_development database. After that, run the following code to setup the database:

    # setup database
    $ rails db:setup

Bootstrap the application

With our database all set up, first thing we’ll do is to set up Devise. Devise is a flexible authentication solution for Ruby on Rails. It helps you set up user authentication in seconds. In your terminal, run the following command:

    # run the devise generator
    $ rails generate devise:install

At this point, a number of instructions will appear in the console, one of which involves adding some code to your application.html.erb file. We’ll also add our Pusher script while we’re at it.

    # app/views/layouts/application.html.erb

      <head>
        .....
        <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
        <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
        <script src="https://js.pusher.com/4.1/pusher.min.js"></script> # add Pusher script
        <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
      </head>
      <body>
        <div class="container"> # add this block of code
          <% if notice %>
            <div class="alert alert-info alert-dismissible fade show" role="alert">
              <p class="notice m-0"><%= notice %></p>
              <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
              </button>
            </div>
          <% end %>
          <% if alert %>
            <div class="alert alert-danger alert-dismissible fade show" role="alert">
              <p class="m-0"><%= alert %></p>
              <button type="button" class="close" data-dismiss="alert" aria-label="Close">
                <span aria-hidden="true">&times;</span>
              </button>
            </div>
          <% end %>
        </div> # code block ends here
        <%= yield %>
      </body>

Next, we’ll generate our authentication view pages and user model using Devise. In your terminal, run the following command:

    # generate Devise view pages
    $ rails generate devise:views

    # generate user model
    $ rails generate devise user

    # generate migration to add extra columns to the user model
    $ rails generate migration add_username_to_users username:string:uniq is_signed_in:boolean

Now that we have our migration files generated, we’ll make some modifications to some files and then run our migrations.

In your migrate folder, open the add_username_to_users migration file and add the following:

    # db/migrate/20180524154037_add_username_to_users.rb

    class AddUsernameToUsers < ActiveRecord::Migration[5.1]
      def change
        add_column :users, :username, :string
        add_index :users, :username, unique: true
        add_column :users, :is_signed_in, :boolean, default: true # update this line
      end
    end

Note that your add_username_to_users file may have a different name from what is above, based on when you ran the migration commands. Now, let’s add some validation to our user model:

    # app/models/user.rb

    class User < ApplicationRecord
      # Include default devise modules. Others available are:
      # :confirmable, :lockable, :timeoutable and :omniauthable
      validates :username, presence: :true, uniqueness: { case_sensitive: false } # add this line
      validates :is_signed_in, inclusion: [true, false] # add this line
      devise :database_authenticatable, :registerable,
             :recoverable, :rememberable, :trackable, :validatable
    end

Update the code in your application controller with the following:

    # app/controllers/application_controller.rb

    class ApplicationController < ActionController::Base
      protect_from_forgery with: :exception
      before_action :configure_permitted_parameters, if: :devise_controller?
      before_action :authenticate_user!

      protected

      def configure_permitted_parameters
        added_attrs = [:username, :email, :password, :password_confirmation, :remember_me]
        devise_parameter_sanitizer.permit :sign_up, keys: added_attrs
        devise_parameter_sanitizer.permit :account_update, keys: added_attrs
      end
    end

Now, we’re ready to run our migration and see our app. In your terminal, run the following:

    # run database migrations
    $ rails db:migrate

After running migrations, start the development server on your terminal by running rails s. Visit http://localhost:3000 in your browser to see your brand new application:

Pusher account setup

Now that our application is up and running, it’s time for us to create our app on Pusher. Head over to Pusher and sign up for a free account.

Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:

Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate with Pusher, to be provided with some boilerplate setup code:

Click the App Keys tab to retrieve your keys

Styling the authentication pages

While bootstrapping the application, we generated some views courtesy of Devise. Those pages amongst others include our sign up and login pages. We’ll add some styling to the login and signup pages. Replace the code in the following files with the ones below:

    # app/views/devise/registrations/new.html.erb

    <div class="container col-11 col-md-7 col-lg-5 bg-info login-container p-4 mt-5">
      <h2>Sign up</h2>
      <%= form_for(resource, as: resource_name, url: registration_path(resource_name)) do |f| %>
        <%= devise_error_messages! %>
        <div class="field">
          <%= f.label :email %><br />
          <%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'form-control' %>
        </div>
        <div class="field">
          <%= f.label :username %><br />
          <%= f.text_field :username, autofocus: true, autocomplete: "username", class: 'form-control' %>
        </div>
        <div class="field">
          <%= f.label :password %>
          <% if @minimum_password_length %>
          <em>(<%= @minimum_password_length %> characters minimum)</em>
          <% end %><br />
          <%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
        </div>
        <div class="field">
          <%= f.label :password_confirmation %><br />
          <%= f.password_field :password_confirmation, autocomplete: "off", class: 'form-control' %>
        </div>
        <div class="actions">
          <%= f.submit "Sign up", class: 'btn btn-primary my-2' %>
        </div>
      <% end %>
      <%= render "devise/shared/links" %>
    </div>
    # app/views/devise/sessions/new.html.erb

    <div class="container col-11 col-md-7 col-lg-5 bg-info login-container p-4 mt-5">
      <h2>Log in</h2>
      <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %>
        <div class="field">
          <%= f.label :email %><br />
          <%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'form-control' %>
        </div>
        <div class="field">
          <%= f.label :password %><br />
          <%= f.password_field :password, autocomplete: "off", class: 'form-control' %>
        </div>
        <% if devise_mapping.rememberable? -%>
          <div class="field">
            <%= f.check_box :remember_me %>
            <%= f.label :remember_me %>
          </div>
        <% end -%>
        <div class="actions">
          <%= f.submit "Log in", class: 'btn btn-primary my-2' %>
        </div>
      <% end %>
      <%= render "devise/shared/links" %>
    </div>

If you visit http://localhost:3000/users/sign_in or http://localhost:3000/users/sign_up, you’ll see our forms are still not looking pretty. Let’s change that with Bootstrap.

Add the following code to your application.js file, all before the last line:

    # app/assets/javascripts/application.js

    .....
    //= require jquery3 # add this line
    //= require popper # add this line
    //= require bootstrap # add this line
    //= require_tree .

Rename your application.css file to application.scss and add the following styles:

    # app/assets/stylesheets/application.scss

     @import "bootstrap";
     @import url('https://fonts.googleapis.com/css?family=Dosis');
     body {
      font-family: 'Dosis', sans-serif;
     }
     .login-container {
       border-radius: 3px;
     }
     .full-page {
       height: 100vh;
     }
     .left {
       overflow-y: scroll;
       height: 86vh;
     }
     .active-user {
       border-radius: 3px;
       padding-left: 0.5rem;
       font-weight: 900;
       margin-right: 0.5rem;
       .online-icon {
         border-radius: 50%;
         width: 0.5rem;
         height: 0.5rem;
       }
     }
     .left::-webkit-scrollbar {
      width: 0.3rem;
    }
    .left::-webkit-scrollbar-thumb {
      background: #fff3; 
    }

If we reload our authentication pages now, we should be greeted with a pretty sight.

If you encounter any error related to application.html.erb, in config/boot.rb, change the ExecJS runtime from Duktape to Node.

    # config/boot.rb
    ENV['EXECJS_RUNTIME'] ='Node'

Building the homepage

With our authentication pages set up, let’s design our homepage. We’ll set our root page to our index file and add some HTML markup and styling to it.

    # config/routes.rb

    Rails.application.routes.draw do
      get 'users/index'
      devise_for :users
      root 'users#index'
      # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
    end

Next, we’ll generate our users controller and add some markup to our index page.

    # generate a users controller with an index view
    $ rails g controller users index

In your index.html.erb file, add the following code:

    # app/views/users/index.html.erb

    <div class="container-fluid full-page">
      <div class="row">
        <div class="col-6 col-md-3 col-lg-2 bg-dark full-page">
          <% if user_signed_in? %>
            <p class="text-white mt-3">Signed in as <%= current_user.username %></p>
          <% end %>
          <h6 class="text-white my-3">Online Users</h6>
          <div class="left">
            <% @users.each do |user| %>
              <% if user.username != current_user.username and user.is_signed_in %>
                <p class="active-user bg-white" data-id="<%= user.id %>">
                  <span class="online-icon d-inline-block bg-success"></span>
                  <span class="username">@<%= user.username %></span>
                </p>
              <% end %>
            <% end %>
          </div>
        </div>
        <div class="col-6 col-md-9 col-lg-10 bg-light full-page right py-3">
          <%= link_to 'Log out', destroy_user_session_path, method: :delete, class: 'btn btn-warning d-inline-block float-right' %>
        </div>
      </div>
    </div>

Lastly, we’ll add the following code to our users controller:

    # app/controllers/users_controller.rb

    class UsersController < ApplicationController
      def index
        @users = User.all
      end
    end

Now you can go ahead and visit http://localhost:3000/ in the browser to see our new homepage; after you create an account.

Realtime service with Pusher

Devise controls our users’ sessions via its sessions controller. For us to know when a user logs in or logs out, all we need to do is publish the events via Pusher. This way, we can subscribe to them and update the client side of our application.

First, let’s initialize our Pusher client. In the config/initializers folder, create a pusher.rb file and add the following code:

    # config/initializers/pusher.rb

    require 'pusher'
    Pusher.app_id = ENV["PUSHER_APP_ID"]
    Pusher.key = ENV["PUSHER_KEY"]
    Pusher.secret = ENV["PUSHER_SECRET"]
    Pusher.cluster = ENV["PUSHER_CLUSTER"]
    Pusher.logger = Rails.logger
    Pusher.encrypted = true

Next, run figaro install in your terminal. It will generate an application.yml file. In the application.yml file add your Pusher keys:

    # config/application.yml

    PUSHER_APP_ID: 'xxxxxx'
    PUSHER_KEY: 'xxxxxxxxxxxxxxxxx'
    PUSHER_SECRET: 'xxxxxxxxxxxxxx'
    PUSHER_CLUSTER: 'xx'

With our Pusher client set up, let’s create a sessions controller that extends the Devise session controller. In your terminal, run the following command:

    # generate a sessions controller
    $ rails generate controller sessions

In the sessions controller, add the following code:

    # app/controllers/sessions_controller.rb

    class SessionsController < Devise::SessionsController
      after_action :notify_pusher_login, only: :create
      before_action :notify_pusher_logout, only: :destroy

      def notify_pusher_login
        user = User.find(current_user.id)
        user.update(is_signed_in: true)
        notify_pusher 'login'
      end

      def notify_pusher_logout
        user = User.find(current_user.id)
        user.update(is_signed_in: false)
        notify_pusher 'logout'
      end

      def notify_pusher(activity_type)
        Pusher.trigger('activity', activity_type, current_user.as_json)
      end
    end

In the code above, we have two callbacks; an after_action for after a user logs in and a before_action for before a user logs out. In both callbacks, we update the user’s is_signed_in status and notify Pusher.

Now, let’s inform Devise of our new sessions controller. In your routes file, add the following code:

    # config/routes.rb

    Rails.application.routes.draw do
      get 'users/index'
      devise_for :users, :controllers => { :sessions => "sessions" } # update this line
      root 'users#index'
    end

Lastly, in our app, after a new user signup, users are automatically logged in. So we need to also publish login events whenever a there is a new signup. Let’s update our user model to achieve this:

    # app/models/user.rb

    class User < ApplicationRecord
      # Include default devise modules. Others available are:
      # :confirmable, :lockable, :timeoutable and :omniauthable
      validates :username, presence: :true, uniqueness: { case_sensitive: false }
      validates :is_signed_in, inclusion: [true, false]
      devise :database_authenticatable, :registerable,
             :recoverable, :rememberable, :trackable, :validatable
      after_create :notify_pusher # add this line

      def notify_pusher # add this method
        Pusher.trigger('activity', 'login', self.as_json)
      end

      def as_json(options={}) # add this method
        super(
          only: [:id, :email, :username]
        )
      end
    end

Updating the UI

Now that Pusher is aware of users’ log in and log out, all we need to do is to subscribe to the event and make the necessary changes to the DOM. Rename your users.coffee file to users.coffee.erb and add the following code:

    # app/assets/javascripts/users.coffee.erb

    $(document).ready =>
      <%# function for adding a user to the DOM when they log in %>
      addLoggenInUser = (user) ->
        $('.left').append """
          <p class="active-user bg-white" data-id="#{user.id}">
            <span class="online-icon d-inline-block bg-success"></span>
            <span class="username">@#{user.username}</span>
          </p>
        """
        return
      <%# function for removing a user from the DOM when they log out %>
      removeLoggedOutUser = (user) ->
        user = $ 'p[data-id=\'' + user.id + '\']'
        $(user).remove()
        return

      <%# subscribe our Pusher client to the activity channel and if there's a login or logout event, call the necessary function %>
      pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
        cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
        encrypted: true)
      channel = pusher.subscribe('activity')
      channel.bind 'login', (data) ->
        addLoggenInUser data
      channel.bind 'logout', (data) ->
        removeLoggedOutUser data
        return
      return

In the code above, we subscribed our Pusher client to the activity channel and listened to the login and logout event. Whenever those events are broadcast, we call the appropriate function to manipulate the DOM.

Bringing it all together

Restart the development server if it is currently running. Visit http://localhost:3000 in two separate incognito browser tabs to test the app. You should see users appear and disappear from the sidebar in realtime as they log in and logout.

Conclusion

In this post, we have successfully created an app to monitor the online presence of users. I hope you found this tutorial helpful and would love to apply the knowledge gained here to easily set up your own application using Pusher. You can find the source code for the demo app on GitHub.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.