Rail6を使った世界一わかりやすいDM機能の実装

## はじめに
今回紹介するのはRails6を使ったDM機能の実装方法です。

最終的にはこのような画面になります。
![chat_image.jng.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3333273/dca08d12-41fd-bf50-326e-d470ee40e45b.jpeg)


## 開発環境
* AWS Cloud9
* Ruby 3.1.2p20 (2022-04-12 revision 4491bb740a) [x86_64-linux]
* Rails 6.1.7.3

## 使用中のGem
* gem "devise"
* gem "jquery-rails"

## 前提環境
* Deviseのインストールが完了していて、ユーザーの新規登録やログインができる状態。
* フォローやフォロワー機能がある状態（なくてもいいです）


## 実装
モデルの作成
```ruby
rails g model Chat user_id:integer room_id:integer message:text
rails g model Room
rails g model UserRoom user_id:integer room_id:integer
```

RoomモデルはDMをする為の部屋番号を管理し、ChatモデルはChatの内容を管理。
UserRoomモデルはUserとRoomを結ぶ中間テーブルになっています。

では、いつも通り、migrateしましょう。

```ruby
rails db:migrate
```

## アソシエーションの設定
続いてアソシエーションの設定を行います。
まずはUserから

```ruby:models/user.rb
has_many :user_rooms
has_many :chats
has_many :rooms, through: user_rooms
```
続いてRoom

```ruby:models/room.rb
class Room < ApplicationRecord
  has_many :user_rooms
  has_many :chats
end
```

続いてUserRoom
```ruby:models/userroom.rb
class UserRoom < ApplicationRecord
  belongs_to :user
  belongs_to :room
end
```

最後はChat
```ruby:models/chat.rb
class Chat < ApplicationRecord
  balongs_to :user
  belongs_to :room

  validates :message, length: { in: 1..140 }
```

送信するメッセージは1~140字以内でないといけない、というバリデーションをつけてみました。アプリの企画によって変更してください。

ユーザーは多くの部屋に参加することができ、ルームは多くのユーザーを持つことができるため、多対多になります。ユーザーとチャットの関係性はユーザーは多くのチャットを投稿することができますが、一つのチャットにかかわるユーザーは一人なので多対一になります。

## ルーティングの設定

```ruby:config/routes.rb
resources :chats, only:[:show, :create]
```

## コントローラの作成
```ruby
rails g controller chats
```
それではコントローラに記述していきましょう

```ruby:chats/controller.rb
class ChatsController < ApplicationController

 def show
   @user = User.find(params[:id:) #チャットする相手
   rooms = current_user_rooms.pluck(:room_id)
   user_rooms = UserRoom.find_by(user_id: @user_id, room_id; rooms) #チャットする相手とのルームがあるかの確認

   unless user_rooms.nil? #ユーザールームがある場合
     @room = user_rooms.room #変数@roomにユーザー（自分と相手）と紐づいているroomを代入
   else #ユーザールームが無かった場合
     @room = Room.new #新しくRoomを作成
     @room.save #そして保存
     UserRoom.create(user_id: current_user.id, room_id: @room.id) #自分の中間テーブルを作る
     UserRoom.create(user_id @user.id, room_id: @room.id) #相手の中間テーブルを作る
   end
   @chats = @room.chats #チャット一覧
   @chat = Chat.new(room_id: @room_id)#チャット投稿
 end

 def create
   @chat = current_user.chats.new(chat_params)
   @room = @chat.room
   @chats = @room.chats
   render :validater unless @chat.save
 end

private

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

```

ここでもし、相互フォローしてないとDMできないようにする為の記述を追加します。また、これを追加しないとURLに打ち込むと別のDMに行けてしまうのでその対策をします。

```ruby:chats/controller.rb
class ChatsController < ApplicationController
  before_action :reject_non_related, only: [:show] #追加

 def show
   @user = User.find(params[:id:) #チャットする相手
   rooms = current_user_rooms.pluck(:room_id)
   user_rooms = UserRoom.find_by(user_id: @user_id, room_id; roomd) #チャットする相手とのルームがあるかの確認

   unless user_rooms.nil? #ユーザールームがある場合
     @room = user_rooms.room #変数@roomにユーザー（自分と相手）と紐づいているroomを代入
   else #ユーザールームが無かった場合
     @room = Room.new #新しくRoomを作成
     @room.save #そして保存
     UserRoom.create(user_id: current_user.id, room_id: @room.id) #自分の中間テーブルを作る
     UserRoom.create(user_id @user.id, room_id: @room.id) #相手の中間テーブルを作る
   end
   @chats = @room.chats #チャット一覧
   @chat = Chat.new(room_id: @room_id)#チャット投稿
 end

 def create
   @chat = current_user.chats.new(chat_params)
   @room = @chat.room
   @chats = @room.chats
   render :validater unless @chat.save
 end

private

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

 def reject_non_related #追加
   user = User.find(params[:id])
   unless current_user.following?(user) && user.following?(current_user)
     redirect_to user_path
  end
   end
end
```

