📬

【Rails】DM機能(非同期通信化)

2023/10/09に公開

本の投稿サイトにDM機能を実装します!

実装要件

  • ユーザー同士で1対1のDMができるようにする(140字まで可)

  • 相互フォローしている人限定でDMが使える

  • 完成イメージ:


モデル作成

考え方

上図のように、ユーザー(users)とルーム(rooms)の関係は「多対多」の関係です。
そのため中間テーブルが必要です。
よって下図のようにします。

user_roomsテーブルとchatsテーブルの2つの中間テーブルを作ることで、1つのユーザーが複数のルームに参加し、各ルームで複数のチャットができるようにします。

テーブル設計

テーブルは以下の通り。

モデルの作成

  1. Roomモデル
ターミナル
rails g model Room
  1. Chatモデル
ターミナル
rails g model Chat user:references room:references message:string

messageカラムを入れます。

  1. User_roomモデル
ターミナル
rails g model UserRoom user:references room:references 

アソシエーション記述

app/models/user.rb
  has_many :user_rooms, dependent: :destroy
  has_many :chats, dependent: :destroy
  has_many :rooms, through: :user_rooms
app/models/room.rb
  has_many :user_rooms, dependent: :destroy
  has_many :chats, dependent: :destroy
app/models/chat.rb
  belongs_to :user
  belongs_to :room

  validates :message, presence: true, length: { maximum: 140 }

メッセージが空でなく、140字以下であることを検証するバリデーションを記載。

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

コントローラの作成

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

ターミナル
rails g controller chats

ルーティングの設定

以下の記述を追加します。

config/routes.rb
resources :chats, only: [:show, :create, :destroy]

実装要件にはありませんが、メッセージの削除ができるようにdestroyアクションも実装します。

コントローラの記述

解説はコメントアウトで記述します。

controllers/chats_controller.rb
class ChatsController < ApplicationController
  # showアクションにおいて、関連のないユーザーをブロックする
  before_action :block_non_related_users, only: [:show]

  # チャットルームの表示
  def show
    # チャット相手のユーザーを取得
    @user = User.find(params[:id])

    # 現在のユーザーが参加しているチャットルームの一覧を取得
    rooms = current_user.user_rooms.pluck(:room_id)

    # 相手ユーザーとの共有チャットルームが存在するか確認
    user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms)

    unless user_rooms.nil?
      # 共有チャットルームが存在する場合、そのチャットルームを表示
      @room = user_rooms.room
    else
      # 共有チャットルームが存在しない場合、新しいチャットルームを作成
      @room = Room.new
      @room.save

      # チャットルームに現在のユーザーと相手ユーザーを追加
      UserRoom.create(user_id: current_user.id, room_id: @room.id)
      UserRoom.create(user_id: @user.id, room_id: @room.id)
    end

    # チャットルームに関連付けられたメッセージを取得
    @chats = @room.chats

    # 新しいメッセージを作成するための空のChatオブジェクトを生成
    @chat = Chat.new(room_id: @room.id)
  end

  # チャットメッセージの送信
  def create
    # フォームから送信されたメッセージを取得し、現在のユーザーに関連付けて保存
    @chat = current_user.chats.new(chat_params)

    # バリデーションに合格しない場合はエラーを表示
    render :validate unless @chat.save
  end

  # チャットメッセージの削除
  def destroy
    # ログイン中のユーザーに関連するチャットメッセージを削除
    @chat = current_user.chats.find(params[:id])
    @chat.destroy
  end

  private

  # フォームから送信されたパラメータを安全に取得
  def chat_params
    params.require(:chat).permit(:message, :room_id)
  end

  # 関連のないユーザーをブロックする
  def block_non_related_users
    # チャット相手のユーザーを取得
    user = User.find(params[:id])

    # ユーザーがお互いにフォローしているか確認し、していない場合はリダイレクト
    unless current_user.following?(user) && user.following?(current_user)
      redirect_to books_path # リダイレクト先は適切なものに変更しましょう
    end
  end
