Ruby
Article

Mini-Chat with Rails

By Ilya Bodrov-Krukowski

Comments - Icon

Comments are everywhere. You’ve probably implemented some kind of commenting system in some of your projects (if not then you may be interested in my “Nested Comments with Rails” article).

But, what if we wanted to make a fully asynchronous commenting system where the user does not experience any page reload while sending or receiving new comments? What tools could we use to achieve this task? What problems might we face and how can they be solved?

In this two-part article series I am going to show you how to create a fully-functional mini-chat on Rails in six iterations. We will discuss the following topics:

  • Authentication via multiple social networks (OAuth 2, OmniAuth)
  • Asynchronous comments posting
  • Instant loading of the new comments without the need to reload the page (we will discuss two possible solutions: AJAX polling and web sockets with Faye)

We are also going to discuss some caveats that you should be aware of with AJAX polling and Faye’s security.

The source code is available at GitHub.

Check out the working demo at http://sitepoint-minichat.herokuapp.com/.

Some Planning

Rails 4.1.5 will be used for this demo but the same solution can be implemented with Rails 3.

Okay, before starting our journey, let’s write down the requirements for our mini-chat app:

  • Only authenticated users should be able to read and post comments.
  • Users should be able to authenticate via Facebook or Twitter.
  • Each comment should be provided with user’s name, avatar, and a link to his profile in social network.
  • Comments should be posted and received instantly (well, nearly instantly), without any page reloads.
  • Empty and very long comments are not permitted.
  • When users enters the chat, they should be able to read some comments that were posted previously. Old comments should be removed automatically.

Great, the requirements are formulated and it is time to proceed to the first (and probably the largest) iteration – hold on tight!

Authentication

Start off by creating a new Rails app without the default testing suite:

$ rails new mini_chat -T

Before digging into the code, stop for a moment and think about the authentication system we are going to implement for this app. Since we want our users to be able to sign in via Facebook and Twitter, the OAuth 2 protocol will be used. You’ve probably heard about OAuth2, but if not – don’t worry – I will explain the basics.

The main idea behind OAuth2 is that the user enters his credentials on the platform’s website itself (Twitter or Facebook in our case). This way the user never exposes his password to our app; instead, the website is only given some information about the user’s account (name, surname, profile URL, avatar, etc.) and a key pair (or a single key, depending on the platform) that is used to issue API calls (for example, to post a message on Facebook on behalf of the user).

The key idea is that the user always knows what actions the third-party resource will be able to perform and if he does not trust this website, he can reject the authentication. Moreover, the key pair issued to the resource cannot be used to perform any action – only those actions that the resource initially requested are allowed (this is called a scope). This key pair has a limited lifetime and the user may also revoke access for any previously authenticated resource manually at any time.

When the authentication phase starts, a third-party website forwards a special application key used to identify the website. An array of actions that the resource wants to perform is also sent. The user is prompted with a dialog explaining that this website wants him to authenticate and asks for the permission to perform a list of actions. If the user agrees, he is redirected back to the site with information about the user that has just authenticated, as well as the key pair to issue API calls.

At first, this may seem complex. In reality, we can easily implement this scenario in a Rails app.

First of all we need a model and a table to store information about the authenticating user. Actually, we don’t even need API keys because we are not going to issue any API calls. For this demo let’s store the following information in the users table:

  • provider (string) – the name of the social network that was used for authentication. In our case it would be either “facebook” or “twitter”.
  • name (string) – the user’s name (optionally with a surname, but that depends on what the user has entered while creating his profile).
  • profile_url (string) – a link to the user’s profile.
  • avatar_url (string) – a link to the user’s avatar.
  • uid (string) – user’s unique identifier on the social network. We will see why it is needed a bit later.

Go on and create the required migration:

$ rails g model User name:string avatar_url:string provider:string profile_url:string uid:string

Now open migration’s file and alter it a bit by adding these lines at the end of the change method:

xxx_create_users.rb

[...]
def change
  [...]
  add_index :users, :uid
  add_index :users, :provider
  add_index :users, [:uid, :provider], unique: true
end
[...]

As you see, we create three indicies here on uid and provider columns, as well as the clustered index on those columns and ask it to enforce uniqueness. The idea behind this is that there can’t be two different users with the same social network name and unique identifier.

Apply the migration:

$ rake db:migrate

