【Ruby on Rails6】 ポートフォリオにフォロー通知、いいね通知、コメント通知を実装

# はじめに
ポートフォリオ作成にあたって通知機能を実装したのでその備忘録を残しておきます。
なお、かなり詳しく解説しているので実装だけできればいいよーと言う方は以下の記事に飛んでください。
実装にあたって下記の記事を参考にさせていただきました。
https://qiita.com/nekojoker/items/80448944ec9aaae48d0a
https://qiita.com/yuto_1014/items/2db1dd4fcd7945b980f7
https://qiita.com/E6YOteYPzmFGfOD/items/c780dd686a81a8ca32e8

# 前提
- 投稿機能を実装済み（この記事では、Productモデル）
- コメント機能を実装済み（この記事では、Commentモデル）
- いいね機能を実装済み（この記事では、Likeモデル）
- フォロー機能を実装済み（この記事では、Relationshipモデル）

# 完成形
![スクリーンショット 2021-02-15 14.28.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880610/185ea6f3-b0d3-4bf0-4867-dc635bcfbf01.png)

# 実装手順
1.Notificationモデルの作成
2.マイグレーションファイルの確認
3.rails db:migarate
4.モデルファイルの確認
5.通知メソッドを作る
6.通知一覧ページ(notifications.controller)
7.通知一覧ページ(view)
8.
9.
10.

# 1. Notificationモデルの作成

```
rails g model Notification visitor:references visited:references product:references comment:references action:string checked:boolean
```

visitor_id : 通知を送ったユーザー
visited_id : 通知を送られたユーザー
product_id : いいねされた投稿のid
comment_id : 投稿されたプロダクトに対するコメントのid
action : 通知の種類（フォロー(follow)、いいね(like)、コメント(comment)）
checked : 通知を送られたユーザーが通知を確認したかどうか

# 2.マイグレーションファイルの確認

![スクリーンショット 2021-02-15 14.48.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880610/5b656cf2-9ebf-22f4-c689-f8b34ca1a1d8.png)

```
t.references :product, null: false, foreign_key: true
t.references :comment, null: false, foreign_key: true
```
の部分は

```
t.references :product, null: true, foreign_key: true
t.references :comment, null: true, foreign_key: true
```
と変更してください。これはフォロー通知を送るときはcomment_idは必要がなく,いいね通知を送るときはproduct_idが必要ないため、null:falseとなっていると空での保存ができないためです。
上記のように変更ができたら3.へ進んでください。

# 3.rails db:migarate

これは単純に

```
rails db:migarate
```
するだけでいいです。

# 4.モデルファイルの確認
ここからはUserモデルファイル、Productモデルファイル、Commentモデルファイル、Notificationモデルファイルの編集を行っていきます。目的はそれぞれのモデルとNotificationモデルを紐づけるためです。

user.rb

```
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
```
を追加。

- active_notifications: 自分が送った通知
- passive_notifications：自分が受け取る通知
イメージとしてはこんな感じです。

![スクリーンショット 2021-02-15 15.01.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880610/b820c7df-9c74-dfd6-2fa4-30b4cdf63f93.png)

product.rb

```
has_many :notifications, dependent: :destroy
```
を追加。

comment.rb

```
has_many :notifications, dependent: :destroy
```
を追加。
notification.rb

```
class Notification < ApplicationRecord
  default_scope -> { order(created_at: :desc) } #新着順で表示する
  belongs_to :visitor, class_name: "User", optional: true 
  belongs_to :visited, class_name: "User", optional: true
  belongs_to :product, optional: true
  belongs
```
を追加。
optionnal:trueとは外部キーの空を許可するもの。フォロー通知のみではないので空の時もある。
上記でも述べたように、フォロー通知を送るときはcomment_idは必要がなく,いいね通知を送るときはproduct_idが必要ないため、空での保存が可能でないとエラーが出てしまうためです。

# 5.通知メソッドを作る
Controller内で通知機能を書くこともできますが、それだとcontrollerファイルのコード量が増えてしまい醜いのでモデルファイルの下部にあらかじめ通知メソッドを書いておいて、それをcontroller内で呼び出す形をします。


## フォロー通知メソッド

relationships.controllerファイルのcreateアクションで「フォロー」が作成されたときこのメソッドも同時に呼び出します。

