🙆‍♀️

相互フォローしているユーザ同士で DM ができるようにする

2023/08/04に公開

要件

・ユーザ同士で 1 対 1 の DM ができるようにする
※ 140 字まで送信可能にする
・相互フォローしている人限定で DM 機能を使えるようにする
・非同期通信を利用する

開発環境

ruby 3.1.2p20
Rails 6.1.7.4
Cloud9

前提

・Userモデルは作成済み
・devise導入済み
・フォロー機能実装済み

https://zenn.dev/airiin/articles/fae2dd6e0ce1b1

モデルとアソシエーション

モデルとアソシエーションは上図のようになります。

DM機能には、Userモデル、Entryモデル、Roomモデル、Messageモデルの4つが必要です。
ユーザーがチャットルームに入って、メッセージを送り合うイメージです。

まず、ユーザー情報を管理する Userモデルがあります。
1User は 多くのRoomを持ちます。(多くの相互フォローユーザーとDMが可能です)
1Room は 複数のUserを持ちます。(1Roomには常に2Userが入っています)
このように、UserとRoomは多対多の関係になるので、中間テーブルが必要になります。

そこで、Entryモデルという中間テーブルを作成します。
1Userは 多くのEntryモデルを持ち、
1Roomは 複数のEntryモデルを持ちます。

次に、メッセージ情報を管理する Messageモデルを作成します。
1Userは 多くのMessageを持ちます。
1Roomも 多くのMessageを持ちます。

ではこれらを前提に、モデルとアソシエーションを作成します。

$ rails g model room name:string
$ rails g model entry user:references room:references
$ rails g model message user:references room:references content:text
$ rails db:migrate
user.erb
  has_many :entries, dependent: :destroy
  has_many :messages, dependent: :destroy
  has_many :rooms, through: :entries
room.erb
  has_many :entries, dependent: :destroy
  has_many :messages, dependent: :destroy
entry.erb
  belongs_to :user
  belongs_to :room
message.erb
  belongs_to :user
  belongs_to :room

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

DMで送信できるメッセージは140字以内なので、バリデーションも作成しておきます。

ルーティング

ルーティングは以下の通りです。

config/routes.rb
  resources :users, only: [:index,:show,:edit,:update] 
  resources :messages, only: [:create]
  resources :rooms, only: [:create, :show]

Usersコントローラ

usersコントローラ
roomsコントローラ
messagesコントローラ
を作成します。

$ rails g controller users index show edit update
$ rails g controller rooms
$ rails g controller messages

流れとしては、
相互フォローの相手ユーザーの showページで「チャット開始」ボタンを押す
→ Roomが作成される
→ Roomの showページでユーザーがMessageを作成する
という感じです。

まず、usersコントローラから記述します。

users_controller.rb
def show
    @user = User.find(params[:id])
    @currentUserEntry = Entry.where(user_id: current_user.id)
    @userEntry = Entry.where(user_id: @user.id)
    unless @user.id == current_user.id
      @currentUserEntry.each do |cu| 
        @userEntry.each do |u| 
          if cu.room_id == u.room_id 
            @isRoom = true
            @roomId = cu.room_id
          end
        end
      end
      if @isRoom
      else
        @room = Room.new
        @entry = Entry.new
      end
    end
  end

内容を一つずつ解説していきます。

@currentUserEntry = Entry.where(user_id: current_user.id)
現在ログインしているユーザーの全Entryデータを取得します。

@userEntry = Entry.where(user_id: @user.id)
@userの全Entryデータを取得します。

    unless @user.id == current_user.id
      @currentUserEntry.each do |cu| 
        @userEntry.each do |u| 
          if cu.room_id == u.room_id 
            @isRoom = true
            @roomId = cu.room_id
          end

unless @user.id == current_user.id
@user と current_user が別人の時

@currentUserEntry.each do |cu|
現在ログインしているユーザーの全Entryデータを1つずつ取り出します。

@userEntry.each do |u|
@userの全Entryデータを1つずつ取り出します。

if cu.room_id == u.room_id
現在ログインしているユーザーのEntryデータのうち、room_idが
@userのEntryデータの持つroom_idと同じ時

@isRoom = true
現在ログインしているユーザーと@userの共通のRoomがあることを明確にしている

@roomId = cu.room_id
@roomIdに その現在ログインしているユーザーと@userの共通のroom_idを代入

      if @isRoom
      else
        @room = Room.new
        @entry = Entry.new
      end
if @isRoom
else

