はじめに
下記2つのQiita記事を参考にして、
- いいね
- コメント
- フォロー
- DM
- ブックマーク
された時に通知される機能を実装しました。
当記事ではDMの通知機能
について
基本文法でつまづいたので、学習記録として残しています。
なお、コントローラーとモデルで分けて
構成していますので、似た構成にしたい方の参考になれば幸いです。
修正が必要な箇所があれば、コメントなどでお知らせください。
参考記事(2つ)
前提
・DM機能を実装済み
実装前に考える…
まず、通知画面のViewで
- 通知を受け取るユーザーに何を情報として伝えたいか
- どこに画面遷移させると使いやすいか
の2点に留意しないといけません。
これは人によって、実装したい思想が異なると思うので
絶対解はないと思います。
私の場合は、DMの通知を想定すると…
-
通知を受け取るユーザーに何を情報として伝えたいか
→送信者はどのユーザーか
、どんな内容のメッセージか
-
どこに画面遷移させると使いやすいか
→クリック1回でDM画面に移動してメッセージを返信できる
と考えました。
モデル設計?
前章で考えた内容から
通知機能を担うNotificationモデルに関連付けする
各モデルを以下の通りにまとめました。
■前章のポイント
DM送信者はどのユーザーか
どんな内容のメッセージか
クリック1回でDM画面に移動してメッセージを返信できる
↓
・Userモデル: DMを送信したユーザー
・Messageモデル: 送信されたメッセージ
・Roomモデル: DMを送受信できる部屋
上記3つのモデルをNotificationモデルに関連付けすることで
Notificationコントローラーから描画されるView内で
各モデルの情報を表示することができます。
反対に言えば、NotificationのView内で
メッセージを表示させる必要がないと考えるのであれば
Messageモデルの関連付けは不要です。
もっと具体的に言うと
メッセージ内容はDM送受信できる部屋に入った後で
表示されれば、それで良いと考える実装例ですね。
では、Controllerから
先に、データを取り出す役割のモデルを
呼び出すコントローラー側から実装していきます。
DMの通知機能では、相手がメッセージを送信したタイミングで
Notificationモデルにデータが保存される様に実装します。
そのため、messageコントローラー
のcreateアクション
を編集します。
同コントローラーのcreateアクションが
動作の起点となること
をイメージしておきましょう。
(DM画面でメッセージ送信ボタンをクリック → notificationモデルにデータ保存)
編集する部分は、以下の通りです。
class MessagesController < ApplicationController
def create
if Entry.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
@message = Message.create(params.require(:message).permit(:user_id, :content, :room_id).merge(user_id: current_user.id))
# (ここから)
# 新規作成された@messageに紐づくroomを@roomに格納する
@room = @message.room
# 本引数を2つ持たせてcreate_notification_dmメソッドを実行
@room.create_notification_dm(current_user, @message.id)
# (ここまで)
redirect_to "/rooms/#{@message.room_id}"
else
redirect_back(fallback_location: root_path)
end
end
end
コード補足:
@message.room
は
・Messageモデル
・Roomモデル
を belongs_to, has_manyメソッドで関連付けしている為
Messageに紐づいたRoomオブジェクトが返されます。
create_notification_dm
は
これからRoomモデルに定義する
create_notification_dmメソッドを呼び出しており
実行後、roomモデルに動作が移ります。
(current_user, @message.id)
は
Notificationモデルに
・現ユーザーのidを保存
・メッセージを保存
するために、メソッドに渡す本引数として設定しています。
Model関連付け
まずは、generateコマンドでnotificationモデルを生成します。
% rails generate model Notification
次にマイグレーションファイルを編集していきます。
Notificationモデルを下記カラムで構成される様に
マイグレーションファイルを編集します。
visitor_id | visited_id | room_id | message_id | action | checked |
---|---|---|---|---|---|
通知を送るユーザー | 通知を受けるユーザー | DMを送受信できる部屋 | メッセージ内容 | 通知の種類 | 通知確認の有無 |
1 | 2 | 1 | 1 | dm | false |
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 :room_id
t.integer :message_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, :room_id
add_index :notifications, :message_id
end
end
マイグレーションファイルが編集できたら、DBに反映します。
% rails db:migrate
続いて、Userモデルを設定していきましょう!
ここでは、通知を
・送る側(active_notifications)
・受ける側(passive_notifications)
の2つに分けて実装します。
お互い参照するモデルはNotificationに設定していますが
外部キーはvisitor_id
とvisited_id
と異なる点に
注意してください。
また、dependent:は依存関係を示す設定なので、
Userが削除されれば、Notificationも削除されます。
class User < ApplicationRecord
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
Messageモデルは下記の通りです。
class Message < ApplicationRecord
has_many :notifications, dependent: :destroy
belongs_to :user
belongs_to :room
end
Roomモデルは下記の通りです。
class Room < ApplicationRecord
has_many :notifications, dependent: :destroy
has_many :messages, dependent: :destroy
has_many :entries, dependent: :destroy
下記はNotificationモデルの設定です。
default_scopeで、作成日時が新しい順番で参照されます。
optional: trueは、データがnilであっても許可するオプションです。
また、関連付けするUserモデルの対象が
・visitor
・visited
となっている点に注意します。
class Notification < ApplicationRecord
default_scope -> { order(created_at: :desc) }
belongs_to :room, optional: true
belongs_to :message, optional: true
belongs_to :visitor, class_name: 'User', optional: true
belongs_to :visited, class_name: 'User', optional: true
end
create_notification_dmメソッドの定義
続いて、Roomモデルで通知データを保存するための
メソッドを実装していきます。
先ほど編集したroom.rbを再度イジります。
なお、create_notification_dmメソッドは
先にmessageコントローラーで記述したもので
コントローラー側で同メソッドを呼び出しにきます。
編集箇所は以下の通りです。
class Room < ApplicationRecord
has_many :notifications, dependent: :destroy
has_many :messages, dependent: :destroy
has_many :entries, dependent: :destroy
# (ここから)
def create_notification_dm(current_user, message_id)
@multiple_entry_records = Entry.where(room_id: id).where.not(user_id: current_user.id)
@single_entry_record = @multiple_entry_records.find_by(room_id: id)
notification = current_user.active_notifications.build(
room_id: id,
message_id: message_id,
visited_id: @single_entry_record.user_id,
action: 'dm'
)
notification.save if notification.valid?
end
# (ここまで)
end
コード補足:
create_notification_dm
→ 定義するメソッド名称なので、任意で命名してください。
(current_user, message_id)
→ 仮引数を2つ設定しています。
コントローラー側で設定した本引数と異なる名称でも動作します。
ただし引数の数が一致しないとエラーが発生しますので注意します。
@multiple_entry_records = Entry.where(room_id: id).where.not(user_id: current_user.id)
↓
・Entryモデルから
・user_idが自分と一致するものを省いて
・room_idが作成されたルームのIDと一致する
・複数レコード
を取得しています。
@single_entry_record = @multiple_entry_records.find_by(room_id: id)
↓
前行では複数レコード(where)だったものを単一レコード(find_by)に変換しています
目的は「single_entry_record.user_id」でIDを抽出する為です
notification = current_user.active_notifications.build(
room_id: id,
message_id: message_id,
visited_id: @single_entry_record.user_id,
action: 'dm'
)
→ 現在のユーザーに紐づくactive_notificationsに()内の
各データを入れて作成しています。
■各データの概説
room_id: コントローラー側で新規作成されたmessageに紐づくroomのid
message_id: 本引数で持ってきた@message.id
(仮引数でmessage_idに名称を変えている)
visited_id: @single_entry_recordからuser_idを抽出
(DMを送信したユーザー)
action: dm
(View内のcase文で条件分岐させる為)
notification.save if notification.valid?
→ 後置if文でnotificationに登録するデータが有効かチェックして保存します。
Viewで表示させる(通知画面の設定)
ここからは、Notificationモデルに保存されたデータを
取り出して表示させる部分です。
まず、genarateコマンドでコントローラー・ビューを生成します。
% rails g controller notifications
次にroutes.rbで、ルーティングを設定します。
これで、notifications_pathというパスで、GETされると、
notificationコントローラーのindexアクションが動きます。
resources :notifications, only: :index
下記は、notifications_controller.rbの設定です。
ここでは、kaminariを使って15件ごとでページネーションして
現在のユーザーに関連する通知情報を@notificationsに格納しています。
@notificationsの中に存在している情報をユーザーが確認すれば
「確認済み」に更新する処理をupdateを用いて実装しています。
class NotificationsController < ApplicationController
def index
@notifications = current_user.passive_notifications.page(params[:page]).per(15)
@notifications.where(checked: false).each do |notification|
notification.update(checked: true)
end
end
end
続いて、コントローラーからのindexアクションで描画される
View(index.html.erb)の設定です。
1行目のコードで、通知を送るユーザーが自分であるレコードを除外しています。
また、renderを使って部分テンプレートを参照しており、
さらにnotificationsと複数形にしていますので、
存在するレコード分で部分テンプレートが生成されます。
つまり、後述する_notification.html.erb
を
複数回で表示するということです。
<% notifications = @notifications.where.not(visitor_id: current_user.id) %>
<% if notifications.exists? %>
<%= render notifications %>
<%= paginate notifications %>
<% else %>
<p>通知はありません</p>
<% end %>
今度は、先のindex.html.erb内のrenderで参照される
部分テンプレート(_notification.html.erb)の設定です。
1行目のコードのvisitorは、通知を送ったユーザーです。
ここでは、case文でactionの値が何かで条件分岐する設定となっています。
今回はDMの通知であるため、action = dmでDM仕様の通知表示となる実装です。
(いいね・コメント・フォロー…といった感じで追加実装するなら
whenでlike,comment,followなどの条件を加えて実装します。)
なお、メッセージ内容はtruncateを使って20文字のみの
一部だけ表示される仕様にしています。
これで、正直UIはかなりお粗末ですが
DM送信者は表示され、
メッセージの一部が表示され、
かつ
クリック1回でDM画面に移動してメッセージを返信できる
実装になっています。
<% visitor = notification.visitor %>
<span>
<%= link_to user_path(visitor) do %>
<%= visitor.name %>
<% end %>
<span>さんが</span>
<br>
<% case notification.action %>
<% when 'dm' %>
<p>あなたに</p>
<h4><i class="fas fa-envelope"></i>DMを送りました</h4>
<span>メッセージ内容:</span>
<%= truncate(notification.message.content, length: 20, omission: '... (一部表示)') %>
<p>投稿時期: <%= time_ago_in_words(notification.created_at).upcase %>前</p>
<%= link_to room_path(notification.room) do %>
<h4>DMの送信画面に移動</h4>
<% end %>
<hr>
<% end %>
</span>
あとは、ユーザーに未読状態の通知対象があるか調べる
ヘルパーメソッドを作成しておきます。
module NotificationsHelper
def unchecked_notifications
@notifications = current_user.passive_notifications.where(checked: false)
end
end
最後に
起点となるnotifications_pathのパスでGETさせる任意のビュー内で
if文を使って、ヘルパーメソッドunchecked_notificationsの有無で
表示させる文言を分岐させれば完成です。
<%= link_to (notifications_path) do %>
<% if unchecked_notifications.any?%>
<p>通知(未読あり)</p>
<% else %>
<p>通知</p>
<% end %>
<% end %>
終わりに
初めてrailsの実装コードをまとめましたが
説明するのが難しい…と痛感しました。
完全に自分メモのレベルですが、
何かちょっとでも参考になったら幸いです。
最後までお読み頂き、ありがとうございました。