LoginSignup
2
1

Rails 通知機能の実装

Last updated at Posted at 2024-03-14

はじめに

今回はXクローンの実装にて通知機能の実装をしたので、自分自身の備忘録として、また今後通知機能を実装する方のお役に立てればと思いこちらの記事を書くことにしました。

通知機能の作成

今回は、いいね リポスト コメント フォローをされた場合に、ウェブ上で通知を確認できる機能と、
メールでお知らせする機能を実装しました。

※ 前提として、投稿機能、いいね機能、リポスト機能、コメント機能、フォロー機能などが既に実装されているものとします。
過去に他の機能は実装しているものもあるので、合わせて参考にしてください。

また、いいね機能だけや、コメント機能だけでも、部分的に実装されている場合でも、引き抜いて実装していただければ問題ありません!!

通知モデルの概要

テーブルの情報

visitor_id visited_id post_id comment_id action checked
1 2 nil nil follow false
1 2 3 nil like false
1 2 3 nil repost false
1 2 nil 4 comment false
  • visitor_id : 通知を送ったユーザーのid
  • visited_id : 通知を送られたユーザーのid
  • post_id : いいねされた投稿のid
  • comment_id : 投稿へのコメントのid
  • action : 通知の種類(フォロー、いいね、コメント)
  • checked : 通知を送られたユーザーが通知を確認したかどうか

こんな感じでテーブルの情報を作成使用と思います。
必要のない情報の部分は、nilを格納するようにしています。

ER図

スクリーンショット 2024-03-14 21.39.49.png

今回も少しややこしいER図です。(間違ってたらご指摘お願いします。)

フォロー機能の実装の時のように、ユーザーでもvisitor_idvisited_idに分かりやすいようにわけています。

モデルの作成

rails g model Notification
create_notifications.rb
class CreateNotifications < ActiveRecord::Migration[7.0]
  def change
    create_table :notifications do |t|
      t.integer :visitor_id, null: false
      t.integer :visited_id, null: false
      t.integer :post_id
      t.integer :comment_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, :post_id
    add_index :notifications, :comment_id
  end
end

値が必ず必要な部分には制約をつけています。
post_idやcomment_idはnilを使用したい時があるので、何も付けていません。

rails db:migrate

関連付けの設定

作成したnotificationモデルと、userモデルpostモデルcommentモデルを関連付けします。

user.rb
# active_notifications:自分からの通知
  has_many :active_notifications, class_name: 'Notification', foreign_key: 'visitor_id', dependent: :destroy
  # passive_notifications:相手からの通知
  has_many :passive_notifications, class_name: 'Notification', foreign_key: 'visited_id', dependent: :destroy

紐付ける名前とクラス名が異なるため、明示的にクラス名とIDを指定して紐付けます。

post.rb
has_many :notifications, dependent: :destroy
comment.rb
has_many :notifications, dependent: :destroy
natification.rb
default_scope -> { order(created_at: :desc) }
  belongs_to :post, optional: true
  belongs_to :comment, optional: true

  # 通知を送ったユーザー
  belongs_to :visitor, class_name: 'User'
  # 通知を送られたユーザー
  belongs_to :visited, class_name: 'User'

default_scopeでは、デフォルトの並び順を「作成日時の降順」で指定しました。
これにより、常に新しい通知からデータを取得することができるようになります。
例えば、Notification.firstを実行すると、一番古い通知ではなく、一番新しい通知が取得できます。

postcommentについているoptional: trueは、nilを許可するものです。
belongs_toで紐付ける場合はnilが許可されないのですが、今回はnilを設定したいので、付けています。

通知メソッドを作成する

いいね通知の作成メソッド

いいねボタンが押されたタイミングで、以下のようなデータを作成します。

visitor_id visited_id post_id comment_id action checked
いいねした人のid いいねされた人のid いいねされた投稿のid nil like False
post.rb
def create_notification_like!(current_user)
    # すでに「いいね」されているか検索
    temp = Notification.where(['visitor_id = ? and visited_id = ? and post_id = ? and action = ? ', 
                                current_user.id,user_id, id, 'like'])
    # いいねされていない場合のみ、通知レコードを作成
    return if temp.present?

    notification = current_user.active_notifications.new(
      post_id: id,
      visited_id: user_id,
      action: 'like'
    )
    # 自分の投稿に対するいいねの場合は、通知済みとする
    notification.checked = true if notification.visitor_id == notification.visited_id
    notification.save if notification.valid?
  end

