Rails 通知機能
はじめに
ポートフォリオ制作中のプログラミング初学者です🔰
今日は通知機能を実装しました!
私は投稿機能が複数に分かれているため、ポリモーフィック関連付けを使ったやり方で実装してみました。
完成イメージ
- まだレイアウトは適当ですが、こんな感じです😣
- 名前も投稿もリンクにしていて飛べます。
- 全削除で全部の通知を消すことができます。
- 未読の通知があれば、ヘッターのアイコンが黄色くなります。既読したら元の色に戻ります。
通知が来る条件
- フォローされた時
- 自分の投稿にいいねがあった時
- 自分の投稿にコメントがあった時
前提
Post_workoutについての
- コメント機能(WorkoutComment)
- いいね機能(WorkoutLike)
と - フォロー機能(Relationship)
が実装されている。
実装
モデル作成
通知モデルを作成!
$ rails g model Notification
出来上がったマイグレーションファイルを開き、以下のように変更します。
class CreateNotifications < ActiveRecord::Migration[6.1]
def change
create_table :notifications do |t|
t.references :subject, polymorphic: true
t.references :end_user, foreign_key: true
t.integer :action_type, null: false
t.boolean :checked
t.timestamps
end
end
end
schema
create_table "notifications", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.string "subject_type"
t.bigint "subject_id"
t.bigint "end_user_id", null: false
t.integer "action_type", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.boolean "checked", default: false, null: false
t.integer "visitor_id"
t.integer "visited_id"
t.index ["end_user_id"], name: "index_notifications_on_end_user_id"
t.index ["subject_type", "subject_id"], name: "index_notifications_on_subject"
end
:subject, polymorphic: true
-
ポリモーフィック関連付けを使うと、ある1つのモデルが他の複数のモデルに属していることを、1つの関連付けだけで表現できます。
relationship_id
、comment_id
、like_id
をsubject
として一つのカラムにまとめます。
こうすることで、notification.subject
の形でLike
,Comment
,Relationship
のインスタンスが呼び出せるようになるみたいです。
:action_type
- enum で使うので integer 型。
- いいね・コメント・フォローが押された時、起こったアクションごとに表示する HTML を変えたい。(一つのファイルに複数の表示処理を書くよりスッキリするから)
そのため、action_type
カラムというものを設置して、そこに Notification インスタンスの状態が、いいね・ コメント・フォロー のどの状態なのかを保存しておく。
:checked
- 通知の既読の判断に使います。初期値は、false(通知未確認)にしておきましょう。
rails db:migrate
忘れずに!
routing
私はpublic配下に作りたかったので下記のように記述しています。
scope module: :public do
resources :notifications, only: [:index]
end
できたURL
notifications GET /notifications(.:format) public/notifications#index
controller
class Public::NotificationsController < ApplicationController
def index
@notifications = current_end_user.notifications.order(created_at: :desc).page(params[:page]).per(20)
@notifications.where(checked: false).each do |notification|
notification.update(checked: true)
end
end
def destroy
@notifications = current_end_user.notifications.destroy_all
redirect_to notifications_path
end
end
current_end_user.notifications
現在ログインしているユーザー(current_end_user)に関連付けられた全ての通知(notifications)を取得しています。(EndUserモデルに記述されたhas_many :notifications
という関連付けにより可能になっています。)
.order(created_at: :desc)
これにより、取得した通知を作成日時(created_at)の降順でソート(並び替え)します。
既読させる
@notifications.where(checked: false).each
ここで取得した通知の中から、まだ確認されていない(checked: false)通知を取り出しています。
そして、それぞれの未確認通知に対して、以下の操作を行います。
notification.update(checked: true)
未確認の通知を確認済み(checked: true)に更新します。
(indexページに入った瞬間、取得された全ての通知のchecked値はtrue(つまり確認済み)になります。この設計は、ユーザが通知ページを開いたときにすべての通知を「読んだ」ものとして扱うためのものです。)
model
class WorkoutLike < ApplicationRecord
belongs_to :end_user
belongs_to :post_workout
has_one :notification, as: :subject, dependent: :destroy
after_create_commit :create_notifications
private
def create_notifications
Notification.create(subject: self, end_user: self.post_workout.end_user, action_type: :liked_to_own_post)
end
end
has_one
メソッド
- 「このモデルは他のモデルを1つだけ持っている」という関連性を示します。
- この場合、WorkoutLikeモデルの各インスタンスはNotificationモデルのインスタンスを1つだけ持つことができます。
as: :subject
- ポリモーフィック関連付けを示しており、NotificationモデルのsubjectがWorkoutLikeモデルになります。
※ after_create_commit :create_notificationsというコールバック
- after_create_commitはRailsのActiveRecordにおけるコールバックメソッドの一つ。
- WorkoutLikeモデルのインスタンスがデータベースに保存された(create)後(after)に、create_notificationsメソッドを実行します。
- このメソッドでは新しいNotificationインスタンスが作成され、そのsubjectはself(WorkoutLikeのインスタンス)、end_userはself.post_workout.end_user、action_typeは:liked_to_own_postとなります。
コールバックとは
とは、オブジェクトのcreate, update, destroyなどの間(ライフサイクル期間)における特定の瞬間に呼び出されるメソッドのこと。コールバックを利用することで、Active Recordオブジェクトが作成/保存/更新/削除/検証/データベースからの読み込み、などのイベント発生時に常に実行されるメソッドを作れる。
workout_comment、relationshipも同様に記述していきます。
class WorkoutComment < ApplicationRecord
belongs_to :end_user
belongs_to :post_workout
has_one :notification, as: :subject, dependent: :destroy
after_create_commit :create_notifications
private
def create_notifications
Notification.create(subject: self, end_user: post_workout.end_user, action_type: :commented_to_own_post)
end
end
class Relationship < ApplicationRecord
belongs_to :follower, class_name: "EndUser"
belongs_to :followed, class_name: "EndUser"
has_one :notification, as: :subject, dependent: :destroy
after_create_commit :create_notifications
private
def create_notifications
Notification.create(subject: self, end_user: followed, action_type: :followed_me)
end
end
class PostWorkout < ApplicationRecord
has_one :notification, as: :subject, dependent: :destroy
end
post_workoutにも記述。
(PostWorkout インスタンスが削除されたときに関連する Notification インスタンスも一緒に削除するため)
class EndUser < ApplicationRecord
has_many :notifications, dependent: :destroy
end
Notification
class Notification < ApplicationRecord
belongs_to :subject, polymorphic: true
belongs_to :end_user
enum action_type: { commented_to_own_post: 0, liked_to_own_post: 1, followed_me: 3}
end
enumについて
view
- 通知一覧を表示するためのリンク
<li class="nav-item">
<%= link_to notifications_path, class: "nav-link" do %>
<i class="fa-sharp fa-solid fa-bell"></i>
通知一覧
<% end %>
</li>
- 通知一覧の表示
<h4>通知</h4>
<% if @notifications.present? %>
<%= link_to "全削除",notification_path(@notifications), method: :delete,class:"btn btn-light" %>
<% @notifications.each do |notification| %>
<%= render "#{notification.action_type}", notification: notification %>
<% end %>
<% else %>
通知はありません
<% end %>
<%= link_to transition_path(notification) do %>
<div>
<%= link_to notification.subject.end_user.first_name, end_user_path(notification.subject.end_user) %>
があなたの
<%= link_to '投稿', post_workout_path(notification.subject.post_workout) %>
にいいねしました
<%= " (#{time_ago_in_words(notification.created_at)}前)" %>
</div>
<% end %>
<%= link_to transition_path(notification) do %>
<div>
<% if notification.subject.follower.guest? %>
ゲストユーザー
<% else %>
<%= link_to notification.subject.follower.first_name, end_user_path(notification.subject.follower) %>
<% end %>
があなたをフォローしました
<%= " (#{time_ago_in_words(notification.created_at)}前)" %>
</div>
<% end %>
<%= link_to transition_path(notification) do %>
<div>
<%= link_to notification.subject.end_user.first_name, end_user_path(notification.subject.end_user) %>
があなたの
<%= link_to '投稿', post_workout_path(notification.subject.post_workout) %>
にコメントしました
<%= " (#{time_ago_in_words(notification.created_at)}前)" %>
</div>
<% end %>
module Public::NotificationsHelper
def transition_path(notification)
case notification.action_type.to_sym
when :commented_to_own_post
post_workout_path(notification.subject.post_workout, anchor: "comment-#{notification.subject.id}")
when :liked_to_own_post
post_workout_path(notification.subject.post_workout)
when :followed_me
end_user_path(notification.subject.follower)
end
end
end
notification.action_type.to_sym
- 通知のaction_typeがシンボルに変換され、それによって行われるアクションを判断します。これはcase文を使用して行われます。
:commented_to_own_post
の場合
- へルパーメソッドは通知が関連づけられた投稿へのパスを返します。その際、アンカーリンク(anchor:)を指定して、ユーザーがクリックしたときにコメント部分までスクロールするようにします。
:liked_to_own_post
の場合
- ヘルパーメソッドは通知が関連づけられた投稿へのパスを返します。
:followed_meの場合
- フォロワーのプロフィールページへのパスを返します。
helperファイルを初めて使ってみましたが、
使う理由としてはviewをシンプルにしてコードを見やすくする為のようです。
ビューで条件分岐を書く必要がなくなる&同じロジックを繰り返し書く必要がなくなります!
通知が来た時にアイコンを変える
view
<li class="nav-item">
<%= link_to notifications_path, class: "nav-link" do %>
<% if current_end_user.notifications.where(checked: false).exists? %>
<i class="fa-sharp fa-solid fa-bell fa-beat" style="color: #f2ee7d;"></i>
<% else %>
<i class="fa-sharp fa-solid fa-bell"></i>
<% end %>
通知一覧
<% end %>
</li>
<% if current_end_user.notifications.where(checked: false).exists? %>
- 現在ログインしているユーザー(current_end_user)の未チェックの通知が存在するかをチェックします。checked: falseの通知があるなら、条件式はtrueを返し、黄色いアイコンの方が表示されます。
controller
ユーザーが通知一覧ページを表示するときに、すべての未チェック通知をチェック済みに更新する記述を足します。
class Public::NotificationsController < ApplicationController
def index
@notifications = current_end_user.notifications.order(created_at: :desc).page(params[:page]).per(20)
@notifications.where(checked: false).each do |notification|
notification.update(checked: true)
end
end
end
@notifications.where(checked: false).each do |notification|
- まだチェックされていない(checked: false)すべての通知に対してループを実行します。
notification.update(checked: true)
- 各未チェック通知を「チェック済み」に更新します。の更新はデータベースに保存され、その結果、その通知は次回以降のこのループでは処理されません。
参考にさせていただいた記事🌱
さいごに
盛りだくさんで、説明不足なところが多々あると思いますが、このような流れで実装できました!
はじめてポリモーフィック関連付けやヘルパーファイルなどに触れて、時間はかかってしまいましたがとてもいい勉強になりました。
ただ、私はいいねとコメントを非同期で実装しているので、リロードしないとヘッターのアイコンが変わらない点が心残りです。時間が余ればどうにかできないか方法を見つけたいです。
この他にももっといい記述法はたくさんあると思いますので、少しでも参考になれば幸いです。
間違いなどあればぜひ教えてください!
Discussion