At this moment, we can move on to the authentication itself. Luckily, there are gems that will make our life much easier – these are omniauth-facebook by Mark Dodwell and omniauth-twitter by Arun Agrawal. What is this “omniauth” suffix about?

OmniAuth is a gem created by Intridea, Inc. aimed at standardizing multi-provider authentication for web applications. OmniAuth allows you to use as many different authentication strategies as you need allowing your users to authenticate via OpenID, Facebook, Twitter, Google, etc. There are a number of strategies to choose from but you can create your own. For this app, we are going to use two strategies: Twitter and Facebook, packed into those two gems.

Drop these gems into Gemfile

Gemfile

[...]
gem 'omniauth-facebook'
gem 'omniauth-twitter'

and run

$ bundle install

Now we have to register our website as an app in Facebook and Twitter. Visit https://developers.facebook.com/apps and create a new app here. Give it a name in the Settings section (I’ve called my “SitePoint Mini-Chat Demo”). Don’t forget that this name will be visible to authenticating user! Next, click “Add platform” and choose “Website”. Fill in “Site URL” (in my case I’ve entered “http://sitepoint-minichat.herokuapp.com/”) and “App Domains” (I’ve entered “sitepoint-minichat.herokuapp.com”). Keep in mind, this domain must be derived from the URL, otherwise your settings won’t be saved. Also provide a “Contact E-mail” and click “Save”. Navigate to “Status & Review” and toggle “Do you want to make this app and all its live features available to the general public?” switch to “Yes”.

For a real app, spend some time adjusting the other app’s settings (like picture, description, tagline, etc.) We are going to head to the “Dashboard” section. Here, note the “App ID” and “App Secret” fields (the latter is masked). Click “Show” to reveal the Secret and enter your password – these are the keys that we will need to set up authentication.

Create a new file called omniauth.rb in config/initializers and paste the following code:

config/initializers/omniauth.rb

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

To store the key pair I am using Heroku environmental variables. As you’ve probably guessed FACEBOOK_KEY is the “App ID” and FACEBOOK_SECRET is the “App Secret”.

The scope is the list of permissions that our app will request. Since we won’t access any Facebook API functions, the only requested permission is the public_profile that means “retrieve user’s basic data”. Read more about Facebook’s permissions here.

display is another parameter sent to Facebook while authenticating that controls how the authentication page will look like. If, for example, you are opening the authentication page in a small popup, then you can provide popup as the display value. Read more here.

The last option is image_size, which controls the size of the user’s avatar that we want to receive. square means a “50x50px image”.

Now we need to do the same for Twitter. Navigate to https://apps.twitter.com/ and
create a new app. Give it a name, a description, and a URL. Also, enter the callback URL in the following
format: “apps’s_URL/auth/twitter/callback”. This format is defined by OmniAuth and is the resource the user will be redirected to after successful authentication. We will take care of that in a moment.

Next, accept the terms and create the app. You may want to spend some time by adjusting other settings of your app, like Organization name, Icon, etc. Also, notice in the Details pane you can change the Access level of the app. We will keep the default “Read-only” setting.

Open API Keys – this is where the API key and API secret reside. Return to your omniauth.rb file and add some more code:

config/initializers/omniauth.rb

Rails.application.config.middleware.use OmniAuth::Builder do
  provider :twitter, ENV['TWITTER_KEY'], ENV['TWITTER_SECRET'], image_size: 'normal'
  [...]

Paste your keys directly into this file or use some other method to store them (which is recommended – they should not be visible for everyone). The image_size option supplies the user’s avatar as a 48x48px image. For a full list of available options, check documentation.

OK, we’ve finished with probably the most tedious task of this demo and are ready to proceed. Remember the callback URL? We need to create the corresponding route now:

config/routes.rb

[...]
get '/auth/:provider/callback', to: 'sessions#create'
get '/auth/failure', to: 'sessions#auth_fail'
get '/sign_out', to: 'sessions#destroy', as: :sign_out
[...]

I’ve also created sign_out and authentication failure routes.

On to the controller:

sessions_controller.rb

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

  def destroy
    cookies.delete(:user_id)
    flash[:success] = "See you!"
    redirect_to root_url
  end
  
  def auth_fail
    render text: "You've tried to authenticate via #{params[:strategy]}, but the following error occurred: #{params[:message]}", status: 500
  end
end

In the create action, call a to-be-created method from_omniauth and pass request.env['omniauth.auth'] as an argument. The request.env['omniauth.auth'] hash contains all the information about the authenticated user (it is called the “authentication hash”).