ここでのポイントは、「いいねされていない場合のみレコードを作成する」という分岐処理が入っているところです。
いいねボタンを連打した場合、押した数だけ相手に通知が行ってしまうと、うるさくなってしまいます。
そのため、以下の検索処理を事前に行い、レコード存在チェックをします。

temp = Notification.where(['visitor_id = ? and visited_id = ? and post_id = ? and action = ? ', 
                            current_user.id,user_id, id, 'like'])

これで、いいねを連続でした場合でも、1度しか相手に通知がいかないようになります。

また、自分の投稿にいいねをして、「自分がいいねしました」と通知が来ても悲しくなるので、そうならないように、自分の投稿に対するいいねは、事前に通知済みの設定をしています。

notification.checked = true if notification.visitor_id == notification.visited_id
    notification.save if notification.valid?

リポスト通知の作成メソッド

リポストはいいねとほぼ変わらない感じです。

リポストボタンが押されたタイミングで、以下のようなデータを作成します。

visitor_id visited_id post_id comment_id action checked
リポストした人のid リポストされた人のid リポストされた投稿のid nil repost False
post.rb
def create_notification_repost!(current_user)
    # すでに「リポスト」されているか検索
    temp_1 = Notification.where(['visitor_id = ? and visited_id = ? and post_id = ? and action = ? ', current_user.id,
                                 user_id, id, 'repost'])
    # リポストされていない場合のみ、通知レコードを作成
    return if temp_1.present?

    notification = current_user.active_notifications.new(
      post_id: id,
      visited_id: user_id,
      action: 'repost'
    )
    # 自分の投稿に対するリポストの場合は、通知済みとする
    notification.checked = true if notification.visitor_id == notification.visited_id
    notification.save if notification.valid?
  end

いいね通知のlikeの部分をrepostに変更すればよいだけです。

コメント通知の作成メソッド

コメントが投稿されたタイミングで、以下のようなデータを作成します。

visitor_id visited_id post_id comment_id action checked
コメントした人のid コメントされた人のid nil コメントのid comment False
post.rb
def create_notification_comment!(current_user, comment_id)
    # 自分以外にコメントしている人をすべて取得し、全員に通知を送る
    temp_ids = Comment.select(:user_id).where(post_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)
    # コメントは複数回することが考えられるため、1つの投稿に複数回通知する
    notification = current_user.active_notifications.new(
      post_id: id,
      comment_id:,
      visited_id:,
      action: 'comment'
    )
    # 自分の投稿に対するコメントの場合は、通知済みとする
    notification.checked = true if notification.visitor_id == notification.visited_id
    notification.save if notification.valid?
  end
end

いいねやリポストの時と少し変わっています。
いいねやリポストの時は、既にいいねされているかどうかを事前にチェックして、されていれば通知の作成はしないような制御をいれていました。

しかしコメントは、1つの投稿に対して複数回コメントを残す可能性があるので、通知レコードの存在チェックはしていません。
なので、コメントが付いた回数だけ通知がいくようになっています。

また、投稿者に対してだけではなく、投稿者ではないけど、そのポストにコメントした人にも通知を届けたいため、最初に以下の処理を追加しています。

temp_ids = Comment.select(:user_id).where(post_id: id).where.not(user_id: current_user.id).distinct

そうすることによって、潔さんのところにこのような通知も来るようになります。
スクリーンショット 2024-03-14 22.35.09.png

フォロー通知の作成メソッド
フォローボタンを押されたタイミングで、以下のようなデータを作成します。

visitor_id visited_id post_id comment_id action checked
フォローした人のid フォローされた人のid nil nil follow False
user.rb
def create_notification_follow!(current_user)
    temp = Notification.where(['visitor_id = ? and visited_id = ? and action = ? ', current_user.id, id, 'follow'])
    return if temp.present?

    notification = current_user.active_notifications.new(
      visited_id: id,
      action: 'follow'
    )
    notification.save if notification.valid?
  end

フォローの場合もいいねのときと同じで、「連続でフォローボタンを押す」ことに備えて、同じ通知レコードが存在しないときだけ、レコードを作成するようにします。

通知メソッドの呼び出し

いいねリポストコメントフォローのタイミングで通知メソッドの呼び出しをします。

いいねを押した時

いいねを押したタイミングで通知レコードを作成するように、先ほど作ったメソッドをlikes_controllercreateアクションに追加していきます。

