相互フォロー間のユーザー同士でDMを送る方法

忘備録、並びに考え方の整理として投稿します
初学者なので効率的なコードの書き方などは出来ていないかと思います。

今回の目的
相互フォローをしているユーザー間でDMを送り合いたい


前提条件
ruby 3.1.2
Rails 6.1.7
device、bootstrap導入済み(bootstrapは見た目の問題なので機能には関係なし)
User及びフォロー・フォロワー機能作成済み



ER図は下記のようになります
作成するのはこの内**UserRoom**, **Room**, **Chat**の3つ
<img width="576" alt="ER図_8a.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950107/354a0d0f-72a7-b16e-e0d8-427e3e7d9c68.png">


# 1.モデルの作成
integer型か、既存のテーブルを参照するreferences型のどちらかで記述
references型では若干書き方が違う(　〜_idが不要になる)ので注意
```ターミナルの書き方.rb
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を作成
```ターミナルの書き方.rb
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.アソシエーションの設定
１人のユーザーはたくさんの投稿が出来るので
ユーザー：チャット＝１対N

1人のユーザーはたくさんのRoomに所属できる
1つのRoomは２人のユーザーを持つ
ユーザー：ルーム＝多対多の関係
今回はuser_roomを中間テーブルとします
よって下記のように記載

```user.rb
class User < ApplicationRecord
  #下記を追記
  has_many :user_rooms
  has_many :rooms, through: :user_rooms
  has_many :chats
end
```
```room.rb
class Room < ApplicationRecord
  has_many :user_rooms
  has_many :chats
end
```
```user_room.rb
class UserRoom < ApplicationRecord
  #referencesで書いた場合はすでに入力されているかと思います
  belongs_to :user
  belongs_to :room
end
```

```chat.rb
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
```
```routes.rb
Rails.application.routes.draw do
  get 'chats/show' #←削除する
  resources :chats, only: [:show, :create] #←こちらを記述 
end
```
## 相互フォローの確認
```chats_controller.rb
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が１、相手のuser_idが2である時　＝　true
・自分のuser_idが１、相手のuser_idが3である時 = false ※自分しかフォローしていない
・自分のuser_idが１、相手のuser_idが4である時 = false ※相手しかフォローしていない
<img width="576" alt="Relationshipテーブル.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950107/734338f1-133f-8a06-5d65-2daf597e0a2d.png">
但し、通常のifとは違い今回は **unless** なので、falseの時に本の一覧画面にリダイレクトします。

**※following?はフォロー・フォロワー機能作成時に記述した中間テーブル及びメソッドです。**
名称は違うかもしれません、各自確認ください。
```user.rb
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の作成
```chats_controller.rb
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　を取得
<img width="576" alt="user_room.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950107/8737f68d-6e3f-3bf4-8a8f-a54afa3f60fa.png">
**user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms)**
上記のroomsと最初に取得してきた相手のuser_idを用いて合致するUserRoomが有るか探す
例：取得した相手のidが2であった場合、user_id = 2 かつroom_idが 1or4 であるものを探す。この場合は無し
<img width="576" alt="user_room.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950107/10bfce5e-1ca2-60f1-dfc4-155e77d92bb3.png">
上の図のように合致するものがない場合
・新しく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の作成
```chats_controller.rb
  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の作成
```show.html.rb
<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の設定はお好みでどうぞ
完成形はこんな形になります
ちなみに送信ボタンは削除しても問題ありません。エンターで送信されます。
<img width="576" alt="user_room.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2950107/139e858f-2b49-fac1-9614-1228c6cecb30.png">

## チャットを開始するボタンの設置
元々作成してあるフォロー・フォロワーボタンの部分に追加
```_info.html.rb
<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"を記述しています。

```show.html.rb
<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の内容を移す
```_index.html.rb
<% 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.html.rb
<%= 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の内容を取得
を追加します
```chats_controller.rb
  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('');は空欄にする処理
```create.js.rb
$("#js_chat_list").html("<%= j(render 'chats/index', chats: @chats) %>");
$(".message").val('');
```

# 最後に
長々と書きましたがご覧いただいた方、ありがとうございました
意見、感想等ありましたらご自由にご記入ください