[Rails] 通知機能

DMが来た時、いいねされた時に通知が来る通知機能を実装した時のメモです。
DMといいね機能は既に実装済みとします。

DMの実装は以下の記事を参考にしてください。

https://qiita.com/aaaasahi_17/items/9e7f344488c720aaf116

# 実装

## ① モデルの作成
通知モデルを作成します。

```terminal:ターミナル 
$ rails g model Notification
```

以下を記述してmigrate。
`action`にはいいねかDMどっちの通知なのかが入ります。
`checked`には通知が既読済みかどうかの部分です。

`add_index`を使用すると検索が早く行われるらしいです。

```ruby:db/migrate/20210712225000_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 :event_id
      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, :event_id
    add_index :notifications, :room_id
    add_index :notifications, :message_id
  end
end
```

```terminal:ターミナル 
$ rails db:migrate
```

## ② モデルの関連付け
作成した`Notification`モデルを関連付けていきます。
関連付けるモデルは`User`、投稿モデルの`Event`、DMに関するモデル`Room`と`Message`です。

- `User`モデル

```ruby:app/models/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`は相手からの通知です。
Userが消えたら通知も削除するので、`dependent: :destroy`

- `Event`モデル

```ruby:app/models/event.rb
has_many :notifications, dependent: :destroy
```

- `Room`モデル

```ruby:app/models/room.rb
has_many :notifications, dependent: :destroy
```

- `Message`モデル

```ruby:app/models/message.rb
has_many :notifications, dependent: :destroy
```

- `Notification`モデル

```ruby:app/models/notification.rb
class Notification < ApplicationRecord
  default_scope -> { order(created_at: :desc) }
  belongs_to :event, optional: true
  belongs_to :comment, optional: true
  belongs_to :room, optional: true
  belongs_to :message, optional: true

  belongs_to :visitor, class_name: 'User', foreign_key: 'visitor_id', optional: true
  belongs_to :visited, class_name: 'User', foreign_key: 'visited_id', optional: true
end
```
`default_scope`で常に新しい通知からデータを取得するようにデフォルトの並び順を作成日時の降順で指定しています。
`optional: true`で`nil`を許可しています。
`nil`を許可することでDM通知ならいいねは`nil`、いいねならDMは`nil`を格納することができます。

## ③ 通知作成メソッド

### いいね
いいねが押された時に通知されるメソッドを作ります。

```ruby:app/models/event.rb
 def create_notification_like!(current_user)
      # すでいいねの通知があるか?
    temp = Notification.where(["visitor_id = ? and visited_id = ? and event_id = ? and action = ? ", current_user.id, user_id, id, 'join'])

      # ない場合いいねの通知作成
    if temp.blank?
      notification = current_user.active_notifications.new(
        event_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
  end
```
いいねされていない場合のみ通知を作成することでいいねを間違って連打した時に初回通知のみで、毎回通知が行くことがなくなります。

あとはいいねモデルのControllerで通知作成メソッドを呼びます。

```ruby:app/controllers/likes_controller.rb
def create
  event = Event.find(params[:event_id])
  event.likes.create!(user_id: current_user.id)

#---------------------追加----------------------
  event.create_notification_like!(current_user)
#----------------------------------------------

  render json: { status: 'ok' }
end
```

### DM
続いてDMです。
DMはMessage Controllerに先程の処理を書いていきます。

```ruby:app/controllers/messages_controller.rb
 def create
   message = Message.new(message_params)
   message.user_id = current_user.id
#-------追加------------
   room = message.room
#----------------------
   if message.save
#-----------------------追加-----------------------------------------
     another_room = Entry.where(room_id: room.id).where.not(user_id: current_user.id)
     theid = another_room.find_by(room_id: room.id)
     notification = current_user.active_notifications.new(
       room_id: room.id,
       message_id: message.id,
       visited_id: theid.user_id,
       visitor_id: current_user.id,
       action: 'dm'
      )
      if notification.visitor_id == notification.visited_id
          notification.checked = true
      end
      notification.save if notification.valid?
#--------------------------------------------------------------------

      redirect_to room_path(message.room)
    else
      redirect_back(fallback_location: root_path)
    end
  end

```
やっていることはいいねの時と基本的には同じです。

