LoginSignup
2
2

Rails DM機能の実装

Posted at

はじめに

今回は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図に表すと

スクリーンショット 2024-03-11 18.07.02.png

説明を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

関連付けの設定

それぞれのモデルの関連付けの記述をしていこうと思います。

user.rb
  has_many :entries, dependent: :destroy
  has_many :messages, dependent: :destroy
room.rb
  has_many :entries, dependent: :destroy
  has_many :messages, dependent: :destroy
entry.rb
  belongs_to :user
  belongs_to :room
message.rb
  validates :message, presence: true
  belongs_to :user
  belongs_to :room

ルーティングの設定

ルーティングの設定は次のようになります。

routes.rb
  resources :messages, only: [:create]
  resources :rooms, only: [:create, :index, :show]

コントローラーの作成

rails g controller rooms
rails g controller messages

users_controller

users_controller.rb
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です。

users/show.html.slim
.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"

2024-03-13 18.27のイメージ (1).jpg
2024-03-13 18.31のイメージ.jpg

ここでは、ログインしているユーザーではないという条件をつけてis_roomの条件を使用して既にroomが存在しているかどうかで分岐させる。
既にroomがあればそのroomに、なければformでパラメータを送っている。

rooms_controller

rooms_controller.rb
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

rooms/index.html.slim
- @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

スクリーンショット 2024-03-13 19.03.19.png

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

rooms/show.html.slim
.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

messages_controller.rb
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のパラメーターとして送られてきた値を許可する。

スクリーンショット 2024-03-13 22.36.18.png

最後に

こんな感じで、実装することができました。
リレーションも複雑で、なかなか理解を深めるのは初心者の私にはとても難しかったです。今でも完璧ではないですが、データの流れなどは実装していくとともに理解できたかなとは思います。
理解が薄い部分はもっと調べて理解を深めていきたいと思います!

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