😽

Rails|非同期通信のコメントの通知機能

2023/09/11に公開

目標

・他のユーザーが自分の投稿にコメントをした際に、通知が来るようにする。
・自分で自分の投稿にコメントした際には通知が来ない。
・未読の通知がある場合、ヘッダーの通知ボタンに🔴がつくようにする。

ER図

開発環境

ruby 3.1.2p20
Rails 6.1.7.4
Cloud9

前提

User, Review, Commentモデルは作成済み。
comment機能は作成済み。

モデルの作成

Notificationモデルを作成する

ターミナル
$ rails g model Notification

マイグレーションファイルを編集する

xxx_create_notifications
class CreateNotifications < ActiveRecord::Migration[6.1]
  def change
    create_table :notifications do |t|

      t.integer :visiter_id, null: false
      t.integer :visited_id, null: false
      t.integer :review_id
      t.string :action
      t.boolean :is_checked, default: false, null: false

      t.timestamps
    end
  end
end

visiter_id
コメントするユーザー

visited_id
コメントされるユーザー

action
何をされたのか

is_checked
既読か未読か

DBに反映する

ターミナル
$ rails db:migrate

ルーティングの編集

以下のように追記する。

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

モデルの編集

notification.rb
class Notification < ApplicationRecord

  default_scope -> { order(created_at: :desc) }
  belongs_to :review
  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

default_scope -> { order(created_at: :desc) }
Notificationのレコードを取得するとき、デフォルトで新着順に並び替えられるように設定。

belongs_to :visiter, class_name: 'User', foreign_key: 'visiter_id', optional: true
visiter(Userモデル)に紐づいており、FKがvisiter_idであることを示している。また、optional: trueで、visiter_idがnilであっても問題ないということにしている。

belongs_to :visited, class_name: 'User', foreign_key: 'visited_id', optional: true
visited(User)モデルに紐づいており、FKがvisited_idである。また、visited_idがnilであっても問題ない。

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

active_notification(Notificationモデル)に紐づいており、FKはvisiter_idである。また、Userが削除された場合、そのUserに紐づくNotificationは削除される。

passive_notification(Notificationモデル)に紐づいており、FKはvisiter_idである。また、Userが削除された場合、そのUserに紐づくNotificationは削除される。

review.rb
has_many :notifications, dependent: :destroy

# 通知作成
  def create_notification_by(current_user)
      notification = current_user.active_notifications.new(
        review_id: id,
        visited_id: user_id,
        action: "comment"
      )

  if notification.visiter_id == notification.visited_id
     notification.is_checked = true
  end

    notification.save if notification.valid?
  end

notification = current_user.active_notifications.new(...)
current_userのactive_notificationsという関連付けから、新しいnotificationオブジェクトを生成する。内容は()の通り。

if notification.visiter_id == notification.visited_id
コメントの送信者と受信者が同一人物の場合、通知は自動的に既読となる。

コントローラの作成

ターミナル
$ rails g controller public/notifications
notifications_controller.rb
class Public::NotificationsController < ApplicationController

  def index
    @notifications = current_user.passive_notifications.page(params[:page]).per(20)
    @notifications.where(is_checked: false).each do |notification|
      notification.update(is_checked: true)
    end
  end

  def destroy
    @notifications = current_user.passive_notifications.destroy_all
    redirect_to notifications_path
  end

end

@notifications = current_user.passive_notifications.page(params[:page]).per(20)
current_userに紐づくpassive_notificationsを表示。ページネーションを導入済み。

@notifications.where(is_checked: false).each do |notification| ... end
未読の通知を取得し、既読処理をする。

コメントコントローラの編集

comments_controller.rb
class Public::CommentsController < ApplicationController
  before_action :authenticate_user!, only: [:create, :destroy]

  def create
    comment = Comment.new(comment_params)
    comment.user_id = current_user.id
    comment.review_id = params[:comment][:review_id]
    if comment.save
      @review = comment.review
      @review.create_notification_by(current_user) ##ココを追記
    else
      flash[:alert] = comment.errors.full_messages.join(", ")
      redirect_back(fallback_location: root_path)
    end
  end
  ...

@review.create_notification_by(current_user)
ここが今回の追記部分。
コメントが保存された後、notificationを作成する。このメソッドの詳細はreviewモデルに記述済み。

ビューの作成


notifications/index.html.erb
<div class="container">
    <div class='row justify-content-center mx-auto'>
        <div class="notification_box mt-5">
            <h5 class="text-center"><i class="fa-solid fa-bell"></i> <span class="translatable-text">通知</span></h5>
            <% if @notifications.exists? %>
                <p class="text-right"><%= link_to "通知削除",notification_path(@notifications), method: :delete, class:"btn btn-secondary translatable-text" %></p>
            <%= render @notifications %>
            <% else %>
            <p class="translatable-text">通知はありません</p>
            <% end %>
    </div>
   </div>
</div>

<%= render @notifications %>
この部分は、省略型の部分テンプレート呼び出し。
Railsが自動的に、同じビューフォルダ内の_notification.html.erbを探す。
そして、_notification.html.erb@notificationsの一つずつのオブジェクトに対して繰り返しレンダリングされる。

notifications/_notification.html.erb
<% visiter = notification.visiter %>
<% review = notification.review %>

<ul class="list-unstyled">
    <li class="translatable-text">
        <%= visiter.name %>さんが
        <%= link_to "あなたの投稿したレビュー", review_path(notification.review_id) %>にコメントしました。
        <%= " (#{time_ago_in_words(notification.created_at)}前)" %></li>
</ul>

shared/_header.html.erb
<% if unchecked_notifications.any? %>
  <li>
    <div class="alert-wrapper">
      <%= link_to notifications_path, class: "btn btn-outline-secondary mr-3 translatable-text" do %>
        <i class="fa-solid fa-bell"></i> 通知
      <% end %>
        <i class="fa-solid fa-circle fa-sm text-danger alert-mark"></i>
      </div>
      <% else %>
      <li>
        <%= link_to notifications_path, class: "btn btn-outline-secondary mr-3 translatable-text" do %>
          <i class="fa-solid fa-bell"></i> 通知
        <% end %>
      <% end %>
      </li>

ヘルパーモジュールの作成

notifications_helper.rb
module Public::NotificationsHelper

  def unchecked_notifications
    current_user.passive_notifications.where(is_checked: false)
  end

end

ヘルパーモジュールとは、ビュー内で使用できるメソッドの集合。上記のビューで使用している。

参考にさせていただいた記事

https://qiita.com/ki_87/items/6feb324140bf5b0349ff

Discussion