```user.rb(userモデルファイルの下部に記述)

def create_notification_follow(current_user)
    notification = current_user.active_notifications.new(
      visited_id: id, 
      action: 'follow'
    )
    notification.save if notification.valid?
  end
```
一つずつ解説していきます。以下のrelationship.controllerのcreateアクションの中で
`  @user.create_notification_follow(current_user)`
でフォロー通知メソッドを呼び出しています。
通知メソッド内では、ログインユーザのactive_notificationsを新しく作成しています。また
`visited_id: id, `
とありますが、このidとは、
`  @user.create_notification_follow(current_user)`
で呼び出した時の@userのidです。つまりフォローを押された人のidとなります。
ここでのactionはfollowです。
`notification.valid?`の意味は、notificationにエラーがなければsaveしなさい。と言う意味です。
![スクリーンショット 2021-02-15 15.31.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880610/a54e475c-3999-62cf-0481-a439a587d0c0.png)


## いいね通知メソッド

```product.rb
def create_notification_like(current_user)
    notification = current_user.active_notifications.new(
      comment_id: nil,
      product_id: id,      
      visited_id: user_id,
      action: 'like'
      )
    if notification.visitor_id == notification.visited_id
       notification.checked = true
    end
    notification.save if notification.valid?
  end
```
一つずつ解説していきます。このメソッドは以下のlikes.controllerのcreateアクションで呼び出しています。
いいね通知メソッド内では、ログインユーザのactive_notificationsを新しく作成しています。また
`product_id: id, `とありますが、このidとは、
`@product.create_notification_like(current_user)`
で呼び出した時の@productのidです。つまりいいねを押された投稿のidとなります。
`visited_id: user_id`のuser_idとは@productに紐づいたuser_id。つまりいいねを押された投稿を作成したuserのidを指す。
`if notification.visitor_id == notification.visited_id`これは通知を送っているuserのidと通知を送られているuserのidが同じ場合は通知を送らないと言うもの。つまり自分の投稿に自分でいいねを押したときは通知を送らないようにしている。

![スクリーンショット 2021-02-15 15.40.27.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880610/21a7fd14-7914-bc0c-c2a6-26ddbf06897e.png)



## コメント通知メソッド
この3つの中で一番めんどくさいですが頑張ってください！！

```product.rb
def create_notification_comment(current_user, comment_id)
    temp_ids = Comment.where(product_id: id).select(:user_id).where.not("user_id = ? or user_id = ?", current_user.id, user_id).distinct 
    temp_ids.each do |temp_id|
      save_notification_comment!(current_user, comment_id, temp_id['user_id'])
    end
    save_notification_comment(current_user, comment_id, user_id)
  end

  def save_notification_comment(current_user, comment_id, visited_id)#(通知をした人,通知されたコメント,通知された人)
    notification = current_user.active_notifications.new(
      product_id: id,
      comment_id: comment_id,
      visited_id: visited_id,
      action: 'comment'
    )
    if notification.visitor_id == notification.visited_id
      notification.checked = true
    end
    notification.save if notification.valid?
  end
```
一つずつ解説していきます。
流れとしては、comments.controllerのcreateアクションでcreate_notification_comment呼び出しています。次にそのcreate_notification_commentの中でsave_notification_commentを呼び出しています。
簡単にそれぞれのメソッドの役割を簡単に説明すると、
create_notification_commentは、フォロー、いいねと違って通知を送るべき人が複数人いるので誰に通知を送るのかを決めている
save_notification_commentは、実際に通知を作成する
`create_notification_comment(current_user, comment_id)`では引数をふたつ受け取っています。ここで`comment_id`が必要な理由としては同じ投稿にコメントしている全ての人に通知を送るためです。
`temp_ids = Comment.where(product_id: id).select(:user_id).where.not("user_id = ? or user_id = ?", current_user.id, user_id).distinct`
ここでは#同じ投稿にコメントしているユーザに通知を送っています（current_userと投稿ユーザーのぞく）
また,`distinct`は重複を許さないものです。つまり同じ投稿に複数回コメントをしている人に対しても一回だけの通知を送ることになります。
`temp_ids.each do |temp_id|
      save_notification_comment!(current_user, comment_id, temp_id['user_id'])
    end`
ここでは先ほど取得したユーザー達へ通知を作成しています。
`(current_user, comment_id, temp_id['user_id'])`引数の説明としては、
現在のユーザ（current_user）がある投稿にコメント(comment_id)している全ユーザ(temp_id['user_id'])に通知を作成するという意味です。
`save_notification_comment(current_user, comment_id, user_id)`はコメントされている投稿を作成した人への通知です。

そうして`def save_notification_comment(current_user(通知をした人）, comment_id(通知されたコメント), visited_id(通知された人)`に渡ってきます。
この中ではフォロー通知、いいね通知同様に必要なカラムに値を代入した上で`active_notifications`を作成しています。

![スクリーンショット 2021-02-15 15.49.57.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880610/0a6c7109-5013-3b14-918d-cc7e6317b024.png)

# 6.通知一覧ページ(notifications.controller)
あと一歩です！！あとは通知一覧画面を作っていきます。頑張りましょー

```
rails g controller notifications
```

```config/routes.rb
resources :notifications, only: [:index, :destroy]
```

```app/controllers/notifications_controlloer.rb
def index
    @notifications = current_user.passive_notifications #ユーザが受け取る通知の全て
    @notifications.where(checked: false).each do |notification| #indexページを開いた瞬間に通知のcheckedは全てtrueに変える
      notification.update(checked: true)
    end
  end
  def destroy
    @notifications =current_user.passive_notifications.destroy_all #
    redirect_to notifications_path
  end
```
indexの中ではユーザが受け取る全ての通知を@notifications の中に入れています。
なんでいきなりpassive_notificationsが登場するのか疑問に思った人もいるかもしれません。
これはuserテーブルとnotificationテーブルの関係が
```
has_many :active_notifications, class_name: "Notification", foreign_key:"visitor_id"
  has_many :passive_notifications, class_name: "Notification", foreign_key:"visited_id"
```
となっていたため参照できるのです。わかりやすくするために図にすると以下のようになります。

![スクリーンショット 2021-02-15 16.52.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/880610/05edc0e3-ec8f-99a6-99e0-ae8fe5349df9.png)

つまり通知メソッドの中でactive_notificationsを作成したので、同じカラムを持つpassive_notificationsからでも参照できるようになったということです！

 destroyの方はそのままです。受け取った通知の全削除です。

# 7.通知一覧ページ(view)

```app/views/notifications/index.html.erb
<h3 class="text-center">通知</h3>
<%= link_to "通知削除", notification_path(@notifications), method: :delete ,class: "fas fa-trash" %>
<% if @notifications.present? %>
    <div class="users-index">
        <%= render @notifications %>
    </div>
<% else %>
    <p>通知はありません</p>
<% end %>
```

```
<%= render @notifications %>
```
は省略形を使っています。もう少し噛み砕くと、

```
render partial: 'notifications/notification', collection: @notifications
```

となります。これでも少しわかりにくいのでさらに噛み砕くと、

```
<% @notifications.each do |notification| %>
  <%= render partial: 'notification', locals: {notification: @notification} %>
<% end %>
```

となります。つまり受け取った通知を一つずつ順番に_notification.html.erbファイルに入れていく処理となります。


次にその飛ばした先のファイルです。
ここでは飛んできた通知を`notification_form(notification)`のヘルパーで設定したメソッドに入れています。

```_notification.html.erb
<div class="notification-view">
  <%= notification_form(notification) %><span class="moderate-font"><%= " (#{time_ago_in_words(notification.created_at)}前)" %></span>
  <br>
  <% if !@comment.nil? %>
    <p class="notification-comment"><%= @comment %></p>
  <% end %>
</div>
```
`<% if !@comment.nil? %>`はコメントの通知ならそのコメントの内容も同時に表示するものです。

```app/helprs/notifications_helper.rb
module NotificationsHelper
  def notification_form(notification)
    @visitor = notification.visitor
    @comment = nil
    @visitor_comment = notification.comment_id
    case notification.action
    when 'follow'
      tag.a(notification.visitor.name, href: user_path(@visitor)) + 'があなたをフォローしました'
    when 'like'
      tag.a(notification.visitor.name, href: user_path(@visitor)) + 'が' + tag.a('あなたの投稿', href: micropost_path(notification.micropost_id)) + 'にいいねしました'
    when 'comment' then      
      @comment = Comment.find_by(id: @visitor_comment)
      @comment_content =@comment.body
      @micropost_title =@comment.product.title
      tag.a(@visitor.name, href: user_path(@visitor)) + 'が' + tag.a("#{@product_title}", href: product_path(notification.product_id)) + 'にコメントしました'
    end
end
```
このヘルパー内の処理は、
フォロー通知なら、「〜〜さんがフォローしました。」
いいね通知なら、「〜〜さんがいいねしました。」
コメント通知なら、「〜〜さんがコメントしました。」
とactionによって表示する文章を分ける処理です。

これで以上です！！！ありがとうございました！！