【Rails】DMが届いた際の通知機能を実装する

# はじめに

下記２つのQiita記事を参考にして、

* いいね
* コメント
* フォロー
* DM
* ブックマーク

された時に通知される機能を実装しました。

当記事では`DMの通知機能`について
基本文法でつまづいたので、学習記録として残しています。

なお、コントローラーとモデルで分けて
構成していますので、似た構成にしたい方の参考になれば幸いです。

修正が必要な箇所があれば、コメントなどでお知らせください。



# 参考記事(2つ)

https://qiita.com/nekojoker/items/80448944ec9aaae48d0a

https://qiita.com/facultyoflaw11/items/538862befd327e8dc918


# 前提

・DM機能を実装済み


# 実装前に考える…

まず、通知画面のViewで

* 通知を受け取るユーザーに何を情報として伝えたいか
* どこに画面遷移させると使いやすいか

の２点に留意しないといけません。

これは人によって、実装したい思想が異なると思うので
絶対解はないと思います。

私の場合は、DMの通知を想定すると…

* 通知を受け取るユーザーに何を情報として伝えたいか
　→ `送信者はどのユーザーか`、 `どんな内容のメッセージか`

* どこに画面遷移させると使いやすいか
　→ `クリック1回でDM画面に移動してメッセージを返信できる`

と考えました。


# モデル設計？

前章で考えた内容から
通知機能を担うNotificationモデルに関連付けする
各モデルを以下の通りにまとめました。

■前章のポイント
`DM送信者はどのユーザーか`
`どんな内容のメッセージか`
`クリック1回でDM画面に移動してメッセージを返信できる`

　↓

`・Userモデル：　DMを送信したユーザー`
`・Messageモデル：　送信されたメッセージ`
`・Roomモデル：　DMを送受信できる部屋`

上記３つのモデルをNotificationモデルに関連付けすることで
Notificationコントローラーから描画されるView内で
各モデルの情報を表示することができます。

反対に言えば、NotificationのView内で
メッセージを表示させる必要がないと考えるのであれば
Messageモデルの関連付けは不要です。

もっと具体的に言うと
メッセージ内容はDM送受信できる部屋に入った後で
表示されれば、それで良いと考える実装例ですね。


# では、Controllerから

先に、データを取り出す役割のモデルを
呼び出すコントローラー側から実装していきます。

DMの通知機能では、相手がメッセージを送信したタイミングで
Notificationモデルにデータが保存される様に実装します。

そのため、`messageコントローラー`の`createアクション`を編集します。

同コントローラーのcreateアクションが
`動作の起点となること`をイメージしておきましょう。
`(DM画面でメッセージ送信ボタンをクリック　→ notificationモデルにデータ保存)`

編集する部分は、以下の通りです。

```ruby:app/controllers/messages_controller.rb

class MessagesController < ApplicationController
  def create
    if Entry.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
      @message = Message.create(params.require(:message).permit(:user_id, :content, :room_id).merge(user_id: current_user.id))

      # (ここから)
      # 新規作成された@messageに紐づくroomを@roomに格納する
      @room = @message.room
      # 本引数を２つ持たせてcreate_notification_dmメソッドを実行
      @room.create_notification_dm(current_user, @message.id)
      # (ここまで)

      redirect_to "/rooms/#{@message.room_id}"
    else
      redirect_back(fallback_location: root_path)
    end
  end
end

```

コード補足：

`@message.room`は
・Messageモデル
・Roomモデル　　
を belongs_to, has_manyメソッドで関連付けしている為
Messageに紐づいたRoomオブジェクトが返されます。

`create_notification_dm`は
これからRoomモデルに定義する
create_notification_dmメソッドを呼び出しており
実行後、roomモデルに動作が移ります。

`(current_user, @message.id)`は
Notificationモデルに
・現ユーザーのidを保存
・メッセージを保存
するために、メソッドに渡す本引数として設定しています。


# Model関連付け

まずは、generateコマンドでnotificationモデルを生成します。

```zsh:ターミナル

% rails generate model Notification

```

次にマイグレーションファイルを編集していきます。

Notificationモデルを下記カラムで構成される様に
マイグレーションファイルを編集します。

| visitor_id | visited_id | room_id | message_id | action | checked |
|:-:|:-:|:-:|:-:|:-:|:-:|
| 通知を送るユーザー | 通知を受けるユーザー | DMを送受信できる部屋 | メッセージ内容 | 通知の種類 | 通知確認の有無 |
| 1 | 2 | 1 | 1 | dm | false |