After the user has been created, store its id in the cookie so that we can check if she is authenticated. Then, set a welcoming message and redirect her to the root_url (also non-existent yet).

The destroy action is even simpler. Just delete the user_id cookie, set the goodbye message, and redirect to the main page.

If you want to have a look at the authentication hash, you may replace create method’s code with this line:

render text: request.env['omniauth.auth'].to_yaml

then navigate to “your_app’s_url/auth/twitter/” (or “auth/facebook”) and observe the results.

We need the User.from_omniauth method. In this method, the user should be either created or found (by her unique id and social network name) and updated:

models/user.rb

class User < ActiveRecord::Base
  class << self
    def from_omniauth(auth)
      provider = auth.provider
      uid = auth.uid
      info = auth.info.symbolize_keys!
      user = User.find_or_initialize_by(uid: uid, provider: provider)
      user.name = info.name
      user.avatar_url = info.image
      user.profile_url = info.urls.send(provider.capitalize.to_sym)
      user.save!
      user
    end
  end
 end

Here I use the symbolize_keys! method to call methods like info.name. As you can see, we fetch all the required user’s information, then save the record, and return the user as a result.

You may notice the line info.urls.send(provider.capitalize.to_sym). Since we are working with multiple providers here and URLs are normally returned in the following way:

:urls => { :Twitter => "https://twitter.com/johnqpublic" }

We just need to know the social network name to fetch user’s profile address. Check out an example
authentication hash here.

At this point we are 95 % finished with the authentication. If you ever need to add more authentication options, like Open ID or Google, that can be done easily. Just modify the initializers/omniauth.rb file by adding another provider and test the authentication hash (some social networks may present a bit different hash from the ones that we’ve just seen).

The last step here is to present our users with the links to authenticate. Before doing this, we need a root route and a corresponding view, so let’s do that in the next iteration.

Comments

In this iteration, we need to switch to the comments – we are building a messaging app after all. Let’s call our model Comment and have it contain the following fields (apart from the default id, created_at, updated_at):

  • body (text) – the actual message.
  • user_id (integer) – foreign key pointing to the user who posted this message.

As you see, this is a really simple model. Create and apply the migration:

$ rails g model Comment body:text user:references
$ rake db:migrate

Don’t forget to open user.rb and add this line:

models/user.rb

[...]
has_many :comments, dependent: :delete_all
[...]

Great, at this point we are ready to add some routes:

config/routes.rb

[...]
resources :comments, only: [:new, :create]
root to: 'comments#new'
[...]

We are only going to need new and create actions for this demo. We are not going to build a backend to manage comments, but you may add this functionality on your own.

The corresponding controller (the methods will be populated later):

comments_controller.rb

class CommentsController < ApplicationController
  def new
    @comment = Comment.new
  end

  def create
  end
end

Before creating the view, hook up Bootstrap to help us with styling:

Gemfile

[...]
gem 'bootstrap-sass'
[...]

application.css.scss

@import 'bootstrap';
@import 'bootstrap/theme';

Also modify your layout to take advantage of Bootstrap’s styles:

layouts/application.html.erb

[...]
<body>
  <div class="navbar navbar-default navbar-static-top">
    <div class="container">
      <div class="navbar-header">
        <%= link_to 'Mini-chat', root_path, class: 'navbar-brand' %>
      </div>
    </div>
  </div>

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

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

Okay, now we can move on to the sole view:

comments/new.html.erb

<div class="jumbotron">
  <h1>Welcome to our mini-chat!</h1>
  <p class="lead">Please sign in via one of the social networks to start chatting</p>
  <%= link_to '', '/auth/twitter', class: 'social twitter' %>
  <%= link_to '', '/auth/facebook', class: 'social facebook' %>
</div>

This is the place where we present users with the authentication links. The auth part is defined by OmniAuth, ollowed by the provider’s name (facebook or twitter in our case). Let’s also style these links a bit. Probably the easiest way to achieve this is using the social networks’ icons that are recognized by virtually any Internet user. To download the official icons, visit these resources: https://www.facebookbrand.com/ and https://about.twitter.com/press/brand-assets. I’ve already created a sprite image (with the help of http://csssprites.com/) that you can use.

Now apply some styling:

application.css.scss

[...]
.social {
  background: image-url('logos.png') no-repeat transparent;
  display: inline-block;
  &.twitter {
    width: 50px;
    height: 42px;
    background-position: 0 -52px;
  }

  &.facebook, &.facebook:active {
    width: 50px;
    height: 50px;
    background-position: 0 0;
  }
}

