Ruby on Rails チュートリアル DM機能の追加

#はじめに
Ruby on Rals チュートリアル(5.1版)の課題の「DM機能」を作成します。
チュートリアルの続きから機能を追加しているためUserモデル・ヘルパーメソッドの作成などは行っておりませんのでご注意下さい。
また、Action Cableを使用しておらずメッセージを送信する度に全てのメッセージ取得するという処理を行っています。

なおこの記事では
Ruby 2.7.2
Rails 5.1
を使用しております。


##作成するもの
![スクリーンショット 2021-01-25 16.20.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1053396/12862801-60e0-3d7a-6cb9-50231821b468.png)
users/:id/messages　にDMを表示し、メッセージ送信機能も持たせることにします。

## messageモデルの作成、Userモデルとの関連付け
`$ rails g model Message from_id:integer to_id:integer content:text`
送信者のid,受信者のidでメッセージを判別しております。  

```db/migrate/一意の数字_create_messages.rb
class CreateMessages < ActiveRecord::Migration[5.1]
  def change
    create_table :messages do |t|
      t.integer :from_id
      t.integer :to_id
      t.text :content

      t.timestamps
    end
    add_index :messages, :from_id
    add_index :messages, :to_id
  end
end
```
from_id,to_idにindexを張り、migrationを行います。
`$ rails db:migrate`
<br>

  
```app/models/message.rb
class Message < ApplicationRecord
  default_scope -> { order(created_at: :asc) }
  validates :from_id, presence: true
  validates :to_id, presence: true
  validates :content, presence: true, length: { maximum: 300 }
end
```
メッセージの並び順、バリデーションを設定します。
<br>

```app/models/user.rb
class User < ApplicationRecord
  has_many :outgoing_messages, class_name: "Message",
                               foreign_key: "from_id",
                               dependent: :destroy
  has_many :incoming_messages, class_name: "Message",
                               foreign_key: "to_id",
                               dependent: :destroy
  .
  .
  .
end
```
Userモデルとの紐付を行います。


##ルーティング
```config/routes.rb
Rails.application.routes.draw do
  resources :users do
    member do
      resources :messages, only: [:create, :index]
    end
  end
end
```
以下のルーティングが完成

```
GET    /users/:id/messages(.:format)           messages#index
POST   /users/:id/messages(.:format)           messages#create
```
indexアクションで特定の相手とのメッセージ一覧をレンダリングし、createアクションで新規メッセージを送信＋viewの更新を行います。


## Messagesコントローラーの処理
`$ rails g controller Messages index create`
Messagesコントローラーを作成します。

```app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  before_action :setup_users
  
  def index
    @messages = Message.where("from_id IN (:ids) AND to_id IN (:ids)",ids:@ids)
    @message = Message.new
  end


  def create
    @message = current_user.outgoing_messages.build(message_params)
    @message.to_id = params[:id] 
    @message.save
    @messages = Message.where("from_id IN (:ids) AND to_id IN (:ids)",ids:@ids)
    respond_to do |format|
      format.html { redirect_to  messages_urL(@to_user)}
      format.js { render "create"}
    end
  end

  private
  def message_params
    params.require(:message).permit(:content)
  end

  def setup_users
    @to_user = User.find(params[:id])
    @ids = [@to_user.id,current_user.id]
  end
end
```
indexメソッドでは
fromまたはtoカラムに自分または相手のidを含むmessageを取り出しインスタンス変数に保存しております。

createメソッドでの処理
今回はAction Cableを使用しないので相手が送信した新規のメッセージを取得するためにメッセージ送信の失敗・成功に関わらずメッセージを全て取得します。

また、current_userメソッドはrails tutorialで作成したものです。

## View
まずはindexメソッドからレンダリングされるindex.html.erbです。

```app/views/messages/index.html.erb
<h2><%= @to_user.name %></h2>
<ol id="messages">
    <%= render @messages %>
</ol>
<div id="error_messages">
    <%= render "shared/error_messages", object:@message %>
</div>
<%= render "shared/message_form" %>
```
```app/views/messages/_message.html.erb
<li <%= "class = my-content" if message.from_id == current_user.id %>>
    <%= gravatar_for message.from, size: 50 %>
    <div class="content">
        <span class="message">
            <%= message.content %>
        </span>

        <span class="timestamp">
            <%= message.created_at %>
        </span>
    </div>
</li>
```
Twitterなどで見られるように相手のメッセージと自分のメッセージの表示方法を変えるため、自分のメッセージの場合は別のcssを適用するためのクラス名をつける処理を行っています。
<br>

```app/views/shared/_message_form.html.erb
<% provide(:button_text, 'Send Message') %>
<%= form_with model: @message do |f| %>
  <%= f.text_area :content, class: 'form-control' %>
  <%= f.submit yield(:button_text), class: "btn btn-primary" %>
<% end %>
```

```app/views/shared/_error_messages.html.erb
<% if object.errors.any? %>
  <div id="error_explanation">
    <div class="alert alert-danger">
      The form contains <%= pluralize(object.errors.count, "error") %>.
    </div>
    <ul>
    <% object.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>
```
<br>
次にcreateメソッドでレンダリングされるviewです。

```app/views/messages/create.js.erb
$(".form-control").val("");
$("#messages").html("<%= escape_javascript(render(@messages)) %>");
$("#error_messages").html("<%= escape_javascript(render('shared/error_messages', object: @message)) %>");
```
フォームのクリア、メッセージの取得、エラーメッセージの表示を行います。



## scss
```app/assets/stylesheets/custom.scss
// messages
#messages {
  list-style: none;
  padding: 0;

  li {
    padding: 10px 0;
    // border-top: 1px solid #e8e8e8;
    display: flex;
    justify-content: flex-start;
  }

  .content {
    display: inline-block;

    .message {
      padding: 20px;
      display: inline-block;
      background:$gray-medium-light;
      border-radius: 10px;
    }

    .timestamp {
      color: $gray-light;
      display: block;
      font-size: .75em;
    }
  }

  .gravatar {
    margin-right: 10px;
    width: 40px;
    height: 40px;
  }

  li.my-content{
    justify-content: flex-end;
    text-align: right;
    .message {
      background:#71151a;
      border-radius: 10px;
      color: #fff;
    }

    .gravatar{
      display: none;
    }
  }
}
```

## 完成
![スクリーンショット 2021-01-26 18.08.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1053396/4d11c618-1000-2d5d-a955-6bf33f42fba6.png)
![スクリーンショット 2021-01-26 18.08.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1053396/5c70e822-270c-8361-1693-223c51573f0c.png)

![スクリーンショット 2021-01-26 18.09.07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1053396/adc84357-f62e-32fd-6210-7e0776995880.png)

今のところ自分がメッセージを送信した時しか新しいメッセージを取得することが出来ていないので、Action Cableを利用してリアルタイムチャット機能の追加などを行いたいと思います。