忘備録、並びに考え方の整理として投稿します
初学者なので効率的なコードの書き方などは出来ていないかと思います。
今回の目的
相互フォローをしているユーザー間でDMを送り合いたい
前提条件
ruby 3.1.2
Rails 6.1.7
device、bootstrap導入済み(bootstrapは見た目の問題なので機能には関係なし)
User及びフォロー・フォロワー機能作成済み
ER図は下記のようになります
作成するのはこの内UserRoom, Room, Chatの3つ
1.モデルの作成
integer型か、既存のテーブルを参照するreferences型のどちらかで記述
references型では若干書き方が違う( 〜_idが不要になる)ので注意
rails g model Room
rails g model UserRoom user_id:integer room_id:integer
rails g model Chat user_id:integer room_id:integer message:text
references書く場合はこちら
referencesでRoomを参照するために先にRoomを作成
rails g model Room
rails g model UserRoom user:references room:references
rails g model Chat user:references room:references message:text
完了したらmigrateも忘れずに
rails db:migrate
2.アソシエーションの設定
1人のユーザーはたくさんの投稿が出来るので
ユーザー:チャット=1対N
1人のユーザーはたくさんのRoomに所属できる
1つのRoomは2人のユーザーを持つ
ユーザー:ルーム=多対多の関係
今回はuser_roomを中間テーブルとします
よって下記のように記載
class User < ApplicationRecord
#下記を追記
has_many :user_rooms
has_many :rooms, through: :user_rooms
has_many :chats
end
class Room < ApplicationRecord
has_many :user_rooms
has_many :chats
end
class UserRoom < ApplicationRecord
#referencesで書いた場合はすでに入力されているかと思います
belongs_to :user
belongs_to :room
end
class Chat < ApplicationRecord
belongs_to :user
belongs_to :room
validates :message, presence: true, length: {maximum: 140}
#validatesで、空の投稿と、文字の制限
end
3.コントローラの作成、ルーティング
rails rails g controller chats show
Rails.application.routes.draw do
get 'chats/show' #←削除する
resources :chats, only: [:show, :create] #←こちらを記述
end
相互フォローの確認
class ChatsController < ApplicationController
before_action :following_check, only: [:show]
def show
end
private
def following_check
user = User.find(params[:id])
unless current_user.following?(user) && user.following?(current_user)
redirect_to books_path
end
end
end
before_action でshowページにアクセスしたときのみfollowing_checkを行います。
current_user.following?(user) && user.following?(current_user)とは?
自分はUser.findで探してきたidのユーザーをフォローしているか、かつUser.findで探してきたidのユーザーは自分をフォローしているかを確認する
例として下記のようなRelationshipテーブルがあった時
・自分のuser_idが1、相手のuser_idが2である時 = true
・自分のuser_idが1、相手のuser_idが3である時 = false ※自分しかフォローしていない
・自分のuser_idが1、相手のuser_idが4である時 = false ※相手しかフォローしていない
但し、通常のifとは違い今回は unless なので、falseの時に本の一覧画面にリダイレクトします。
※following?はフォロー・フォロワー機能作成時に記述した中間テーブル及びメソッドです。
名称は違うかもしれません、各自確認ください。
has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
has_many :followings, through: :active_relationships, source: :followed
def following?(user)
followings.include?(user)#followingsは上記のfollowingsを指す
end
showの作成
def show
@user = User.find(params[:id])#① 送られてきたidでUserを検索
rooms = current_user.user_rooms.pluck(:room_id) #② current_userの持つroom_idを取得
user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms) #③ ①と②を用いて合致するUserRoomが有るか探す
if user_rooms.nil? # ③がnil、つまり見つからなかった場合
chat_room = Room.new # 新しくRoomを作成
chat_room.save
UserRoom.create(user_id: current_user.id, room_id: chat_room.id)
UserRoom.create(user_id: @user.id, room_id: chat_room.id)
# 新しくUserRoomを作成(createは保存も同時に行われるのでsave不要)
else
chat_room = user_rooms.room #user_roomのroomの情報を抜き出す = roomのidを取得
#roomはuserモデルで記載したhas_many :room, through: :user_rooms のroom
end
@chats = @room.chats #roomのidに合致するchatの内容を取得する
#chatsはuserモデルで記載したhas_many :chats のchats
@chat = Chat.new(room_id: chat_room.id) #空のインスタンスの作成
end
解説
rooms = current_user.user_rooms.pluck(:room_id)
自分のuser_idと一致するUserRoomのroom_idを取得
例:自分のidが1であった場合、user_id = 1と一致する room_id 1、4 を取得
user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms)
上記のroomsと最初に取得してきた相手のuser_idを用いて合致するUserRoomが有るか探す
例:取得した相手のidが2であった場合、user_id = 2 かつroom_idが 1or4 であるものを探す。この場合は無し
上の図のように合致するものがない場合
・新しくRoomを作成
・save (saveしないとRoomのidが発行されない為)
・Roomのidを用いてUserRoomを新規作成となる
chat_room = user_rooms.room
roomの情報を取得する。
例:rails consoleで確認すると以下のように表示される
[2] pry(main)>chat_room = user_room.room
id: 1,created_at:以下略
@chats = chat_room.chats
先程取得したchat_roomと合致するchatテーブルの情報を持ってくる
例:上記のrails consoleの続き。chat_roomで取得したid、つまりroom_id:1のデータを取得
[3] pry(main)>chat_room.chats
id: 6,
user_id: 2,
room_id: 1,
message: "hello",
created_at:以下略
id: 7,
user_id: 2,
room_id: 1,
message: "good",
created_at:以下略
createの作成
def create
@chat = current_user.chats.new(chat_params)
@chat.save
redirect_to request.referer
end
private
def chat_params
params.require(:chat).permit(:message, :room_id)
end
def following_check
#以下略
DMの投稿は常に自分であるためcurrent_user.chats.newになる
最初に設定したストロングパラメータの設定も忘れずに
4.viewの作成
<div class="container">
<div class ="row">
<div class="col-8">
<div class="d-flex justify-content-between">
<h5 class="text-left">ユーザー名: <%= @user.name %></h5>
<h5 class="text-right">ユーザー名: <%= current_user.name %></h5>
</div>
<% @chats.each do |chat| %>
<% if chat.user_id == current_user.id %>
<p class="text-right"><%= chat.message %></p>
<% else %>
<p class="text-left"><%= chat.message %></p>
<% end %>
<% end %>
<%= form_with model: @chat do |f| %>
<%= f.text_field :message %>
<%= f.hidden_field :room_id %>
<%= f.submit "送信" %>
<% end %>
</div>
</div>
</div>
bootstrapの設定はお好みでどうぞ
完成形はこんな形になります
ちなみに送信ボタンは削除しても問題ありません。エンターで送信されます。
チャットを開始するボタンの設置
元々作成してあるフォロー・フォロワーボタンの部分に追加
<div class ="mb-4">
<% unless current_user == user %>
<% if current_user.following?(user) %>
<%= link_to "フォロー外す",user_relationship_path(user.id), method: :delete, class: "btn btn-info" %>
<% else %>
<%= link_to "フォローする",user_relationships_path(user.id), method: :POST, class: "btn btn-success" %>
<% end %> #ここから下
<% if current_user != user && current_user.following?(user) && user.following?(current_user) %>
<%= link_to 'chatを始める', chat_path(user.id) %>
<% end %> #ここまで
<% end %>
</div>
補足:非同期化の場合
イマイチ理解が足りてません。一応問題なく動いてはいますが、違う方法等有りましたらコメント頂けると幸いです。
まずshowページの一部部分テンプレート化。
この時chats/indexのdivに、id = "js_chat_list"を記述しています。
<div class="container">
<div class ="row">
<div class="col-8">
<div class="d-flex justify-content-between">
<h5 class="text-left">ユーザー名: <%= @user.name %></h5>
<h5 class="text-right">ユーザー名: <%= current_user.name %></h5>
</div>
<div id = "js_chat_list"> #ここから、idも記述
<%= render 'chats/index', chats: @chats %>
</div>
<div>
<%= render 'chats/form', chat: @chat %>
</div> #ここまで
</div>
</div>
</div>
app/views/chats以下に_indexと_formのファイルを作成してshowの内容を移す
<% chats.each do |chat| %>
<% if chat.user_id == current_user.id %>
<p class="text-right"><%= chat.message %></p>
<% else %>
<p class="text-left"><%= chat.message %></p>
<% end %>
<% end %>
_formにはlocal: false、text_fieldにclassを設定しています。
<%= form_with model: @chat, local: false do |f| %> #local:false追加
<%= f.text_field :message, class:'message' %> #class:'messege'追加
<%= f.hidden_field :room_id %>
<%= f.submit "送信" %>
<% end %>
chats_controllerではredirect_toを削除、こうすることでcreate.js.erbを探しに行ってくれます
またrender処理と同じように値を渡してあげなければならないため
chat_room = @chat.room ←roomのidを取得
@chats = chat_room.chats ←取得したroomのidを元にchatの内容を取得
を追加します
def create
@chat = current_user.chats.new(chat_params)
@chat.save
# redirect_to request.referer ←削除
chat_room = @chat.room #追加
@chats = chat_room.chats #追加
end
最後にcreate.js.erbの書き方
記述場所はapp/views/chats/create.js.erbつまりchatsのviewファイル内になります
js_chat_listはshowのdivに記述した、id = "js_chat_list"のこと
また.messageは_formのext_fieldに設定したclass名、val('');は空欄にする処理
$("#js_chat_list").html("<%= j(render 'chats/index', chats: @chats) %>");
$(".message").val('');
最後に
長々と書きましたがご覧いただいた方、ありがとうございました
意見、感想等ありましたらご自由にご記入ください