参考記事
1.RailsでややこしいDM機能を1万字でくわしく解説してみた
2.【Rails】通知機能を誰でも実装できるように解説する【いいね、コメント、フォロー】
3. [Rails] 同じmodelを参照する外部キーを一つのmodelでもつ方法
前提
・DM機能(テーブル設計に関しては参考記事1と全く同じ)
Roadmap
・モデルの作成
・モデルのリレーション
・通知機能の作成
・モデルに切り出し
・読んだ際に既読になる機能の作成
モデルの作成
$ 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.references :room, foreign_key: true, null: false
t.references :message, foreign_key: true, null: false
t.boolean :checked, default: false, null: false
t.timestamps
end
end
end
visitor_idにDMを送ったUserのIDが入り、visited_idにDMを受け取ったUserのIDが入ります。boolean型のcheckedカラムには未読の場合false,既読の場合trueが入るようにします。初期の状態では未読なのでdefault: falseにしておきます。
モデル間のリレーション
- UserとNotificationのリレーション
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
DMを送る側も受け取る側もどちらもUserモデルであるためそれを明示します。これは参考記事3の説明がわかりやすかったです。
- RoomとNotificationのリレーション
has_many :nofifications, dependent: :destroy
- MessageとNotificationのリレーション
has_many :notifications, dependent: :destroy
- Notificationから見たリレーション
belongs_to :visitor, class_name: 'User', foreign_key: 'visitor_id'
belongs_to :visited, class_name: 'User', foreign_key: 'visited_id'
belongs_to :room
belongs_to :message
通知機能の作成
メッセージを送信した際に通知機能が動くようにしたいので、messages_controllerのcreateアクションが実行されるときに同時にNotificationレコードが作成されるようにします。
def create
if Entry.where(user_id: current_user.id, room_id: params[:message][:room_id])
@message = Message.new(message_params)
#ここから
@room = @message.room
#ここ
if @message.save
#ここから
@receiver = Entry.where(room_id: @room.id).where.not(user_id: current_user.id).find_by(room_id: @room.id)
notification = current_user.active_notifications.new(
visited_id: @receiver.user_id,
room_id: @room.id,
message_id: @message.id
)
notification.save if notification.valid?
#ここ
redirect_to(room_path(@message.room_id))
else
flash[:alert] = "メッセージ送信に失敗しました。"
redirect_to(room_path(@message.room_id))
end
else
flash[:alert] = "メッセージ送信に失敗しました。"
redirect_to(room_path(@message.room_id))
end
end
visitor_id | visited_id | room_id | message_id | checked |
---|---|---|---|---|
Notificationsテーブルの構成は上のようになっていて、Notificationレコードを作成するにはこの5つの値が必要になってきます。visitor_id(current_user.id), message_id(@message.id), checkedは既知であるためvisited_id, room_idに入る値を考えます。 | ||||
room_idに関してはmessageとroomのリレーションがあるので@message.roomで得ることができます。(4行目) | ||||
visited_idには相手のuser_idが入ればいいのでEntriesテーブルから探します。EntriesテーブルはUsersテーブルとRoomsテーブルの中間テーブルであり、Entry.where(room_id: @room.id)の時点ではEntryレコードが2つ抽出されます。そのうち自分ではない方を選べば良いため.where.not(user_id: current_user.id)にします。 | ||||
このときすでに得られたレコードは1つですが、whereはActiveRecord::Relationを返すメソッドであるためデータ出力のためにfind_byでもう一度特定しています | ||||
(参考: find、find_by、whereなどモデルを取得する方法まとめ) |
モデルに切り出し
で、これでも動きますがこれだと太っちょなcontroller?(この辺はよくわかっていません)になってしまうので、controllerに記述したロジックをモデルに切り出します。
def create_notification_comment(current_user, message_id)
receiver = Entry.where(room_id: id).where.not(user_id: current_user.id).find_by(room_id: id)
save_notification_comment(current_user, message_id, receiver.user_id)
end
def save_notification_comment(current_user, message_id, visited_id)
notification = current_user.active_notifications.new(
visited_id: visited_id,
room_id: id,
message_id: message_id
)
notification.save if notification.valid?
end
def create
if Entry.where(user_id: current_user.id, room_id: params[:message][:room_id])
@message = Message.new(message_params)
@room = @message.room
if @message.save
@room.create_notification_comment(current_user, @message.id) #<-メソッドとして呼び出す
redirect_to(room_path(@message.room_id))
else
flash[:alert] = "メッセージ送信に失敗しました。"
redirect_to(room_path(@message.room_id))
end
else
flash[:alert] = "メッセージ送信に失敗しました。"
redirect_to(room_path(@message.room_id))
end
end
読んだ際に既読になる機能の作成
メッセージを受け取る側がトークルームに入った時にNotificationのcheckedの値がfalseからtrueになるように実装します。今回の場合ではrooms_controllerのshowアクションが実行されたときにcheckedの値をfalseからtrueにアップデートするようにします。
def show
@room = Room.find_by(id: params[:id])
if Entry.where(user_id: current_user.id, room_id: @room.id)
@messages = @room.messages
@message = Message.new
@entries = @room.entries
else
redirect_to(root_path)
end
@notifications = current_user.passive_notifications.where(room_id: @room.id)
@notifications.where(checked: false).each do |notification|
notification.update(checked: true)
end
end
ここではログインしているユーザー(current_user)が受け取っているが、まだ未読であるメッセージの通知を取得してそれをeach文で回しています。each文で回すことによって、メッセージ通知が複数件の場合でも全てのNotificationレコードのcheckedの値をfalseからtrueにすることができます。
あとはviewをいじるだけなのでここまでにしときます。