0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

【Rails】DMが届いた際の通知機能を実装する

Last updated at Posted at 2021-09-24

はじめに

下記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モデルにデータ保存)

編集する部分は、以下の通りです。

app/controllers/messages_controller.rb

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
db/migrate/20210924000000_create_notifications.rb

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_idvisited_idと異なる点に
注意してください。

また、dependent:は依存関係を示す設定なので、
Userが削除されれば、Notificationも削除されます。

app/models/user.rb

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モデルは下記の通りです。

app/models/message.rb

class Message < ApplicationRecord
  has_many :notifications, dependent: :destroy
  belongs_to :user
  belongs_to :room
end

Roomモデルは下記の通りです。

app/models/room.rb

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
となっている点に注意します。

app/models/notification.rb

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コントローラーで記述したもので
コントローラー側で同メソッドを呼び出しにきます。

編集箇所は以下の通りです。

app/models/room.rb

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アクションが動きます。

config/routes.rb

resources :notifications, only: :index

下記は、notifications_controller.rbの設定です。

ここでは、kaminariを使って15件ごとでページネーションして
現在のユーザーに関連する通知情報を@notificationsに格納しています。

@notificationsの中に存在している情報をユーザーが確認すれば
「確認済み」に更新する処理をupdateを用いて実装しています。

app/controllers/notifications_controller.rb

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
複数回で表示するということです。

app/views/notifications/index.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画面に移動してメッセージを返信できる
実装になっています。

app/views/notifications/_notification.html.erb

<% 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>

あとは、ユーザーに未読状態の通知対象があるか調べる
ヘルパーメソッドを作成しておきます。

app/helpers/notifications_helper.rb
module NotificationsHelper
  def unchecked_notifications
    @notifications = current_user.passive_notifications.where(checked: false)
  end
end

最後に
起点となるnotifications_pathのパスでGETさせる任意のビュー内で
if文を使って、ヘルパーメソッドunchecked_notificationsの有無で
表示させる文言を分岐させれば完成です。

app/views/任意ファイル
<%= link_to (notifications_path) do %>
  <% if unchecked_notifications.any?%>
    <p>通知(未読あり)</p>
  <% else %>
    <p>通知</p>
  <% end %>
<% end %>

終わりに

初めてrailsの実装コードをまとめましたが
説明するのが難しい…と痛感しました。

完全に自分メモのレベルですが、
何かちょっとでも参考になったら幸いです。

最後までお読み頂き、ありがとうございました。

0
3
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
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?