Skip to content

Instantly share code, notes, and snippets.

@carlows
Last active October 7, 2016 22:34
Show Gist options
  • Save carlows/7c6ed88a02d1471c77a79bdef8ddb92b to your computer and use it in GitHub Desktop.
Save carlows/7c6ed88a02d1471c77a79bdef8ddb92b to your computer and use it in GitHub Desktop.

Disclaimer

WIP

Author

Carlos Martinez

Problem Overview

The app needs a way to notify users they have been mentioned in a comment.

We are going to display these notifications through a notification panel, just like github or facebook display notifications to the users.

As in the future we want to add notifications for other events, the notification system should be generic enough to support any kind of event, as well of the panel component should be able to render any kind of notification.

Also, these notifications should be pushed in real time to the users, they should receive notifications without refreshing the browser.

Constraints

  • Websockets should be used instead of ajax polling.
  • Notifications should be generic to support multiple types

Background

For a notification system, there are multiple gems for rails that solve this problem very well, amongs them, the most popular is public_activity.

The gem will allow us to track activities for specific models, it is pretty easy to add custom activities, so this is what I'll use for notifications.

There are multiple approaches to push the notifications to the panel.

  • Using ajax polling to fetch new activities in an interval
  • Using websockets to create a connection between the client and the server

The first approach isn't very optimal, so we're going to implement an approach that involves using websockets to push notifications to the panel. For this, ActionCable comes handy, it easily allows us to create that realtime communication.

Here are examples on how to use action cable for RTM push:

Theory of operation

The stack involved to add this feature is the following:

  • public_activity for the nofitication system.
  • ActionCable to push new notifications to the user.
  • Redis is used by ActionCable as its subscription adapter.

The implementation will involve the following steps:

  1. Setting up ActionCable.
  2. Setting up public_activity gem and migrations.
  3. Adding an user_name field to users that is unique within a company.
  4. Fetch mentioned users from the comments.
  5. Create a custom :comment activity with the user ids mentioned.
  6. Execute a broadcast ActiveJob passing the new activity.
  7. Serialize the activity as JSON.
  8. Broadcast the activity to each user channel.
  9. Display the new activity in the client.

Setting up action cable

As we're already using Rails 5, ActionCable is already installed in the app. However, we still need to do some configuration to make it work properly.

  1. Install redis with brew install redis.
  2. Ensure the redis server is running. (run redis-server).
  3. Generate a new channel for notifications running rails g channel Notifications.
  4. Add a route for the action cable server in the routes.rb

Setting up public_activity

  1. Add gem 'public_activity' to the Gemfile.
  2. Run bundle install.
  3. Run rails g public_activity:migration.
  4. Run rails db:migrate.

Adding an user_name field to the users

To make it easy to add mentions to users, we need a way to identify each user through text.

For example when the user types @car and clicks on the user 'Carlos', we would like to insert an unique id that identifies that user through text, so we insert @carlows in the comment. These usernames should be unique within a company.

ActiveJob

To broadcast messages we will create a NotificationBroadcastJob, this will run in a different process in parallel. Within this ActiveJob we will serialize the data for the notification as JSON and then broadcast to each user channel.

Identifying users to broadcast data

When a user connects to a channel, we start a stream using stream_from. This is a nice opportunity to identify users.

For this, we'll use the current_user.id like this:

class NotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notifications:#{current_user.id}"
  end
end

This will allow us to broadcast data to an specific user this way:

user = User.first
ActionCable.server.broadcast "notifications:#{user.id}", message: 'hello world'

Black box

        +------------------------+
        |  User creates          |
        |  a comment mentioning  |
        |  @carlos and @orlando  |
        +-----------+------------+
                    |
                    |
 +------------------v--------------------+
 |  Server broadcasts the notification   |
 |  to all mentioned users               |
 +------------------+--------------------+
                    |
+-------------------v--------------------+
|    Every user connected receives the   |
|    notification in real time           |
+----------------------------------------+

Goals

  • Implementing a generic notification system
  • Real time push notifications
  • Notifications panel will fetch the last 10 notifications (if any)

Functional Specification

./db/migrate/create_activities.rb

class CreateActivities < ActiveRecord::Migration
  def self.up
    create_table :activities, id: :uuid do |t|
      ... default activity fields
      t.text :content
      
      t.timestamps
    end

    ...
  end
end

./app/models/user.rb

class User
	# validate uniqueness of username with scope of company
end

./config/routes.rb

...

mount ActionCable.server => '/cable'

./app/channels/notifications_channel.rb

class NotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "notifications:#{current_user.id}"
  end
end

./app/controllers/comments_controller.rb

class CommentsController < ApplicationController
	def create
		...
		
		if @comment.save 
			create_notification(@comment)
		end
	end
	
	def update
		...
		
		if @comment.save
			create_notification(@comment)
		end
	end
	
	private
	
	create_notification(comment)
		# fetch mentioned users with a regex
		# find users by username
		
		# create activity with:
		#  key: :mention
		# 	content: @comment.content
		# 	owner: current_user
		#	parameters: { users_involved: user_ids }
			
		# schedule job to broadcast message
		NotificationBroadcastJob.perform_later(created_activity, users)
	end
end

./app/jobs/notification_broadcast_job.rb

In this broadcast we check if any user was passed, and we broadcast the notification to each user.

class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(notification, users_involved)
    if any user involved
  		# serialize data from notification and user as JSON
  		# broadcast notification to each user involved
  	end
  end
end

./app/controllers/application_controller.rb

class ApplicationController
	before_action :get_latest_notifications
	
	protected
	
	def get_latest_notifications
		# Fetch latest notifications for current_user
	end
end

./app/assets/javascripts/components/notification_panel.js.jsx

class NotificationsPanel extends React.Component {
	constructor(props) {
		this.state = {
			notifications: [],
			unreadCount: 0,
			opened: false
		};
	}
	
	togglePanel() {
		// toggle the panel opened state
	}
	
	openPanel() {
		// set unreadCount to 0
	}
	
  	componentDidMount() {
	   this.notificationSubscription = App.cable.subscriptions.create('NotificationsChannel', {
	      received: (data) => {
	        this.addNotificationToPanel(data);
	      }
    	});
   }
   
   addNotificationToPanel(data) {
   		// append new notification
   		// increment unreadCount
   }

   componentWillUnmount() {
	   this.notificationSubscription.unsubscribe();
   }

   render() {
   	  // map notifications to an specific component
   	  // e.g: notification.key === 'comment.mention'
   	  // <MentionNotification />
   
     return (
       <div>
         { notifications }
       </div>
     );
   }
}

Open Issues

  • Prerendering is not working when using action cable
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment