[Rails]通知機能の実装

## 実装すること
自分の投稿にいいねされた時・コメントをもらった時、フォローされた時に通知が来るようにします。

## 完成イメージ
通知を未確認の状態では、ヘッダーのNOTICEにマークが付きます。
通知を確認すると、マークが消えます。
<img width="316" alt="通知イメージ(マーク).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539971/77628694-77b1-2e68-701f-cf3d457ceb41.png">
ユーザーは自分への通知を見ることができます。
通知はまとめて全削除することができます。
<img width="639" alt="通知一覧.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539971/e4c5c798-209a-724a-c303-09e34036d2e2.png">

## ER図
**Notificationテーブル**
item_id : いいねされた投稿のid
visiter_id : 通知を送ったユーザーのid
visited_id : 通知を送られたユーザーのid
comment_id : 投稿へのコメントのid
action : 通知の種類（フォロー、いいね、コメント）
checked : 通知を送られたユーザーが通知を確認したかどうか default: false

<img width="645" alt="通知ER図.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539971/e6fbbf9f-a9fb-987b-ce37-7dc6a1cc4773.png">

## モデルの作成
Userモデル、Itemモデルは作成した前提で進めていきます。
作成手順は下記リンク先で説明しております。
[Rails]Ajaxを用いて非同期で投稿機能といいね機能の実装https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448

```
$ rails g model Notification visiter_id:integer visited_id:integer item_id:integer comment_id:integer action:string checked:boolean
```
## マイグレーションファイルの追記
checked(通知を送られたユーザーが通知を確認したかどうか)のdefault: false,null: falseを追記します。

```app/db/migrate/20200119094201_create_notification.rb
class CreateNotifications < ActiveRecord::Migration[5.2]
  def change
    create_table :notifications do |t|
      t.integer :visiter_id
      t.integer :visited_id
      t.integer :item_id
      t.integer :comment_id
      t.string :action
      t.boolean :checked, default: false, null: false ←追記

      t.timestamps
    end
  end
end
```

```
$ rails db:migrate
```

## アソシエーションの確認
### Userモデル
・自分が作った通知(active_notifications)と自分宛の通知(passive_notifications)の関連付けメソッドを実装します。
・`active_notifications`では、`class_name: "Notification"`でNotificationモデルの、`foreign_key: "visiter_id"` で、visiter_idを参考に、active_notificationsモデルへアクセスするようにしています。
passive_notificationsも同じです。

```app/models/user.rb
class User < ApplicationRecord

   has_many :active_notifications, class_name: "Notification", foreign_key: "visiter_id", dependent: :destroy
   has_many :passive_notifications, class_name: "Notification", foreign_key: "visited_id", dependent: :destroy

end
```

## Itemモデル

```app/models/item.rb
class Item < ApplicationRecord

   has_many :notifications, dependent: :destroy

end
```

## Notificationモデル
`optional: true`はitem_idにnilを許容するものです。railsではbelongs_toのつけられたカラムには自動的にallow_nil: falseが付与されます。フォロー通知ではitem_idは関与しないためnilとなるので、このオプションをつけないとフォロー通知が有効になりません。

```app/models/notification.rb
class Notification < ApplicationRecord

    #スコープ(新着順)
	default_scope->{order(created_at: :desc)}

  	belongs_to :item, optional: true
  	belongs_to :comment, optional: true
  	belongs_to :visiter, class_name: 'User', foreign_key: 'visiter_id', optional: true
	belongs_to :visited, class_name: 'User', foreign_key: 'visited_id', optional: true

end
```
## コントローラーの作成

```
$ rails g controller notifications
```

## ルーティングの設定

```config/routes.rb
Rails.application.routes.draw do

    resources :notifications, only: :index

end
```

## 通知メソッドの作成
### Itemモデル(いいねとコメントのメソッド)
`distinctメソッド`は、重複レコードを1つにまとめるためのメソッドです。

```app/models/item.rb
    def create_notification_by(current_user)
	    notification = current_user.active_notifications.new(
	      item_id: id,
	      visited_id: user_id,
	      action: "like"
	    )
	    notification.save if notification.valid?
  	end

  	def create_notification_comment!(current_user, comment_id)
	    # 自分以外にコメントしている人をすべて取得し、全員に通知を送る
	    temp_ids = Comment.select(:user_id).where(item_id: id).where.not(user_id: current_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) if temp_ids.blank?
  end

  	def save_notification_comment!(current_user, comment_id, visited_id)
        # コメントは複数回することが考えられるため、１つの投稿に複数回通知する
        notification = current_user.active_notifications.new(
          item_id: id,
          comment_id: comment_id,
          visited_id: visited_id,
          action: 'comment'
        )
        # 自分の投稿に対するコメントの場合は、通知済みとする
        if notification.visiter_id == notification.visited_id
          notification.checked = true
        end
        notification.save if notification.valid?
     end
```

### Userモデル(フォローのメソッド)

```app/models/user.rb
 #フォロー時の通知
 def create_notification_follow!(current_user)
    temp = Notification.where(["visiter_id = ? and visited_id = ? and action = ? ",current_user.id, id, 'follow'])
    if temp.blank?
      notification = current_user.active_notifications.new(
        visited_id: id,
        action: 'follow'
      )
      notification.save if notification.valid?
    end
  end
```

## いいねの通知(likes_controller)
いいねの作成と同時に通知も作成されます。

```app/controllers/likes_controller.rb
  def create
      like = current_user.likes.new(item_id: @item.id)
      like.save
      @item = Item.find(params[:item_id])
      #通知の作成
      @item.create_notification_by(current_user)
      respond_to do |format|
        format.html {redirect_to request.referrer}
        format.js
      end
  end
```

## コメントの通知(comments_controller)

```app/controllers/comments_controller.rb
def create
    @item = Item.find(params[:item_id])
    #投稿に紐づいたコメントを作成
    @comment = @item.comments.build(comment_params)
    @comment.user_id = current_user.id
    @comment_item = @comment.item
    if @comment.save
      #通知の作成
      @comment_item.create_notification_comment!(current_user, @comment.id)
      render :index
    end
end
```

## フォローの通知(relationships_contoroller)

```app/controllers/comments_controller.rb
def create
	  @user = User.find(params[:following_id])
	  current_user.follow(@user)
	  #通知の作成
	  @user.create_notification_follow!(current_user)
end
```

## 通知一覧ページの作成
### notification_controller
・indexを開いたらchecked:falseが全てchecked:trueになるようにしています。
・ログインユーザーの通知全削除では、`destroy_allメソッド`を使用しています。
参考:https://pikawaka.com/rails/destroy_all

```app/controllers/notification_controller.rb
class Users::NotificationsController < ApplicationController

	def index
      #current_userの投稿に紐づいた通知一覧
    	@notifications = current_user.passive_notifications
      #@notificationの中でまだ確認していない(indexに一度も遷移していない)通知のみ
    	@notifications.where(checked: false).each do |notification|
      		notification.update_attributes(checked: true)
        end
  	end

  	def destroy_all
      #通知を全削除
  		@notifications = current_user.passive_notifications.destroy_all
  		redirect_to users_notifications_path
  	end

end

```


### 通知一覧(notifications/index.html.erb)
通知内容は、パーシャルにします。
<img width="639" alt="通知一覧.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539971/66559ead-0309-3a17-6575-355d265c1e54.png">


```app/views/notifications/index.html
<h3 class="text-center">通知</h3>
    <%= link_to destroy_all_users_notifications_path, method: :delete do %>
        <i class="fas fa-trash" style="color: black;"></i>
        <h7 style="color: black;">全削除</h7>
    <% end %>
    <hr>
<% if @notifications.exists? %>
    <div class="users-index">
        <%= render @notifications %>
    </div>
<% else %>
    <p>通知はありません</p>
<% end %>
```

### 通知内容
#### notification_helper
通知内容によって、表示内容を分岐させます。

```app/helpers/notifications_helper.rb
module Users::NotificationsHelper

   def notification_form(notification)
	  @visiter = notification.visiter
	  @comment = nil
	  your_item = link_to 'あなたの投稿', users_item_path(notification), style:"font-weight: bold;"
	  @visiter_comment = notification.comment_id
	  #notification.actionがfollowかlikeかcommentか
	  case notification.action
	    when "follow" then
	      tag.a(notification.visiter.name, href:users_user_path(@visiter), style:"font-weight: bold;")+"があなたをフォローしました"
	    when "like" then
	      tag.a(notification.visiter.name, href:users_user_path(@visiter), style:"font-weight: bold;")+"が"+tag.a('あなたの投稿', href:users_item_path(notification.item_id), style:"font-weight: bold;")+"にいいねしました"
	    when "comment" then
	    	@comment = Comment.find_by(id: @visiter_comment)&.content
	    	tag.a(@visiter.name, href:users_user_path(@visiter), style:"font-weight: bold;")+"が"+tag.a('あなたの投稿', href:users_item_path(notification.item_id), style:"font-weight: bold;")+"にコメントしました"
	  end
	end

end
```

#### notifications/_notification.html.erb

```app/views/notifications/_notification.html
<% visiter = notification.visiter %>
<% item = notification.item %>

<div class="user-view clearfix ">
  <%= link_to users_user_path(notification.visiter) do %>
    <%= attachment_image_tag visiter, :profile_image, :fill,100,100, format: "jpeg", fallback: "no_image.jpg", size: "50x50", class:"profile-img-circle" %>
  <% end %>
  <%= notification_form(notification) %><span class="moderate-font"><%= " (#{time_ago_in_words(notification.created_at)} 前)" %></span>
  <br>
  <% if !@comment.nil? %>
    <p class="moderate-font text-center" style="color: #C0C0C0;"><%= @comment %></p>
  <% end %>
</div>
```

## 未確認の通知があるときはマークで知らせる
### notifications_helper
未確認の通知(checked:falseの通知)を示すunchecked_notificationsメソッドをnotifications_helperに書きます。

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

### layouts/application.html.erb
未確認の通知があるときは、黄色いマークで知らせます。

<img width="316" alt="通知イメージ(マーク).png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/539971/5c2f153d-a5e1-49dd-57be-055a10ec840a.png">


```app/views/layouts/application.html
<% if unchecked_notifications.any? %>
    <i class="fa fa-circle" style="color: gold;"></i>
<% end %>
<li>
    <%= link_to "NOTICE", users_notifications_path, class: "btn-default" %>
</li>
```


## 最後に
最後までご覧いただきありがとうございます。
初学者ですので間違っていたり、分かりづらい部分もあるかと思います。
何かお気付きの点がございましたら、お気軽にコメントいただけると幸いです。
いいねとコメントとフォローは下記リンク先で実装しています。
↓
[Rails]Ajaxを用いて非同期で投稿機能といいね機能の実装
https://qiita.com/yuto_1014/items/78d8b52d33a12ec33448
[Rails]Ajaxを用いて非同期でコメント機能の実装
https://qiita.com/yuto_1014/items/c7d6213139a48833e21a
[Rails]Ajaxを用いて非同期でフォロー機能の実装
https://qiita.com/yuto_1014/items/8d508b84fd0c2316ba01


## 参考
【学習アウトプット4】通知機能の作り方
https://qiita.com/tktk0430/items/bdb8fbcf4ce3258b2d41