Note that I am using image-url here because, otherwise, my background image will not be rendered on Heroku.

Before moving on to the commenting form, we need to check if the user is authenticated. Traditionally, this method is called current_user:

application_controller.rb

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

helper_method :current_user

With this method present, we can present an authenticated user with a “Sign Out” link:

layouts/application.html.erb

[...]
<div class="navbar navbar-default navbar-static-top">
  <div class="container">
    <div class="navbar-header">
      <%= link_to 'Mini-chat', root_path, class: 'navbar-brand' %>
    </div>

    <% if current_user %>
      <ul class="nav navbar-nav navbar-right">
        <li><%= image_tag current_user.avatar_url %></li>
        <li><%= link_to 'Sign out', sign_out_path %></li>
      </ul>
    <% end %>
  </div>
</div>
[...]

Returning to the view:

comments/new.html.erb

<% if current_user %>
  <div class="page-header">
    <h1>Join the discussion!</h1>
  </div>

  <%= render 'form' %>
<% else %>
  <%= render 'welcome' %>
<% end %>

I’ve extracted the welcome message along with the authentication links to a separate partial:

comments/_welcome.html.erb

<div class="jumbotron">
  <h1>Welcome to our mini-chat!</h1>
  <p class="lead">Please sign in via one of the social networks to start chatting</p>
  <%= link_to '', '/auth/twitter', class: 'btn social twitter' %>
  <%= link_to '', '/auth/facebook', class: 'btn social facebook' %>
</div>

Now, create the form partial:

comments/_form.html.erb

<div class="well">
  <%= form_for @comment do |f| %>
    <div class="form-group">
      <%= f.label :body, 'Enter your comment:' %>
      <%= f.text_area :body, rows: 3, class: 'form-control', required: true, maxlength: 2000 %>
      <small class="label label-warning">Cannot be blank or contain more than 2000 symbols.</small>
    </div>

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

Nothing fancy going on here – a single textarea for entering a message and a “Post” button. We also need a validation to prevent users from sending empty or very long messages:

models/comment.rb

[...]
validates :body, presence: true, length: {maximum: 2000}
[...]

Later, you may also add some client-side validation (for example, with the help of jQuery Validate plugin).

The last thing to do here is to flesh out the create action:

comments_controller.rb

[...]
def create
  if current_user
    @comment = current_user.comments.build(comment_params)
    if @comment.save
      flash[:success] = 'Your comment was successfully posted!'
    else
      flash[:error] = 'Your comment cannot be saved.'
    end
  end
  redirect_to root_url
end

private

def comment_params
  params.require(:comment).permit(:body)
end
[...]

Our users now can post their comments. However, they won’t see them. Let’s fix that!

comments_controller.rb

[...]
def new
  @comment = Comment.new
  @comments = Comment.order('created_at DESC')
end
[...]

comments/new.html.erb

<% if current_user %>
  <div class="page-header">
    <h1>Join the discussion!</h1>
  </div>

  <%= render 'form' %>
  <div class="panel panel-default" id="comments">
    <div class="panel-body">
      <ul class="media-list">
        <%= render @comments %>
      </ul>
    </div>
  </div>
<% else %>
  <%= render 'welcome' %>
<% end %>

I am using yet another partial that will be rendered for each comment in the @comments array:

comments/_comment.html.erb

<li class="media comment">
  <%= link_to image_tag(comment.user.avatar_url, alt: comment.user.name, class: "media-object"),
              comment.user.profile_url, target: '_blank', class: 'pull-left' %>
  <div class="media-body">
    <h4 class="media-heading"><%= link_to comment.user.name, comment.user.profile_url, target: '_blank' %> says
      <small class="text-muted">[at <%= comment.created_at.strftime('%-d %B %Y, %H:%M:%S') %>]</small></h4>
    <p><%= comment.body %></p>
  </div>
</li>

Lastly, a bit of styling:

application.css.scss

[...]
#comments {
  max-height: 500px;
  overflow: auto;
}

At this point, fire up the server, authenticate via one of the social networks, and try to post some comments. We are ready to move on to the next iteration!

Removing Excessive Comments

We do not want to present users with all the messages that were posted since the creation of the app. In this iteration, we are going to create a scheduled task to remove excessive comments (obviously, this problem can be solved in many ways).

