9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Railsで作成した1対1のメッセージ機能をコードリーディング

Last updated at Posted at 2020-01-22

はじめに

Railsで1対1のメッセージ機能を実装したアプリをコードリーディングします。Qiita初投稿のプログラミング初心者です!温かい目で見てください涙

開発環境

ruby: 2.6.5
rails: 5.2.4.1
postgresqlを使用

設計

  • テキスト形式で会話(メッセージのやり取り)ができる
  • 会話を取っている相手間でのみ情報が公開される
  • 会話を取っている相手間のメンバー毎に「会話ひとつ」が定義される

要件

  • ユーザ一覧、あるいはユーザ個別の画面からメッセージ画面を表示できる
  • メッセージ画面では相手と自分の名前が表示される
  • メッセージ画面には個々のメッセージの発言者と内容が時系列で表示される
  • メッセージ一覧リンクから過去に自分がやりとりしたメッセージすべてを見ることができる

テーブルの仕様スクリーンショット 2020-01-22 13.21.26.png

このアプリはログインできるUser機能を持ったアプリケーションに機能を追加して行く形で実装して行きました。

コード内容

route

config/route.rb
Rails.application.routes.draw do
  # 省略
  resources :conversations do
    resources :messages
  end
end

controller

app/controllers/conversations_controller.rb
class ConversationsController < ApplicationController
  before_action :authenticate_user
  def index
    @conversations = Conversation.all
  end

  def create
    if Conversation.between(params[:sender_id], params[:recipient_id]).present?
      @conversation = Conversation.between(params[:sender_id], params[:recipient_id]).first
    else
      @conversation = Conversation.create!(conversation_params)
    end
    redirect_to conversation_messages_path(@conversation)
  end

  private

  def conversation_params
    params.permit(:sender_id, :recipient_id)
  end
end
app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  before_action do
    @conversation = Conversation.find(params[:conversation_id])
  end

  def index
    @messages = @conversation.messages

    if @messages.length > 10
      @over_ten = true
      @messages = Message.where(id: @messages[-10..-1].pluck(:id))
    end

    if params[:m]
      @over_ten = false
      @messages = @conversation.messages
    end

    if @messages.last
      @messages.where.not(user_id: current_user.id).update_all(read: true)
    end

    @messages = @messages.order(:created_at)
    @message = @conversation.messages.build
  end

  def create
    @message = @conversation.messages.build(message_params)
    if @message.save
      redirect_to conversation_messages_path(@conversation)
    else
      render 'index'
    end
  end

  private

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

model

app/models/conversation.rb
class Conversation < ApplicationRecord
  belongs_to :sender, foreign_key: :sender_id, class_name: 'User'
  belongs_to :recipient, foreign_key: :recipient_id, class_name: 'User'

  has_many :messages, dependent: :destroy

  validates_uniqueness_of :sender_id, scope: :recipient_id

  scope :between, -> (sender_id,recipient_id) do
    where("(conversations.sender_id = ? AND conversations.recipient_id =?) OR (conversations.sender_id = ? AND  conversations.recipient_id =?)", sender_id,recipient_id, recipient_id, sender_id)
  end

  def target_user(current_user)
    if sender_id == current_user.id
      User.find(recipient_id)
    elsif recipient_id == current_user.id
      User.find(sender_id)
    end
  end
end
app/models/message.rb
class Message < ApplicationRecord
  belongs_to :conversation
  belongs_to :user

  validates_presence_of :body, :conversation_id, :user_id

  def message_time
    created_at.strftime("%m%d%y at %l:%M %p")
  end
end

view

app/views/conversations/index.html.erb
<table class="table table-hover">
  <thead>
    <h2>メッセージ一覧</h2>
  </thead>
  <tbody>
    <% @conversations.each do |conversation| %>
      <td>
        <% if conversation.target_user(current_user).present? %>
          <%= link_to conversation.target_user(current_user).name, conversation_messages_path(conversation)%>
        <% end %>
      </td>
    <% end %>
  </tbody>
</table>
app/views/messages/index.html.erb
<% if @over_ten %>
  <%= link_to '以前のメッセージ', '?m=all' %>
<% end %>

<div class="ui segment">
  <% @messages.each do |message| %>
    <% if message.body.present? %>
      <div class="item">
        <div class="content">
          <div class="header"><strong><%= message.user.name %></strong> <%= message.message_time %></div>
          <div class="list">
            <div class="item">
              <i class="right triangle icon"></i>
              <%= message.body %> /
              <% if message.user == current_user %>
                <%= message.read ? '既読' : '未読' %>
              <% end %>
            </div>
          </div>
        </div>
      </div>
    <% end %>
  <% end %>
</div>

<%= form_with(model: [@conversation, @message]) do |f| %>
  <div class="field">
    <%= f.text_area :body, class: "form-control" %>
  </div>
    <%= f.text_field :user_id, value: current_user.id, type: "hidden"  %>
  <div>
    <%= f.submit "メッセージを送る" %>
  </div>
<% end %>

コードリーディング

それではコードリーディングしてきます。
わかりにくかった部分を記載します。

わからなかった部分のコードその1

app/models/conversation.rb
scope :between, -> (sender_id,recipient_id) do
  where(["(conversations.sender_id = ? AND conversations.recipient_id = ?) OR (conversations.sender_id = ? AND conversations.recipient_id = ?)", sender_id ,recipient_id, recipient_id, sender_id])
end
app/controllers/conversations_controller.rb
def create
  if logged_in?
    if Conversation.between(params[:sender_id], params[:recipient_id]).present?
      @conversation = Conversation.between(params[:sender_id], params[:recipient_id]).first
    else
      @conversation = Conversation.create!(conversation_params)
    end
    redirect_to conversation_messages_path(@conversation)
  else  
    redirect_to root_path
  end 

