📨

相互フォローのDM機能

に公開

2人のユーザーが相互フォロー関係にある時のDM機能を実装します。
以下の機能が実装可能となります。

  • ユーザー詳細画面のフォローボタンの横に相互フォローになるとメッセージルームへのリンクが出現する。
  • メッセージルームで非同期でメッセージが投稿できる。

完成図

ER図

ここで難しいのはUser_roomテーブル、Chatテーブルの2つの中間テーブルがある点です。
それぞれ、多対多の関係を図解してみます。

User_roomテーブルの必要性

Chatテーブルの必要性

必要なモデルの作成

上記のER図の構造に沿って、Roomモデル、User_roomモデル、Chatモデルを作成します。
(Userテーブルはすでに作成済みという前提です)

rails g model Room
rails g model UserRoom user_id:integer room_id:integer
rails g model Chat user_id:integer room_id:integer message:string

アソシエーションの設定

user.rb
has_many :user_rooms
has_many :chats
room.rb
has_many :user_rooms
has_many :chats
user_room.rb
belongs_to :user
belongs_to :room
chat.rb
belongs_to :user
belongs_to :room

validates :message, presence: true, length: { maximum: 140 }
  • validates :message, presence: true, length: { maximum: 140 }
    DMを140字までの文字制限をするための記述を入れてあります。

ルーティングの設定

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

コントローラーの設定

chats_controller.rb
class ChatsController < ApplicationController
  before_action :reject_non_related, 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.new(room_id: @room.id)
  end
  
  def create
    @chat = current_user.chats.new(chat_params)
    render :validater unless @chat.save
  end

  private
  
  def chat_params
    params.require(:chat).permit(:message, :room_id)
  end

  def reject_non_related
    user = User.find(params[:id])
    unless current_user.following?(user) && user.following?(current_user)
      redirect_to books_path
    end
  end
  
end
  • rooms = current_user.user_rooms.pluck(:room_id)
    pluckメソッドを使って、ログインユーザーの持つroom_idを全て取得し、roomsに格納します。
  • user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms)
    @userで定義されているのは「チャットへ」のリンクを押された側のユーザーです。
    find_byメソッドを使って、一致する最初の一件のデータをuser_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
  • [unless user_rooms.nil?](https://www.sejuku.net/blog/75155)
    条件式を使って、user_roomsの値がnilだった場合は新規にRoomページを作成しています。
def create
  @chat = current_user.chats.new(chat_params)
  render :validater unless @chat.save
end
  • render :validater unless @chat.save
    @chatが保存されなかった場合にvalidaterにrenderします。
before_action :reject_non_related, only: [:show]
・
・
 def reject_non_related
   user = User.find(params[:id])
   unless current_user.following?(user) && user.following?(current_user)
     redirect_to books_path
    end
 end
  • reject_non_relatedでユーザー同士が相互フォローにない場合のリダイレクト先を指定しています。

ビューの設定

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

チャットルームへのリンクボタンを実装します。

  • current_user != user
    ログインユーザーではない
  • 'current_user.following?(user)'
    相手方がログインユーザーをフォローしている
  • 'user.following?(current_user)'
    相手方がログインユーザーをフォローしている
chats/show.html.erb
<h1 id="room"><%= @user.name %> さんとのチャット</h1>

<div class="message" style="width: 400px;">
  <% @chats.each do |chat| %>
    <% if chat.user_id == current_user.id %>
      <p style="text-align: right;"><%= chat.message %></p>
    <% else %>
      <p style="text-align: left;"><%= chat.message %></p>
    <% 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.hidden_field :room_id %>
<% end %>

チャットルームのshowページを作成します。

<% @chats.each do |chat| %>
    <% if chat.user_id == current_user.id %>
      <p style="text-align: right;"><%= chat.message %></p>
    <% else %>
      <p style="text-align: left;"><%= chat.message %></p>
    <% end %>
  <% end %>
  • ログインユーザーのチャットを右側、相手方のチャットを左側に配置します。
<%= form_with model: @chat, data: {remote: true} do |f| %>
  <%= f.text_field :message %>
  <%= f.hidden_field :room_id %>
<% end %>

-<%= f.hidden_field :room_id %>
コントローラーで設定したchat_paramsへデータを送るため、
ユーザーには見えない形でroom_idをコントローラーへ送ります。

Jsファイルの設定

非同期通信化についてはこちらの記事もご参照ください。
https://zenn.dev/yayu1303/articles/6dcf1cd5076dbe

create.js.erb
$('.message').append("<p style='text-align: right;'><%= @chat.message %></p>");
$('input[type=text]').val("")
  • .appendでHTML要素をJsファイル内に記述しています。
    この部分で、ログインユーザーのDM投稿は非同期通信化されます。
  • $('input[type=text]').val("")
    input[type=text]f.text_fieldを表しています。val("")とすることで、投稿後の記入欄に空白を追加します。
validater.js.erb
$('.errors').html("<%= j(render 'layouts/errors', obj: @chat) %>");
  • エラー部分の非同期通信かを行っています。

いかがだったでしょうか?
なんとか動きはしたものの、文法理解が難しかったです・・・
季節の変わり目もあって、くたくたになりながらコードを打ってました。

下記のような他にもやり方がありそうなので、
そちらのほうも試してみたいですね♪

それでは!

https://qiita.com/bindingpry/items/6790c91f374acc25bea2#users_controller

Discussion