エンジニア転職日記

エンジニア転職に向けての日記です

DM機能の実装(メッセージ投稿)

概要

チャットルーム管理に引き続き、オリジナルアプリのDM機能を実装していきます。今回はメッセージ投稿機能です。

 

shangang7321.hatenablog.com

 

完成図

まずは完成図を確認します。

f:id:shangang7321:20200904084419g:plain

左側のチャット相手の名前をクリックすると、その人とのチャットルームに移動します。下のメッセージ入力欄にメッセージを入力して送信を押すとメッセージが投稿されます。画像を投稿することもできます。

画像ではわかりづらいですが、チャットルーム画面に遷移した時、最新のメッセージが表示されるように一番下のメッセージまでスクロールするようにしてあります。

 

実装方法

早速実装していきます。

まずは、モデルを作成します。

% rails g model message

マイグレーションファイルを編集します。

class CreateMessages < ActiveRecord::Migration[6.0]
def change
create_table :messages do |t|
t.text :content
t.references :room, foreign_key: true
t.references :user, foreign_key: true
t.timestamps
end
end
end

マイグレーションします。

% rails db:migrate

 

続いて、アソシエーションを組みます。モデル間の関係はDB設計の記事で解説しています。

 

shangang7321.hatenablog.com

 

# message.rb
class Message < ApplicationRecord
belongs_to :room
belongs_to :user

 

# room.rb
class Room < ApplicationRecord
 
has_many :messages, dependent: :destroy

 

# user.rb
class User < ApplicationRecord
 
has_many :messages

 

ルーティングを設定します。メッセージを投稿する時、どのルームで投稿されたメッセージなのかをパスから判断できるようにしたいので、roomにネストさせて記述します。

# routes.rb
resources :rooms do
resources :messages
end

 

ルーティングを設定したので、チャット画面のサイドバーからルームを選択できるように実装します。

<div class="rooms">
<% current_user.rooms.each do |room| %>
<div class="room">
<div class="room-name">
<% @another_user = (room.users.where.not(id: current_user.id).to_a)[0] %>
<%= link_to @another_user.nickname, room_messages_path(room) %>
</div>
 
</div>
<% end %>
</div>

解説します。

<% current_user.rooms.each do |room| %>

この記述で、ログイン中のユーザーの属しているルームを1つずつ取り出し、変数roomに格納します。

<% @another_user = (room.users.where.not(id: current_user.id).to_a)[0] %>

roomに属するユーザーのうち、自分ではない方のユーザーを@another_userに格納します。

<%= link_to @another_user.nickname, room_messages_path(room) %>

@another_userのユーザー名を表示させ、リンク先を各ルームに設定しています。

これで、サイドバーの編集は完了です。

 

続いて、入力フォームを作成します。

# _main_chat_html.erb
<%= form_with model: [@room, @message], html: {class: "form"},
local: true do |f|%>
<div class="form-input">
<%= f.text_field :content, class: 'form-message',
placeholder: 'メッセージを入力してください' %>
<label class="form-image">
<span class="image-file">画像</span>
<%= f.file_field :image, class: 'hidden' %>
</label>
</div>
<%= f.submit '送信', class: 'form-submit' %>
<% end %>

form_withのモデルの引数に@roomと@messageを渡しているのは、ルーティングをネストさせているためです。○○ルームの××というメッセージという形でサーバー側にリクエストします。

 

この2つのインスタンス変数をコントローラー側で生成します。

# messages_controller.rb
def index
@message = Message.new
@room = Room.find(params[:room_id])
end

 

それでは、送られてきた値をDBに保存できるようにします。

# messages_controller.rb
def create
@room = Room.find(params[:room_id])
@message = @room.messages.new(message_params)
if @message.save
redirect_to room_messages_path(@room)
else
@messages = @room.messages.includes(:user)
render :index
end
end
 
private

def message_params
params.require(:message).permit(:content, :image).merge(user_id: current_user.id)
end

パラメーターからルームIDとメッセージIDを受け取り、インスタンス変数を生成します。メッセージが保存できたらそのままそのルームにリダイレクトします。

保存できなかった場合は、@messagesにルームのメッセージを格納して再度メッセージ一覧を表示します。

includesメソッドを利用することでN+1問題を回避しています。

 

indexアクションでも、一覧表示ができるようにします。

def index
@message = Message.new
@room = Room.find(params[:room_id])
@messages = @room.messages.includes(:user)
end

 

メッセージを表示できるようにします。

# _main_chat.html.erb
<div class="messages">
<%= render partial: 'message', collection: @messages %>
</div>

collectionオプションは、指定したインスタンス変数の中にある要素の数だけ部分テンプレートが繰り返し表示されるオプションです。

 

# _message.html.erb
<div class="message">
<div class="upper-message">
<div class="message-user">
<!-- 投稿したユーザー名情報を出力する -->
<%= message.user.nickname%>
</div>
<div class="message-date">
<!-- 投稿した時刻を出力する -->
<%= l message.created_at%>
</div>
</div>
<div class="lower-message">
<div class="message-content">
<!-- 投稿したメッセージ内容を記述する -->
<%= message.content%>
</div>
<%= image_tag message.image.variant(resize:'500x500'),
class: 'message-image' if message.image.attached? %>
</div>
</div>

ユーザー名、投稿日時、投稿内容、画像が表示されるようにします。

 

l (エル)メソッドは日付を表示するメソッドです。lメソッドを使って日本時間に対応しています。

日本時間を指定する作業を行います。

# config/application.rb
module Original28318
class Application < Rails::Application
config.load_defaults 6.0config.i18n.default_locale = :ja #追加
config.time_zone = 'Tokyo' #追加
config.action_view.field_error_proc = Proc.new { |html_tag, instance| html_tag }
↑この記述をするとバリデーションでのビュー崩れが防げる
end
end

 

 

# config/locales/ja.yml
ja:
time:
formats:
default: "%Y/%m/%d %H:%M:%S"

これでサーバー再起動をすると日本時間が表示されます。

画像投稿機能に関しては、ActiveStorageを利用しています。

 

shangang7321.hatenablog.com

 

最後に、メッセージの自動スクロール機能の実装です。

ここまでの記述でメッセージは表示されるようになりましたが、メッセージの表示順は上から下に古い順に表示されます。順番はLINEなどのチャットアプリでも下に新しいメッセージが表示されるので問題ないのですが、ルームに遷移した際、一番上が表示されるので、いちいちスクロールして新しいメッセージを見なければなりません。

それを回避するために、遷移と同時に一番下までスクロールする記述をします。

# _main_chat.html.erb
<script type="text/javascript">
const $scrollAuto = $('.messages');
$scrollAuto.animate({scrollTop: ($scrollAuto[0].scrollHeight + 1000)}, 'fast')
</script>

 

メッセージ全体の要素を$scrollAutoに代入し、ロード時にその要素に関してanimate以下の動きを実行するようにしています。

scrollTopは垂直方向のスクロール量を指定できるプロパティです。メッセージ全体の高さ+1000px分スクロールするようにしています。'fast'はスクロールする速さです。

 

これでメッセージ投稿機能の実装が完了しました。

参考

https://narunaru7638.hatenablog.com/entry/2019/04/12/005634

https://qiita.com/masahisa/items/eaacb0c3b82f4a11fc13