はじめに
今回は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図
今回も少しややこしいER図です。(間違ってたらご指摘お願いします。)
フォロー機能の実装の時のように、ユーザーでもvisitor_id
とvisited_id
に分かりやすいようにわけています。
モデルの作成
rails g model Notification
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モデル
を関連付けします。
# 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を指定して紐付けます。
has_many :notifications, dependent: :destroy
has_many :notifications, dependent: :destroy
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
を実行すると、一番古い通知ではなく、一番新しい通知が取得できます。
post
とcomment
についているoptional: trueは、nilを許可するものです。
belongs_toで紐付ける場合はnilが許可されないのですが、今回はnilを設定したいので、付けています。
通知メソッドを作成する
いいね通知の作成メソッド
いいね
ボタンが押されたタイミングで、以下のようなデータを作成します。
visitor_id | visited_id | post_id | comment_id | action | checked |
---|---|---|---|---|---|
いいねした人のid | いいねされた人のid | いいねされた投稿のid | nil | like | False |
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 |
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 |
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
そうすることによって、潔さんのところにこのような通知も来るようになります。
フォロー通知の作成メソッド
フォロー
ボタンを押されたタイミングで、以下のようなデータを作成します。
visitor_id | visited_id | post_id | comment_id | action | checked |
---|---|---|---|---|---|
フォローした人のid | フォローされた人のid | nil | nil | follow | False |
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_controller
のcreate
アクションに追加していきます。
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_controller
のcreate
アクションに追加していきます。
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_controller
のcreate
アクションに追加していきます。
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_controller
のcreate
アクションに追加します。
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
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
未確認の通知レコードだけ取り出したあと、「未確認→確認済」になるように更新をしています。
ルーティングの設定
resources :notifications, only: :index
viewページの設定
/ 自分の投稿に対するいいね、コメントは通知に表示しない
- 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として部分テンプレートで使用できる
- 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
こんな感じで通知をみれるようになりました。
最後に
これで通知機能のweb上での通知が完成しました。あとはメールでの通知機能の設定をしたいと思います。
こちらはまた後日記事にして作成したいと思います。
通知機能も初めての実装で手こずることが多かったですが、なんとか実装することができました!
参考ページ