0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Rails DM機能の作り方

Posted at

はじめに

この記事はプログラミング初学者が他の記事を参考にしたり、実際に実装してみたりして、アウトプットの一環としてまとめたものです。内容に不備などあればご指摘いただますと幸いです。

今回Xクローン作成中DM機能を実装しました。
そのDM機能について備忘録も兼ねて記事を作成していきます。

実現したいこと

ユーザー同士、1対1でメッセージのやり取りできる機能を実装すること。

DM機能の流れ

相手ユーザーのツイートorサイドバーのメニューからメッセージ画面に遷移する。

roomがなければ作成する。

メッセージ画面でメッセージを送信する。

このように一般的なDM機能を作成していきます。

最終的にはこのような画面になります。
message.gif

DM機能について

テーブル設計は以下のようになります。

message_er.png

まず、usersテーブルはユーザーを管理するテーブルです。
roomsテーブルはチャットルーム(メッセージをやり取りする場所)となります。

ユーザーは複数のroomを持つ可能性があります。また、roomは複数(2人)のユーザーを持ちます。これは多対多の関係になるので、中間テーブルとしてentriesテーブルを置き情報を管理します。

ユーザーは複数のメッセージを送ることができます。また、roomでは複数のメッセージを受け取ることになります。これも多対多の関係になるので、中間テーブルとしてmessagesテーブルを置き情報を管理します。

モデルとテーブルの作成

それでは、実装していきましょう!
以下のコマンドでモデルを作成します。

$ rails g model room
$ rails g model entry user_id:integer room_id:integer
$ rails g model message user_id:integer room_id:integer content:text

生成されたentryモデルのマイグレーションファイルを以下のように編集します。

db/migrate/○○○_create_entries.rb
class CreateEntries < ActiveRecord::Migration[7.0]
  def change
    create_table :entries do |t|
      t.integer :user_id
      t.integer :room_id

      t.timestamps
    end

    add_index :entries, %i[user_id room_id], unique: true # 追加
  end
end

同じルームに同じユーザーが入ることを避けたいので、add_indexを使って一意制約を記述します。
これで、user_idとroom_idの組み合わせをユニーク(1通り)に設定します。

以下のコマンドでマイグレーションを実行しましょう。

$ rails db:migrate

アソシエーションの設定

それぞれのファイルに以下を追加します。

app/models/user.rb
has_many :entries, dependent: :destroy
has_many :messages, dependent: :destroy
app/models/room.rb
has_many :entries, dependent: :destroy
has_many :messages, dependent: :destroy
app/models/entry.rb
belongs_to :user
belongs_to :room

# 同じルームに同じユーザーが入ることを避けたいので、一意制約を記述します。
# これで、user_idとroom_idの組み合わせをユニーク(1通り)に設定します。
validates :user_id, uniqueness: { scope: :room_id }

DB側と同じようにアプリケーション側にも一意制約を記述しています。

app/models/message.rb
belongs_to :user
belongs_to :room

validates :content, presence: true

ルーティングの設定

以下のように記述します。

config/routes.rb
resources :users do
  resources :rooms, only: [:create]
end

resources :rooms, only: [:index] do
  resources :messages, only: [:create]
end

ネストすることで、users/:user_id/roomsrooms/:room_id/messagesのようなURLができます。
こうすることで、createアクションでuser_idparams[:user_id]として、room_idparams[:room_id]として取得できます。

Controllerの作成

以下のコマンドでコントローラを作成します。

$ rails g controller rooms
$ rails g controller messages

生成されたコントローラのファイルを以下のように編集します。

