Real Time Rails Chat Application (Part 2)

Link phần 1: https://viblo.asia/raincatcher/posts/oOVlYEzal8W Trong phần này, chúng ta sẽ bắt đầu tạo conversation để gửi tin nhắn, kèm theo các chức năng close, minimum, ...

Bắt đầu 1 conversation

Thêm vào routes.rb

Rails.application.routes.draw do
  root 'home#index'

  devise_for :users

  resources :conversations, only: [:create]
end

Update conversation.rb

class Conversation < ApplicationRecord
  has_many :messages, dependent: :destroy
  belongs_to :sender, foreign_key: :sender_id, class_name: User
  belongs_to :recipient, foreign_key: :recipient_id, class_name: User

  validates :sender_id, uniqueness: { scope: :recipient_id }

  scope :between, -> (sender_id, recipient_id) do
    where(sender_id: sender_id, recipient_id: recipient_id).or(
      where(sender_id: recipient_id, recipient_id: sender_id)
    )
  end

  def self.get(sender_id, recipient_id)
    conversation = between(sender_id, recipient_id).first
    return conversation if conversation.present?

    create(sender_id: sender_id, recipient_id: recipient_id)
  end

  def opposed_user(user)
    user == recipient ? sender : recipient
  end
end

Chúng ta đã thêm 1 scope để trả về cuộc nói chuyện giữa 2 request user. Nếu cả 2 đã từng có cuộc trò chuyện, chúng ta sẽ lấy nó. Nếu không có sẽ tạo mới.

Thêm vào ConversationsController:

class ConversationsController < ApplicationController
  def create
    @conversation = Conversation.get(current_user.id, params[:user_id])
    
    add_to_conversations unless conversated?

    respond_to do |format|
      format.js
    end
  end

  private

  def add_to_conversations
    session[:conversations] ||= []
    session[:conversations] << @conversation.id
  end

  def conversated?
    session[:conversations].include?(@conversation.id)
  end
end

Trong create method, chúng ta lấy conversation giữa current user và requested user. Nếu trong session không được add conversation_id, chúng ta sẽ thêm nó vào, nếu có, ta respond với 1 file js

Update home.index.html dòng 18 từ:

<li><%= user.email %></li>

thành:

<li><%= link_to user.email, conversations_path(user_id: user), remote: true, method: :post %></li>

Tạo file conversations/create.js.erb:

var conversations = $('#conversations-list');
var conversation = conversations.find("[data-conversation-id='" + "<%= @conversation.id %>" + "']");

if (conversation.length !== 1) {
  conversations.append("<%= j(render 'conversations/conversation', conversation: @conversation, user: current_user) %>");
  conversation = conversations.find("[data-conversation-id='" + "<%= @conversation.id %>" + "']");
}

conversation.find('.panel-body').show();

var messages_list = conversation.find('.messages-list');
var height = messages_list[0].scrollHeight;
messages_list.scrollTop(height);

Đầu tiên, chúng ta tìm kiếm existing conversation window trong thẻ div ‘#conversations-list’. Nếu nó không tồn tại (length !== 1), chúng ta thêm 1 conversation mới vào ‘#conversations-list’. Sau đó, chúng ta gọi method .show() để hiển thị conversation’s window.

Cuối cùng, chúng ta kiểm tra height of the window và scroll xuống dưới cùng. Nếu chúng ta send message mới, chúng ta sẽ luôn ở dưới cùng của cuộc trò chuyện

Đây là kết quả:

Closing và minimizing 1 conversation

Update file routes.rb:

Rails.application.routes.draw do
  root 'home#index'

  devise_for :users

  resources :conversations, only: [:create] do
    member do
      post :close
    end
  end
end

Thay dòng thứ 5 trong file _converastion.html.erb thành:

<%= link_to "x", close_conversation_path(conversation), class: "btn btn-default btn-xs pull-right", remote: true, method: :post %>

Thêm vào Conversations controller:

class ConversationsController < ApplicationController
  ...

  def close
    @conversation = Conversation.find(params[:id])

    session[:conversations].delete(@conversation.id)

    respond_to do |format|
      format.js
    end
  end

  ...
end

Add file close.js.erb

$('#conversations-list').find("[data-conversation-id='" + "<%= @conversation.id %>" + "']").parent().remove();

Thêm method để minimizing 1 window:

//= require jquery
//= require jquery_ujs
//= require_tree .

(function() {
  $(document).on('click', '.toggle-window', function(e) {
    e.preventDefault();
    var panel = $(this).parent().parent();
    var messages_list = panel.find('.messages-list');

    panel.find('.panel-body').toggle();
    panel.attr('class', 'panel panel-default');

    if (panel.find('.panel-body').is(':visible')) {
      var height = messages_list[0].scrollHeight;
      messages_list.scrollTop(height);
    }
  });
})();

Gửi 1 tin nhắn Edit file routes.rb

resources :conversations, only: [:create] do
    ...
    resources :messages, only: [:create]
end

Thêm form vào _converastion.html.erb sau ‘.message-list’

<li>
  <div class="panel panel-default" data-conversation-id="<%= conversation.id %>">
    <div class="panel-heading">
      <%= link_to conversation.opposed_user(user).email, '', class: 'toggle-window' %>
      <%= link_to "x", close_conversation_path(conversation), class: "btn btn-default btn-xs pull-right", remote: true, method: :post %>
    </div>

    <div class="panel-body" style="display: none;">
      <div class="messages-list">
        <ul>
          <%= render 'conversations/conversation_content', messages: conversation.messages, user: user %>
        </ul>
      </div>
      <%= form_for [conversation, conversation.messages.new], remote: true do |f| %>
        <%= f.hidden_field :user_id, value: user.id %>
        <%= f.text_area :body, class: "form-control" %>
        <%= f.submit "Send", class: "btn btn-success" %>
      <% end %>
    </div>
  </div>
</li>

Thêm MessagesController

class MessagesController < ApplicationController
  def create
    @conversation = Conversation.includes(:recipient).find(params[:conversation_id])
    @message = @conversation.messages.create(message_params)

    respond_to do |format|
      format.js
    end
  end

  private

  def message_params
    params.require(:message).permit(:user_id, :body)
  end
end

Tạo file create.js.erb

var conversation = $('#conversations-list').find("[data-conversation-id='" + "<%= @conversation.id %>" + "']");
conversation.find('.messages-list').find('ul').append("<%= j(render 'messages/message', message: @message, user: current_user) %>");
conversation.find('textarea').val('');

Thêm css vào application.scss

.messages-list {
  max-height: 200px;
  overflow-y: auto;
  overflow-x: hidden;
}


.message-sent {
  position: relative;
  background-color: #D9EDF7;
  border-color: #BCE8F1;
  margin: 5px 20px;
  padding: 10px;
  float: right;
}


.message-received {
  background-color: #F1F0F0;
  border-color: #EEEEEE;
  margin: 5px 20px;
  padding: 10px;
  float: left;
}

Và đây là sản phẩm:


All Rights Reserved