## ④ 通知一覧
通知の一覧画面を作っていきます。

```terminal:ターミナル
$ rails g controller notifications
```

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

```ruby:app/controllers/notifications_controller.rb 
class NotificationsController < ApplicationController
  before_action :authenticate_user!

  def index
    @notifications = current_user.passive_notifications
  end

  def update
    notification = Notification.find(params[:id]) 
    if notification.update(checked: true) 
      redirect_to notifications_path
    end
  end
end 
```
`index`は受け取った通知一覧を表示。
`update`は通知を既読済みにするためのものです。

Viewを作成します。


```ruby:app/views/notifications/index.html.haml
# 自分の投稿にいいねした場合は一覧に表示されないように
- notifications = @notifications.where.not(visitor_id: current_user.id)
.container 
  - breadcrumb :notice
  = breadcrumbs separator: " &rsaquo; "
  .title 
    %h2= I18n.t('notice.title')
  - if notifications.exists?
    = render notifications
  - else
    %p 通知はありません 
```

- テンプレート部分

```ruby:app/views/notifications/_notification.html.haml
- visitor = notification.visitor
- visited = notification.visited
- if notification.checked == false
  .card 
    .card-body
      .d-flex
        .avatar-wrapper
          = image_tag visitor.avatar_image, class:'avatar-notice'
        .notice-wrapper
          = link_to account_path(visitor),class: 'btn btn-link notice-link' do
            = visitor.display_name
          さんが
          - case notification.action
          - when 'dm' then
            あなたに
            = link_to 'メッセージ', notification.room, class: 'btn btn-link notice-link'
            を送りました。
            = link_to I18n.t('notice.read'),notification_path(notification.id),method: :put, class: 'btn btn-link notice-link'
          - when 'like'
            = link_to 'あなたのイベント', notification.event, class: 'btn btn-link notice-link'
            にいいねしました。
            = link_to I18n.t('notice.read'),notification_path(notification.id),method: :put, class: 'btn btn-link notice-link'
          .time 
            = notification.how_long_ago
```

`action`で表示を分岐しています。
ここは実際に画像で見てもらった方が早いので画像を載せておきます。
![スクリーンショット 2021-10-06 10.13.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974719/bf790e01-08ac-2742-7be7-c4b3a7b54ba6.png)
ちなみに通知を受けった時間を表示したい場合は以下のメソッドを追加するといい感じに表示してくれます！

```ruby:app/decorators/notification_decorator.rb
module NotificationDecorator
  def how_long_ago
    if (Time.now - self.created_at) <= 60 * 60
      ((Time.now - self.created_at) / 60).to_i.to_s + "分前"
    elsif (Time.now - self.created_at) <= 60 * 60 * 24
      ((Time.now - self.created_at) / 3600).to_i.to_s + "時間前"
    elsif (Date.today - self.created_at.to_date) <= 30
      (Date.today - self.created_at.to_date).to_i.to_s + "日前"
    else
      self.created_at
    end
  end
end 
```


## ⑤ 通知があるか 
最後に通知があるかどうかを表示させます。

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

helperに未確認の通知を検索するメソッドを作ります。

あとは条件分岐で振り分けてCSSを当てれば完成です。

```ruby:app/views/layouts/application.html.haml
 %li.nav-item
   - if unchecked_notifications.any?
     = link_to I18n.t('header.notice'), notifications_path , class: "nav-link notice-active"
  - else  
     = link_to I18n.t('header.notice'), notifications_path , class: "nav-link"
```

完成イメージ
通常時
![スクリーンショット 2021-10-06 10.11.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974719/a2fafb5d-8329-6aee-437f-dfcb0c5366fe.png)


通知がある時
![スクリーンショット 2021-10-06 10.10.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/974719/238b4cbc-3568-1282-c2ff-c17314094b56.png)

これで完成！