likes_controller.rb
def create
    @post = Post.find(params[:post_id])
    @like = current_user.likes.new(post_id: @post.id)
    @like.save
    # ここから
    @post.create_notification_like!(current_user)
    # ここまでを追加
    redirect_to request.referer
  end

リポストを押した時

リポストを押したタイミングで通知レコードを作成するように、先ほど作ったメソッドをreposts_controllercreateアクションに追加していきます。

reposts_controller.rb
def create
    if current_user.reposts.find_by(user_id: current_user.id, post_id: @post.id)
      redirect_to root_path, alert: '既にリポスト済みです'
    else
      @repost = current_user.reposts.create(user_id: current_user.id, post_id: @post.id)
      redirect_to request.referer, notice: 'リポストしました'
      @post = Post.find(params[:post_id])
      # ここから
      @post.create_notification_repost!(current_user)
      # ここまでを追加
    end
  end

コメントを投稿したとき

コメントを投稿したタイミングで通知レコードを作成するように、先ほど作成したメソッドをcomments_controllercreateアクションに追加していきます。

comments_controller.rb
def create
    @post = Post.find(params[:post_id])
    @comment = @post.comments.build(comment_params)
    @comment.user = current_user
    @comment.save
    # ここから
    @post.create_notification_comment!(current_user, @comment.id)
    # ここまで追加
    redirect_to request.referer
  end

フォローしたとき

フォローボタンを押したタイミングで通知レコードを作成するように、先ほど作成したメソッドをrelationships_controllercreateアクションに追加します。

relationsips_controller.rb
def create
    @user = User.find(params[:user_id])
    following = current_user.relationships.build(follower_id: params[:user_id])
    following.save
    # ここから
    @user.create_notification_follow!(current_user)
    # ここまで追加
    redirect_to request.referer
  end

notifications_contorollerの作成

rails g controller notifications
notifications_controller.rb
class NotificationsController < ApplicationController
  def index
    @notifications = current_user.passive_notifications.page(params[:page]).per(20)
    @notifications.where(checked: false).each do |notification|
      notification.update_attribute(:checked, true)
    end
  end
end

未確認の通知レコードだけ取り出したあと、「未確認→確認済」になるように更新をしています。

ルーティングの設定

routes.rb
resources :notifications, only: :index

viewページの設定

notifications/index.html.slim
/ 自分の投稿に対するいいね、コメントは通知に表示しない
  - notifications = @notifications.where.not(visitor_id: current_user.id)
  - if notifications.exists?
    = render notifications
    = paginate notifications
  - else
    p
      | 通知はありません

自分の投稿に対する「いいね」と「コメント」は、通知済みとしてレコードを登録しました。
しかし、そのままでは通知一覧に「自分の投稿に対する通知」も表示されてしまいます。
そこで、「自分以外」という条件をここで追加しています。

#自分の投稿に対するいいね、コメントは通知に表示しない
  - notifications = @notifications.where.not(visitor_id: current_user.id)
= render notifications

上記の記述で、レンダリングするときに、単数だけでなく複数のインスタンスを渡すことができます。

  • インスタンスの数だけ、部分テンプレートnotifications/_notification.html.slimが呼び出される
  • 個別のインスタンスを、notificationとして部分テンプレートで使用できる
_notification.html.slim
- visitor = notification.visitor
- visited = notification.visited
.col-md-12.mx-auto
  .form-inline.ms-3
    span
      = link_to user_path(visitor) do
        = image_tag visitor.icon, class: "icon_img"
        strong
          = visitor.name
      = 'さんが'

      - case notification.action
      - when 'follow' then
        = "あなたをフォローしました"
      - when 'like' then
        = link_to 'あなたの投稿', notification.post, style: "font-weight: bold;"
        = "にいいねしました"
      - when 'repost' 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
              = image_tag notification.post.user.icon, class: "icon_img" 
              strong
                = notification.post.user.name + 'さんの投稿'
        = "にコメントしました"
        p.text-muted.mb-0
          = notification.comment&.comment_content

  .small.text-muted.text-right.ms-3.mt-2
    = time_ago_in_words(notification.created_at).upcase
    | 
  hr

スクリーンショット 2024-03-14 23.33.38.png

こんな感じで通知をみれるようになりました。

最後に

これで通知機能のweb上での通知が完成しました。あとはメールでの通知機能の設定をしたいと思います。
こちらはまた後日記事にして作成したいと思います。

通知機能も初めての実装で手こずることが多かったですが、なんとか実装することができました!

参考ページ

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1