Activity Feeds with Rails

Share this article

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!

Frequently Asked Questions (FAQs) about Activity Feeds in Rails

How do I create an activity feed in Rails?

Creating an activity feed in Rails involves several steps. First, you need to generate a model for the activity feed. This model will store the activities that occur in your application. You can generate this model using the Rails generator command. After generating the model, you need to create a controller for the activity feed. The controller will handle the requests related to the activity feed. Finally, you need to create views for the activity feed. These views will display the activities to the users.

What are the best practices for implementing activity feeds in Rails?

There are several best practices for implementing activity feeds in Rails. One of the most important is to keep the activity feed model as simple as possible. This means that the model should only store the necessary information about the activities. Another best practice is to use background jobs for creating activities. This will ensure that the creation of activities does not slow down the response time of your application. Finally, it’s a good practice to paginate the activity feed. This will prevent the loading of too many activities at once, which can slow down your application.

How can I customize the appearance of my activity feed in Rails?

Customizing the appearance of your activity feed in Rails can be done by modifying the views of the activity feed. You can use HTML and CSS to style the activity feed according to your preferences. You can also use Rails helpers to simplify the creation of the views.

How can I filter activities in my activity feed in Rails?

Filtering activities in your activity feed in Rails can be done by adding conditions to the query that retrieves the activities from the database. You can add conditions based on the type of the activity, the user who performed the activity, or any other attribute of the activity.

How can I test my activity feed in Rails?

Testing your activity feed in Rails can be done using Rails’ built-in testing framework. You can write unit tests for the activity feed model, controller tests for the activity feed controller, and feature tests for the activity feed views. These tests will ensure that your activity feed works as expected.

How can I optimize the performance of my activity feed in Rails?

Optimizing the performance of your activity feed in Rails can be done by using eager loading for the activities, paginating the activity feed, and using background jobs for creating activities. Eager loading will reduce the number of queries to the database, pagination will prevent the loading of too many activities at once, and background jobs will ensure that the creation of activities does not slow down the response time of your application.

How can I add real-time updates to my activity feed in Rails?

Adding real-time updates to your activity feed in Rails can be done by using Action Cable, a part of Rails that provides WebSockets support. With Action Cable, you can broadcast new activities to the users as soon as they occur.

How can I handle errors in my activity feed in Rails?

Handling errors in your activity feed in Rails can be done by adding error handling code to the activity feed controller. This code will catch any exceptions that occur when creating, retrieving, or displaying activities, and it will display an appropriate error message to the user.

How can I secure my activity feed in Rails?

Securing your activity feed in Rails can be done by adding authentication and authorization checks to the activity feed controller. These checks will ensure that only authenticated users can access the activity feed, and that users can only see the activities that they are authorized to see.

How can I make my activity feed more interactive in Rails?

Making your activity feed more interactive in Rails can be done by adding features such as likes, comments, and shares to the activities. You can also add real-time updates to the activity feed, so that users can see new activities as soon as they occur.

Ilya Bodrov-KrukowskiIlya Bodrov-Krukowski
View Author

Ilya Bodrov is personal IT teacher, a senior engineer working at Campaigner LLC, author and teaching assistant at Sitepoint and lecturer at Moscow Aviations Institute. His primary programming languages are Ruby (with Rails) and JavaScript. He enjoys coding, teaching people and learning new things. Ilya also has some Cisco and Microsoft certificates and was working as a tutor in an educational center for a couple of years. In his free time he tweets, writes posts for his website, participates in OpenSource projects, goes in for sports and plays music.

GlennG
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week