はじめに
私はプログラミングの初学者です。
自分のサービスを開発する中でチャット機能を作成いたしました。
同じようなところで困っている人や学習のアウトプットのために投稿させていただきます。
実現したいこと
RailsでLINEのような非同期のチャット機能ををつけたいです。
付ける場所としては投稿詳細画面のところです。
もしよろしければゲストログインして触ってみてください。
以下実際のアプリURL
使用した技術
・Rails7系で開発
・クライアントサイドの実装としてJavaScriptを使用
・データベースはPostgresql
・Bootstrapを使用
・gem device
作成手順
非常に分かりやすくまとめてくれている以下の記事を参考にさせていただきました。
以下をご覧になられていることを前提に説明していきます。
https://techtechmedia.com/action-cable-rails6
見た目
そこから私が必要に応じて追加じた部分をまとめました。
下記がLINE風の見た目になるviewのところです。
<!-- Messages -->
<div class="card-body swiper scrollbar-hover overflow-hidden w-100 pb-0" data-swiper-options='{
"direction": "vertical",
"slidesPerView": "auto",
"freeMode": true,
"scrollbar": {
"el": ".swiper-scrollbar"
},
"mousewheel": true
}'>
<div class="swiper-wrapper">
<div class="swiper-slide h-auto">
<!-- User message -->
<div data-post-id="<%= @post.id %>" id="messages">
<% @messages.each do |message| %>
<% if message.user_id == current_user.id %>
<!-- Own message -->
<div class="message-own d-flex align-items-start justify-content-end mb-3" data-user-id="<%= message.user_id %>">
<div class="pe-2 me-1" style="max-width: 348px;">
<div class="bg-info bg-gradient text-light p-3 mb-1" style="border-top-left-radius: .5rem; border-bottom-right-radius: .5rem; border-bottom-left-radius: .5rem;"><%= message.content %></div>
<div class="d-flex justify-content-end align-items-center fs-sm text-muted">
<%= message.created_at.strftime('%Y年%m月%d日 %H:%M') %>
</div>
</div>
</div>
<% else %>
<!-- Message from others -->
<div class="message-others d-flex align-items-start mb-3" data-user-id="<%= message.user_id %>">
<%= message.user.username %>
<div class="ps-2 ms-1" style="max-width: 348px;">
<div class="bg-secondary p-3 mb-1" style="border-top-right-radius: .5rem; border-bottom-right-radius: .5rem; border-bottom-left-radius: .5rem;"><%= message.content %></div>
<div class="fs-sm text-muted">
<%= message.created_at.strftime('%Y年%m月%d日 %H:%M') %>
</div>
</div>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
<div class="swiper-scrollbar end-0"></div>
</div>
自分以外の人のメッセージ
<% message = local_assigns[:message] %>
<div class="d-flex align-items-start mb-3">
<%= message.user.username %>
<div class="ps-2 ms-1" style="max-width: 348px;">
<div class="bg-secondary p-3 mb-1" style="border-top-right-radius: .5rem; border-bottom-right-radius: .5rem; border-bottom-left-radius: .5rem;"><%= message.content %></div>
<div class="fs-sm text-muted"><%= message.created_at.strftime('%Y年%m月%d日 %H:%M') %></div>
</div>
</div>
自分のメッセージ
<% message = local_assigns[:message] %>
<div class="d-flex align-items-start justify-content-end mb-3" data-user-id="<%= message.user_id %>">
<div class="pe-2 me-1" style="max-width: 348px;">
<div class="bg-info bg-gradient text-light p-3 mb-1" style="border-top-left-radius: .5rem; border-bottom-right-radius: .5rem; border-bottom-left-radius: .5rem;"><%= message.content %></div>
<div class="d-flex justify-content-end align-items-center fs-sm text-muted">
<%= message.created_at.strftime('%Y年%m月%d日 %H:%M') %>
</div>
</div>
</div>
解説:
bootstrapを使用しているのでcard-bodyやswiperのところは適宜自分なりに変更をしてください。
私が躓いたところとしては、Rails7系はturboを使用するのでJSのところが以下のようになります。
document.addEventListener("turbo:load", function() {
JSのところ
以下のところは参考記事を見ながら処理の流れを確認してみてください!
おすすめとしては参考記事のチャンネルの流れを確認しながらです!!
app/javascript/application.js
// Entry point for the build script in your package.json
import "@hotwired/turbo-rails"
import "./controllers"
import * as bootstrap from "bootstrap"
import "./channels"
//= require cable
app/javascript/channels/post_channel.js
import consumer from "./consumer"
document.addEventListener("turbo:load", () => {
const messages = document.getElementById('messages');
if (messages === null) {
return;
}
const currentUserId = document.body.dataset.currentUserId;
const postId = messages.dataset.postId;
const appPost = consumer.subscriptions.create({channel: "PostChannel", post_id: postId}, {
received(data) {
// 送信者か受信者かを確認するロジックを追加
let messageHtml;
if (data.sender_id.toString() === currentUserId) {
messageHtml = data.message;
} else {
messageHtml = data.second_message;
}
messages.insertAdjacentHTML('beforeend', messageHtml);
setTimeout(function() {
window.swiper.update(); // 更新
const swiperContainer = document.querySelector('.swiper');
if (swiperContainer !== null) {
swiperContainer.scrollTop = swiperContainer.scrollHeight;
} else {
console.log('Error: No swiper container found.');
}
}, 100);
},
speak: function(message, postId) {
return this.perform('speak', {message: message, post_id: postId});
}
});
const input = document.getElementById('post_input');
const button = document.getElementById('submit_button');
if(input) {
input.addEventListener("keypress", function(e) {
if (e.keyCode === 13) {
appPost.speak(e.target.value, postId);
e.target.value = '';
e.preventDefault();
}
});
} else {
console.error("Could not find element with id 'post_input'");
}
if(button) {
button.addEventListener("click", function(e) {
if(input.value !== '') {
appPost.speak(input.value, postId);
input.value = '';
}
e.preventDefault();
});
} else {
console.error("Could not find element with id 'submit_button'");
}
});
// Popstate event
window.addEventListener('popstate', function(event) {
setTimeout(function() {
window.swiper.update();
const swiperContainer = document.querySelector('.swiper');
if (swiperContainer) {
swiperContainer.scrollTop = swiperContainer.scrollHeight;
}
}, 100);
});
解説:
以下の部分はjavascriptのところでcurrent_userが使えないので独自に考えた処理です。
ここで送信者によって渡すviewを変えています。
const appPost = consumer.subscriptions.create({channel: "PostChannel", post_id: postId}, {
received(data) {
// 送信者か受信者かを確認するロジックを追加
let messageHtml;
if (data.sender_id.toString() === currentUserId) {
messageHtml = data.message;
} else {
messageHtml = data.second_message;
}
message_broadcast_job.rb
app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
queue_as :default
def perform(message)
Rails.logger.info "Broadcasting message #{message.id}"
PostChannel.broadcast_to(message.post, message: render_message(message), second_message: render_second_message(message), sender_id: message.user_id)
end
private
def render_message(message)
ApplicationController.renderer.render(partial: 'messages/own_message', locals: { message: message })
end
def render_second_message(message)
ApplicationController.renderer.render(partial: 'messages/other_message', locals: { message: message })
end
end
解説:
ここでは実際にユーザーに送られるviewを判別しているところになります。
参考にしたURL