#[Rails]DM、チャット通知機能+チャット一覧、チャット機能の実装
##目標
チャットの通知機能の実装に関する記事が少ないように感じたので投稿しました。
こんな感じでの画面です。チャットのページを開くと通知の青いマークが消えます。
チャット機能、チャットの一覧ページと通知の実装手順を紹介します。
(*Deviseを用いたログイン機能を実装している前提でコードを書いています。)
チャット機能そのものについては、下記記事を参考にしました。
【Ruby on Rails】DM、チャット機能
##モデル作成
$ rails g model Room
$ rails g model Chat user:references room:references message:string
$ rails g model Room user:references room:references
class CreateChats < ActiveRecord::Migration[5.2]
def change
create_table :chats do |t|
t.references :user, foreign_key: true, null: false
t.references :room, foreign_key: true, null: false
t.string :message, null: false
t.boolean :checked, default: false, null: false
t.timestamps
end
end
end
class CreateUserRooms < ActiveRecord::Migration[5.2]
def change
create_table :user_rooms do |t|
t.references :user, foreign_key: true, null: false
t.references :room, foreign_key: true, null: false
t.timestamps
end
add_index :user_rooms, [:user_id, :room_id], unique: true
end
end
Roomのmigrationはそのままです。マイグレーションの内容は、本旨ではないので割愛しますが、外部キー制約等のDB単位での制限を設定しています。
上記変更後、ターミナルで、rails db:migrateを実行。
####アソシエーション(とバリデーション)
has_many :user_rooms, dependent: :destroy
has_many :chats, dependent: :destroy
has_many :user_rooms, dependent: :destroy
has_many :chats, dependent: :destroy
has_many :users, through: :user_rooms
belongs_to :user
belongs_to :room
validates :user_id, presence: true
validates :room_id, presence: true
has_many :chats, through: :room
belongs_to :user
belongs_to :room
validates :user_id, presence: true
validates :room_id, presence: true
validates :message, presence: true, length: { maximum: 140 }
## ルーティング
resources :rooms, only: %i[create index show]
resource :chats, only: [:create]
コントローラーの作成
$ rails g controller rooms
$ rails g controller chats
class RoomsController < ApplicationController
before_action :authenticate_user!
before_action :same_room_user!, only: [:show]
# 取得している情報はUserRoomsだが、実質Roomのother_userとのリレーションを取得しているためRoomのindexに記述
def index
# ログインユーザーが持っているuser_roomのroom_idを取得。
my_rooms_ids = current_user.user_rooms.select(:room_id)
# ユーザールームから、room_idがmy_rooms_idsと一致し、user_idがcurrent_user_idではないものを取得。
@user_rooms = UserRoom.includes(:chats, :user).where(room_id: my_rooms_ids)
.where.not(user_id: current_user.id).reverse_order
end
def create
user_rooms = UserRoom.find_user_rooms(current_user, @user)
if user_rooms.nil?
room = Room.create
UserRoom.create(user_id: current_user.id, room_id: room.id)
UserRoom.create(user_id: @user.id, room_id: room.id)
else
room = user_rooms.room
end
redirect_to room_path(room.id)
end
def show
room = Room.find(params[:id])
room.check_chats_notification(current_user)
user_id = room.user_rooms.where.not(user_id: current_user.id).select(:user_id)
@user = User.where(id: user_id).first # グループチャットの実装を意識し、whereメソッドで取得。
@chats = room.chats.includes(:user)
@chat = Chat.new(room_id: room.id)
end
private
# URLの直打ちによるアクセスを回避
def same_room_user!
return if Room.find(params[:id]).users.include?(current_user)
flash[:danger] = 'ユーザーにはアクセスする権限がありません'
redirect_to root_path
end
end
class Public::ChatsController < ApplicationController
before_action :authenticate_user!
def create
@chat = current_user.chats.new(chat_params)
if @chat.save
redirect_to request.referer
else
render 'shared/error'
end
end
private
def chat_params
params.require(:chat).permit(:message, :room_id)
end
end
エラー発生時のみ非同期通信にしています。
$("#flash_messages").html('<p class="alert alert-danger">1文字以上140文字以下で投稿してください</p>')
##モデルに記述するメソッド(下記をモデルに追記)
# 未読の通知が存在するか確認(チャット)
def unchecked_chats?
my_rooms_ids = UserRoom.select(:room_id).where(user_id: id)
other_user_ids = UserRoom.select(:user_id).where(room_id: my_rooms_ids).where.not(user_id: id)
Chat.where(user_id: other_user_ids, room_id: my_rooms_ids).where.not(checked: true).any?
end
# チャット通知を既読にするためのメゾット
def check_chats_notification(current_user)
unchecked_chats = chats.includes(:user).where(checked: false).where.not(user_id: current_user.id)
# &.を記述することで、uncheked_chatsがnilの場合エラーをNoMethodErrorを出さずにnilを返す。
unchecked_chats&.each { |unchecked_chat| unchecked_chat.update(checked: true) }
end
# チャット相手とのルームを検索
def self.find_user_rooms(current_user, other_user)
rooms_ids = current_user.user_rooms.pluck(:room_id)
UserRoom.find_by(user_id: other_user.id, room_id: rooms_ids)
end
# 個別のルームに未読の通知があるか確認(チャット)
def massage_checked
chats.where(user_id: user_id, checked: false).any?
end
##viewの編集
# チャットルーム作成のリンク
<% if current_user != @user %>
<% link_to 'chatを始める', rooms_path(user_id: @user), method: :post %>
<% end %>
*user_showページを例にしているが、相手ユーザーのIDを取得できればどのページからでもroomの作成はできます。
<%= link_to rooms_path, class: 'nav-link text-dark chat-notice' do %>
<i class="far fa-envelope fa-lg.fa-stack-2x fa-backcolor" style="font-size: 1.5em;"></i>
<% if current_user.unchecked_chats? %>
<i class="fa fa-circle fa-xs n-circle-chat" style="color: blue;"></i>
<% end %>
<% end %>
<div class="partner text-center border-bottom border-secondary py-1">
<%= link_to user_path(@user) do %>
<%= attachment_image_tag @user, :image, :fill, 50, 50, format: 'jpg', fallback: "no_image.jpg", size:'40x40', class: "rounded-circle"%>
<%= @user.name %>
<% end %>
</div>
<div class="messaging">
<div class="inbox_msg">
<div class="mesgs scroll">
<div class="msg_history" id="scroll-inner">
<% @chats.each do |chat| %>
<% if chat.user == current_user %>
<div class="outgoing_msg">
<div class="sent_msg">
<p><%= show_contents_with_uri(chat.message).html_safe %></p>
<span class="time_date"><%= chat.created_at %></span></div>
</div>
<% else %>
<div class="incoming_msg d-flex">
<div class="incoming_msg_img">
<%= attachment_image_tag @user, :image, :fill, 40, 40, format: 'jpg', fallback: "no_image.jpg", size:'40x40', class: "rounded-circle"%>
</div>
<div class="received_msg">
<div class="received_withd_msg">
<p><%= safe_join(chat.message.split("\n"),tag(:br)) %></p>
<span class="time_date"><%= chat.created_at.strftime("%Y/%m/%d %H:%M") %></span></div>
</div>
</div>
<% end %>
<% end %>
</div>
<div class="type_msg">
<%= form_with model: [@user, @chat] do |f| %>
<%= f.text_area :message, class: "chat-form mt-3", :size=>"35x3" %>
<%= f.hidden_field :room_id %>
<div class="chatt-action">
<%= f.submit "送信", class: "btn btn-dark-blues btn-rounded d-block ml-auto" %>
</div>
<% end %>
</div>
</div>
</div>
</div>
*画像の読み込みにrefileを使用しています。(チャットページは検索するとHTML形式ですが、テンプレがたくさん出てくるので好みのテンプレを参考にすると楽にレイアウトができると思います。
<div class="row my-5">
<% if @user_rooms.empty? %><p class="ml-4">メッセージはありません</p><% end %>
<% @user_rooms.each do |room| %>
<div class="col-8 mx-auto mb-3 border border-secondary rounded pt-2 px-2">
<div class="user border-bottom border-secondary">
<%= link_to user_path(room.user) do %>
<%= attachment_image_tag room.user, :image, :fill, 35, 35, format: 'jpg', fallback: "no_image.jpg", size:'35x35', class: "rounded-circle" %>
<span class="text-break"><%= room.user.name %></span>
<% end %>
</div>
<%= link_to room_path(room.room_id), class: "text-decoration-none" do %>
<div class=" chat-min-heiht">
<% if room.chats != [] %>
<p class="pt-3 ml-3 mb-0 text-dark text-break"><%= room.chats.last.message %></p>
<div class="small text-muted text-right text-break">
<span>
<% if room.massage_checked %>
<i class="fa fa-circle fa-x" style="color: blue;"></i>
<% end %>
</span>
<%= (room.chats.last).created_at.strftime("%Y/%m/%d %H:%M") %>
</div>
<% end %>
</div>
<% end %>
</div>
<% end %>
</div>
// ーーーーチャット通知マークーーーーー
.chat-notice {
position: relative;
}
.n-circle-chat {
position: absolute;
top: 25px;
left: 35px;
}
// ーーーーーーチャットルームーーーーーー
.scroll {
overflow-y: scroll;
}
#scroll-inner{
height: 350px;
}
.inbox_msg {
border: 1px solid #c4c4c4;
clear: both;
overflow: hidden;
}
.chat_ib h5{ font-size:15px; color:#464646; margin:0 0 8px 0;}
.chat_ib h5 span{ font-size:13px; float:right;}
.chat_ib p{ font-size:14px; color:#989898; margin:auto}
.chat_img {
float: left;
width: 11%;
}
.chat_ib {
float: left;
padding: 0 0 0 15px;
width: 88%;
}
.incoming_msg_img {
display: inline-block;
// width: 6%;
}
.received_msg {
margin-top: 15px;
display: inline-block;
vertical-align: top;
width: 92%;
}
.received_withd_msg p {
background: #ebebeb none repeat scroll 0 0;
border-radius: 3px;
color: #646464;
font-size: 14px;
margin: 0;
padding: 5px 10px 5px 12px;
width: 100%;
word-wrap: break-word;
}
.time_date {
color: #747474;
display: block;
font-size: 12px;
margin: 8px 0 0;
}
.received_withd_msg { width: 57%;}
.mesgs {
min-height: 350px;
width: 100%;
float: left;
padding: 0 15px;
}
.sent_msg p {
background: #82bccc none repeat scroll 0 0;
border-radius: 3px;
font-size: 14px;
margin: 0; color:#fff;
padding: 5px 10px 5px 12px;
width:100%;
word-wrap: break-word;
}
.outgoing_msg{ overflow:hidden; margin-top :15px;}
.sent_msg {
float: right;
width: 46%;
}
.input_msg_write input {
background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
border: medium none;
color: #4c4c4c;
font-size: 15px;
min-height: 48px;
width: 100%;
}
.type_msg {border-top: 1px solid #c4c4c4;position: relative;}
.msg_send_btn {
background: #05728f none repeat scroll 0 0;
border: medium none;
border-radius: 50%;
color: #fff;
cursor: pointer;
font-size: 17px;
height: 33px;
position: absolute;
right: 0;
top: 11px;
width: 33px;
}
.msg_history {
height: 510px;
overflow-y: auto;
}
.chat-form {
display: block;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0.25rem;
transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.chat-btn {
margin-left: auto;
}
.chat-form:focus {
color: #495057;
background-color: #fff;
border-color: #80bdff;
outline: 0;
box-shadow: 0 0 0 0.1rem rgba(0,0,0,0.2);
}
.chat-min-heiht {
min-height: 30px;
}
.btn-rounded {
border-radius: 35px;
}
.btn-dark-blues {
width: 100px;
background: #80b6cc;
color: #fff;
border: 3px solid #eee;
}
最後に
プログラミングの学習を始めて、3ヶ月程度で本記事を記述しています。
知識が浅く、間違っている部分もあるかと思いますが、不適切な記述があればご教授いただけると幸いです。
下記ポートフォリオで本記事で紹介したチャット機能を実装していますので、お時間あればご覧頂けると幸いです。