Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save excid3/4ca7cbead79f06365424b98fa7f8ecf6 to your computer and use it in GitHub Desktop.
Save excid3/4ca7cbead79f06365424b98fa7f8ecf6 to your computer and use it in GitHub Desktop.

Realtime Notifications with ActionCable

In this episode we're going to be adding realtime notifications into your app using ActionCable. We've talked about notifications a few times in the past and we used AJAX polling for that. 95% of the time, polling is the solution that would be recommended for it.

But 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 with rails 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.

If you enjoyed this episode please remember to give the video a thumbs up or heart it. Thanks for reading and watching everyone!

@rorykoehler
Copy link

rorykoehler commented Feb 15, 2017

Great write-up .

I believe

has_many :notifications, as: :recipient

should be

has_many :notifications, foreign_key: :recipient_id

as: is used for polymorphic associations and is not applicable here. Running @user.notifications will return an error as it will look for recipient_type column in the notifications table which doesn't exist.

@monroemann
Copy link

Thanks for all your great tutorials. I'm confused, however: should I create notifications using your gorails tutorial that uses json and coffeescript, or is it preferable to use actioncable? Thanks very much for any guidance.

@nickybismar
Copy link

nickybismar commented Jul 16, 2019

Hi,
I tried following your tutorial and many others on internet and I don't seem to get it to work. In other words, I open two different user accounts in my app, I generate a notification and no user is notified including the one I specified in "notifications:#{notification.recipient_id}". This is the code I'm using:

#I'm using a sidekiq worker instead of a regular job
class NotificationWorker
  include Sidekiq::Worker
  sidekiq_options retry: false
  def perform(notificationID)
    notification = Notification.find_by_id(notificationID)
    user = User.find_by_id(notification.target_id)
    ActionCable.server.broadcast 'notifications_#{notification.target_id}', render_notification(user, notification)
  end
  private
  def render_notification(user, notification)
    ApplicationController.render_with_signed_in_user(user, partial: 'notifications/index')
  end
end

In application_controller:

def self.render_with_signed_in_user(user, *args)
	   ActionController::Renderer::RACK_KEY_TRANSLATION['warden'] ||= 'warden'
	   proxy = Warden::Proxy.new({}, Warden::Manager.new({})).tap{|i| i.set_user(user, scope: :user) }
	   renderer = self.renderer.new('warden' => proxy)
	   renderer.render(*args)
	 end

I had to use this method instead of the usual rendering method because otherwise i get the following error:

ActionView::Template::Error: Devise could not find the 'Warden::Proxy' instance on your request environment.

My channel looks like this:

#notifications_channel
class NotificationsChannel < ApplicationCable::Channel
  def subscribed
     stream_from "notifications_#{current_user.id}"
  end
  def unsubscribed
    stop_all_streams
  end
  def speak
  end
end

in connection.rb:

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user
    def connect
      self.current_user = find_verified_user
    end 
    protected
      def find_verified_user
        if verified_user = env['warden'].user
          verified_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

And the javascript part looks like this:

// app/assets/javascripts/channels/notifications.js
App.notifications = App.cable.subscriptions.create("NotificationsChannel", {  
  received: function(data) {
    return $('#notifications').html(this.renderMessage(data));
  },
  connected:function(){
  },
  renderMessage: function(data) {
    return data;
  }
});

Just for the record, when I want to notify all users (just using: stream_from "notifications") it actually work and notifies all users realtime.
One more thing, by giving a closer look to the logs I've noticed that no data is received (the method received in notifictions.js is not called). So my point is: there's no realtime rendering, seems like the problem is at the "App.cable.subscriptions.create" level.

PS: sorry for my rough english I'm not a native speaker.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment