はじめに
今回はXクローンの実装にてDM機能の実装をしたので、自分自身の備忘録として、また今後フォロー機能を実装する方のお役に立てればと思い今回も記事を書くことにしました。
DM機能の作成
要件として、ユーザー同士(1対1)でメッセージを送り合うことができる機能を実装する。
※ 前提として、ユーザー登録がすでに実装されているものとします。
DM機能の流れ
相手ユーザーの詳細ページorポストのメニューorサイドバーのメニューからチャットに進む
↓
roomがなければ新規で作成され、そうでなければ存在するroomに遷移する
↓
roomでメッセージを送ることができるようになる
このように至って普通なDM機能を実装していきたいと思います。
DM機能について
DM機能では、usersテーブル、roomsテーブル、entriesテーブル、messageテーブルを使用します。
2人のユーザーがチャットルームでメッセージをやりとりするというイメージを思い浮かべながら実装していきます。
まず、ユーザーを管理するusersテーブルがあります。
ユーザーはroomsテーブルというチャットルームに属していて、2人で1つのチャットルームが作成される。
ユーザー1人1人は、色々なユーザーとチャットをするので、たくさんのチャットルームをもつ可能性がある。
なので、usersテーブルとroomsテーブルは多対多の関係になる。
そのため、中間テーブルとしてentriesテーブルを置き、情報の管理をします。
また、roomsでは、複数(2人)のユーザーが複数のメッセージを送るという多対多の関係なので、これも中間テーブルとしてmessageテーブルを置き、情報の管理をします。
ER図に表すと
説明をER図に表すと上のようなER図になります。
モデルの作成
userは既に作成済みなので、残りのroom,entry,messageのモデルを作成する。
rails g model room
rails g model entry user:references room:references
rails g model message user:references room:references message:text
rails db:migrate
関連付けの設定
それぞれのモデルの関連付けの記述をしていこうと思います。
has_many :entries, dependent: :destroy
has_many :messages, dependent: :destroy
has_many :entries, dependent: :destroy
has_many :messages, dependent: :destroy
belongs_to :user
belongs_to :room
validates :message, presence: true
belongs_to :user
belongs_to :room
ルーティングの設定
ルーティングの設定は次のようになります。
resources :messages, only: [:create]
resources :rooms, only: [:create, :index, :show]
コントローラーの作成
rails g controller rooms
rails g controller messages
users_controller
class UsersController < ApplicationController
before_action :authenticate_user!
def show
@user = User.find_by(id: params[:id])
# roomがcreateされた時に現在ログインしているユーザーと、
# チャット相手になるユーザーの両方をEntriseテーブルから取得する。
@current_entry = Entry.where(user_id: current_user.id)
@another_entry = Entry.where(user_id: @user.id)
unless @user.id == current_user.id
@current_entry.each do |current|
@another_entry.each do |another|
if current.room_id == another.room_id
@is_room = true
@room_id = current.room_id
end
end
end
unless @is_room
@room = Room.new
@entry = Entry.new
end
end
@posts = @user.posts.order(created_at: :desc).page(params[:page]).per(2)
@like_posts = @user.likes.order(created_at: :desc).page(params[:page]).per(2)
@repost_posts = @user.reposts.order(created_at: :desc).page(params[:page]).per(2)
@comment_posts = @user.comments.order(created_at: :desc).page(params[:page]).per(3)
end
~解説~
@current_entry = Entry.where(user_id: current_user.id)
@another_entry = Entry.where(user_id: @user.id)
ログインしているユーザーと、メッセージの相手のユーザーの情報をentryテーブルから検索して取得し、@current_entry
と@another_entry
に代入する。
unless @user.id == current_user.id
@current_entry.each do |current|
@another_entry.each do |another|
if current.room_id == another.room_id
@is_room = true
@room_id = current.room_id
end
end
end
unless @is_room
@room = Room.new
@entry = Entry.new
end
end
unless文でログインしていないユーザーという条件を設定します。
そしてさっき取得した2つのユーザー情報をそれぞれeachで取り出して、entryテーブル内に同じroom_id
が存在するかどうかを調べます。
同じroom_id
が存在する場合は既にroomがあるということなのでroom_idの変数とroomが存在するかどうかの条件であるis_roomを渡す。
同じroom_idが存在しない場合は新しくインスタンスを作成します。
続いて、viewです。
.d-flex.justify-content-end.mt-4.me-4
.me-2
- unless @user.id == current_user.id
- if @is_room == true
= link_to room_path(@room_id) do
i.fa-regular.fa-envelope.message
- else
= form_for @room do |f|
= fields_for @entry do |e|
= e.number_field :user_id, value: @user.id, hidden: true
= f.submit "メッセージを送る", class: "message_link"
ここでは、ログインしているユーザーではないという条件をつけてis_roomの条件を使用して既にroomが存在しているかどうかで分岐させる。
既にroomがあればそのroomに、なければformでパラメータを送っている。
rooms_controller
class RoomsController < ApplicationController
before_action :authenticate_user!
def create
@room = Room.create
@current_entry = @room.entries.create(user_id: current_user.id)
@another_entry = @room.entries.create(user_id: params[:entry][:user_id])
redirect_to room_path(@room)
end
def index
my_room_id = current_user.entries.pluck(:room_id)
@another_entries = Entry
.where(room_id: my_room_id)
.where.not(user_id: current_user.id)
.preload(room: :messages).preload(user: { icon_attachment: :blob })
end
def show
@room = Room.find(params[:id])
if @room.entries.where(user_id: current_user.id).present?
@messages = @room.messages.all
@message = Message.new
@entries = @room.entries
@another_entry = @entries.where.not(user_id: current_user.id).first
else
redirect_back(fallback_location: root_path)
end
end
end
~解説~
def create
@room = Room.create
@current_entry = @room.entries.create(user_id: current_user.id)
@another_entry = @room.entries.create(user_id: params[:entry][:user_id])
redirect_to room_path(@room)
end
ここでusers/show.html.slimで部屋が存在しなかった場合にformで送られてきたパラメーターを受け取って、createさせます。
現在ログインしているユーザーに対しては、@current_entry
として、entriesテーブルにRoom.createで作成された@room
に紐づくidと、現在ログインしているユーザーのidを保存させる記述をする。
@another_entry
では、メッセージの相手の情報をEntriesテーブルに保存するための記述。
users/show.html.slimのfields_for @entry
で保存したparamsの情報(:user_id, :room_id)を許可し、現在ログインしているユーザーと同じく@room
にひもづくidを保存する記述をしている。
最後にチャットルームが開くようにredirectをしている。
def index
my_room_id = current_user.entries.pluck(:room_id)
@another_entries = Entry
.where(room_id: my_room_id)
.where.not(user_id: current_user.id)
.preload(room: :messages)
.preload(user: { icon_attachment: :blob })
end
続いてindex
indexでは現在やり取りしていて存在しているroomの一覧を取得する。
まず最初にログイン中のユーザーがやり取りをしている全てのチャットルームのIDを取得する。
current_user.entries.pluck(:room_id)
で現在ログインしているユーザーが参加しているentriesテーブルから、room_id
カラムだけを抜き出して配列として取得する。
次に、最初に取得したroom_idに参加しているが、ログイン中のユーザーでない相手のエントリー情報を取得する。
続いてindexのview
- @another_entries.each do |entry|
= link_to room_path(entry.room), style: "text-decoration: none;" do
.card
.card-body
div
.d-flex
.chat-avatar
object
= link_to user_path(entry.user) do
= image_tag entry.user.icon, class: "message_icon_img me-3"
.chat-user-name
span.fw-bold.me-2
= entry.user.name
span.username
| @
= entry.user.username
.chat-text
= entry.room.messages.last&.message
def show
@room = Room.find(params[:id])
if @room.entries.where(user_id: current_user.id).present?
@messages = @room.messages.all
@message = Message.new
@entries = @room.entries
@another_entry = @entries.where.not(user_id: current_user.id).first
else
redirect_back(fallback_location: root_path)
end
end
showアクションでは、@room
で1つのroomを表示させる。
@messages
で過去のメッセージを全て表示させ、@message
で新しいメッセージを作るためのインスタンスを用意している。
@entries
と@another_entry
でview側で相手の名前を表示させられるようにしている。
showのview
.col-md-12.border_right.border_left.d-flex.flex-column.justify-content-between.vh-100
.d-flex.align-items-center.py-2
= link_to root_path, class: "text-dark" do
i.fa-solid.fa-arrow-left.arrow-left
.profile_top_name
h3
= @another_entry.user.name
| さんとのメッセージ
.chat-body.p-3.flex-grow-1.overflow-auto
- @messages.each do |m|
- if m.user_id == current_user.id
.d-flex.justify-content-end.mb-3
.mycomment.p-2.rounded.d-inline-block
= m.message
- else
.d-flex.justify-content-start.mb-3
.fukidasi.d-flex
.faceicon
= image_tag m.user.icon, class: "message_icon_img mr-2"
.chatting.p-2.rounded.bg-light.d-inline-block
.says
= m.message
.chat-form-box.p-3
= form_for @message do |f|
.chat-form-group.d-flex
= f.text_field :message, class: "form-control mr-2 flex-grow-1"
= f.number_field :room_id, value: @room.id, hidden: true
= f.submit "送信", class: "btn message_submit"
@another_entry
で相手の名前などを表示させることができる。
メッセージの部分をif文を用いて分岐させることによって、DMっぽく自分のメッセージは右側、相手のメッセージは左側のようにスタイリングすることができます。
メッセージを送る際のform部分では、@messageがどこのroomに所属しているかを判断するために、room_idの情報を持たせている。
そしてこのformで送られたパラメータがmessage_controllerに渡っていきます。
messages_controller
class MessagesController < ApplicationController
before_action :authenticate_user!
def create
message = current_user.messages.build(message_params)
if message.save
redirect_to room_path(message.room)
else
redirect_back(fallback_location: root_path)
end
end
private
def message_params
params.require(:message).permit(:message, :room_id)
end
end
rooms/show.html.slimで送られてきた、form_forのパラメータを実際に保存させるための記述をしています。
form_forで送られてきたmessageを含む全てのメッセージの情報の:messageと:room_idのキーがちゃんと入っているかということを条件で確認する。
もしその条件がtrueだったら、メッセージを保存するためにMessage.buildをし、Messagesテーブルに:message、room_idのパラメーターとして送られてきた値を許可する。
最後に
こんな感じで、実装することができました。
リレーションも複雑で、なかなか理解を深めるのは初心者の私にはとても難しかったです。今でも完璧ではないですが、データの流れなどは実装していくとともに理解できたかなとは思います。
理解が薄い部分はもっと調べて理解を深めていきたいと思います!