alt text

Create messaging system between Reseller and Customer.

Denshobato Github Repository

Part 2

In PART 1 we built a basic app with reseller and customer models, devise authentication and index templates to list our models. In part 2 we’ll make the rest of our app.

In Part 2:

  • We’ll create conversations
  • Send messages to conversations
  • Send conversations to Trash
  • Add users to BlackList

Go to Reseller and Customer Models.

Add denshobato_for :your_class to these models. This method does a lot of things - sets up associations and adds useful methods for you.

denshobato_for :your_class

Add it to your models.

class Reseller < ActiveRecord::Base
  denshobato_for :reseller
end

class Customer < ActiveRecord::Base
  denshobato_for :customer
end

Go to Resellers and Customers contoller. Add to index action conversation builder for our form.

@conversation = current_account.hato_conversations.build

Add this form to your index page, which shows all your resellers and customers.

// => resellers/index.html.slim

- @resellers.each do |reseller|
  = link_to reseller.email, reseller if current_account != reseller
  = form_for @conversation, url: :conversations do |form|
    = fill_conversation_form(form, reseller)
    = form.submit 'Start Conversation', class: 'btn btn-primary'
  hr

fill_conversation_form is a Denshobato view helper, it helps to create a conversation.

Don’t forget to add route for conversations resource :conversations

On a reseller’s page (if you’re signed as a reseller) you can see an extra button for creating conversation with yourself. There is also a useful helper which can hide this button.

- if can_create_conversation?(current_account, reseller)
    = form_for @conversation, url: :conversations do |form|
      = fill_conversation_form(form, reseller)
      = form.submit 'Start Conversation', class: 'btn btn-primary'

Add this to both forms (for reseller and customer).

Okay, now if we click the button, we got an error uninitialized constant ConversationsController

Let`s create this contoller.

class ConversationsController < ApplicationController
  def create
    @conversation = current_account.hato_conversations.build(conversation_params)
    if @conversation.save
      redirect_to conversation_path(@conversation)
    else
      redirect_to :conversations, notice: @conversation.errors
    end
  end

  def destroy
    @conversation = Denshobato::Conversation.find(params[:id])
    redirect_to :conversations if @conversation.destroy
  end

  private

  def conversation_params
    params.require(:denshobato_conversation).permit(:sender_id, :sender_type,
       :recipient_id, :recipient_type)
  end
end

Done, we’re ready to create a form for messages.

First, add resources :messages to routes.rb

In ConversationController, show action add form for message.

def show
  @conversation = Denshobato::Conversation.find(params[:id])
  @message_form = current_account.hato_messages.build
  @messages     = @conversation.messages.includes(:author)
end

We set url: :messages for correct resource path (by default it searches for :denshobato_messages)

// => conversations/show.html.slim

= form_for @message_form, url: :messages do |form|
  = form.text_field :body, class: 'form-control'
  = fill_message_form(form, current_account, @conversation)
  = form.submit 'Send message', class: 'btn btn-primary'

fill_message_form is a Denshobato helper, it helps you to create a message form.

To show all messages for this conversation add this to the same view below. show.html.slim

- @messages.each do |msg|
  = msg.body
  hr

Okay, when we try to create a message, we got an error, we don’t have MessagesController, so create it.

class MessagesController < ApplicationController
  def create
    conversation_id = params[:denshobato_message][:conversation_id]
    @message = current_account.send_message_to(conversation_id, message_params)

    if @message.save
      @message.send_notification(conversation_id)
      redirect_to conversation_path(conversation_id), notice: 'Message Sent'
    else
      redirect_to conversation_path(conversation_id), notice: @message.errors
    end
  end

  private

  def message_params
    params.require(:denshobato_message).permit(:body, :author_id, :author_type)
  end
end

Notice: Don`t forget to send_notification to conversation after you save message, it sends notification both for you and recipient, so both of you get access to messages.

Now we’ll make our conversation list.

Open /layouts/_links.html.slim

li = link_to 'Conversations', :conversations

Add to index action in ConversationContoller

def index
  @conversations = current_account.my_conversations.includes(:sender)
end

And in index view

- @conversations.each do |room|
  = link_to "Conversation with: #{room.recipient.email}", conversation_path(room)
  = button_to 'Remove Conversation', conversation_path(room),
    method: :delete, class: 'btn btn-danger'
  hr

Now we can list all your conversations.

It is not safe now, because now everyone have an access to your conversations. So you need to go to conversation show action, and add this line

Notice: We use user_in_conversation?(current_account, @conversation) method in this example. This method checks if current_user presents in conversations.

def show
  @conversation = Denshobato::Conversation.find(params[:id])
  redirect_to :conversations, notice: 'You can`t join this conversation' unless user_in_conversation?(current_account, @conversation)

  @message_form = current_account.hato_messages.build
  @messages     = @conversation.messages
end

