はじめに
Railsで1対1のメッセージ機能を実装したアプリをコードリーディングします。Qiita初投稿のプログラミング初心者です!温かい目で見てください涙
開発環境
ruby: 2.6.5
rails: 5.2.4.1
postgresqlを使用
設計
- テキスト形式で会話(メッセージのやり取り)ができる
- 会話を取っている相手間でのみ情報が公開される
- 会話を取っている相手間のメンバー毎に「会話ひとつ」が定義される
要件
- ユーザ一覧、あるいはユーザ個別の画面からメッセージ画面を表示できる
- メッセージ画面では相手と自分の名前が表示される
- メッセージ画面には個々のメッセージの発言者と内容が時系列で表示される
- メッセージ一覧リンクから過去に自分がやりとりしたメッセージすべてを見ることができる
テーブルの仕様
このアプリはログインできるUser機能を持ったアプリケーションに機能を追加して行く形で実装して行きました。
コード内容
route
Rails.application.routes.draw do
# 省略
resources :conversations do
resources :messages
end
end
controller
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
class MessagesController < ApplicationController
before_action do
@conversation = Conversation.find(params[:conversation_id])
end
def index
@messages = @conversation.messages
if @messages.length > 10
@over_ten = true1
@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
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
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
<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>
<% 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
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 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_id
とrecipient_id
に送り主と送り主のidで作成し、@conversation
に代入。
そして、redirect_to conversation_messages_path(@conversation)
で@conversation
とともにmessageのコントローラ、indexアクションに飛ぶ。
わからなかった部分のコードその2
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
<% 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が送られ、
if params[:m]
@over_ten = false
@messages = @conversation.messages
end
の処理が実行される。なお、allの部分は任意の文字でよくて、とにかくmに変数を入れてあげればいい。
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)
↓
※に行く
以上です。