## レイアウトも整えてみよう
```ruby:stylesheets/application.scss
.chat-container {
  display: flex;
  flex-direction: column;
  height: 100%;
  max-width: 800px;
  margin: 0 auto;
}

.chat-topbar {
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #1E90FF;
  color: #fff;
  height: 50px;
  padding: 0 20px;
}

.chat-messages {
  flex: 1;
  overflow-y: scroll;
  padding: 20px;
}

.message {
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;
}

.message-sender {
  margin-bottom: 5px;
  font-size: 0.8em;
  color: #666;
}

.message-body {
  padding: 10px;
  border-radius: 10px;
  max-width: 70%;
}

.self .message-body {
  align-self: flex-end;
  background-color: #1E90FF;
  color: #fff;
}

.other .message-body {
  align-self: flex-start;
  background-color: #f5f5f5;
  color: #333;
}

.chat-form {
  background-color: #f5f5f5;
  padding: 20px;
}

.chat-form form {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
}

.chat-form input[type="text"] {
  flex: 1;
  margin-right: 10px;
  padding: 10px;
  font-size: 1em;
  border: none;
  border-radius: 5px;
  background-color: #fff;
}

.chat-form input[type="submit"] {
  flex: 0 0 auto;
  padding: 10px;
  font-size: 1em;
  border: none;
  border-radius: 5px;
  background-color: #29b6f6;
  color: #fff;
  cursor: pointer;
}
```

## 続いてビューの作成

非同期通信にしたいので部分テンプレートを作ります。

app/viewa/chat
に新しく[_message.js.erb]というファイルを作成してください

```ruby:app/views/chats/_message.js.erb
<div class="chat-messages">
  <% chats.each do |chat| %>
    <% if chat.user_id == current_user.id %>
      <div class="message self">
        <div class="message-body">
          <%= chat.message %>
        </div>
      </div>
    <% else %>
      <div class="message other">
        <div class="message-sender"><%= chat.user.name %></div>
        <div class="message-body">
          <%= chat.message %>
        </div>
      </div>
    <% end %>
  <% end %>
</div>
```

さらに、app/viewa/chat
に新しく[_message.html.erb]を作成。

```ruby:app/views/chats/_message.html.erb
<div class="chat-messages">
  <% chats.each do |chat| %>
    <% if chat.user_id == current_user.id %>
      <div class="message self">
        <div class="message-body">
          <%= chat.message %>
        </div>
      </div>
    <% else %>
      <div class="message other">
        <div class="message-sender"><%= chat.user.username %></div>
        <div class="message-body">
          <%= chat.message %>
        </div>
      </div>
    <% end %>
  <% end %>
</div>
```

続いて
app/viewa/chat
に新しく[show.html.erb]を作成。
```ruby:app/views/chats/show.html.erb
<div class="chat-container">
  <div class="chat-topbar">
    <h2><%= @user.username %> さんとのチャット</h2>
  </div>
  <div class="message">
    <%= render "chats/messages", chats: @chats %>
  </div>

  <div class="chat-form">
    <%= form_with model: @chat, data: {remote: true} do |f| %>

      <%= f.text_field :message, placeholder: "メッセージを入力してください", autocomplete: "off" %>
      <%= f.hidden_field :room_id, value: @room.id %>
      <%= f.submit "送信" %>
    <% end %>
  </div>
</div>
```

続いて
app/viewa/chat
に新しく[create.js.erb]を作成。

```ruby:app/views/chats/create.js.erb
$('.message').html("<%= j(render 'chats/messages', chats: @chats) %>");
$('input[type=text]').val("")
```

続いて
app/viewa/chat
に新しく[validator.js.erb]を作成。

```ruby:app/views/chats/validator.js.erb
$('.errors').html("<%= j(render 'layouts/errors', obj: @chat) %>");
```

続いて
app/javasprict/chats
に新しく[validator.js]を作成。

```ruby:app/javasprict/chats/validator.js.erb
$(document).ready(function() {
  $('.errors').html("<%= j(render 'layouts/errors', obj: @chat) %>");
});
```

最後に
app/viewa/chat
に新しく[_info.html.erb]を作成

```ruby:app/views/chats/_info.html.erb
<% if current_user != @user && current_user.following?(@user) && @user.following?(current_user) %>
  <%= link_to 'チャットを始める', chat_path(@user.id), class: "ml-3" %>
<% end %>
```


長くなりましたが以上で実装の完了です！
お疲れ様でした。

もしよければコメント、いいねお待ちしてます！