【Rails】非同期通信でチャット/DM機能を実装する

ユーザー同士1対1のチャット機能を非同期通信（Ajax）で実装する方法をまとめています！
お気づきの点などあれば、コメント頂戴できれば幸いです。

#前提

- deviseでUserモデルを作成している
- 各ユーザーの詳細画面(show）が作成されている（ユーザーの詳細画面にチャットへのリンクを作ります）

　👆以上の前提で進めていきます

###使用するモデル

- User = ユーザーの定義
- Chat = チャットの定義
- Room = チャットルームの定義
- UserRoom = ユーザーとチャットルームの関連付けの定義（中間テーブル）

###大まかな流れ

1. モデル生成とアソシエーション
1. コントローラー作成 & ルーティング設定
1. ```chats_controller```に記述
1. チャットのビューを記述
1. ```create.js.erb```ファイルを作成＆記述


#STEP1: モデル生成とアソシエーション

まずUserモデル以外に必要なモデルを作成していきましょう！

```:terminal
$ rails g model Room
$ rails g model Chat user_id:integer room_id:integer message:text
$ rails g model UserRoom user_id:integer room_id:integer

$ rails db:migrate
```

モデルが生成できたら、アソシエーションを設定します。
（それぞれ前後の記述は省略しています）

```Ruby:app/models/user.rb

has_many :user_rooms, dependent: :destroy
has_many :chats, dependent: :destroy
```

```Ruby:app/models/user_room.rb
belongs_to :user
belongs_to :room
```

```Ruby:app/models/room.rb
has_many :chats
has_many :user_rooms  #1つのルームにいるユーザ数は2人のためhas_manyになる
```

```Ruby:app/models/chat.rb
 belongs_to :user
 belongs_to :room
```

👆```user_rooms```テーブルは```users```テーブルと```rooms```テーブルの中間テーブルです。
外部キーとして```user_id```と```room_id```を持っています。

　```user_rooms```テーブルは、ユーザーとルームの紐づけを行います：
　- ユーザーにどのroom_idが紐づいているか
　- ルームにどのuser_idが紐づいているか

例えば、ＡさんとＢさんのチャットの場合、
- Aさん(user_id=3)がroom_id=1
- Bさん(user_id=4)がroom_id=1

であれば、AさんとBさんは共通のroom_idを持ちます。

#STEP2: コントローラー作成 & ルーティング設定

では、```chats_controller```と```show```アクションを一緒に作成します👇

```:terminal
$ rails g controller chats show
```

今回は、```chats_controller```の```show```アクションでチャットを行うので、以下のようにルーティングを設定しておきましょう。
ちなみに```create```アクションは後でコントローラに定義するように、コメントの投稿に必要になってきます。

```Ruby:config/routes.rb
get 'chat/:id', to: 'chats#show', as: 'chat'
resources :chats, only: [:create]
```

#STEP3: ```chats_controller```に記述

まずはコメントなしの記述内容がこちら👇

```Ruby:app/controllers/chats_controller.rb
class ChatsController < ApplicationController

 def show
  @user = User.find(params[:id])
  rooms = current_user.user_rooms.pluck(:room_id)
  user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms)

  if user_rooms.nil?
   @room = Room.new
   @room.save
   UserRoom.create(user_id: @user.id, room_id: @room.id)
   UserRoom.create(user_id: current_user.id, room_id: @room.id)
  else
   @room = user_rooms.room
  end

  @chats = @room.chats
  @chat = Chat.new(room_id: @room.id)
 end

 def create
  @chat = current_user.chats.new(chat_params)
  @chat.save
 end

 private

 def chat_params
  params.require(:chat).permit(:message, :room_id)
 end

end
```

それぞれ何をしているかコメントを入れたものがこちらです👇
※ちなみに、どちらのユーザーに関する記述なのかがわかるように「Aさん」「Bさん」がコメントに出てきますが、「AさんがBさんに対してチャットする」想定です。つまり、Aさんが```current_user```になります。

```Ruby:app/controllers/chats_controller.rb
class ChatsController < ApplicationController

 def show
  #BさんのUser情報を取得
   @user = User.find(params[:id])
  #user_roomsテーブルのuser_idがAさんのレコードのroom_idを配列で取得
   rooms = current_user.user_rooms.pluck(:room_id)
  #user_idがBさん(@user)で、room_idがAさんの属するroom_id（配列）となるuser_roomsテーブルのレコードを取得して、user_room変数に格納
  #これによって、AさんとBさんに共通のroom_idが存在していれば、その共通のroom_idとBさんのuser_idがuser_room変数に格納される（1レコード）。存在しなければ、nilになる。
   user_room = UserRoom.find_by(user_id: @user.id, room_id: rooms)

  #user_roomでルームを取得できなかった（AさんとBさんのチャットがまだ存在しない）場合の処理  
   room = nil
   if user_room.nil?
   #roomのidを採番
    room = Room.new
    room.save
    #採番したroomのidを使って、user_roomのレコードを2人分（Aさん用、Ｂさん用）作る（＝AさんとBさんに共通のroom_idを作る）
    #Bさんの@user.idをuser_idとして、room.idをroom_idとして、UserRoomモデルのがラムに保存（1レコード）
     UserRoom.create(user_id: @user.id, room_id: room.id)
    #Aさんのcurrent_user.idをuser_idとして、room.idをroom_idとして、UserRoomモデルのカラムに保存（1レコード）
     UserRoom.create(user_id: current_user.id, room_id: room.id)
   else
    #user_roomに紐づくroomsテーブルのレコードをroomに格納
     room = user_room.room
   end

   #roomに紐づくchatsテーブルのレコードを@chatsに格納
    @chats = room.chats
   #form_withでチャットを送信する際に必要な空のインスタンス
   #ここでroom.idを@chatに代入しておかないと、form_withで記述するroom_idに値が渡らない
    @chat = Chat.new(room_id: room.id)
  end

  def create
    @chat = current_user.chats.new(chat_params)
    @chat.save
  end

  private

  def chat_params
    params.require(:chat).permit(:message, :room_id)
  end

end
```

#STEP4: チャットのビューを記述

チャット画面を記述していきましょう👇
なお```form_with```は非同期通信処理のため```remote: true```にしています（省略可）。

<details>
<summary> 以下の記述でチャット画面はこんな感じになります</summary>
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1215197/0bc2b1bc-14cf-1567-ddd5-c85caa7da162.png)
</details>

