Build a social feed with Ruby on Rails and PostgreSQL

  • Christian Nwamba
May 23rd, 2018
To follow this tutorial, you will need Ruby and Rails installed on your machine. A basic understanding of Ruby, PostgreSQL and CoffeeScript will help you get the most out of this tutorial.

Introduction

The common feature at the heart of every social media platform is posts. In one way or another, every social media platform out there provides a summary of these posts in feeds. The best social media platforms offer realtime updates of posts in user feeds. This way, no social content is delayed and everyone gets access to the latest information as soon as it drops.

In this post, we’ll build a simple app with realtime feed and likes. Posts will appear in our feed as soon as they are posted as well as their like counts increase in realtime once they’re liked.

A sneak-peek into what we will build in this post:

Prerequisites

A basic understanding of Ruby, CoffeeScript and PostgreSQL will help you get the best out of this tutorial. You can check the PostgreSQL, Ruby and Rails documentation for installation steps.

Setting up the application

Before starting off, ensure that you have Ruby and Rails installed. Run the following command to confirm your version:

    $ ruby -v      // 2.1 or above
    $ rails -v   // 4.2 or above

Open your terminal and run the following Rails commands to create our demo application:

    # create a new Rails application
    $ rails new pusher-live-feeds -T --database=postgresql

Go ahead and change directory into the newly created pusher-live-feeds folder:

    # change directory
    $ cd pusher-live-feeds

In the root of your pusher-live-feeds directory, open your Gemfile and add the following gems:

    # Gemfile

    gem 'bootstrap', '~> 4.1.0'
    gem 'jquery-rails'
    gem 'pusher'
    gem 'figaro'

In your terminal, ensure you are in the pusher-live-feeds project directory and install the gems by running:

    $ bundle install

Database setup

To get our app up and running, we’ll go ahead and create a database for it to work with. You can check out this article on how to create a Postgres database and an associated user and password.

Once you have your database details, in your database.yml file, under the development key, add the following code:

    # config/database.yml

    ...
    development:
      <<: *default
      database: pusher-live-feeds_development // add this line if it isn't already there
      username: database_user // add this line
      password: user_password // add this line
    ...

Ensure that the username and password entered in the code above has access to the pusher-live-feeds_development database. After that, run the following code to setup the database:

    # setup database
    $ rails db:setup

Bootstrap the application

With our database all set up, we’ll go ahead and create our models and controllers. In your terminal, while in the project’s directory, run the following code:

    # generate a post model
    $ rails g model post username:string post:text

    # generate a like model
    $ rails g model like like_count:integer post:references

    # generate a posts controller with the index, new and create view
    $ rails g controller posts index new create

Next, we’ll modify our like model migration file to contain an initial default value for likes. In the db/migrate folder, look for the create likes migration file. It should be a file with the current date stamp and ends with _create_likes.rb. In that file, update the code there with the following:

    # db/migrate/20180520125755_create_likes.rb

    class CreateLikes < ActiveRecord::Migration[5.1]
      def change
        create_table :likes do |t|
          t.integer :like_count, default: 0 # add the default: 0 part.
          t.references :post, foreign_key: true
          t.timestamps
        end
      end
    end

In our post model, we’ll also add an association to the likes model. In your post model, add the following code:

    # app/models/post.rb

    class Post < ApplicationRecord
      has_many :likes
    end

Now, we’re ready to run our database migrations and see our new app. In your terminal, run the following code:

    # run database migrations
    $ rails db:migrate

After running migrations, start the development server on your terminal by running rails s. Visit http://localhost:3000 in your browser to see your brand new application:

Pusher account setup

It’s time for us to create our app on Pusher. Head over to Pusher and sign up for a free account.

Create a new app by selecting Channels apps on the sidebar and clicking Create Channels app button on the bottom of the sidebar:

Configure an app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher with for a better setup experience:

Click the App Keys tab to retrieve your keys

Building the homepage