@isRoomがfalseの時
つまり、現在ログインしているユーザーと@userの共通のRoomがない時

@room = Room.new
@entry = Entry.new
新しい Roomと Entryを作成

Roomsコントローラ

rooms_controller.rb
  before_action :authenticate_user!
  before_action :reject_non_related, only: [:show]

  def create
    @room = Room.create
    @entry1 = Entry.create(room_id: @room.id, user_id: current_user.id)
    @entry2 = Entry.create(params.require(:entry).permit(:user_id, :room_id).merge(room_id: @room.id))
    redirect_to "/rooms/#{@room.id}"
  end

  def show
    @room = Room.find(params[:id])
    if Entry.where(user_id: current_user.id, room_id: @room.id).present?
      @messages = @room.messages
      @message = Message.new
      @entries = @room.entries
    else
      redirect_back(fallback_location: root_path)
    end
  end

  private

    def reject_non_related
      room = Room.find(params[:id])
      user = room.entries.where.not(user_id: current_user.id).first.user
      unless (current_user.following?(user)) && (user.following?(current_user))
        redirect_to books_path
      end
    end

こちらも一つずつ解説します。

  def create
    @room = Room.create
    @entry1 = Entry.create(room_id: @room.id, user_id: current_user.id)
    @entry2 = Entry.create(params.require(:entry).permit(:user_id, :room_id).merge(room_id: @room.id))
    redirect_to room_path(@room.id)
  end

新しいRoomを作成しています。
Roomは2つのEntryを持ちますので、Entryも2つ作成しています。

  def show
    @room = Room.find(params[:id])
    if Entry.where(user_id: current_user.id, room_id: @room.id).present?
      @messages = @room.messages
      @message = Message.new
      @entries = @room.entries
    else
      redirect_back(fallback_location: root_path)
    end
  end

こちらはチャットルームの画面です。

  private

    def reject_non_related
      room = Room.find(params[:id])
      user = room.entries.where.not(user_id: current_user.id).first.user
      unless (current_user.following?(user)) && (user.following?(current_user))
        redirect_to books_path
      end
    end

こちらは、相互フォローでないユーザーが直接チャットルームのURLを検索しても、チャットルームに入れないようにするためのメソッドです。

Messagesコントローラ

messages_controller.rb
  def create
    if Entry.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
      @message = Message.new(message_params)
      @message.user_id = current_user.id
      @message.save
    else
      flash[:alert] = "メッセージ送信に失敗しました。"
    end
    render :validater unless @message.save
  end

  private

    def message_params
      params.require(:message).permit(:user_id, :room_id, :content)
    end

こちらも内容を解説します。

if Entry.where(user_id: current_user.id, room_id: params[:message][:room_id]).present?
user_idが current_user.idで、
room_idが params[:message]の room_idである Entryが存在するなら

render :validater unless @message.save
@messageが保存されなかった場合、validaterにrenderする。
※validaterは後ほどjsファイルとして作成します。

users/showページの作成

users/show.html.erb

  <% if @user != current_user %>
    <% if (current_user.following?(@user)) && (@user.following?(current_user)) %>
      <% if @isRoom == true %>
        <%= link_to "チャットへ",  room_path(@roomId), class: "btn btn-primary" %>
      <% else %>
        <%= form_for @room do |f| %>
          <%= fields_for @entry do |e| %>
            <%= e.hidden_field:user_id, value: @user.id %>
          <% end %>
          <%= f.submit "チャットを始める", class: "btn btn-primary user-show-chat" %>
        <% end %>
      <% end %>
    <% end %>
    <% end %>
  <% end %>
</div>

こちらも一つずつ解説します。
<% if @user != current_user %>
@userと現在ログインしているユーザーが異なるユーザーの時

<% if (current_user.following?(@user)) && (@user.following?(current_user)) %>
@userと現在ログインしているユーザーが相互フォローの関係の時

※following?(user)メソッドは、userモデルに記述します。

user.rb
  def following?(user)
    followings.include?(user)
  end

<% if @isRoom == true %>
<%= link_to "チャットへ", room_path(@roomId), class: "btn btn-primary" %>
@isRoomが trueなら(現在ログインしているユーザーと@userが共通のRoomをすでに持っていたら)、「チャットへ」というリンクを表示。

      <% else %>
        <%= form_for @room do |f| %>
          <%= fields_for @entry do |e| %>
            <%= e.hidden_field:user_id, value: @user.id %>
          <% end %>
          <%= f.submit "チャットを始める", class: "btn btn-primary user-show-chat" %>