```ruby:db/migrate/20210924000000_create_notifications.rb

class CreateNotifications < ActiveRecord::Migration[6.1]
  def change
    create_table :notifications do |t|
      t.integer :visitor_id, null: false
      t.integer :visited_id, null: false
      t.integer :room_id
      t.integer :message_id
      t.string :action, default: '', null: false
      t.boolean :checked, default: false, null: false

      t.timestamps
    end

    add_index :notifications, :visitor_id
    add_index :notifications, :visited_id
    add_index :notifications, :room_id
    add_index :notifications, :message_id
  end
end

```

マイグレーションファイルが編集できたら、DBに反映します。

```zsh:ターミナル 

% rails db:migrate

```

続いて、Userモデルを設定していきましょう！

ここでは、通知を
・送る側(active_notifications)
・受ける側(passive_notifications)
の２つに分けて実装します。

お互い参照するモデルはNotificationに設定していますが
外部キーは`visitor_id`と`visited_id`と異なる点に
注意してください。

また、dependent:は依存関係を示す設定なので、
Userが削除されれば、Notificationも削除されます。

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

class User < ApplicationRecord
  has_many :active_notifications, class_name: 'Notification', foreign_key: 'visitor_id', dependent: :destroy
  has_many :passive_notifications, class_name: 'Notification', foreign_key: 'visited_id', dependent: :destroy

```

Messageモデルは下記の通りです。

```ruby:app/models/message.rb

class Message < ApplicationRecord
  has_many :notifications, dependent: :destroy
  belongs_to :user
  belongs_to :room
end

```

Roomモデルは下記の通りです。

```ruby:app/models/room.rb

class Room < ApplicationRecord
  has_many :notifications, dependent: :destroy
  has_many :messages, dependent: :destroy
  has_many :entries, dependent: :destroy

```

下記はNotificationモデルの設定です。

default_scopeで、作成日時が新しい順番で参照されます。
optional: trueは、データがnilであっても許可するオプションです。

また、関連付けするUserモデルの対象が
・visitor
・visited
となっている点に注意します。


```ruby:app/models/notification.rb

class Notification < ApplicationRecord
  default_scope -> { order(created_at: :desc) }
  belongs_to :room, optional: true
  belongs_to :message, optional: true
  belongs_to :visitor, class_name: 'User', optional: true
  belongs_to :visited, class_name: 'User', optional: true
end

```

# create_notification_dmメソッドの定義

続いて、Roomモデルで通知データを保存するための
メソッドを実装していきます。

先ほど編集したroom.rbを再度イジります。

なお、create_notification_dmメソッドは
先にmessageコントローラーで記述したもので
コントローラー側で同メソッドを呼び出しにきます。

編集箇所は以下の通りです。

```ruby:app/models/room.rb

class Room < ApplicationRecord
  has_many :notifications, dependent: :destroy
  has_many :messages, dependent: :destroy
  has_many :entries, dependent: :destroy

  # (ここから)
  def create_notification_dm(current_user, message_id)
    @multiple_entry_records = Entry.where(room_id: id).where.not(user_id: current_user.id)
    @single_entry_record = @multiple_entry_records.find_by(room_id: id)
    notification = current_user.active_notifications.build(
      room_id: id,
      message_id: message_id,
      visited_id: @single_entry_record.user_id,
      action: 'dm'
    )

    notification.save if notification.valid?
  end
  # (ここまで)

end

```

コード補足：

create_notification_dm
→ 定義するメソッド名称なので、任意で命名してください。

(current_user, message_id)
→ 仮引数を２つ設定しています。
　コントローラー側で設定した本引数と異なる名称でも動作します。
　ただし引数の数が一致しないとエラーが発生しますので注意します。

**`@multiple_entry_records = Entry.where(room_id: id).where.not(user_id: current_user.id)`**
↓
`・Entryモデルから`
`・user_idが自分と一致するものを省いて`
`・room_idが作成されたルームのIDと一致する`
**`・複数レコード`**
を取得しています。

**`@single_entry_record = @multiple_entry_records.find_by(room_id: id)`**
↓
`前行では複数レコード(where)だったものを単一レコード(find_by)に変換しています`
`目的は「single_entry_record.user_id」でIDを抽出する為です`

notification = current_user.active_notifications.build(
　room_id: id,
　message_id: message_id,
　visited_id: @single_entry_record.user_id,
　action: 'dm'
)
→ 現在のユーザーに紐づくactive_notificationsに()内の
　各データを入れて作成しています。

■各データの概説
room_id：　コントローラー側で新規作成されたmessageに紐づくroomのid
message_id：　本引数で持ってきた@message.id
　　　　　　　　(仮引数でmessage_idに名称を変えている)
visited_id：　@single_entry_recordからuser_idを抽出
　　　　　　　(DMを送信したユーザー)
action：　dm
　　　　　(View内のcase文で条件分岐させる為)

notification.save if notification.valid?
→ 後置if文でnotificationに登録するデータが有効かチェックして保存します。


# Viewで表示させる(通知画面の設定)

ここからは、Notificationモデルに保存されたデータを
取り出して表示させる部分です。

まず、genarateコマンドでコントローラー・ビューを生成します。

```zsh:ターミナル 