With our Pusher account set up, let’s change our app’s landing page to something eye-worthy. Let’s set our homepage to our post’s index page and add the necessary routes for our app. In your routes file, add the following code:

    # config/routes.rb

    Rails.application.routes.draw do
      resources :posts
      post '/likes/:post_id', to: 'posts#add_like', as: "add_likes"
      root 'posts#index'
    end

Next, we’ll require Bootstrap and add some styling. Add the following code to your application.js file, all before the last line:

    # app/assets/javascripts/application.js

    .....
    //= require jquery3 # add this line
    //= require popper # add this line
    //= require bootstrap # add this line
    //= require_tree .

Rename your application.css file to application.scss and add the following code:

    # app/assets/stylesheets/application.scss

     @import "bootstrap";
     @import url('https://fonts.googleapis.com/css?family=Tajawal');

     body {
     font-family: 'Tajawal', sans-serif;
    }
    #post {
      min-height: 5rem;
      max-height: 8rem;
    }
    .card-header {
      padding: 0.25rem 0.85rem;
      font-weight: 700;
    }
    .card-body {
      padding: 0.55rem 0.85rem;
    }
    .far {
      cursor: pointer;
    }

Now, we’ll add the HTML markup for our homepage in our index.html.erb file:

    # app/views/posts/index.html.erb

    <div class="container-fluid">
      <div class="container">
        <div class="container bg-light p-3 col-8 col-lg-6 welcome-page">
          <h5 class="text-center">Enter your username</h5>
          <input type="text" id="new-user-form" class="form-control my-5" required />
        </div>
        <div class="container bg-light p-3 col-8 col-lg-6 post-page collapse">
          <div class="post-form-wrapper">
            <p class="current-user"></p>
            <%= form_with(model: @post, scope: :post, format: :json, id: 'post-form') do |form| %>
              <div class="field">
                <%= form.text_area :post, id: :post, class: "form-control post-textarea", required: true %>
                <%= form.hidden_field :username, id: :username %>
              </div>
              <div class="actions text-right">
                <%= form.submit 'Submit post', class: "btn btn-success btn-sm mt-1" %>
              </div>
            <% end %>
          </div>
          <div class="posts mt-5">
            <% @posts.each do |post| %>
              <div class="post-wrapper col-12 mb-2 p-0">
                <div class="card">
                  <div class="card-header">
                    @<%= post.username %>
                    <small class="float-right mt-1"><%= post.created_at.strftime("at %I:%M%p") %></small>
                  </div>
                  <div class="card-body">
                    <p class="card-text"><%= post.post %></p>
                    <%= link_to '', add_likes_path(post_id: post.id), remote: true, method: :post, class: "far fa-thumbs-up add-like" %><span class="ml-2" data-post="<%= post.id %>"><%= post.likes[0].like_count %></span>
                  </div>
                </div>
              </div>
            <% end %>
          </div>
        </div>
      </div>
    </div>

Next, we’ll add the JavaScript code to display the feeds page after collecting the user’s username. Add the following code to your post.coffee file:

    # app/assets/javascripts/post.coffee

    $(document).ready =>
      currentUser = ''
      welcomePage = $('.welcome-page')
      postPage = $('.post-page')
      newUserForm = $('#new-user-form')

      # when user enters a username, store it and show the post page
      newUserForm.on 'keyup', (event) ->
        if event.keyCode == 13 and !event.shiftKey
          currentUser = event.target.value
          newUserForm.val('')
          welcomePage.addClass('collapse')
          postPage.removeClass('collapse')
          greeting = """welcome @#{currentUser}"""
          $('.current-user').html(greeting)
          $('#username').val(currentUser)
        return

In our posts controller, we’ll instantiate a posts object. In your posts_controller.rb file, add the following code:

    # app/controllers/posts_controller.rb

    class PostsController < ApplicationController
      def index
        @posts = Post.all.order(created_at: :desc) # add this line
      end
      ...
    end