@isRoomが falseなら(現在ログインしているユーザーと@userが共通のRoomをまだ持っていなかったら)、user_idが @user.idの roomとentryを作成します。
「チャットを始める」ボタンを押すと、@user.idが Roomsコントローラの createメソッドに渡されます。

rooms/showページの作成

rooms/show.html.erb
<div class="container">
  <div class="left-button">
    <%= link_to "ユーザー一覧に戻る", users_path, class:"btn btn-info" %>
  </div>

  <% @entries.each do |e| %>
    <% unless e.user == current_user %>
    <h2><%= e.user.name %>さんとのチャットルーム</h2>
    <% end %>
  <% end %>

  <hr>
  <div class="chat">
    <div class="chats">
      <% if @messages.present? %>
        <% @messages.each do |m| %>
          <% if m.user_id == current_user.id %>
              <div class="chat-fukidashi" style="text-align: right;">
                <strong><%= m.content %></strong>
              </div>
          <% else %>
              <div class="chat-fukidashi" style="text-align: left;">
                <strong><%= m.content %></strong>
              </div>
          <% end %>
        <% end %>
      <% end %>
    </div>

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

    <div class="posts">
      <%= form_with model: @message, local: false do |f| %>
        <%= f.text_field :content, placeholder: "140字以内でメッセージを入力してください", size: 70, class: "form-text-field" %>
        <%= f.hidden_field :room_id, value: @room.id %>
        <%= f.submit "投稿", class: "form-submit" %>
      <% end %>
    </div>
  </div>
</div>

こちらも一つずつ解説します。

  <% @entries.each do |e| %>
    <% unless e.user == current_user %>
    <h2><%= e.user.name %>さんとのチャットルーム</h2>
    <% end %>
  <% end %>

Roomsコントローラ showメソッドで定義した
@room = Room.find(params[:id])
@entries = @room.entries
これらを使います。
@roomに入ってる2つのentriesのうち、current_userでない方のuserのnameを使って、チャットルーム名を表示します。

      <% if @messages.present? %>
        <% @messages.each do |m| %>
          <% if m.user_id == current_user.id %>
              <div class="chat-fukidashi" style="text-align: right;">
                <strong><%= m.content %></strong>
              </div>
          <% else %>
              <div class="chat-fukidashi" style="text-align: left;">
                <strong><%= m.content %></strong>
              </div>
          <% end %>
        <% end %>
      <% end %>

既に送付済みの @messagesが存在するならば、
@messagesを一つずつ取り出して、contentを表示させます。

自分が送付したメッセージなら text-align: right; に、
相手が送付したメッセージなら text-align: left; にします。

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

エラーがあった時に表示されるよう、layouts/errorsを呼び出します。

lauouts/_errors.html.erb
<% if obj.errors.any? %>
  <div id="error_explanation">
    <h3><%= pluralize(obj.errors.count, "error") %> prohibited this obj from being saved:</h3>
    <ul>
      <% obj.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>
    <div class="posts">
      <%= form_with model: @message, local: false do |f| %>
        <%= f.text_field :content, placeholder: "140字以内でメッセージを入力してください", size: 70, class: "form-text-field" %>
        <%= f.hidden_field :room_id, value: @room.id %>
        <%= f.submit "投稿", class: "form-submit" %>
      <% end %>
    </div>

form_withを使って、メッセージ送信部分を作成します。
非同期通信にしたいので、local: false としています。
f.hidden_field を使って、room_idに @room.idを代入しています。

非同期通信用のjsファイル作成

非同期通信でDMでのやり取りが表示されるようにしたいので、jsファイルを作成します。

views/messages/create.js.erb
$('.chats').append(`
    <div class="chat-fukidashi" style='text-align: right;'>
      <strong><%= j @message.content %></strong><br>
      ${'<%= created_at %>'}
    </div>
`)
$('input[type=text]').val("")

Messagesコントローラの createアクションが実行された後の表示部分です。
Rooms/showページの chatsというクラス名の部分について記載しています。

views/messages/validater.js.erb
$('.errors').html("<%= j(render 'layouts/errors', obj: @message) %>");

エラーが発生した時は、こちらのファイルが参照されて、エラーメッセージが表示されます。

以上で完成です!

参照

https://qiita.com/bindingpry/items/6790c91f374acc25bea2#usersshowhtmlerb
https://qiita.com/nojinoji/items/2b3f8309a31cc6d88d03
https://zenn.dev/goldsaya/articles/71758cf3024dc1#users%2Fshow.html.erb

Discussion