Theory of Coding

Web Development blog

In-App Messaging

Building a Messaging System

After trying to deploy to heroku with an in-app messaging system with the mailboxer gem, the messaging system seemed to be broken. After trying to fixing it for 4 hours, with a lack of documentation on it on the internet, I decided to build my own messaging app from scratch.

This messaging system was built for my project, CampusBazaar, which allows users to message sellers about items to ask questions or to pick places to meet up on campus. Just thinking about how to organize the app was the most difficult part of the app. Knowing how the messaging app is associated with other parts of the app is important.

For my purpose, I wanted a messaging system that knows about the item that the sender is inquiring about. Any time someone inquires about an item, a new conversation is started between the two users. If the user inquires about another item from the same user, there will be a separate conversation, because it solely based on the item.

Basically, this is what should happen You will click on Contact seller button on the item show page Imgur

Then, a new conversation will be started between you two. This means the message form will be nested inside the conversation form Imgur

Lastly, this will be the messaging app containing all your conversations, and you can see all the messages that belong to that particular conversation. Imgur

To start, we should make a migration for conversations. Essentially, there will be a conversation between two users, and it is connected to an item. This is basically a join table.

1
2
3
4
5
6
7
8
9
10
class CreateConversation < ActiveRecord::Migration
  def change
    create_table :conversations do |t|
      t.integer :user1_id
      t.integer :user2_id
      t.integer :item_id
      t.timestamps null: false
    end
  end
end

Here, we have the messages migration. There will be a sender_id and recipient_id and not a user1_id and a user2_id like in the conversation migration because it matters who is the sender or not, whereas in the conversation migration, it is just two people talking to each other, regardless of who sent the message first, or who is the sender or recipient. Each messsage will belong to a conversation, hence the conversation_id.

1
2
3
4
5
6
7
8
9
10
11
12
class CreateMessage < ActiveRecord::Migration
  def change
    create_table :messages do |t|
      t.integer :sender_id
      t.integer :recipient_id
      t.integer :conversation_id
      t.text :content
      t.boolean :read, :default => false
      t.timestamps null: false
    end
  end
end

Now for the associations. This is the conversation model. It will belong to a item, the users that are talking to each other, and has many messages. It belongs to a user with a foreign key of class name ‘User’ so that we can do ask the console about the user of that conversation when doing Conversation.user1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Conversation < ActiveRecord::Base
  belongs_to :item
  belongs_to :user1, :foreign_key => :user1_id, :class_name => 'User'
  belongs_to :user2,:foreign_key => :user2_id, :class_name => 'User'
  has_many :messages

#self is the conversation itself, from the message form @conversation
  def recipient(current_user)
    if (current_user.id == self.user1_id)
      self.user2_id
    else (current_user.id == self.user2_id)
      self.user1_id
    end
  end
end

This isthe messsage associations. It will belong to a conversation, also belong to a recipient and sender, associating that with the User class like in the Conversation model with foreign keys. The message will also belong to a sender. There will be many people inquiring about an item, but there will only be one buyer, who is actually going to buy the item.

1
2
3
4
5
6
class Message < ActiveRecord::Base
  belongs_to :conversation
  belongs_to :recipient, :foreign_key => :recipient_id,:class_name => 'User'
  belongs_to :sender, :foreign_key => :sender_id, :class_name => 'User'

end

Remember that if you have a belongs_to association, the counterpart will have a has_many associtation. A user will have many items and conversations as user1 or user2.

1
2
3
4
5
6
class User < ActiveRecord::Base
  belongs_to :community
  has_many :items, :foreign_key => 'seller_id'
  has_many :offers_sent, :class_name => "Offer", :foreign_key => 'buyer_id'
  has_many :conversations, :foreign_key => 'user1_id'
  has_many :conversations, :foreign_key => 'user2_id'

Finally, the item will have many conversations, because the conversation also belongs to an item. There will be many inquiries from potential buyers but there will just be one buyer.

1
2
3
4
5
6
7
8
9
class Item < ActiveRecord::Base
  attr_accessor :delete_product, :delete_avatar

  belongs_to :seller, :class_name => 'User'
  delegate :community, to: :seller
  has_many :item_categories
  has_many :categories, through: :item_categories
  has_many :offers
  has_many :conversations

Let’s not forget about our routes. There will be messages nested under routes, but only the GET request of index and POST of create will be needed because we will show all the messages for that particular conversation, and we need to create new messages. There will be no need of other routes such as the Show method because we will not be showing each individual message.

1
2
3
  resources :conversations do
    resources :messages, only: [:index, :create]
  end

Now, understanding that we will click Contact Seller from the items page, it will do a GET request to go to the new_conversation_path to see the form to create a new conversation. We will nest the message form inside the conversation form because upon creation of a conversation, there should be a message.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class ConversationsController < ApplicationController

  def index
    @users = User.all
    @conversations = Conversation.all
  end

  def create
    @conversation = Conversation.new(conversation_params)
    @conversation.save
    @message = Message.new
    @message.conversation_id = @conversation.id
    @message.recipient_id = @conversation.user2_id
    @message.sender_id = @conversation.user1_id
    @message.content = params[:conversation][:messages][:content]
    @message.save
    redirect_to conversations_path
  end

  def new
    #binding.pry
    @item = Item.find(params[:item_id])
  end

private
  def conversation_params
    params.require(:conversation).permit(:user1_id, :user2_id, :item_id, :content)
  end

end

This is the form. I added hidden fields to pass into private params in th controller. Once the form is submitted, it will send a POST request to the server and do its thing at the def create in the Conversation Controller. The params will be set in the new instance Conversation and saved, and a new message will also be created with all the passed in information.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%= form_for Conversation.new do |f| %>
  Seller: <%= @item.seller.name%><br>
  Buyer: <%= current_user.name %><br>

  Message for item: <%= @item.name %><br />

  <%= f.hidden_field :item_id, :value => @item.id%>
  <%= f.hidden_field :user2_id, :value => @item.seller.id%>
  <%= f.hidden_field :user1_id, :value => current_user.id%>
#this is the nested form
  <%= f.fields_for :messages do |message_form| %>
  <%= message_form.label :content %>
  <%= message_form.text_area :content, :rows => "5", placeholder: 'Write your message here'%><br />
  <% end %>
  <%= f.submit 'Send Message' %>
<% end %>

Once that conversation is created, it will redirect to the def index, to see all their conversations they ever had. If they clicked on a conversation, it will basically send a GET request to the Messages Controller to give back the index page of the messages pertaining to that conversation, given that the id of the conversation is passed in.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MessagesController < ApplicationController

  def index
    @conversation = Conversation.find(params[:conversation_id])
    @recipient = @conversation.recipient(current_user)
    @messages = @conversation.messages
    respond_to do |format|
        format.html {}
        format.js {}
      end
  end

  def create
    @message = Message.new(message_params)
    @message.save
    redirect_to conversation_messages_path
  end

private
  def message_params
    params.require(:message).permit(:conversation_id, :recipient_id, :sender_id, :content)
  end

end

Where it’s actually going is /conversations/:id/messages, so params will have the id of the conversation. Javascript was used to render the partials so that when you click on the conversation, it will allow ajax to render the messages, without the page ever refreshing.

Imgur