Lastly, we’ll be making use of Font Awesome icons, so we need to add the Font Awesome CDN to our app head tag. While we’re at it, we’ll also add the Pusher library.

    # app/views/layouts/application.html.erb

    <head>
      ....
      <link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.13/css/all.css" integrity="sha384-DNOHZ68U8hZfKXOrtjWvjxusGo9WQnrNx2sqG0tfsghAvtVlRW3tvkXWZh58N9jp" crossorigin="anonymous"> # add this line
      <script src="https://js.pusher.com/4.1/pusher.min.js"></script> # add this line
      <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    </head>

If you’ve followed the tutorial so far, when you reload the homepage, you should see the first image below. On entering a username, you should see the second image below.

If you encounter a RegExp error while trying to set up Bootstrap, In config/boot.rb, change the ExecJS runtime from Duktape to Node.

    # config/boot.rb
    ENV['EXECJS_RUNTIME'] ='Node'

Adding posts

Now that our app’s UI is set up, we’ll go ahead and start adding posts. In the posts controller, we’ll add code for creating posts and liking them. Update your posts controller with following:

    # app/controllers/posts_controller.rb

    class PostsController < ApplicationController
      def index
        @posts = Post.all.order(created_at: :desc)
      end

      def new
        @post = Post.new
      end

      def create
        @post = Post.new(post_params)
        @post.likes.build()
        respond_to do |format|
          if @post.save
            format.json { render :show, status: :created }
          else
            format.json { render json: @chat.errors, status: :unprocessable_entity }
          end
        end
      end

      def add_like
        @post = Post.find(params[:post_id])
        if @post
          @post.likes[0].like_count +=1

          if @post.likes[0].save
            respond_to do |format|
              format.json { render :show, status: :ok }
            end
          end
        end
      end

      private
        def post_params
          params.require(:post).permit(:post, :username)
        end
    end

We’ll build our server response to JSON using Jbuilder. In your posts views folder, create a show.json.jbuilder file and add the following code:

    # app/views/posts/show.json.jbuilder

    json.extract! @post, :id, :username, :post, :created_at
    json.url post_url(@post, format: :json)
    json.likes @post.likes[0].like_count

Whenever a new post is created, we’ll handle it via AJAX and prepend it to our current feed so the new feeds are at the top. Update your posts.coffee file with the following:

    # app/assets/javascripts/posts.coffee

      # function for adding new posts to the feed
      updateFeed = (post) ->
        postTime = new Date(post.created_at.replace(' ', 'T')).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })
        $('.posts').prepend """
          <div class="post-wrapper col-12 mb-2 p-0">
            <div class="card">
              <div class="card-header">@#{post.username}<small class="float-right mt-1">at #{postTime}</small></div>
              <div class="card-body">
                <p class="card-text">#{post.post}</p>
                <a class="far fa-thumbs-up add-like" data-remote="true" rel="nofollow" data-method="post" href="/likes/#{post.id}"></a>
                <span class="ml-2" data-post="#{post.id}">#{post.likes[0].like_count}</span>
              </div>
            </div>
          </div>
        """
        return

      # if the post was successfully saved, get the post and pass it to the updateFeed function
      $('#post-form').on 'ajax:success', (data) ->
        post = data.detail[0]
        updateFeed post
        $('#post-form')[0].reset()
        return

With that, we should be able to create new posts and see them appear in our feed. Next, we’ll add our killer realtime feature.

Realtime feed with Pusher

To make our feed realtime, whenever a new post is created, we publish it on the server via Pusher and subscribe to it on the frontend of our app. Before we can do this though, we need to initialize our Pusher client. In the config/initializers folder, create a pusher.rb file and add the following code:

    # config/initializers/pusher.rb

    require 'pusher'
    Pusher.app_id = ENV["PUSHER_APP_ID"]
    Pusher.key = ENV["PUSHER_KEY"]
    Pusher.secret = ENV["PUSHER_SECRET"]
    Pusher.cluster = ENV["PUSHER_CLUSTER"]
    Pusher.logger = Rails.logger
    Pusher.encrypted = true