※ビューの記述は練習用ということで最低限にしてあります。
　スタイリッシュな感じにぜひ仕上げてください。

```HTML:app/app/views/chats/show.html.erb
<div class="container">
    <div class="row">
        <div class="col-xs-6">
            <h2>CHAT WITH <%= @user.name %></h2>

            <table class="message table">
              <thead>
                <tr>
                  <th style="text-align: left; font-size: 20px;"><%= current_user.name %></th>
                  <th style="text-align: right; font-size: 20px;"><%= @user.name %></th>
                </tr>
              </thead>
              <% @chats.each do |chat| %>
                <% if chat.user_id == current_user.id %>
                <tbody>
                  <tr>
                    <th>
                      <p style="text-align: left;"><%= chat.message %></p>
                    </th>
                    <th></th>
                  </tr>
                <% else %>
                  <tr>
                    <th></th>
                    <th>
                      <p style="text-align: right;"><%= chat.message %></p>
                    </th>
                  </tr>
                </tbody>
                <% end %>
              <% end %>
            </table>

            <%= form_with model: @chat, remote: true do |f| %>
              <%= f.text_field :message %>
              <%= f.hidden_field :room_id %>
              <%= f.submit "SEND", class:"btn btn-sm btn-success chat-btn" %>
            <% end %>
        </div>
    </div>
</div>
```

さて、ユーザーの詳細画面(show)から、そのユーザーとチャットができるように、ビューに記述を加えましょう👇

```HTML:app/app/views/chats/show.html.erb
<!-- current_userと@userが一致していない場合に`Begin Chat`を表示 -->

<% if current_user != @user %>
  <%= link_to 'Begin Chat', chat_path(@user.id) %>
<% end %> 
```

もし```users_controller```の```show```アクションに、```@user```が定義されていない場合は、忘れず記述してください👇

```Ruby:app/controllers/users_controller.rb
def show
  @user = User.find(params[:id])
end
```

#STEP5. ```create.js.erb```ファイルを作成＆記述

最後に、非同期通信の設定をしていきましょう。
といってもすでにform_withに```remote: true```を記述したので、あとは```create.js.erb```ファイルを作成＆記述するだけです。

手動で構わないので、```views/chats```フォルダ直下に```create.js.erb```ファイルを作成して、以下を記述しましょう。

```JavaScript:app/views/chats/create.js.erb
/*.messageを部分更新。appendメソッドで要素を追加 */
$('.message').append("<p style='text-align: left;'><%= @chat.message %></p>");

/*チャット投稿後、フォームの値に前回の投稿内容が残らないようにする */
$('input[type=text]').val("")
```

これで完成です！おつかれさまでした。

#参考資料

- [【Rails】DM(チャット)機能 + Ajax 実装！](https://qiita.com/IsakiMatsuo/items/9923d0724c69a2dfa259#%E3%81%BE%E3%81%88%E3%81%8C%E3%81%8D)
- [【Ruby on Rails】DM、チャット機能](https://qiita.com/japwork/items/c2625e6f3c74b5260e60)