end

ビューの記述

チャット開始ボタン追加

必要箇所に開始ボタンを追加してあげます。

  <% if current_user != user && current_user.following?(user) && user.following?(current_user) %>
    <%= link_to 'chatを始める', chat_path(user.id), class: "ml-3" %>
  <% end %>

解説:
<% if current_user != user && current_user.following?(user) && user.following?(current_user) %>:

  • current_user != user は、現在のユーザーがチャット相手と異なる場合。つまり、自分自身にはチャットを始めることはできないということ。
  • current_user.following?(user)user.following?(current_user) は、ユーザー同士がお互いにフォローしている状態を確認しています。

チャットルーム

部分テンプレート作成

非同期化するために、自分の送るメッセージ部を部分テンプレートにしてあげます。

views/chats/_chat.html.erb
<div class="right-container d-flex justify-content-end">
  <p id="chat_<%= chat.id %>" style="background-color: rgba(0, 185, 0, 0.6); padding: 5px; border-radius: 10px; "><%= chat.message %></p>
  <%= button_to "削除", chat_path(chat), method: :delete, data: { confirm: "本当に削除しますか?" }, class: "btn btn-danger btn-sm ml-2", remote: true, id: "delete_chat_#{chat.id}" %>
</div>

解説:
自身のチャットメッセージは右寄せで表示できるようにします。
id="chat_<%= chat.id %>id: "delete_chat_#{chat.id}"でメッセージごとに異なるIDをつけてあげることができます。そうすることで、消したいメッセージを非同期で削除できるようにします。

チャットルーム(show)の作成

views/chats/show.html.erb
<h2 id="room" data-room="<%= @room.id %>" data-user="<%= current_user.id %>"><%= @user.name %> さんとのチャット</h2>

<div class="message" style="width: 400px;">
  <% @chats.each do |chat| %>
    <% if chat.user_id == current_user.id %>
      <%= render "chats/chat", chat: chat %>
    <% else %>
      <div class="left-container d-flex">
        <%= image_tag @user.get_profile_image, size: '30x30', style: 'border-radius: 50%' %>
        <p class=ml-1 style="text-align: left;"><span style="background-color: #F5F5DC; padding: 5px; border-radius: 10px;"><%= chat.message %></span></p>
      </div>
    <% end %>
  <% end %>
</div>

<div class="errors">
  <%= render "layouts/errors", obj: @chat %>
</div>

<%= form_with model: @chat, data: {remote: true} do |f| %>
  <%= f.text_field :message %>
  <%= f.submit "送信", class: "btn btn-dark btn-sm" %>
  <%= f.hidden_field :room_id %>
<% end %>

解説:
相手側のメッセージは左寄せで表示するようにします。
また、部分テンプレートも呼び出してあげます。

ポイント
<%= f.hidden_field :room_id %>はフォーム内に隠しフィールドを作っています。これを使うことで、ユーザーには表示したくない情報も一緒に送ることができます。メッセージを送信する際、、どのチャットルームにメッセージを送るのかという情報が必要です。そのため、チャットルームIDを一緒に送信してあげます。

create.jsの作成

views/chats/create.js.erb
$('.message').append('<%= j(render "chats/chat", chat: @chat) %>');
$('input[type=text]').val("")

$('input[type=text]').val("")で、メッセージ送信後にテキストボックスを空にできます。

destroy.jsの作成

views/chats/destroy.js.erb
$('#chat_<%= @chat.id %>').remove();
$('#delete_chat_<%= @chat.id %>').remove();

IDを指定することで、消したいメッセージと削除ボタンを消すことができます。

validater.jsの作成

$('.errors').html("<%= j(render 'layouts/errors', obj: @chat) %>");

空のメッセージ等を送るとエラー表示を出すようにします。


完成!
やり残していた記事でした。何個かあるのでこれからアップしていきます!

続きはこちら。
https://zenn.dev/ganmo3/articles/9f236faaf9ebb3

Discussion