Realtime Notifications with ActionCable

Published Jan 21, 2017Last updated Feb 22, 2017
Realtime Notifications with ActionCable

In this tutorial we're going to be adding realtime notifications into your app using ActionCable.

If you're looking for a good introduction into ActionCable then this is a decent one because we're only really using it for one way from the server side to the client side.

Getting started

So to get started we're starting with an app that has Bootstrap installed and then we created a Main controller with an index view which is where we will list our Notifications as for this example.

Before we generate our channels let's install a few things

Gemfile

# Uncomment out redis, or add it if you don't have it.
gem 'redis', '~> 3.0'

gem 'devise'

Make sure you have redis installed if you haven't already and make sure it is running.

config/cable.yml

development:
  adapter: redis
  url: redis://localhost:6379/1

Here we've configured the development to use the Redis server running locally now.

Then run bundle install and restart your rails server so the new gems and the ActionCable configuration will take effect.

Devise install

We need to then install our users with rails g devise:install and rails g devise User

Now lets generate our Notifications model

rails g model Notification user:references recipient_id:integer action notifiable_type notifiable_id:integer

Run your migrations withrails db:migrate if you're on Rails 5 or above or use rake db:migrate if you're on Rails 4.2 or below.

We're going to be referencing two users in this model because the user reference will be who did the action and the recipient_id will be who is getting the Notification.

app/models/user.rb

has_many :notifications, as: :recipient
app/models/notification.rb

belongs_to :user
belongs_to :recipient, class_name: "User"
belongs_to :notifiable, polymorphic: true

Now you can restart your rails server

rails server

app/views/main/index.html.erb

<div id="notifications">
</div>

That will be where we insert the notifications when they show up live. Now we'll generate a channel for the Notifications
rails g channel Notifications

We'll use this to scope by the user so that when you join you'll only receive your notifications.

app/channels/notifications_channel.rb

# Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading.
class NotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notifications:#{current_user.id}"
  end

  def unsubscribed
    stop_all_streams
  end
end

Now we need to go into the connection class for ActionCable and make the current_user work with ActionCable. So let's open that up and modify it to look like so

app/channels/application_cable/connection.rb

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verfied_user
    end

    protected

    def find_verfied_user
      if current_user = env['warden'].user
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

This prevents users from connecting to the websockets without a user account logged in.

app/assets/javascripts/channels/notifications.js

App.notifications = App.cable.subscriptions.create("NotificationsChannel", {
  connected: function() {
    // Called when the subscription is ready for use on the server
  },

  disconnected: function() {
    // Called when the subscription has been terminated by the server
  },

  received: function(data) {
    // Called when there's incoming data on the websocket for this channel
    $("#notifications").prepend(data.html);
  }
});

Now we can jump into the console with rails console and run

ActionCable.server.broadcast "notifications:1", {html: "<div>Hello world</div>"}

Now we just need to send over the HTML over the websockets and the app will render it onto the page.

We can use the new ApplicationController.render functionality to accomplish this.

So you'll want to make a second user if you don't already have more than one, and then in the rails console you can run

Notification.create(recipient: User.first, user: User.last, action: "followed", notifiable: User.first)

So our last user is following the first as far as the notification is concerned.

Now we need to make the notifications view folders. This is exactly like our refactored notifications episode we did previously. So if you have questions you can go back and check out that episode.

mkdir app/views/notifications/users

app/views/notifications/users/_followed.html.erb

<div><%= notification.user.email %> <%= notification.action %> you!</div>

Now in the console you can run

ApplicationController.render partial: "notifications/#{notification.notifiable_type.underscore.pluralize}/#{notification.action}", locals: {notification: notification}, formats:[:html]

If you run that you'll see it runs and renders the partial to a string that returns that for us. So what we can do is broadcast this to the users. We'll put this in a background job.

We use background jobs because we want them to be done in the background so they don't hang up the site for the users.

rails g job NotificationRelay

app/jobs/notification_relay_job.rb

class NotificationRelayJob < ApplicationJob
  queue_as :default

  def perform(notification)
    html = ApplicationController.render partial: "notifications/#{notification.notifiable_type.underscore.pluralize}/#{notification.action}", locals: {notification: notification}, formats: [:html]
    ActionCable.server.broadcast "notifications:#{notification.recipient_id}", html: html
  end
end

This job will broadcast the rendered partial to the recipient of the notification.

Now you're wondering how we trigger the job. So let's go back to the console after reloading it and run

notification = Notification.first
NotificationRelayJob.perform_later(notification)

Now you'll see the notification partial rendered on the browser properly. So what's the best way to trigger this NotificationRelayJob? Well one way you could do this is an after_commit hook on the Notification model.

app/models/notification.rb

after_commit -> { NotificationRelayJob.perform_later(self) }

Now if you go test it out and create a new notification you should have the job performing automatically per the after_commit hook. You can find more information on the callbacks here: ActiveRecord Callbacks

This has been a whirlwind tour of ActionCable, there are tons of things to add in to manage things like the front-end. As you may have noticed there seems to be a lot more complexity compared to our previous episodes with the AJAX polling version.

Authenticating ActionCable with Devise

# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verfied_user
    end

    protected

    def find_verfied_user
      # We can just access Warden directly to find out if the user is logged in or not
      # Using Warden directly will give us access to that as it's what Devise uses internally for authentication
      if current_user = env['warden'].user
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

Credits for GoRails

This transcript was written by Andrew Fomera.

Discover and read more posts from Victor H
get started