I am going to use Heroku Scheduler add-on here but you may employ another solution, like Rufus Scheduler.

This add-on can be installed with the following command (please note that your app should be already created on Heroku):

$ heroku addons:add scheduler:standard

Next create the actual task:

lib/tasks/scheduler.rake

desc "This task is called by the Heroku scheduler add-on"

task remove_excessive_comments: :environment do
  puts "Removing excessive comments now..."
  Comment.remove_excessive!
end

and the remove_excessive! method:

models/comment.rb

[...]
class << self
  def remove_excessive!
    if all.count > 100
      order('created_at ASC').limit(all.count - 50).destroy_all
    end
  end
end
[...]

The numbers here are arbitrary, so adjust them as you like. The general idea here is, if there are many comments posted, delete all except for the last 50.

You may test this task by running

$ heroku run rake remove_excessive_comments

When you are ready run

$ heroku addons:open scheduler

and create a new task here (enter rake remove_excessive_comments in the “Task” field) with the required frequency.

You may also implement infinite scrolling to gradually load older comments as the user scrolls down – check out my “Infinite Scrolling in Rails: The Basics” article to learn more.

Adding a Bit of AJAX

This will be the last iteration for today. Let’s add a bit of AJAX to make the comments posting process asynchronous. This way, the page won’t be reloaded every time the user has posted his comment.

If you are using Turbolinks I recommend adding jquery-turbolinks gem to bring back default page load events (because initially Turbolinks has its own events).

Gemfile

[...]
gem 'jquery-turbolinks'
[...]

application.js

[...]
//= require jquery.turbolinks
//= require turbolinks

To equip our form with AJAX, only one modification is needed:

comments/_form.html.erb

<%= form_for @comment, remote: true do |f| %>

The controller’s create method should also be modified to handle AJAX requests:

comments_controller.rb

[...]
def create
  respond_to do |format|
    if current_user
      @comment = current_user.comments.build(comment_params)
      if @comment.save
        flash.now[:success] = 'Your comment was successfully posted!'
      else
        flash.now[:error] = 'Your comment cannot be saved.'
      end
      format.html {redirect_to root_url}
      format.js
    else
      format.html {redirect_to root_url}
      format.js {render nothing: true}
    end
  end
end
[...]

The respond_to method is used here to respond with the correct format: either HTML (if the user has disabled JavaScript in his browser) or JS. A new view is also needs to be created:

comments/create.js.erb

<% if @comment.new_record? %>
  alert('Your comment cannot be saved.');
<% else %>
  $('#comments').find('.media-list').prepend('<%= j render @comment %>');
  $('#comment_body').val('');
<% end %>

We are using a if @comment.new_record? condition to check if the comment was saved. If yes – prepend a new comment to the list of comments (by rendering the _comment.html.erb partial) and clear the textarea. You may extend this functionality a bit by alerting (or displaying in some other way) the list of errors that were found if the comment was not saved.

Go on and try out the new AJAX-powered form. Indeed, it works nicely, but there is still a lot to be done. The main problem is that the user still needs to reload the page to see what others have posted, however in a real chat, she would see the new comments as soon as they are added. So this is our task for next time!

Conclusion

We’ve created a messaging app allowing our users to sign in via one of the social networks and post their comments. We’ve also added a scheduler task to remove excessive comments.

In the next part, we are going to talk about the two possible ways of displaying new comments for the user:
AJAX polling and web sockets. We will discuss which solution is better suited for our needs, as well as pinpoint some gotchas.

Free Guide:

7 Habits of Successful CTOs