# 省略
  private  

  def conversation_params
    params.permit(:sender_id, :recipient_id)
  end 
end

下記のように理解しました。その1

まずログインしているか確認して(ログインしてなかったらルートパスへ)、

if Conversation.between(params[:sender_id], params[:recipient_id]).present?
      @conversation = Conversation.between(params[:sender_id], params[:recipient_id]).first
    else
      @conversation = Conversation.create!(conversation_params)
    end

このコードでConversationテーブルに、送り主(メッセージボタンを押した人、ログインユーザー)の idにsender_idがあって、かつ、受け取り主(メッセージボタンを押された人)のidにrecipient_idがある、もしくは受け取り主のidにsender_idがあって、かつ、送り主のidにrecipient_idがある(つまり、チャットルームがある)場合とそうでない場合で分ける。

チャットルームがある場合、
そのチャットルームに必要な情報が紐づいている、Conversationの1レコードの情報を@conversationに代入。
なぜfirstクエリメソッドを使っているかというと、Conversationに「sender_idに送り主のid、recipient_idに受け取り主のid」の組み合わせと「sender_idに受け取り主のid、recipient_idに送り主のid」の組み合わせの2つパターンどちらかが存在するから。(2つは同じチャットルーム)

チャットルームがない場合は、Conversationに新規でsender_idrecipient_idに送り主と送り主のidで作成し、@conversationに代入。

そして、redirect_to conversation_messages_path(@conversation)@conversationとともにmessageのコントローラ、indexアクションに飛ぶ。

わからなかった部分のコードその2

app/controllers/messages_controller.rb
def index
  @messages = @conversation.messages


  if @messages.length > 10
    @over_ten = true
    @messages = Message.where(id: @messages[-10..-1].pluck(:id))
  end


  if params[:m]
    @over_ten = false
    @messages = @conversation.messages
  end


  if @messages.last
    @messages.where.not(user_id: current_user.id).update_all(read: true)
  end


  @messages = @messages.order(:created_at)
  @message = @conversation.messages.build
end

下記のように理解しました。その2

@messages = @conversation.messages

@messagesにconversations#createから送られてきた@conversation(そのチャットルーム)に、関連された全メッセージを代入。

if @messages.length > 10
    @over_ten = true
    @messages = Message.where(id: @messages[-10..-1].pluck(:id))
  end

もし、@messagesが10個より多かった場合、
10より大きいというフラグを有効にする。
@messages[-10..-1]で10件のみメッセージ情報を取得、しかしこの書き方だとArrayクラスになり、whereクエリメソッドなどが使えないため、pluckで一度配列でidのみを受けり、その配列をもとにwhereで検索して@messagesに代入。ちなみにmessages[-10..-1]は後ろから10番目と後ろから1番目の間のメッセージ情報という意味。

if @messages.last
    @messages.where.not(user_id: current_user.id).update_all(read: true)
  end

最新(最後)のメッセージが存在している場合、ログインユーザー以外のメッセージを全て抽出して、readカラムをtrueにする(既読にする)。ログインユーザーはread: falseのままで、チャット相手がログインしてmessages#indexに飛んでくれないとread: trueにならない。

@message = @conversation.messages.build

そのチャットルームに関連された、messageのインスタンスを生成。

わからなかった部分のコードその3

app/views/messages/index.html.erb
<% if @over_ten %>
  <%= link_to '以前のメッセージ', '?m=all' %>
<% end %>

<div class="ui segment">
  <% @messages.each do |message| %>
    <% if message.body.present? %>
      <div class="item">
        <div class="content">
          <div class="header"><strong><%= message.user.name %></strong> <%= message.message_time %></div>
          <div class="list">
            <div class="item">
              <i class="right triangle icon"></i>
              <%= message.body %> /
              <% if message.user == current_user %>
                <%= message.read ? '既読' : '未読' %>
              <% end %>
            </div>
          </div>
        </div>
      </div>
    <% end %>
  <% end %>
</div>

<%= form_with(model: [@conversation, @message]) do |f| %>
  <div class="field">
    <%= f.text_area :body, class: "form-control" %>
  </div>
    <%= f.text_field :user_id, value: current_user.id, type: "hidden"  %>
  <div>
    <%= f.submit "メッセージを送る" %>
  </div>
<% end %>

下記のように理解しました。その3

<% if @over_ten %>
  <%= link_to '以前のメッセージ', '?m=all' %>
<% end %>

このメッセージ数が10個以上の場合、以前のメッセージのリンクが表示され、クリックすると/conversations/:id/messages?m=allが送られ、

app/controllers/messages_controller.rb
if params[:m]
    @over_ten = false
    @messages = @conversation.messages
  end

の処理が実行される。なお、allの部分は任意の文字でよくて、とにかくmに変数を入れてあげればいい。

app/model/conversation.rb
def target_user(current_user)
    if sender_id == current_user.id
      User.find(recipient_id)
    elsif recipient_id == current_user.id
      User.find(sender_id)
    end
  end

ログインユーザーに関連している他のユーザーの情報を取得するメソッド。
ログインユーザーがチャットルームを作成したパターンと、ログインユーザーの相手がチャットルームを作成したパターンのどちらかで存在するから、どちらにも対応できるようになっている。

バリデーションに引っかからない、エラーを吐かない場合の全体の流れ 

user/index
↓メッセージボタン
conversations_controller(create)
↓redirect_to
massage_controller(index)

massages/index ※
↓メッセージを送るボタン
massages_controller(create)

※に行く

以上です。

9
9
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?