Track online presence in a Ruby on Rails app
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">×</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">×</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.
27 May 2018
by Christian Nwamba