[Rails] DM機能を解説する

ポートフォリオに実装した機能を解説していきます！
今回はDM（ダイレクトメッセージ）機能。

# 目標

![ezgif.com-gif-maker (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974719/4f04485a-65d2-6d77-fc0c-12b2af954139.gif)

機能としては、
ユーザー詳細ページからメッセージルームに飛んで個別にメッセージ、
またやりとりしたメッセージルームは一覧でマイページから取得できる。
ざっくりですがこんな感じ
では行こう！


# 前提
前提としてdeviseの導入を忘れずに！
deviseの導入はこちらの記事を参考にしてください。

https://qiita.com/Hal_mai/items/350c400e8763ce0487a3

# 設計
4つのモデルを使って実装します。
Userモデル =>　Userの情報（devise）
Roomモデル =>　自分と相手の2人のユーザーが入ります
Entryモデル => どのUserがどのRoomに所属しているかを判断
Messageモデル => UserがどのRoomでどんなMessageを送ったか


![スクリーンショット 2021-06-20 9.10.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974719/5e0fdccf-9cb7-9b7f-cffb-94bfbe10b073.png)

ER図はこんな感じ
User1人1人は他のUserに対して沢山のRoomを持っていることになるのでUserとRoomの関係性は多対多になり中間テーブルとしてEntryテーブルを用意。
またRoom内で２人のUserが沢山のMessageでやりとりするのでこちらも多対多で中間テーブルにMassegeを用意します


# 実装

では行こう！！

# モデルの準備

```ruby:ターミナル 
$ rails g model room
$ rails g model entry user:references room:references
$ rails g model message user:references room:references body:text
```

```ruby:ターミナル 
$ rails db:migrate
```

## リレーション

```ruby:user.rb
 has_many :entries, dependent: :destroy
 has_many :messages, dependent: :destroy
```

```ruby:room.rb
 has_many :entries, dependent: :destroy
 has_many :messages, dependent: :destroy
```

```ruby:entry.rb
 belongs_to :user
 belongs_to :room
```

```ruby:message.rb
 belongs_to :user
 belongs_to :room
```

## routes

```ruby:routes.rb
resources :messages, only: [:create]
resources :rooms, only: [:create, :index, :show]
```


## Controller & View

### users_controller.rb

```ruby:users_controller.rb
class UsersController < ApplicationController
  def show
    @user = User.find(params[:id])
    @current_entry = Entry.where(user_id: current_user.id)
    @another_entry = Entry.where(user_id: @user.id)
    unless @user.id == current_user.id
      @current_entry.each do |current|
        @another_entry.each do |another|
          if current.room_id == another.room_id
            @is_room = true
            @room_id = current.room_id
          end
        end
      end
      unless @is_room
        @room = Room.new
        @entry = Entry.new
      end
    end
  end
```

### 解説

```ruby:
@current_entry = Entry.where(user_id: current_user.id)
@another_entry = Entry.where(user_id: @user.id)
```
ログインしてるユーザーとメッセージ相手のユーザー情報をEntryテーブルから検索して取得します。

```ruby:
unless @user.id == current_user.id
  @current_entry.each do |current|
    @another_entry.each do |another|
      if current.room_id == another.room_id
        @is_room = true
        @room_id = current.room_id
      end
     end
   end
   unless @is_room
     @room = Room.new
     @entry = Entry.new
    end
  end
end
```

unlessでログインしていないユーザーという条件をつけます。
そしてさっき取得した2つのユーザー情報をそれぞれeachで取り出してEntryテーブル内に同じroom_idが存在するかどうかを調べます。
同じroom_idが存在する場合は既にroomが存在しているということなのでroom_idの変数とroomが存在するかどうかの条件であるis_roomを渡します。
同じroom_idが存在しない場合は新しくインスタンスを作成します。

### users/show.html.haml

続いてviewです。
viewはhamlで書いてます。

```ruby:show.html.haml
- unless @user.id == current_user.id
  - if @is_room == true
    = link_to 'メッセージへ', room_path(@room_id)
  - else
    = form_for @room do |f|
      = fields_for @entry do |e|
        - e.hidden_field :user_id, value: @user.id 
      = f.button type: :submit do
        メッセージを送る　 
```

ここではログインしているユーザーではないという条件をつけてis_roomの条件を使って既に部屋が存在しているかどうかで分岐させます。
既に部屋が存在していればその部屋に、していなければformでコントローラーにパラメーターを送っています。

### rooms_controller.rb

```ruby:rooms_controller.rb
class RoomsController < ApplicationController
  before_action :authenticate_user!
  def create
    room = Room.create
    current_entry = Entry.create(user_id: current_user.id, room_id: room.id)
    another_entry = Entry.create(user_id: params[:entry][:user_id], room_id: room.id)
    redirect_to room_path(room)
  end

  def index
    # ログインユーザー所属ルームID取得
    current_entries = current_user.entries
    my_room_id = []
    current_entries.each do |entry|
      my_room_id << entry.room.id
    end
    # 自分のroom_idでuser_idが自分じゃないのを取得
    @another_entries = Entry.where(room_id: my_room_id).where.not(user_id: current_user.id)
  end

  def show
    @room = Room.find(params[:id])
    @messages = @room.messages.all
    @message = Message.new
    @entries = @room.entries
    @another_entry = @entries.where.not(user_id: current_user.id).first
  end
end 
```
### 解説

```ruby
def create
  room = Room.create
  current_entry = Entry.create(user_id: current_user.id, room_id: room.id)
  another_entry = Entry.create(user_id: params[:entry][:user_id], room_id: room.id)
  redirect_to room_path(room)
end

```

ここでusers/show.html.hamlで部屋が存在しなかった場合にformで送られてきたパラメーターきます。
やっていることとしては現在ログインしているユーザーとメッセージ相手のユーザーそれぞれの情報をroom_idで紐付けてEntryテーブルにcreateしています。


```ruby
def index
  current_entries = current_user.entries
  my_room_id = []
  current_entries.each do |entry|
    my_room_id << entry.room.id
  end
  @another_entries = Entry.where(room_id: my_room_id).where.not(user_id: current_user.id)
end

```
続いてindex
indexでは現在やりとりしているroom一覧を取得します。

```ruby
current_entries = current_user.entries
my_room_id = []
current_entries.each do |entry|
  my_room_id << entry.room.id
end
```
まずここでログインユーザーがやりとりしているroomのIDをすべて取得しそれを配列化してmy_room_idとします。

```ruby
@another_entries = Entry.where(room_id: my_room_id).where.not(user_id: current_user.id)
```
そしてEntryテーブルからmy_room_idでuser_idが自分のIDじゃないレコードを取り出します。
こうすることで現在自分が参加中のroomを相手の情報で取得しviewで表示することができます。

### rooms/index.html.haml

```ruby:index.html.haml
- @another_entries.each do |entry|
  = link_to room_path(entry.room) do
    .card
      .card-body
         %div
           .d-flex
             .chat-avatar
                %object
                  = link_to user_path(entry.user) do
                    = image_tag entry.user.avatar_image
              .chat-user-name
                = entry.user.display_name
           chat-text
             = Message.find_by(id: entry.room.message_ids.last)&.body
```

```ruby
 = Message.find_by(id: entry.room.message_ids.last)&.body
```
ここではroomの最後のメッセージを表示しています。


```ruby
def show
    @room = Room.find(params[:id])
    @messages = @room.messages.all
    @message = Message.new
    @entries = @room.entries
    @another_entry = @entries.where.not(user_id: current_user.id).first
    end
  end
```
ここでは@roomで1つのroomを表示
@messagesで過去のやりとりを全て表示し@messageで新しいメッセージを作るためのインスタンスを作成。
@@entriesと@another_entryでview側に相手の名前を表示させています。

### rooms/show.html.haml

```ruby:show.html.haml
.title
  %h2 
    = ("#{@another_entry.user.email}さんとのメッセージ")

- @messages.each do |m|
  - if m.user_id == current_user.id
    .mycomment
      %p 
        = m.body
- else 
  .fukidasi
    .faceicon 
      = image_tag m.user.email
    .chatting
      .says
        %p 
          = m.body

.chat-form-box
  = form_for @message do |f|
    .chat-form-group
      = f.text_field :body
      = f.hidden_field :room_id, :value => @room.id
      = f.submit "送信する"
```

@anoter_entryで相手の名前やメールアドレスで表示させることができます。

@messagesをそれぞれIDで分岐させることでLineっぽく自分のメッセージは右、相手のメッセージは左みたいなスタイルを作ることができます。

form部分では@messageの他にこのmessageがどのroomに所属しているかを判断するためにhidden_fieldにroomの情報を持たせてます。

そしてこのformで送られたパラメーターがmessages_controllerに渡ります。


## messages_controller.rb


```ruby:messages_controller.rb
class MessagesController < ApplicationController
  before_action :authenticate_user!

  def create
    message = Message.new(message_params)
    message.user_id = current_user.id
    if message.save
      redirect_to room_path(message.room)
    else
      redirect_back(fallback_location: root_path)
    end
  end

  private

    def message_params
      params.require(:message).permit(:room_id, :body)
    end
end 
```
最後にmessage部分。
rooms/show.html.hamlから送られたパラメータをcreateします。
やっていることは、自分のmessageかどうかで分岐させてtrueならroom_idとbodyをパラメーターで許可してcreateしています。
この辺はいつものcreate actionと同じですね。

# 完成
中間テーブルが２つあって複雑ですが1つ1つ取り出してみるとやっていることは初心者の僕でも理解できるような内容でした。