通知機能実装

自分用にまとめます。

# モデルの作成

```
$ rails g model Notification visiter_id:integer visited_id:integer
  micropost_id:integer comment_id:integer action:string checked:boolean
```

```ruby:20200724090751_create_notifications.rb
class CreateNotifications < ActiveRecord::Migration[5.2]
  def change
    create_table :notifications do |t|
      # 通知を送ったユーザーのid
      t.integer :visiter_id
      # 通知を受け取ったユーザーのid
      t.integer :visited_id
      # いいねされた投稿のid
      t.integer :item_id
      # 投稿へのコメントのid
      t.integer :comment_id
      # 通知の種類（いいね、コメント、フォロー）
      t.string :action
      # 通知を確認したかどうか（初期値をfalseに設定）
      t.boolean :checked, default: false, null: false

      t.timestamps
    end
  end
end
```

```
$ rails db:migrate
```

# モデルの関連付け

```ruby:user.rb
  # 自分が作った通知（自分がいいねやフォローなどをしたら作成される通知）
  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
```

```ruby:post.rb
  has_many :notifications, dependent: :destroy
```

```ruby:notification.rb

  # 新しい通知が上に来るように指定
  default_scope -> { order(created_at: :desc) }

  belongs_to :post, optional: true
  belongs_to :comment, optional: true
  
  # 関連するカラム名（visiter_id/visited_id）とモデル名（User）を指定している
  belongs_to :visiter, class_name: 'User', foreign_key: 'visiter_id', optional: true
  belongs_to :visited, class_name: 'User', foreign_key: 'visited_id', optional: true
```

# 通知メソッド作成

#### いいね通知

```ruby:post.rb

  def create_notification_like(current_user)
    # すでにいいねの通知をしたことがないかを検索
    liked = Notification.where(visiter_id: current_user.id, visited_id: user_id, post_id: id, action: 'like')
    # いいねの通知をしたことがなければ、新規通知作成
    if liked.blank?
      notification = current_user.active_notifications.new(post_id: id, visited_id: user_id, action: 'like')
      # 自分の投稿に対するいいねは通知が行かないようにする
      if notification.visiter_id == notification.visited_id
        notification.checked = true
      end
      notification.save if notification.valid?
    end
  end
```


#### コメント通知

```ruby:post.rb
  def create_notification_comment(current_user, comment_id)
    # 投稿にコメントした自分以外のユーザーをすべて取得し、全員に通知を送る
    commented_ids = Comment.select(:user_id).where(post_id: id).where.not(user_id: current_user.id).distinct
    commented_ids.each do |commented_id|
      save_notification_comment(current_user, comment_id, commented_id['user_id'])
    end
    # まだ誰もコメントしていない場合は、投稿者に通知を送る
    save_notification_comment(current_user, comment_id, user_id) if commented_id.blank?
  end

  def save_notification_comment(current_user, comment_id, visited_id)
    # コメントは複数回することがあるため、複数回通知する
    notification = current_user.active_notifications.new(post_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
```

#### フォロー通知

```ruby:user.rb
  def create_notification_follow!(current_user)
    # すでにフォローの通知をしたことがないかを検索
    followed = Notification.where(visiter_id: current_user.id, visited_id: id, action: 'follow')
    # いいねの通知をしたことがなければ、新規通知作成
    if followed.blank?
      notification = current_user.active_notifications.new(visited_id: id, action: 'follow')
      notification.save if notification.valid?
    end
  end
```

# コントローラで定義

#### いいね

```ruby:likes_controller.rb
  def create
    @post = Post.find(params[:post_id])
    unless @post.liked_by?(current_user)
      @like = current_user.likes.new(post_id: @post.id)
      @like.save
      # 追記
      @post.create_notification_like(current_user)
    end
  end
```

#### コメント

```ruby:comments_controller.rb
  def create
    @post = Post.find(params[:post_id])
    @comment = @post.comments.new(comment_params)
    @comment.user_id = current_user.id
    @comments = @post.comments.all
    if @comment.save
      # 追記
      @post.create_notification_comment(current_user, @comment.id)
      render :create
    else
      @post = Post.find(params[:post_id])
      @comments = @post.comments.all
      render 'posts/show'
    end
  end
```

#### フォロー

```ruby:relationships_controller.rb
  def follow
    @user = User.find(params[:id])
    current_user.follow(params[:id])
    #追記
    @user.create_notification_follow(current_user)
    @posts = @user.posts.all
    render :create
  end
```

# コントローラの作成
```
$ rails g controller notifications
```

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

```ruby:notifications_controller.rb
class NotificationsController < ApplicationController

  def index
    # current_userが受け取った通知一覧
    @notifications = current_user.passive_notifications
    # @notificationの中でまだ確認していない通知
    @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 notifications_path
  end

end
```




# ビューの作成

```ruby:notifications/index.html.slim
.notification-visual
  .container
    .row
      .col-lg-5
      .col-lg-2.text-center
        h4.mt-5.mb-4.font-weight-bold.notification-caption 通知

    .row
      .col-lg-3
      .col-lg-6
        .frame-notification
          = link_to notifications_destroy_all_path, method: :delete do
            i.fas.fa-trash style="color: black;"
                h7 style="color: black;" 全削除
            <hr>
          - notifications = @notifications.where.not(visiter_id: current_user.id)
          - if notifications.exists?
            # _notification.html.slimファイルにパーシャルで飛ばす
            = render notifications
          - else
            p 通知はありません
```

```ruby:notifications/_notification.html.slim
- visiter = notification.visiter
- visited = notification.visited

.form-inline
  span
    = link_to user_path(visiter) do
      = attachment_image_tag visiter, :image, format: 'jpeg', fallback: "noimage.png", size: "40x30", class: "mr-2"
      = visiter.name
    = ' さんが'

    - case notification.action
    - when 'follow' then
      = "あなたをフォローしました"
    - when 'like' then
      = link_to 'あなたの投稿', notification.post, style: "font-weight: bold;"
      = "にいいねしました"
    - when 'comment' then
      - if notification.post.user_id == visited.id
        = link_to "あなたの投稿", notification.post, style: "font-weight: bold;"
      - else
        span
          = link_to post_path(notification.post) do
            = attachment_image_tag notification.post.user, :image, format: 'jpeg', fallback: "noimage.png", size: "25x20", class: "mr-1 ml-1"
            strong
              = notification.post.user.name + 'さんの投稿'
      = "にコメントしました"
      p.text-muted.mb-0
        = Comment.find_by(id: notification.comment_id)&.comment

.small.text-muted.text-right
  = time_ago_in_words(notification.created_at).upcase
hr
```
<br>

通知がきたら通知のボタン部分に丸いアイコンを表示させる

```ruby:layout/_user.html.slim
-if unchecked_notifications.any?
  li.btn.btn-light.home-btn= link_to notifications_path, class: 'list' do
    i.fas.fa-circle.n-circle
    i.fas.fa-bell 
    |  通知 
-else
  li.btn.btn-light.home-btn= link_to notifications_path, class: 'list' do
    i.fas.fa-bell
    |  通知 
```

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

**完成！**