"What makes a great CTO?" Engineering skills? Business savvy? An innate tendency to channel a mythical creature (ahem, unicorn)? All of the above? Discover the top traits of the most successful CTOs in this free guide.

  • Oleg Bovykin

    Seems like unrails way for me

    if @comment.new_record?

    • Ilya Bodrov

      Why so? How would you re-write this piece?

      • Oleg Bovykin

        You putting together validation and persistance. But its two different things.
        I think you should use @comment.errors.any? or @comment.valid? methods for this.

        • Ilya Bodrov

          The problem is that even if the comment is valid it may not be saved due to some other reason (some constraints on the lower level)

          • Oleg Bovykin

            in that case you should handle this situation in controller. Also, if you call save in controller and check for errors in the view – its ok. But checking persistence – is not

          • Ilya Bodrov

            Well, I would try to think of something but honestly I do not find it that serious.

          • Ilya Bodrov

            You do have a point, actually. But to put it simple – I am trying to be pragmatic and for this exact (a somewhat simple) demo this line seems to be okay. After all, this is not a chapter from the book “The Rails Way” or something like that. I am only covering some specific concepts and by no means I am trying to dictate how to write code in a “rails way”. Actually, this is good that readers note pieces of code that could be re-written in some other way and share their thoughts.

    • http://bitsnut.com/ Yoochan Seo

      How is that line unrails way? It’s just a piece from Rails parts. it is a rails way.

  • Gee Bee

    If I change ‘user_id’ field in cookie I can write as somebody else. Just checked.
    Set your secret_key_base in production.
    Two more things:
    – turbolinks are not necessary (not connected with remote: true option passed to form builder)
    – flash messages are not working (not updated in create.js.erb)

    • Ilya Bodrov

      Thank you for the feedback.

      Yeah, obviously that is an issue with cookies, made some modifications :)

      Indeed, turbolinks are not necessary however I wanted to warn folks who are using them to watch out for problems with $(document).ready() (in the next part of the series).

      I was not planning to set flash from there – if you see your message appeared, obviously it was posted.

  • Darren Gussin

    I get “undefined method ‘delete_all’ for # when trying to run the scheduler task. Am I missing something? So there is no delete_all method on an array and first(of some limit) returns an array. Has this code been tested?

    first(all.count – 50).delete_all

    • Ilya Bodrov

      Oh, I am sorry – forgot to change that line. Indeed, `first` returns an array and we can’t issue AR’s `delete_all` on that. One of the possible solutions is to use:

      Comment.order(‘created_at ASC’).limit(Comment.all.count – 50).unscoped.delete_all

      • Darren Gussin

        Thanks Ilya for your reply.

        I’m new to Rails and I’m still having an issue.

        Comment.order(‘created_at ASC’).limit(Comment.all.count – 50).unscoped.delete_all

        seems to delete all the comments.

        I have 105 comments.

        x = Comment.order(‘created_at ASC’).limit(Comment.all.count-50)

        x.count => 55

        x.unscoped.delete_all => 105

  • Darren Gussin

    Got it working with ….

    def remove_excessive!
    if all.count > 100
    ids = Comment.order(‘created_at ASC’).limit(Comment.all.count – 50).pluck(:id)
    Comment.delete(ids)
    end
    end

    • Ilya Bodrov

      Great! Thank you for the feedback!

  • Suresh Kumar

    Hi Ilya Bodrov

    Please add login by google plus also please

  • Guest

    I’m getting an error when I try to migrate: SQLite3::SQLException: no such table: main.users: CREATE INDEX “index_users_on_uid” ON “users”

  • Slava

    In create.js.erb, you should change the condition or use unless. Because right now you will get “Your comment cannot be saved.” if the comment was saved successfully.

    • Ilya Bodrov

      Quite the opposite :) If the comment cannot be saved, it will be marked as the new record – that is, a record that is not persisted. If it is not a new record, it is saved. Check out the demo app, it works correctly.

  • Bill Kidder

    I’m trying to get this to work with Ruby 2.1.2/rails 4.2.5 and can’t get the CSS or Images to show (but the JS works). It could be an asset pipeline thing. Anyone have any suggestions?

    • Ilya Bodrov

      Any errors? No such issues have been reported so far :(

      • Bill Kidder

        No errors, but I’m about to try your github source and compare mine against it.

        • Bill Kidder

          I don’t understand why, but I had both app.css.scss and app.css in the app/assets/ss dir. When I deleted application.css the CSS was correctly rendered. BTW, the shebang lines in your bin dir files contain these mysterious “.exe” suffixes that prevent your app from running.

          • Ilya Bodrov

            Great! Not sure about .exe though. I don’t see them anywhere..

  • Dawid Jagieła

    I solved it by changing scope in omniauth.rb to something like this:
    scope: ’email’, info_fields: ’email,name,link’

    You can add into info_fields fields you need :)

  • Александр Николаев

    I fixed the problem with authentication I had, but now the comments just don’t post. every time I try to post a comment, it flashes me an error message, and I just can’t manage why :(

  • Fernando Fabreti

    Great work! From head to tail with all problems included and explained.

  • poombavai sivamani

    Hello, I’m getting an error when I try to migrate: SQLite3::SQLException: no
    such table: main.users: CREATE INDEX “index_users_on_uid” ON “users”.
    How should I correct it?

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

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