If we go back to reseller’s or customer’s index page, we can still see start conversation button, even if the conversation is already started. Let`s hide it.

In our index.html.slim templates for reseller and customer, use conversation_exists? method.

- @resellers.each do |reseller|
  = link_to reseller.email, reseller if current_account != reseller
  - unless conversation_exists?(current_account, reseller)
    - if can_create_conversation?(current_account, reseller)
      = form_for @conversation, url: :conversations do |form|
        = fill_conversation_form(form, reseller)
        = form.submit 'Start Conversation', class: 'btn btn-primary'
  hr

Good, now go to conversation index page, we’ll use some view helpers to show our recipient’s name and avatar. You will also see the last message of a shown conversation.

- @conversations.each do |room|
  = interlocutor_image(room.recipient, :user_avatar, 'img-circle')
  = link_to "Conversation with: #{interlocutor_info(room.recipient,
    :first_name, :last_name)}", conversation_path(room)
  p = room.messages.last.try(:body)

  = interlocutor_image(room.messages.last.try(:author),
    :user_avatar, 'img-circle')
  - if room.messages.last.present?
    p = "Last message from: #{message_from(room.messages.last, :first_name, :last_name)}"
  = button_to 'Remove Conversation', conversation_path(room),
    method: :delete, class: 'btn btn-danger'
  hr

It should look like this.

Of course, your models should have an image url, and at least one message in conversation

alt text

Next, go into a conversation and use this helpers to make it look better.

Open conversation/show.html.slim and replace messages with formatted outp

- @messages.each do |msg|
  p = interlocutor_info(msg.author, :first_name, :last_name)
  = interlocutor_image(msg.author, :user_avatar, 'img-circle')
  p = msg.body
  hr

Great! We are almost in the end, but we have to do two more features - send conversation to trash and ignore users.

We start with trash feature.

Trash

First, create buttons for these actions.

Again, open your conversation index view and add the button

- @conversations.each do |room|
	// ....
  = button_to 'Move to Trash', to_trash_path(id: room),
    class: 'btn btn-warning', method: :patch

Add route

patch :to_trash, to: 'conversations#to_trash', as: :to_trash

And action

def to_trash
  room = Denshobato::Conversation.find(params[:id])
  room.to_trash
  redirect_to :conversations
end

Add to index action our trashed conversations

def index
  @conversations = current_account.my_conversations
  @trash         = current_account.trashed_conversations
end

And back to our index view, add this under conversations.

- if @trash.any?
  h1 Trash
  - @trash.each do |room|
    = link_to "Conversation with #{interlocutor_info(room.recipient,
    :first_name, :last_name)}",
      conversation_path(room)
    = button_to 'Remove Conversation', conversation_path(room),
      method: :delete, class: 'btn btn-danger'
    = button_to 'Move from Trash', from_trash_path(id: room),
      class: 'btn btn-warning', method: :patch
    hr

As you can see, we add ‘Move from Trash button’

= button_to 'Move from Trash', from_trash_path(id: room),
  class: 'btn btn-warning', method: :patch

Define route for this action

patch :from_trash, to: 'conversations#from_trash', as: :from_trash

Add an action for it. As longs as we have two similar actions we can DRYing it by ruby define_method, like this.

%w(to_trash from_trash).each do |name|
  define_method name do
    room = Denshobato::Conversation.find(params[:id])
    room.send(name)
    redirect_to :conversations
  end
end

Great, it works!

BlackList

We have one last thing to do, it`s a blacklist. You can add model to your blacklist, blocked model can’t start conversation with you or send messages, and vice versa, if you want to send a message to this model, remove it from blacklist.

- @customers.each do |customer|
  = link_to customer.email, customer if current_account != customer
  - unless conversation_exists?(current_account, customer)
    - if can_create_conversation?(current_account, customer)
      - if user_in_black_list?(current_account, customer)
        p 'This user in your blacklist'
        = button_to 'Remove from black list',
          remove_from_blacklist_path(user: customer, klass: customer.class.name), class: 'btn btn-info'
      - else
        = button_to 'Add to black list',
          black_list_path(user: customer, klass: customer.class.name), class: 'btn btn-danger'
        = form_for @conversation, url: :conversations do |form|
          = fill_conversation_form(form, customer)
          = form.submit 'Start Conversation', class: 'btn btn-primary'
  hr

Looks terrible. Here is an advice for you: move some logic to helpers or decorators.

Okay, we added these two buttons - add to black list and remove, - and helpers to show or hide it.

- if user_in_black_list?(current_account, u)
  p 'This user in your blacklist'
  = button_to 'Remove from black list',
    remove_from_blacklist_path(user: u, klass: u.class.name), class: 'btn btn-info'
 - else
   = button_to 'Add to black list',
    black_list_path(user: u, klass: u.class.name), class: 'btn btn-danger'

Go to routes.rb and define routes for these actions.

post :black_list, to: 'blacklists#add_to_blacklist', as: :black_list
post :remove_from_blacklist, to: 'blacklists#remove_from_blacklist', as: :remove_from_blacklist

Create BlacklistsController

class BlacklistsController < ApplicationController
  [%w(add_to_blacklist save), %w(remove_from_blacklist destroy)].each do |name, action|
    define_method name do
      user   = params[:klass].constantize.find(params[:user])
      record = current_account.send(name, user)
      record.send(action) ? (redirect_to user.class.name.downcase.pluralize.to_sym) : (redirect_to :root)
    end
  end
end

Now you can create page with your blacklist. For example, page in your blacklists_controller

def blacklist
  @blacklist = current_account.my_blacklist
end

We use define_method again for very similar methods. Now, it looks as it should look. Feel free to customize it, e.g, add Mailer, when you send message. When reseller sends message to customer, we can also send a notification to customer’s email. Like a “You have a new message from John Doe…” etc. If something is unclear, read documentation on the Denshobato repo

Thanks for reading.