こんばんは!
PF作成でモーダルを使った通知に憧れ、記事を参考にしてみたもの非同期通信や難しいJavascriptの記述が理解できず、エラー沼から抜け出せなくなったので、もう簡単にできるモーダル通知を作るしかない!と思いBootstrapを使って実装してみました。なので少しだけ楽!な実装です(笑)
ちなみに確かに元の記事と比べると簡単にはなりますが、記述箇所が多くて大変なのでなるべく丁寧に解説できるように紹介をしていきたいと思います。
この記事ではまずは通知を表示させるというところまで解説します。
通知の既読などはもし実装に余裕があれば載せようかとおもいますので、ご了承ください。また、元の記事を一度参考にして作った状態から色々作り直しているので、いらない記述などがもしあれば教えていただきたいです。
→2024/3/12に通知数の表示と既読処理の追記をしました!
参考記事はこちら。
完成形・実装時の諸注意
完成形はこちら!ベルマークを押すと通知が表示されます。また、通知を開いて詳細にいくと通知が減ります(通知を見た瞬間に0になる仕様ではないのでご注意ください。)。
実装時の注意
投稿(post)→目標(goal)、いいね(favorite)→応援(cheer)、名前(name)→ニックネーム(nickname)などモデルやカラム名が一般的なものと異なる場合があるので、適宜変更をお願いします!
今回はヘッダーの部分テンプレートに記述しています。いいね、コメント、フォロー機能のうちどれかできていれば実装可能です。
Bootstrap最新版(ver.5以降)を導入しています。他のバージョンでは不具合が出る可能性があります!その場合は公式を見て導入するか、過去記事に書いているのでそちらの通りに導入してください。
モデル作成
まずは通知用のNotificationモデルを作成。
rails g model Notification
マイグレーションファイルに記入。
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 :goal_id #適宜変更
t.integer :comment_id
t.integer :cheer_id #適宜変更
t.integer :relationship_id
t.string :action, null: false, default: ''
t.boolean :is_checked, null: false, default: false
t.timestamps
end
end
end
解説
visitor_id: 通知を作った人(いいね、コメント、フォローをした人本人)
visited_id: 通知を受け取る人
goal_id: 投稿の外部キー(適宜post_idなどに変更してください)
cheer_id: いいねの外部キー(適宜favorite_idなどに変更してください)
relationship_id: フォローやフォロワーを管理するテーブルの外部キー
is_checked: 通知を確認したかしていないか
ここですかさず
rails db:migrate
モデル関連づけ
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
各モデル.rb(いいね、コメント、relationshipなど)
has_many :notifications, dependent: :destroy
notification.rb
goal→post, cheer→favoriteなど名称を適宜変えてください
class Notification < ApplicationRecord
default_scope -> { order(created_at: "DESC") }
belongs_to :goal, optional: true <!-- -->
belongs_to :comment, optional: true
belongs_to :cheer, optional: true
belongs_to :relationship, 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
コントローラ―作成
コントローラー作成。今回は部分テンプレートのみ使用し、コントローラ―のアクションの記述は一切書きません少し記述します。部分テンプレートに必要なコントローラの記述は後で入れ込みます。あくまでviewファイルを入れるだけの存在です。ルーティングすらいりません。
rails g controller notifications
モデルに通知のメソッド作成
フォローに関する通知
# フォロー通知を作成するメソッド
def create_notification_follow!(current_user)
# すでにフォロー通知が存在するか検索
existing_notification = Notification.find_by(visitor_id: current_user.id, visited_id: self.id, action: 'follow')
# フォロー通知が存在しない場合のみ、通知レコードを作成
if existing_notification.blank?
notification = current_user.active_notifications.build(
visited_id: self.id,
action: 'follow'
)
notification.save if notification.valid?
end
end
いいねの通知
# postへのいいね通知機能
def create_notification_favorite_post!(current_user)
# 同じユーザーが同じ投稿に既にいいねしていないかを確認
existing_notification = Notification.find_by(post_id: self.id, visitor_id: current_user.id, action: "favorite_post")
# すでにいいねされていない場合のみ通知レコードを作成
if existing_notification.nil? && current_user != self.user
notification = Notification.new(
post_id: self.id,
visitor_id: current_user.id,
visited_id: self.user.id,
action: "favorite_post"
)
if notification.valid?
notification.save
end
end
end
コメントの通知
# コメントが投稿された際に通知を作成するメソッド
def create_notification_comment!(current_user, comment_id)
# 自分以外にコメントしている人をすべて取得し、全員に通知を送る
other_commenters_ids = Comment.select(:user_id).where(post_id: id).where.not(user_id: current_user.id).distinct.pluck(:user_id)
# 各コメントユーザーに対して通知を作成
other_commenters_ids.each do |commenter_id|
save_notification_comment!(current_user, comment_id, commenter_id)
end
# まだ誰もコメントしていない場合は、投稿者に通知を送る
save_notification_comment!(current_user, comment_id, user_id) if other_commenters_ids.blank?
end
# 通知を保存するメソッド
def save_notification_comment!(current_user, comment_id, visited_id)
notification = current_user.active_notifications.build(
post_id: id,
comment_id: comment_id,
visited_id: visited_id,
action: 'comment'
)
# 自分の投稿に対するコメントの場合は、通知済みとする
notification.is_checked = true if notification.visitor_id == notification.visited_id
# 通知を保存(バリデーションが成功する場合のみ)
notification.save if notification.valid?
end
コントローラ―での通知作成
フォロー通知
class Public::RelationshipsController < ApplicationController
def create
:
# フォロー通知を作成・保存
@user.create_notification_follow!(current_user)
end
:
end
いいね通知
class Public::PostFavoritesController < ApplicationController
def create
:
if current_user != @post.user
@post.create_notification_favorite_post!(current_user)
end
:
end
コメント通知
class Public::CommentsController < ApplicationController
def create
:
# コメントの投稿に対する通知を作成・保存
@post.create_notification_comment!(current_user, @comment.id)
:
end
end
notificationコントローラ―
class Public::NotificationsController < ApplicationController
before_action :authenticate_user!
def update_checked
current_user.passive_notifications.update_all(is_checked: true)
head :no_content
end
end
view作成
ヘッダーにベルマークと通知数を表示させる
<% if user_signed_in? %>
<button type="button" class="btn btn-light" data-bs-toggle="modal" data-bs-target="#headerModal">
<i class="fa-regular fa-bell rounded-pill"></i
<span class="badge badge-danger"><%= current_user.passive_notifications.where(is_checked: false).count %></span>
</button>
<% end %>
モーダルの表示
<div class="modal fade" id="headerModal" tabindex="-1" aria-labelledby="headerModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="headerModalLabel">通知一覧</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<% if user_signed_in? %>
<%= render 'notifications/index' %>
<% end %>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary rounded-pill" data-bs-dismiss="modal">閉じる</button>
</div>
</div>
</div>
</div>
注意!ベルの記述したファイルの最下部で、タグが閉じ終わったところの真下にモーダルの記述を追記しないと、親子関係がうまくいかずモーダルとして機能しません!ベルの真下には間違っても書かないように!
モーダルに表示したい内容を部分テンプレートで作る
notification配下に_index.html.erbを作成します。ヘッダーでまずここにrenderしています。部分テンプレートに今回は記述しているので、コントローラ―の定義は一番上の<% %>内に記述しています。
<%
@notices = current_user.passive_notifications.order(created_at: :desc)
@unchecked_notifications = @notices.where(is_checked: false)
# 確認済みの通知を取得
@checked_notifications = @notices.where(is_checked: true).limit(20)
# 通知を確認済みに更新
current_user.passive_notifications.update_all(is_checked: true)
%>
<% notifications = @unchecked_notifications.where.not(visitor_id: current_user.id) %>
<% if (@unchecked_notifications.present? && @unchecked_notifications.any?) || (@checked_notifications.present? && @checked_notifications.any?) %>
<% if @unchecked_notifications.present? %>
<%= render "notifications/notification", notifications: @unchecked_notifications %>
<% end %>
<% if @checked_notifications.present? %>
<%= render "notifications/notification", notifications: @checked_notifications %>
<% end %>
<% else %>
<p>通知はありません</p>
<% end %>
_index.html.erbからのrender先の_notification.html.erbを作成後記述します。
通知のlink_toのパスを自分のパスと一致しているか確認してください。
goal→post, cheer→favorite, nickname→nameなどに書き換えてください。
imageなどは今回get_profile_imageというように定義を使って呼び出しているので、ご自身の定義もしくはimageなどに置き換えてください。
通知で表示する文章は適宜変更してください。
<% notifications.each do |notification| %>
<% visitor = notification.visitor %>
<% visited = notification.visited %>
<div class="col-md-12 mx-auto notification-container">
<div class="notification-box d-flex align-items-center">
<div class="notification-icon">
<%= link_to user_path(visitor), data: { turbolinks: false } do %>
<%= image_tag visitor.get_profile_image, size: '40x40', class: 'rounded-circle mr-3' %>
<% end %>
</div>
<!-- 通知の種類に応じて表示内容を切り替え -->
<!-- フォロー通知の場合 -->
<% if notification.action == 'follow' %>
<strong><%= visitor.nickname %></strong>さんがあなたをフォローしました
<!-- 投稿がいいねされた通知の場合 -->
<% elsif notification.goal && notification.action == 'cheer_goal' %>
<%= link_to goal_path(notification.goal) do %>
<strong><%= visitor.nickname %></strong>さんが<strong><%= truncate(notification.goal.title, length: 10) %></strong>の目標を応援しました
<% end %>
<!-- コメントが投稿された通知の場合 -->
<% elsif notification.action == 'comment' %>
<!-- 自分自身の投稿へのコメントの場合 -->
<% if notification.goal && notification.goal.user_id == visited.id %>
<%= link_to goal_path(notification.goal) do %>
<strong><%= visitor.nickname %></strong>さんが<strong><%= truncate(notification.goal.title, length: 10) %></strong>の目標にコメントしました
<% comment = Comment.find_by(id: notification.comment_id) %>
<div class="notification-comment">
<%#= truncate(comment, length: 30) %>
</div>
<% end %>
<!-- 投稿者が異なり自身がコメントした投稿に他のユーザーがコメントした場合 -->
<% else %>
<span>
<% if notification.goal %>
<%= link_to goal_path(notification.goal) do %>
<strong><%= visitor.nickname %></strong>さんが<strong><%= notification.goal.user.nickname + 'さんの投稿' %></strong>にコメントしました
<% comment = Comment.find_by(id: notification.comment_id) %>
<div class="notification-comment">
<%#= truncate(comment, length: 30) %>
</div>
<% end %>
<% end %>
</span>
<% end %>
<% end %>
</div>
<div class="small text-muted text-right">
<%= time_ago_in_words(notification.created_at).upcase %>前
</div>
</div>
<% end %>
実装はこれで以上です!お疲れ様です!
長かったと思いますし、おそらく色々置き換える場所が多くて最初はエラーが出ると思いますが、少しは楽に実装できることを願っています。
初心者ですので間違っているところなどあればお気軽にコメントなどでご指摘ください。
追記
もし通知の何時間前などの数字がうまく表示されない場合、config/locales/ja.ymlファイルに記述してください。
ja:
time:
formats:
default: "%Y/%m/%d %H:%M"
datetime:
distance_in_words:
half_a_minute: 半分
less_than_x_seconds:
one: 1秒未満
other: "%{count}秒未満"
x_seconds:
one: 1秒
other: "%{count}秒"
less_than_x_minutes:
one: 1分未満
other: "%{count}分未満"
x_minutes:
one: 1分
other: "%{count}分"
about_x_hours:
one: 約1時間
other: 約%{count}時間
x_days:
one: 1日
other: "%{count}日"
about_x_months:
one: 約1ヶ月
other: 約%{count}ヶ月
x_months:
one: 1ヶ月
other: "%{count}ヶ月"
about_x_years:
one: 約1年
other: 約%{count}年
over_x_years:
one: 1年以上
other: "%{count}年以上"
そもそも日本語設定してない、i18nのgemを入れていないという方はこちらの記事を参考にしてみてください。deviseの日本語化の記事ではありますが、最初の流れは同じです。何ならこの機会にdeviseの日本語化もしてしまいましょう(どちらにせよ日本語化するとdeviseで日本語訳できませんというエラーが出るので)。時間の記述と違ってja.ymlファイルの記述ではないのでそこは注意してください。