% rails g controller notifications

```

次にroutes.rbで、ルーティングを設定します。

これで、notifications_pathというパスで、GETされると、
notificationコントローラーのindexアクションが動きます。

```ruby:config/routes.rb

resources :notifications, only: :index

```

下記は、notifications_controller.rbの設定です。

ここでは、kaminariを使って15件ごとでページネーションして
現在のユーザーに関連する通知情報を@notificationsに格納しています。

@notificationsの中に存在している情報をユーザーが確認すれば
「確認済み」に更新する処理をupdateを用いて実装しています。

```ruby:app/controllers/notifications_controller.rb

class NotificationsController < ApplicationController
  def index
    @notifications = current_user.passive_notifications.page(params[:page]).per(15)
    @notifications.where(checked: false).each do |notification|
      notification.update(checked: true)
    end
  end
end

```

続いて、コントローラーからのindexアクションで描画される
View（index.html.erb）の設定です。

１行目のコードで、通知を送るユーザーが自分であるレコードを除外しています。

また、renderを使って部分テンプレートを参照しており、
さらにnotificationsと複数形にしていますので、
存在するレコード分で部分テンプレートが生成されます。

つまり、後述する`_notification.html.erb`を
複数回で表示するということです。

```erb:app/views/notifications/index.html.erb

<% notifications = @notifications.where.not(visitor_id: current_user.id) %>
<% if notifications.exists? %>
  <%= render notifications %>
  <%= paginate notifications %>
<% else %>
  <p>通知はありません</p>
<% end %>

```

今度は、先のindex.html.erb内のrenderで参照される
部分テンプレート(_notification.html.erb)の設定です。

１行目のコードのvisitorは、通知を送ったユーザーです。

ここでは、case文でactionの値が何かで条件分岐する設定となっています。
今回はDMの通知であるため、action = dmでDM仕様の通知表示となる実装です。
(いいね・コメント・フォロー…といった感じで追加実装するなら
whenでlike,comment,followなどの条件を加えて実装します。)

なお、メッセージ内容はtruncateを使って20文字のみの
一部だけ表示される仕様にしています。

これで、正直UIはかなりお粗末ですが
`DM送信者は表示され、`
`メッセージの一部が表示され、`　かつ
`クリック1回でDM画面に移動してメッセージを返信できる`
実装になっています。

```erb:app/views/notifications/_notification.html.erb

<% visitor = notification.visitor %>

<span>
  <%= link_to user_path(visitor) do %>
    <%= visitor.name %>
  <% end %>
  <span>さんが</span>
  <br>

  <% case notification.action %>
  <% when 'dm' %>
    <p>あなたに</p>
    <h4><i class="fas fa-envelope"></i>DMを送りました</h4>
    <span>メッセージ内容:</span>
      <%= truncate(notification.message.content, length: 20, omission: '... (一部表示)') %>
    <p>投稿時期： <%= time_ago_in_words(notification.created_at).upcase %>前</p>
    <%= link_to room_path(notification.room) do %>
      <h4>DMの送信画面に移動</h4>
    <% end %>
    <hr>
  <% end %>
</span>

```

あとは、ユーザーに未読状態の通知対象があるか調べる
ヘルパーメソッドを作成しておきます。

```ruby:app/helpers/notifications_helper.rb
module NotificationsHelper
  def unchecked_notifications
    @notifications = current_user.passive_notifications.where(checked: false)
  end
end
```

最後に
起点となるnotifications_pathのパスでGETさせる任意のビュー内で
if文を使って、ヘルパーメソッドunchecked_notificationsの有無で
表示させる文言を分岐させれば完成です。


```erb:app/views/任意ファイル
<%= link_to (notifications_path) do %>
  <% if unchecked_notifications.any?%>
    <p>通知（未読あり）</p>
  <% else %>
    <p>通知</p>
  <% end %>
<% end %>
```



# 終わりに

初めてrailsの実装コードをまとめましたが
説明するのが難しい…と痛感しました。

完全に自分メモのレベルですが、
何かちょっとでも参考になったら幸いです。

最後までお読み頂き、ありがとうございました。