app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
  def create
    @user = User.find_by(id: params[:user_id]) # チャットする相手を取得
    my_room_ids = current_user.entries.pluck(:room_id) # ログイン中のユーザーの全てのチャットルームIDを取得 
    existing_room = @user.entries.find_by(room_id: my_room_ids) # チャットする相手とのルームがあるか確認

    if existing_room.nil # ルームがない場合
      @room = Room.create # 新しくルームを作成
      @room.entries.create(user_id: current_user.id) # 自分の中間テーブルを作成
      @room.entries.create(user_id: @user.id) # 相手の中間テーブルを作成
    end
    redirect_to rooms_path
  end

  def index
    my_room_ids = current_user.entries.pluck(:room_id) # ログイン中のユーザーの全てのチャットルームIDを取得
    # 相手のエントリー情報を取得
    @another_entries = Entry.where(room_id: my_room_ids)
                            .where.not(user_id: current_user.id)
                            .includes(user: { icon_image_attachment: :blob })

    if params[:tab] == 'room' # 右側のチャットルームでメッセージを表示できるようにする
      @room = Room.find(params[:room_id]) # ルームを取得する
      @messages = @room.messages # ルームに関連したメッセージを取得
      @message = Message.new # form_withに渡すため空のインスタンスを生成
      # 相手のエントリー情報を取得
      @another_entry = @room.entries.where.not(user_id: current_user.id).first
    end
  end
end

createアクションでは、ログイン中のユーザーと相手ユーザーのroomが存在するか調べ、なければ作成します。

indexアクションのif params[:tab] == 'room'は、link_toのクエリパラメータで渡された値を使っています。

app/controllers/messages_controller.rb
class MessagesController < ApplicationController
  def create
    @room = Room.find(params[:room_id])
    @message = @room.messages.build(user_id: current_user.id, content: params[:message][:content])
    @message.save!
    redirect_to request.referer, notice: 'メッセージが送信されました'
  end
end

Viewの作成

チャットルーム一覧の表示、メッセージ一覧の表示、メッセージ投稿フォームの設置をしていきます。

app/views/rooms/index.html.slim
div.message-contents
  = render 'layouts/sidebar'

  main.main.border-start.border-end
    div.bar.d-flex.align-items-center
      div.bar-name
        | メッセージ   

    # チャットルーム一覧の表示
    div
      - @another_entries.each do |entry|
        = link_to rooms_path(tab: 'room', room_id: "#{entry.room.id}"), class: "room-link" do # クエリパラメータを渡す
          div.d-flex class="#{'active-room' if params[:room_id] == "#{entry.room.id}"} one-room"
            div
              = image_tag entry.user.icon_image.variant(resize_to_fill: [40, 40]).processed, class: "post-user-img rounded-circle"
            div
              div
                span.room-name
                  = entry.user.name
                span.room-date
                  = entry.room.messages.last&.created_at&.to_fs(:room_datetime_jp) #「ぼっち演算子 &.」レシーバがnilであった場合でもエラーが発生しない
              div.room-content
                span
                  = entry.room.messages.last&.content

  div.message-container.border-end
    -if params[:tab] == 'room' # チャットルーム一覧をクリックでメッセージ一覧が表示される
      div.bar.d-flex.align-items-center.border-bottom
        div
          = image_tag @another_entry.user.icon_image.variant(resize_to_fill: [35, 35]).processed, class: "post-user-img rounded-circle"
        div.bar-room-name
          = @another_entry.user.name

      # メッセージ一覧の表示 
      div.exchanging-messages-container.pt-4
        div.exchanging-messages
          - @messages.each do |message|
            - if message.user == current_user # 自分のメッセージを表示
              div.d-flex.flex-column.align-items-end.mb-3
                div.my-message
                  = simple_format(message.content, class: "simple_format")
                div.message-date
                  = message.created_at.to_fs(:room_datetime_jp)
            - else # 相手のメッセージを表示
              div.d-flex.flex-column.align-items-start.mb-3
                div.another-message
                  = message.content
                div.message-date
                  = message.created_at.to_fs(:room_datetime_jp)

        # メッセージ投稿フォームの設置
        div.message-form-container
          = form_with model: [@room, @message], url: room_messages_path(@room) do |f|
            div.d-flex.align-items-center.justify-content-center
              = f.text_area :content, class: "message-form", placeholder: "新しいメッセージを作成"
              = button_tag type: "submit", class: "form-btn" do
                i.bi.bi-send

メッセージ一覧の部分をif文を用いて分岐させることによって、DMっぽく自分のメッセージは右側、相手のメッセージは左側のようにスタイリングすることができます。

form_withを記述する際、ルーティングでネストを定義している時は[@room, @message]のように、配列で二つ ([関連元のインスタンス, 関連先のインスタンス]) 渡す必要があります。

おわりに

最後まで読んでいただきありがとうございました。
少しでも皆さんの参考になれば幸いです。

参考にしたサイト

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?