Ruby
Article

Activity Feeds with Rails

By Ilya Bodrov-Krukowski

Screenshot 2015-02-22 08.05.08

Activity feeds are all over the place. For example, Github’s main page lists all of the activity in your social coding network: one of your friends created or cloned a repo, a new issue was opened, your repo was forked or starred, etc.

github

Twitter displays the latest tweets, retweets, and replies.

twitter

That’s a convenient and easy way for users to learn about recent updates. How about trying to replicate the same functionality with Rails? Turns out, there is a convenient gem that makes the development process very easy!

In this article, I am going to talk about public_activity, a gem created by Piotrek Okoński to easily track activity for models (and more, actually). I will show you how to easily create an activity feed and employ various features of public_activity to extend the app further.

Before proceeding, I wanted to make a small note. Some months ago, I wrote an article Versioning with PaperTrail that covered paper_trail, a gem used to implement version control of an application’s models. In some ways, paper_trail is similar to public_activity and could be used to implement a solution similar to the one presented in this article. However, paper_trail is aimed at crafting versioning systems, whereas public_activity was created specifically to implement activity feeds.

The working demo is available at sitepoint-public-activity.herokuapp.com.

The source code is available on GitHub.

Preparing the App

Suppose we want to create an app that will allow users to share (cool) stories. Users should be able to sign in, but guests will be allowed to post stories, as well. There will also be a “Like” button for each story. The most important piece of functionality, however, will be an activity feed, so users are able to see what has happened recently. Example activities in the feed are a story was added, deleted, or liked.

Time for the ground work. Let’s call our app Storyteller:

$ rails new Storyteller -T

For this demo I am using Rails 4.2.0, but the same solution (with only a few tweaks) can be implemented with Rails 3.

Drop in some gems that we are going to use:

Gemfile

[...]
gem 'bootstrap-sass', '~> 3.3.1'
gem 'autoprefixer-rails'
gem 'public_activity'
gem 'omniauth-facebook'
[...]

and run

$ bundle install

bootstrap-sass and autoprefixer-rails are completely optional – the first one is used for styling and the second one automatically adds browser vendor prefixes to CSS rules. public_activity is the star today, as it will help us set up the activity feed. omniauth-facebook will be used to set up authentication.

If you’re following along, hook up Bootstrap’s styles:

application.scss

@import "bootstrap-sprockets";
@import "bootstrap";
@import 'bootstrap/theme';

Now, we have to prepare a couple of models. The first one will be called Story and contain the following attributes (I am skipping the default id, created_at and updated_at):

  • title (string) – title of the story
  • body (text) – body of the story
  • user_id (integer) – foreign key to reference the author of the story

The second one will be called User and have these attributes:

  • name (string) – name (probably, with a surname) of the user
  • uid (string) – user’s unique identifier provided by the social network
  • avatar_url (string) – user avatar’s URL

Create and apply the required migrations:

$ rails g model User name:string uid:string:index avatar_url:string
$ rails g model Story title:string body:text user:references
$ rake db:migrate

Tweak the model files like this:

user.rb

class User < ActiveRecord::Base
  has_many :stories
end

story.rb

class Story < ActiveRecord::Base
  belongs_to :user

  validates :title, presence: true
  validates :body, presence: true
end

Set up routes:

[...]
resources :stories

delete '/logout', to: 'sessions#destroy', as: :logout
get '/auth/:provider/callback', to: 'sessions#create'

root to: 'stories#index'
[...]

The /auth/:provider/callback is the callback route used by Facebook as a part of the OAuth2 sign in process. The :provider piece means that you may use any other Omniauth authentication strategy (or multiple strategies at once).

Focus on the layout now:

views/layouts/application.html.erb

[...]
<div class="navbar navbar-inverse">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Storyteller', root_path, class: 'navbar-brand' %>
    </div>
    <ul class="nav navbar-nav pull-right">
      <% if current_user %>
        <li><span><%= image_tag current_user.avatar_url, alt: current_user.name %></span></li>
        <li><%= link_to 'Log Out', logout_path, method: :delete %></li>
      <% else %>
        <li><%= link_to 'Log In', '/auth/facebook' %></li>
      <% end %>
    </ul>
  </div>
</div>

<div class="container">
  <div class="page-header">
    <h1><%= yield :page_header %></h1>
  </div>

  <% flash.each do |key, value| %>
    <div class="alert alert-<%= key %>">
      <%= value %>
    </div>
  <% end %>

  <%= yield %>
</div>
[...]

Nothing special here apart from the yield :page_header piece of code that relies on a page_header helper method. Create it now:

application_helper.rb

module ApplicationHelper
  def page_header(header)
    content_for(:page_header) {header.to_s}
  end
end

Great, the next step is creating the controller to put this all together:

stories_controller.rb

class StoriesController < ApplicationController
  before_action :find_story, only: [:destroy, :show, :edit, :update]

  def index
    @stories = Story.order('created_at DESC')
  end

  def new
    @story = Story.new
  end

  def create
    @story = Story.new(story_params)
    if @story.save
      flash[:success] = 'Your story was added!'
      redirect_to root_path
    else
      render 'new'
    end
  end

  def edit
  end

  def update
    if @story.update_attributes(story_params)
      flash[:success] = 'The story was edited!'
      redirect_to root_path
    else
      render 'edit'
    end
  end

  def destroy
    if @story.destroy
      flash[:success] = 'The story was deleted!'
    else
      flash[:error] = 'Cannot delete this story...'
    end
    redirect_to root_path
  end

  def show
  end

  private

  def story_params
    params.require(:story).permit(:title, :body)
  end

  def find_story
    @story = Story.find(params[:id])
  end
end

This is a very basic controller, but it gets more interesting in a bit.

Now, the views:

views/stories/index.html.erb

<% page_header "Our cool stories" %>

<p><%= link_to 'Tell one!', new_story_path, class: 'btn btn-primary btn-large' %></p>

<% @stories.each do |story| %>
  <div class="well well-lg">
    <h2><%= link_to story.title, story_path(story) %></h2>

    <p><%= truncate(story.body, length: 350) %></p>

    <div class="btn-group">
      <%= link_to 'Edit', edit_story_path(story), class: 'btn btn-info' %>
      <%= link_to 'Delete', story_path(story), data: {confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-danger' %>
    </div>
  </div>
<% end %>

As you can see, we are calling the page_header helper method that was defined a moment ago. Here’s what is displayed for each story:

views/stories/show.html.erb

<% page_header @story.title %>

<p><%= @story.title %></p>

<div class="btn-group">
  <%= link_to 'Edit', edit_story_path(@story), class: 'btn btn-info' %>
  <%= link_to 'Delete', story_path(@story), data: {confirm: 'Are you sure?'}, method: :delete, class: 'btn btn-danger' %>
</div>

The new and edit views are even simpler:

views/stories/new.html.erb

<% page_header "New cool story" %>

<%= render 'form' %>

views/stories/edit.html.erb

<% page_header "Edit cool story" %>

<%= render 'form' %>

A partial for the form to create a story:

views/stories/_form.html.erb

<%= form_for @story do |f| %>
  <%= render 'shared/errors', object: @story %>

  <div class="form-group">
    <%= f.label :title %>
    <%= f.text_field :title, class: 'form-control', required: true %>
  </div>

  <div class="form-group">
    <%= f.label :body %>
    <%= f.text_area :body, class: 'form-control', required: true, cols: 3 %>
  </div>

  <%= f.submit 'Post', class: 'btn btn-primary' %>
<% end %>

The shared/_errors partial is referenced here, so we have to create it too:

views/shared/_errors.html.erb

<% if object.errors.any? %>
  <div class="panel panel-danger">
    <div class="panel-heading">
      <h3 class="panel-title">The following errors were found while submitting the form:</h3>
    </div>

    <div class="panel-body">
      <ul>
        <% object.errors.full_messages.each do |msg| %>
          <li><%= msg %></li>
        <% end %>
      </ul>
    </div>
  </div>
<% end %>

And some basic styles:

application.scss

[...]
.well {
  h2 {
    margin-top: 0;
  }
}
[...]

That was the easy part. Before proceeding let’s also set up authentication via Facebook.

Authentication via Facebook

Create a new file omniauth.rb inside the config/initializers directory with the following content:

config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :facebook, ENV['FACEBOOK_KEY'], ENV['FACEBOOK_SECRET'], scope: 'public_profile'
end

To obtain the Facebook key and secret, visit the developers.facebook.com page and create a new application (with a type “Website”). On the Dashboard page of the newly created app, find the “App ID” and “App Secret” (a password will have to be provided to display this one) – this is what you need. Note: The key pair should not be available publicly – I am using environmental variables to store it.

While on Facebook Developers page, navigate to the Settings page and click the “Add Platform” button, then select “Website”. Next, fill in the following fields:

  • Site URL (if you are testing the app on local machine, enter “http://localhost:3000”)
  • App Domains (if you are on local machine, leave this blank)
  • Contact Email

Click “Save Changes”.

Lastly, navigate to “Status & Review” and toggle the “Do you want to make this app and all its live features available to the general public?” to “Yes”. Your app is now live and users may log in using it.

The scope parameter specifies what actions to allow our application to perform. For this case, we only need to fetch basic information about the user that is logging in.

The next step is creating the controller that will handle log in and log out requests:

sessions_controller.rb

class SessionsController < ApplicationController
  def create
    user = User.from_omniauth(request.env['omniauth.auth'])
    session[:user_id] = user.id
    flash[:success] = "Welcome, #{user.name}"
    redirect_to root_url
  end

  def destroy
    session[:user_id] = nil
    flash[:success] = "Goodbye!"
    redirect_to root_url
  end
end

request.env['omniauth.auth'] contains all the information about the user. TheUser.from_omniauth class method needs to be created:

models/user.rb

class << self
  def from_omniauth(auth)
    user = User.find_or_initialize_by(uid: auth['uid'])
    user.name = auth['info']['name']
    user.avatar_url = auth['info']['image']
    user.save!
    user
  end
end

We are storing the necessary information and returning the user object as a result. The find_or_initialize_by method will either create a new user or update an existing one if the uid is already present in the database. This is done to prevent the same user being created multiple times.

And, lastly, the current_user method that will return the currently logged in user or nil:

controllers/application_controller.rb

[...]
private

def current_user
  @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end

helper_method :current_user
[...]

helper_method ensures that this method can be called from the views as well.

When you are done, boot up the server. Try to authenticate and add a couple of stories to check that everything is working. We are ready to move to the interesting part.

Integrating public_activity

public_activity‘s idea is quite simple: use callbacks to automatically store information about the changes that happen in the specified table. That information is then used to display all the recent activity. You might be wondering if it is possible to record activity without actually touching the table. Well, it is, and we will talk about it more in a bit.

For now, let’s do the basic setup. Run these commands:

$ rails g public_activity:migration
$ rake db:migrate

This will generate and apply public_activity‘s migration. A new table called activities is created.

To enable tracking for the Story model:

models/story.rb

[...]
include PublicActivity::Model
tracked
[...]

Pretty simple, isn’t it? Now, whenever you perform actions such as save, update_attributes, destroy, and others, public_activity‘s callback will be fired to record that event.

This gem also supports MongoMapper and Mongoid adapters – refer to the Database setup section in the docs to learn more.

Displaying the Activity Feed

Let’s display the events. You may either create a separate page to display the activity feed or render it
on each page of your site. I’m going to stick with the latter option.

Tweak the controller:

stories_controller.rb

[...]
before_action :load_activities, only: [:index, :show, :new, :edit]

private

def load_activities
  @activities = PublicActivity::Activity.order('created_at DESC').limit(20)
end
[...]

As you can see, the Activity model is inside the PublicActivity namespace to prevent naming collisions. We are ordering activities by creation date (the newest the first) and taking the first twenty of them. Read more here.

Our layout needs to be changed a bit so that the activity feed is being placed on the right side of the website:

views/layouts/application.html.erb

[...]
<div class="col-sm-9">
  <%= yield %>
</div>

<%= render 'shared/activities' %>
[...]

views/shared/_activities.html.erb

<div class="col-sm-3">
  <ul class="list-group">
    <%= @activities.inspect %>
  </ul>
</div>

For those who don’t know, Bootstrap uses a 12-column grid layout, so by specifying col-sm-9 we are using 9 columns (75 % of the available space) for the main content. col-sm-3, in turn, leaves 3 columns for the activity feed. sm here means that columns will be displayed one beneath another (horizontal grid) on smaller displays. More information is available here.

Reload the page to check what the @activities array contains and how it displays. To render an array of activities there is a special helper method render_activities:

views/shared/_activities.html.erb

<div class="col-sm-3">
  <%= render_activities @activities %>
</div>

public_activity expects that there is a public_activity folder inside the views that, in turn, has a story folder (or any other folder with a singular name of the model that is related to the specific activity). Inside the story directory, there should be the following partials: _create.html.erb, _update.html.erb, _destroy.html.erb. Each partial, as you’ve probably guessed, is rendered for the corresponding action. Inside those partials there is a local variable activity (aliased as a) available.

Go ahead and create those files:

views/public_activity/story/_create.html.erb

<li class="list-group-item">
  <%= a.trackable.title %> was added.
</li>

views/public_activity/story/_update.html.erb

<li class="list-group-item">
  <%= a.trackable.title %> was edited.
</li>

views/public_activity/story/_destroyed.html.erb

<li class="list-group-item">
  <%= a.trackable.title %> was deleted.
</li>

The trackable is a polymorphic association which has all the necessary information about the model that was modified.

There is a problem, however. If you create and then delete a story, you’ll see an error undefined method 'title' for nil:NilClass. This is because the we are trying to fetch the title of the record that was deleted. It is easy enough to fix:

views/public_activity/story/_create.html.erb

<li class="list-group-item">
  <% if a.trackable %>
    <%= a.trackable.title %> was created.
  <% else %>
    An article that is currently deleted was added.
  <% end %>
</li>

views/public_activity/story/_update.html.erb

<li class="list-group-item">
  <% if a.trackable %>
    <%= a.trackable.title %> was edited.
  <% else %>
    An article that is currently deleted was edited.
  <% end %>
</li>

views/public_activity/story/_destroyed.html.erb

<li class="list-group-item">
  An article was deleted.
</li>

Pretty nice, but not very informative. When did the action take place? Can we navigate directly to the article that was modified? Who modified it? Well, the first two issues can be fixed easily:

views/public_activity/story/_create.html.erb

<li class="list-group-item">
  <span class="glyphicon glyphicon-plus"></span>
  <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/>
  <% if a.trackable %>
    <%= link_to a.trackable.title, story_path(a.trackable) %> was added.
  <% else %>
    An article that is currently deleted was added.
  <% end %>
</li>

views/public_activity/story/_update.html.erb

<li class="list-group-item">
  <span class="glyphicon glyphicon-edit"></span>
  <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/>
  <% if a.trackable %>
    <%= link_to a.trackable.title, story_path(a.trackable) %> was edited.
  <% else %>
    An article that is currently deleted was edited.
  <% end %>
</li>

views/public_activity/story/_destroyed.html.erb

<li class="list-group-item">
  <span class="glyphicon glyphicon-remove"></span>
  <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/>
  An article was deleted.
</li>

I’ve also added some Bootstrap Glyphicons so that everything looks a bit prettier.

Displaying information about the user responsible for the change involves a bit more work.

Storing Information About the User

There is a special field called owner in the activities table that is intended to store information about the user responsible for the action. The problem, however, is that the current_user method is not available inside the model so we have to use a pretty hacky solution.

Tweak the controller:

application_controller.rb

[...]
include PublicActivity::StoreController

def current_user
  @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]
end

helper_method :current_user
hide_action :current_user
[...]

Note that I’ve removed the private keyword because otherwise we won’t be able to call current_user from inside the model. Adding hide_action :current_user makes sure that this method is not considered a controller action.

Now the model:

models/story.rb

[...]
tracked owner: Proc.new { |controller, model| controller.current_user ? controller.current_user : nil }
[...]

The procedure takes two arguments: controller and model. In this case, we only need controller to call the current_user method; model stores the object that was modified.

With this in place, log in, add/change some stories and check the activities table. The owner field should be populated with user’s id.

The last step is modifying the partials:

views/public_activity/story/_create.html.erb

<li class="list-group-item">
  <span class="glyphicon glyphicon-plus"></span>
  <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/>
  <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong>
  <% if a.trackable %>
    added the story <%= link_to a.trackable.title, story_path(a.trackable) %>.
  <% else %>
    added the story that is currently deleted.
  <% end %>
</li>

views/public_activity/story/_update.html.erb

<li class="list-group-item">
  <span class="glyphicon glyphicon-edit"></span>
  <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/>
  <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong>
  <% if a.trackable %>
    edited the story <%= link_to a.trackable.title, story_path(a.trackable) %>.
  <% else %>
    edited the story that is currently deleted.
  <% end %>
</li>

views/public_activity/story/_destroyed.html.erb

<li class="list-group-item">
  <span class="glyphicon glyphicon-remove"></span>
  <small class="text-muted"><%= a.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/>
  <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong>
  deleted a story.
</li>

Now, for the newly performed actions you will see your name displayed; for the previous ones there should be a “Guest” string provided instead.

Everything is working fine, but our partials contain a lot of duplication – actually, the code is nearly identical. So let’s spend some time refactoring it.

Using I18n Fallbacks to Refactor Activity Partials

I want to completely get rid of those activity partials and work only with the shared/_activities.html.erb file. You should know, however, that there are some other possible solutions.

The basic structure of the partial will be as follows:

views/shared/_activities.html.erb

<div class="col-sm-3">
  <ul class="list-group">
    <% @activities.each do |activity| %>
      <li class="list-group-item">
        <!-- render activities here -->
      </li>
    <% end %>
  </ul>
</div>

It appears that only two things in the activity partials are different: the icon and the text saying what happened to the trackable object.

The issue with the icon can be easily fixed using regular expressions and some Sass magic:

views/shared/_activities.html.erb

<div class="col-sm-3">
  <ul class="list-group">
    <% @activities.each do |activity| %>
      <li class="list-group-item">
        <span class="glyphicon glyphicon-<%= activity.key.match(/\.(.*)/)[1] %>"></span>
      </li>
    <% end %>
  </ul>
</div>

The key fields contains the string in the form of . so for example in our case it might be story.create, story.update or story.destroy. Of course, Bootstrap applies no styles to classes like glyphicon-create but it can be easily changed:

application.scss

.glyphicon-update {
  @extend .glyphicon-edit;
}

.glyphicon-create {
  @extend .glyphicon-plus;
}

.glyphicon-destroy {
  @extend .glyphicon-remove;
}

We are employing Sass’ @extend directive to apply the styles to our new classes.

The issue with the text can be solved with the help of fallbacks. As we’ve already seen, public_activity by default will search for the partials inside the public_activity/ directory. However if we provide the display: :i18n option, I18n translations will be used instead.

The structure for those translation look like:

activity:
  model_name:
    create: '...'
    destroy: '...'
    update: '...'
    other_action: '...'

This way, we are also solving the possible internationalization problem that might arise in the future.

The partial contains the following code:

views/shared/_activities.html.erb

<div class="col-sm-3">
  <ul class="list-group">
    <% @activities.each do |activity| %>
      <li class="list-group-item">
        <span class="glyphicon glyphicon-<%= activity.key.match(/\.(.*)/)[1] %>"></span>
        <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong>
        <%= render_activity activity, display: :i18n %>
        <% if activity.trackable %>
          "<%= link_to activity.trackable.title, story_path(activity.trackable) %>"
        <% else %>
          with unknown title.
        <% end %>
      </li>
    <% end %>
  </ul>
</div>

Here the render_activity helper method is used – its alias render_activities we saw previously.

Now the translations file:

config/locales/en.yml

en:
  activity:
    story:
      create: 'has told his story'
      destroy: 'has removed the story'
      update: 'has edited the story'

The previously created public_activity folder may be removed completely.

This will only work, however, for a single trackable model. For multiple models you will have to create the corresponding partials. It is possible to create a separate layout (which actually is yet another partial) for your activities. Here is a demo app created by the author of public_activity that uses a layout to render activities.

Create Custom Activities

Until now, we only worked with the basic CRUD operations that caused activities to be saved automatically. But what if we wanted to track some custom events? Or if there is a need to trigger an activity without touching the model?

Not to worry, this can be done quite easily. Suppose we want to add the “Like” button and count likes for each post. Moreover, a special activity should be recorded, too.

First of all, we need to add a new column to the stories table:

$ rails g migration add_likes_to_stories likes:integer
$ rake db:migrate

Now the new route:

config/routes.rb

[...]
resources :stories do
  member do
    post :like
  end
end
[...]

Add the “Like” button to the view:

views/stories/show.html.erb

<% page_header @story.title %>

<p>
  <span class="label label-default"><%= pluralize(@story.likes, 'like') %></span>
  <%= link_to content_tag(:span, '', class: 'glyphicon glyphicon-thumbs-up') + ' Like it',
               like_story_path(@story), class: 'btn btn-default btn-sm', method: :post %>
</p>

[...]

And the controller action:

stories_controller.rb

before_action :find_story, only: [:destroy, :show, :edit, :update, :like]

[...]

def like
  @story.increment!(:likes)
  @story.create_activity :like
  flash[:success] = 'Thanks for sharing your opinion!'
  redirect_to story_path(@story)
end
[...]

@story.increment!(:likes) just adds 1 to the likes and saves the result to the database. @story.create_activity :like actually creates a new activity by providing the like key (we’ve talked about keys before when refactoring partials). This will require us to modify the translations file:

config/locales/en.yml:

en:
  activity:
    story:
      like: 'has liked the story'
      [...]

If you are dealing with partials instead, then you’ll have to create the views/public_activity/story/_like.html.erb partial.

The create_activity method is called to trigger a custom activity – it does not require a model change.

We are not done yet, however. There is one problem that will allow me to show one more feature of public_activitydisabling model tracking. You see, the @story.increment!(:likes) code fires an update which causes public_activity to record an update event. So, @story.create_activity :like will result in recording two activities for one action. This is obviously not what we want. The first operation should be done without any tracking at all.

public_activity allows disabling tracking globally or for a specific model. For global disabling, use

PublicActivity.enabled = false

To disable tracking on model’s level use

Story.public_activity_off

We are going to employ the latter solution, as the first one is obviously overkill:

stories_controller.rb

def like
  Story.public_activity_off
  @story.increment!(:likes)
  Story.public_activity_on
  @story.create_activity :like
  flash[:success] = 'Thanks for sharing your opinion!'
  redirect_to story_path(@story)
end

This can be simplified further:

stories_controller.rb

def like
  without_tracking do
    @story.increment!(:likes)
  end
  @story.create_activity :like
  flash[:success] = 'Thanks for sharing your opinion!'
  redirect_to story_path(@story)
end

private

def without_tracking
  Story.public_activity_off
  yield if block_given?
  Story.public_activity_on
end

Now the increment operation will not cause the update activity to be triggered. Great!

Saving Custom Information

Suppose we want to store some additional information about the activity. How could we do this?

public_activity presents two ways to achieve the desired result. First of all, there is the serialized parameters field present in the activities table that we can access right away:

@story.create_activity :like, parameters: {why: 'because'}

Later, we can access this information, like so:

activity.parameters['why']

This is not always convenient, however. If you need some data to be saved for each activity, this approach requires calling create_activity for each action.

For other scenarios, there are custom fields available. Let’s, for example, store the title of the story so that we can fetch it even if the story is later deleted.

The setup is very simple. Create a new migration:

xxx_add_title_to_activities.rb

class AddTitleToActivities < ActiveRecord::Migration
  def change
    change_table :activities do |t|
      t.string :title
    end
  end
end

and apply it:

$ rake db:migrate

And now tweak the model like this:

models/story.rb

[...]
tracked owner: Proc.new { |controller, model| controller.current_user ? controller.current_user : nil },
        title: Proc.new { |controller, model| model.title }
[...]

Let me remind you, that model stores the object that was changed.

And now we can display this title in case the model is destroyed:

shared/_activities.html.erb

<div class="col-sm-3">
  <ul class="list-group">
    <% @activities.each do |activity| %>
      <li class="list-group-item">
        <span class="glyphicon glyphicon-<%= activity.key.match(/\.(.*)/)[1] %>"></span>
        <small class="text-muted"><%= activity.created_at.strftime('%H:%M:%S %-d %B %Y') %></small><br/>
        <strong><%= activity.owner ? activity.owner.name : 'Guest' %></strong>
        <%= render_activity(activity, display: :i18n) %>
        <% if activity.trackable %>
          "<%= link_to activity.trackable.title, story_path(activity.trackable) %>"
        <% elsif activity.title %>
          <span class="text-muted">"<%= activity.title %>"</span>
        <% else %>
          with unknown title.
        <% end %>
      </li>
    <% end %>
  </ul>
</div>

Conclusion

I’ve shown you nearly all the features provided by public_activity – hopefully you will use it one of your future projects!

Have you ever implemented the similar activity feed? Which tools did you use? Share your experience! As always your feedback or article requests are very much welcome.

Happy hacking!

Comments
mafuba

Good stuff! As someone who has built and worked with activity streams for years, I will say this is a good start. One thing I would suggest looking at is the Activity Streams protocol specification - http://activitystrea.ms. This provides a standard data format for activity stream data that can then be used with any standardized activity streams tool.

Once you start to move beyond simple streams - e.g. to add different types of activity, enable commenting, and especially allow following and building of feeds, it's my belief that you shouldn't try to build it yourself. It starts to get very complex to enable that level of functionality and maintain any kind of performance. I actually built a company around solving this problem - it's called Collabinate (http://www.collabinate.com). We provide an API that allows you to build streams for any object in an application, manages the relationship of followers to streams, lets you pull back feeds in real time, enables likes and comments, utilizes the Activity Streams protocol, and does it at huge scale if necessary (billions of objects). We should talk some time if you're interested in learning more about it.

Thanks again for the article!

bodrovis

Thank you!

Your service seems like a nice solution - I may be interested in writing an article about it. Could you contact me via e-mail (http://radiant-wind.com - e-mail and some other contact options are listed here) if that suits you?

rizalmuthi

Good stuff..
Anyway, I checked your github apps for this tutorial, seems there is folder public_activity for the view.
A bit confused about that.

Thanks

bodrovis

Sorry, didn't quite understand. I have no such folder: https://github.com/bodrovis/SitePoint-PublicActivity/tree/master/app/views because I am sticking with I18n for this demo.

rizalmuthi

Yeah, sorry my bad, I did not read it carefully. So I found out you deleted that because of using the I18n.. Cool stuff. Thank you very much.

Btw, is is possible to make it to more than 1 model ? like 2-3 models?
Thanks

rizalmuthi

By the way, I have couple of questions
First one, if you have 2 users type (user and admin) how do you set the owner?
Second, how do you filter them on the render activity or the view?
and lastly, this is applicable to many models?

Thanks

bodrovis

Nope, with more than 1 model you will have to use partial views like described here: https://github.com/pokonski/public_activity#activity-views

bodrovis

You will want to tweak this

 @current_user ||= User.find_by(id: session[:user_id]) if session[:user_id]

and this code

tracked owner: Proc.new { |controller, model| controller.current_user ? controller.current_user : nil }[...]

accordingly. Just more checkings come into play - you also check if the person is logged in as admin.

bodrovis

By the owner field, if I understood the question correctly. It has the polymorphic association (https://github.com/pokonski/public_activity/blob/4ea9f56628a7699370a6a3daad31ddf4b076a321/lib/public_activity/orm/active_record/activity.rb#L13) and you can use it for find only activities for admin or user for example.

bodrovis

Of course, multiple models may be used. Just remember to use partials, not I18n fallbacks.

rizalmuthi

Okaaay
I have tried to play around this. Seems pretty good.

Thank you very much for your time

cheers

I am big fans of you smiley

bodrovis

Oh, thank you for kind words! Don't hesitate to contact me if anything else will seem unclear.

Anthony_Galli

AWESOME article! Really gave me a great grasp on the activity_feed. Is there a way to 1) limit liking to one time per post so that if you click on "Like it" a second time it unlikes it, like Facebook 2) show names of likers next to like button so you know who is liking it without having to scroll down the activity feed? Might be a nice expansion to your wonderful tutorial because I can't find these answers anywhere!

bodrovis

Thank you! smile

1&2. Of course that is possible, but this is not related to activity feeds so I did not consider add such info to the currect article. However I think that is a nice suggestion for a new topic so I'll probably work on this in some time, maybe expanding a bit further. It is really great to receive so many feedback!

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the latest in Ruby, once a week, for free.