Next, install Figaro by running figaro install in your terminal. It will generate an application.yml file. In the application.yml file add your Pusher keys:

    # config/application.yml

    PUSHER_APP_ID: 'xxxxxx'
    PUSHER_KEY: 'xxxxxxxxxxxxxxxxx'
    PUSHER_SECRET: 'xxxxxxxxxxxxxx'
    PUSHER_CLUSTER: 'xx'

Now we can go ahead and publish new posts and likes whenever they’re created. Add the following code to your post and like models:

    # app/models/post.rb

    class Post < ApplicationRecord
      after_create :notify_pusher, on: :create
      has_many :likes

      def notify_pusher
        Pusher.trigger('feed', 'new-post', self.as_json(include: :likes))
      end
    end
    # app/models/like.rb

    class Like < ApplicationRecord
      after_save :notify_pusher, on: :create
      belongs_to :post

      def notify_pusher
        Pusher.trigger('feed', 'new-like', self.post.as_json(include: :likes))
      end
    end

In the code above, we add an after_create and after_save callback to the post and like models respectively. These callbacks call the function to publish new posts and likes.

Updating the UI

Now that our server is publishing data each time it’s created, it’s up to the client to listen for those changes and do something with that data. Lets rename our posts.coffee file to posts.coffee.erb and update it with the following code:

    $(document).ready =>
      currentUser = ''
      welcomePage = $('.welcome-page')
      postPage = $('.post-page')
      newUserForm = $('#new-user-form')

      <%# when user enters a username, store it and show the post page %>
      newUserForm.on 'keyup', (event) ->
        if event.keyCode == 13 and !event.shiftKey
          currentUser = event.target.value
          newUserForm.val('')
          welcomePage.addClass('collapse')
          postPage.removeClass('collapse')
          greeting = """welcome @#{currentUser}"""
          $('.current-user').html(greeting)
          $('#username').val(currentUser)
        return

      <%# function for adding new posts to the feed %>
      updateFeed = (post) ->
        postTime = new Date(post.created_at.replace(' ', 'T')).toLocaleString('en-US', { hour: 'numeric', minute: 'numeric', hour12: true })
        $('.posts').prepend """
          <div class="post-wrapper col-12 mb-2 p-0">
            <div class="card">
              <div class="card-header">@#{post.username}<small class="float-right mt-1">at #{postTime}</small></div>
              <div class="card-body">
                <p class="card-text">#{post.post}</p>
                <a class="far fa-thumbs-up add-like" data-remote="true" rel="nofollow" data-method="post" href="/likes/#{post.id}"></a>
                <span class="ml-2" data-post="#{post.id}">#{post.likes[0].like_count}</span>
              </div>
            </div>
          </div>
        """
        return

      <%# if the post was successfully saved, get the post and pass it to the updateFeed function %>
      $('#post-form').on 'ajax:success', (data) ->
        post = data.detail[0]
        $('#post-form')[0].reset()
        return

      <%# suscribe our Pusher client to the feed channel. Whenever there is a new post or new like, update the view with it %>
      pusher = new Pusher('<%= ENV["PUSHER_KEY"] %>',
        cluster: '<%= ENV["PUSHER_CLUSTER"] %>'
        encrypted: true)
      channel = pusher.subscribe('feed')
      channel.bind 'new-post', (data) ->
        updateFeed data
      channel.bind 'new-like', (data) ->
        <%# whenever there is a new like, find the liked post via it's "data-post" attribute then update its likes count %>
        currentPost = $ 'span[data-post=\'' + data.id + '\']'
        currentPost.text(data.likes[0].like_count)
        return
      return 

In the code above, we subscribed our Pusher client to the feed channel and listened for the new-post and new-like events. Once those events are emitted, we get the data and update the feed and likes count with it.

Bringing it all together

Restart the development server if it is currently running. Visit http://localhost:3000 in two separate browser tabs and test out the realtime feed and likes.

Conclusion

In this post, we have been able to create a realtime feed app using Pusher. I hope you found this tutorial helpful and would love to apply the knowledge gained here to easily set up your own application using Pusher. You can find the source code for the demo app on GitHub.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.