相互フォローしているユーザ同士で DM ができるようにする
要件
・ユーザ同士で 1 対 1 の DM ができるようにする
※ 140 字まで送信可能にする
・相互フォローしている人限定で DM 機能を使えるようにする
・非同期通信を利用する
開発環境
ruby 3.1.2p20
Rails 6.1.7.4
Cloud9
前提
・Userモデルは作成済み
・devise導入済み
・フォロー機能実装済み
モデルとアソシエーション
モデルとアソシエーションは上図のようになります。
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
has_many :entries, dependent: :destroy
has_many :messages, dependent: :destroy
has_many :rooms, through: :entries
has_many :entries, dependent: :destroy
has_many :messages, dependent: :destroy
belongs_to :user
belongs_to :room
belongs_to :user
belongs_to :room
validates :content, presence: true, length: { maximum:140 }
DMで送信できるメッセージは140字以内なので、バリデーションも作成しておきます。
ルーティング
ルーティングは以下の通りです。
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コントローラから記述します。
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コントローラ
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コントローラ
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ページの作成
<% 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モデルに記述します。
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ページの作成
<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を呼び出します。
<% 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ファイルを作成します。
$('.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というクラス名の部分について記載しています。
$('.errors').html("<%= j(render 'layouts/errors', obj: @message) %>");
エラーが発生した時は、こちらのファイルが参照されて、エラーメッセージが表示されます。
以上で完成です!
参照
Discussion