はじめに
メッセージ(DM機能)を作成した際、私が作成したメッセージ機能の場合、同じユーザ同士ではなく、ShopユーザとMemberユーザという異なるユーザとのやり取りになりました。
この場合のメッセージの表示方法などに何度も混乱させられました。
正直、あまり良いやり方とは自分でも思えないのですが、頑張ったので振り返りのために記録しておこうと思います。
前提
もっと簡単な方法があったんじゃないかという気もしている前提条件。
member用のメッセージデータのmember_messagesと
shop用のメッセージデータのshop_messagesを作っています。
※Roomsにあるis_take_careは対応待ちと対応済みの判定を行うためのデータ。
(trueなら対応済みでfalseなら対応待ち)
この形になった流れ…
messagesを作ってmember_idとshop_idを設定すればいいのでは?
↓
どっちのメッセージか分からなくなりそう…
↓
memberのメッセージならmember_idだけ、shopのメッセージならshop_idだけを保存すればいいのでは?
↓
確実に空になるカラムが存在するってどうなんだろうか…
↓
両方のidを保存して、尚且つどっちのメッセージかを記録するカラムを作る?
↓
回りくどい気がする…
↓
いっそmessagesをshop用とmember用に分けるか
という感じです。
これがベストだったのかは正直分かりません…。
ちなみにモデルの関連付けはこのようになっています。
# member.rb
has_many :member_messages, dependent: :destroy
has_many :rooms, dependent: :destroy
# shop.rb
has_many :shop_messages, dependent: :destroy
has_many :rooms, dependent: :destroy
# member_message.rb
belongs_to :member
belongs_to :room
# shop_message.rb
belongs_to :shop
belongs_to :room
# room.rb
has_many :member_messages, dependent: :destroy
has_many :shop_messages, dependent: :destroy
belongs_to :member
belongs_to :shop
実装内容
ここからは実際の記述内容です。
ほとんど全部のせたので長くなっています。
コントローラ
【Memberユーザ側】
※Memberユーザは一般ユーザになるので「public」としています。
class Public::MessagesController < ApplicationController
before_action :authenticate_member!
before_action :block_non_related_member
# Shop詳細ページからメッセージを送るのでparamsではshopのidが送られる
def show
@shop = Shop.find(params[:id])
# shop_idとmember_idが一致するroomがないか探して、なければ新しいroomを作り、あれば既存のroomを開く
room = Room.find_by(member_id: current_member, shop_id: @shop)
unless room.nil?
@room = room
else
@room = Room.new
@room.member_id = current_member.id
@room.shop_id = @shop.id
@room.save
end
# メッセージのやり取りを表示するために、member_messagesとshop_messagesを足してcreate順に並べる
@messages = (@room.member_messages + @room.shop_messages).sort_by(&:created_at)
# 送信フォームのための新しいメッセージ作成ボックス
@message = MemberMessage.new(room_id: @room.id)
end
# 一覧ではやり取りしているメッセージの中で一番新しいメッセージを表示したい
def index
@rooms = Room.where(member_id: current_member)
@messages = []
@rooms.each do |room|
# roomの中からMemberMessageとShopMessageそれぞれの一番新しいメッセージを取得
new_member_message = MemberMessage.where(room_id: room.id).order(created_at: "DESC").first
new_shop_message = ShopMessage.where(room_id: room.id).order(created_at: "DESC").first
# new_member_messageとnew_shop_message両方あるならcreated_atが新しい方を@messagesに入れる
if new_member_message && new_shop_message
new_message = [new_member_message, new_shop_message].max_by(&:created_at)
@messages << new_message
# new_member_messageだけなら@messagesにnew_member_messageを入れる
elsif new_member_message
@messages << new_member_message
# new_shop_messageだけなら@messagesにnew_shop_messageを入れる
elsif new_shop_message
@messages << new_shop_message
end
# @messagesのcreated_atを取得し、宇宙船演算子で比較して並び替え
@messages.sort! { |a, b| b.created_at <=> a.created_at }
end
end
def create
@message = MemberMessage.new(member_message_params)
@message.member_id = current_member.id
if @message.save
# メッセージが送られたらroomのステータスを対応待ちにするための設定
+ Room.find_by(id: @message.room_id).update(is_take_care: false)
redirect_to request.referer
else
flash[:alert] = "送信に失敗しました"
redirect_to request.referer
end
end
def destroy
message = MemberMessage.find(params[:id])
message.destroy
redirect_to request.referer
end
private
def member_message_params
params.require(:member_message).permit(:message, :room_id)
end
# 自分以外のメッセージルームは開けないようにするための設定
def block_non_related_member
if room = Room.find_by(member_id: current_member, shop_id: @shop)
redirect_to root_path
end
end
end
max_by
Ruby の Enumerable モジュールに含まれるメソッドで、ブロックの評価結果に基づいて、コレクションの中から最大の要素を返します。
具体的には、各要素に対して指定された基準(ブロック内の評価結果)を比較し、その中で最大の評価結果を持つ要素を選びます。
sort!
Ruby の配列メソッドで、配列そのものをソートして変更します(破壊的メソッド)。
sort は元の配列を変更せずにソートされた新しい配列を返しますが、sort! は元の配列をソートされた状態に変更します。
<=>
今回のようなb.created_at <=> a.created_at の場合、created_at が新しい順に並べ替えます。
具体的には、b の作成日時が a の作成日時より新しければ b が前に来ます。
逆に、a の作成日時が b の作成日時より新しければ a が前に来ます。
【Shopユーザ側】
shopユーザ側もほぼ同じ…と思いきや、shopからはメッセージを送れず、メッセージのやり取りを開始できるのはmemberユーザのみという設定にしてしまったがために、showページのコントローラは少し違う記述になっています。
この設定が混乱の原因でした。
class Shop::MessagesController < ApplicationController
before_action :authenticate_shop!
before_action :block_non_related_shop, only: [:show, :destroy]
def show
# shopからはメッセージを送れないので、単純にroomのidを取得(ここが良くない気がする)
@room = Room.find(params[:id])
@messages = (@room.member_messages + @room.shop_messages).sort_by(&:created_at)
@message = ShopMessage.new(room_id: @room.id)
end
def index
@rooms = Room.where(shop_id: current_shop)
@messages = []
@rooms.each do |room|
new_member_message = MemberMessage.where(room_id: room.id).order(created_at: "DESC").first
new_shop_message = ShopMessage.where(room_id: room.id).order(created_at: "DESC").first
if new_member_message && new_shop_message
new_message = [new_member_message, new_shop_message].max_by(&:created_at)
@messages << new_message
elsif new_member_message
@messages << new_member_message
elsif new_shop_message
@messages << new_shop_message
end
@messages.sort! { |a, b| b.created_at <=> a.created_at }
end
end
def create
@message = ShopMessage.new(shop_message_params)
@message.shop_id = current_shop.id
if @message.save
redirect_to request.referer
else
flash[:alert] = "送信に失敗しました"
redirect_to request.referer
end
end
# 対応状況(is_take_care)を更新
def update
@room = Room.find(params[:id])
if @room.update(room_params)
flash[:notice] = "更新しました"
redirect_to request.referer
else
end
end
def destroy
message = ShopMessage.find(params[:id])
message.destroy
redirect_to request.referer
end
private
def shop_message_params
params.require(:shop_message).permit(:message, :room_id)
end
def room_params
params.require(:room).permit(:is_take_care)
end
def block_non_related_shop
room = Room.find(params[:id])
if (shop_signed_in? && room.shop_id != current_shop.id) || (member_signed_in? && room.member_id != current_member.id)
redirect_to root_path
end
end
end
showアクション以外はだいたいmember側と同じ。
ここで問題と感じているのが、
member側のメッセージルームのURLで取得するidがshop.idなのに対し、
shop側のメッセージルームのURLで取得するidがroom.idという点です。
希望としては両方ともroom.idとしたかったのですが、member側からは新しいメッセージルームを作る必要があるのでshop.idの取得が必要。
それならいっそ、
shop側のメッセージルームのURLで取得するidをmember.idにした方が分かりやすいかな…?
それで上手くいきそうだったら、そのうち直しておこうと思います。
ビュー
ビューもほとんど同じなのでshopページだけのせておきます。
レイアウトにはBootstrapをメインに使ってますが、CSSで設定している部分もあるので、Bootstrapと関係ないclass名も混ざっています。
【メッセージルーム(showページ)】
<div class="container d-flex flex-row-reverse flex-wrap pt-3">
<div id="messages-top" class="main-content col-9 px-3 pt-2">
<h2><%= @room.member.member_full_name %> 様とのメッセージ</h2>
<hr>
<!--対応状況の変更-->
<%= form_with model: @room, url: shop_message_path do |f| %>
<div class="d-flex justify-content-center bg-light p-3 mt-3 mb-3">
<div class="d-flex flex-row flex-wrap me-3">
<div class="pe-2">
<%= f.radio_button :is_take_care, true, class: "form-check-input" %><%= f.label :is_take_care_true, "対応済み", class: "form-check-label text-success fw-bold ps-2" %>
</div>
<div class="pe-2">
<%= f.radio_button :is_take_care, false, class: "form-check-input" %><%= f.label :is_take_care_false, "対応待ち", class: "form-check-label text-danger fw-bold ps-2" %>
</div>
</div>
<div>
<%= f.submit "変更する", class: "btn btn-info p-2" %>
</div>
</div>
<% end %>
<div class="col-12">
<!--メッセージ-->
<div id="messages-container" class="py-2 messages-container">
<% @messages.each do |message| %>
<div class="bg-light p-2">
<!--メッセージの背景レイアウトを変えたかったのでmessageがShopMessageだったらmy-message、MemberMessageだったらother-messageのclass名を設定-->
<div class="p-3 <%= (message.is_a?(ShopMessage) ? "my-message" : "" ) %> <%= (message.is_a?(MemberMessage) ? "other-message" : "" ) %>">
<!--メッセージがShopMessageだった場合の設定-->
<% unless message.is_a?(ShopMessage) %>
<span class="px-1 py-0">
<%= link_to shop_member_path(message.member) do %>
<%= message.member.member_full_name %> 様
<% end %>
</span>
<% end %>
<p class="pt-1">
<%= message.message %>
</p>
<small class="text-body-tertiary">(<%= message.created_at.strftime("%Y年%m月%d日 %H:%M:%S") %>)</small>
<% unless message.is_a?(MemberMessage) %>
<%= link_to "削除", shop_message_path(message), method: :delete, "data-confirm" => "本当に削除しますか?" %>
<% end %>
</div>
</div>
<% end %>
</div>
<!--送信フォーム-->
<div class= "py-2">
<%= form_with model: @message, url: shop_messages_path do |f| %>
<%= f.text_area :message, class: "form-control" %>
<div class= "py-2 text-end">
<%= f.submit "送信", class: "btn btn-info" %>
</div>
<%= f.hidden_field :room_id %>
<% end %>
</div>
</div>
</div>
<!--左ナビ-->
<div class="left-content col-3 px-3">
<%= render 'layouts/shop_menu' %>
</div>
</div>
is_a?
Ruby のオブジェクト指向プログラミングにおいて、オブジェクトが特定のクラスまたはモジュールのインスタンスであるかどうかをチェックするために使用されます。
今回の場合、message.is_a?(ShopMessage)では、message オブジェクトが ShopMessage クラスのインスタンスであるかどうかをチェックします。
【メッセージ一覧(indexページ)】
<div class="container d-flex flex-row-reverse flex-wrap pt-3">
<div class="main-content col-9 px-3">
<h2>新着メッセージ</h2>
<hr>
<% messages.each do |message| %>
<!--メッセージルーム詳細ページのリンク…id="messages-top"の位置を開きたかったのでanchorを指定-->
<%= link_to shop_message_path(message.room_id, anchor: 'messages-top') do %>
<div class="d-flex flex-row my-3">
<div class="col-1">
<% if message.room.is_take_care %>
<span class="text-success">
対応済み
</span>
<% else %>
<span class="text-danger">
対応待ち
</span>
<% end %>
</div>
<div class="col-11 d-flex flex-row flex-wrap p-3 <%= (message.is_a?(ShopMessage) ? "my-message" : "" ) %> <%= (message.is_a?(MemberMessage) ? "other-message" : "" ) %>">
<div class="col-9">
<% if message.is_a?(MemberMessage) %>
<%= message.member.member_full_name %> 様 [ <%= message.member.id %> ]
<% elsif message.is_a?(ShopMessage) %>
<%= message.room.member.member_full_name %> 様 [ <%= message.room.member.id %> ]
<% end %>
<p class="pt-1"><%= message.message %></p>
</div>
<div class="col-3">
<small class="text-secondary"><%= message.created_at.strftime("%Y年%m月%d %H:%M:%S") %></small>
</div>
</div>
</div>
<% end %>
<% end %>
<% if @messages.blank? %>
<p>メッセージはありません</p>
<% end %>
</div>
<!--左ナビ-->
<div class="left-content col-3 px-3">
<%= render 'layouts/shop_menu' %>
</div>
</div>
anchor
ページ内の特定の場所へジャンプするために使う機能です。
Ruby on Rails の link_to ヘルパーで anchor オプションを使うと、リンクをクリックしたときにページ内の特定の位置に移動するリンクを作ることができます。
おまけ…レイアウトとか
DM機能と言えば、画面の下に送信フォームがあり、メッセージのやり取りも下から新着順に並ぶのが一般的ではないかと思います。
ここからは、その表示のために設定した記述の内容です。
.my-message {
background-color: #CCE4E4;
width: 85%;
margin-left: auto;
border-radius: 15px 15px 2px 15px;
}
.other-message {
background-color: #EAEAEA;
width: 85%;
margin-right: auto;
border-radius: 15px 15px 15px 2px;
}
// スクロールバーを設置するため
.messages-container {
max-height: 500px;
overflow-y: scroll;
}
overflow-y: scroll;
CSS のプロパティで、要素の垂直方向のコンテンツが要素の高さを超える場合に、スクロールバーを表示するように指示します。
このプロパティを使用すると、コンテンツが要素の領域内に収まらないとき、垂直スクロールバーが常に表示されます。
javascript
scroll_messages.jsを作成しました。
// Turbolinksによるページロードが完了したときに特定のコードを実行
document.addEventListener('turbolinks:load', function() {
// 指定した時間(300ミリ秒)後に特定の関数を実行するためのタイマーを設定
setTimeout(function() {
// messages-containerのHTML要素を取得
var messagesContainer = document.getElementById('messages-container');
if (messagesContainer) {
// messages-container要素のスクロール位置を、その要素内のコンテンツの高さに設定
messagesContainer.scrollTop = messagesContainer.scrollHeight;
}
},300);
});
scrollTop = scrollHeight
要素内のスクロール位置がコンテンツの最下部に移動します。
おわりに
こうして振り返ってみるとそんなに悪くないような気もしてきました。
記述している最中はなんでこうなるんだー!と頭を抱えることも多かったですが、こうして冷静に見てみると、あの時うまくいかなかったのは当然だったな…というミスが思い浮かびます。
作成中はけっこう冷静になれないことも多くなりがちなのかなと感じたので、分からなくなったときは落ち着いて内容をまとめるべきだなと感じました。焦りは禁物です。