...

【Rails】LINE風のリアルタイムなチャット機能を実装する方法

RailsでLINE風のリアルタイムなチャット機能を実装する方法

どうもおはようポテト(@ohayoupoteto22)です。

LINEみたいなリアルタイムなチャットってどう作るん?

というわけで今回は

Railsでリアルタイムなチャット機能を実装したい

LINEのようなチャット機能を簡単に作りたい

という方に向けて「RailsでLINE風のリアルタイムなチャット機能を実装する方法」をまとめてみました。

初学者の備忘録ゆえ至らない点もあると思いますが参考になれば幸いです⸝⸝- ̫ -⸝⸝

ブログ主

早速いってみよう!

 

完成イメージ

できたのはこんな感じ。

別のログインユーザーとリアルタイムでチャットをすることができます。

 

ER図

今回作成したモデルはUserモデルとChatモデルの2つ。

UserモデルとChatモデルは1対多の関係で、Chatモデルの「partner_id」はチャット相手のidのこと。

後述しますがUserモデルはdeviseの認証モデルとして作成します

 

手順

ステップは大きく分けて3つ。

手順

step1
deviseで認証モデル作成
step2
チャット機能を実装
step3
Action Cableでチャットをリアルタイムで行えるように
「Action Cableってなんぞや」という方に向けて簡単に説明すると「フロント側とサーバ側が互いを監視し合って、リアルタイムで色々できるよ。やったね。」という機能です。
(詳しくはRailsガイドがとても参考になります。)
というわけでやっていこう!

ソロモン

 

RailsでLINE風のリアルタイムなチャット機能を実装する方法

deviseでログイン認証機能を付ける

まずdeviseを使ってログイン認証機能を付け、Userモデルを作成します

ここで行ったことは以下の記事でまとめているのでそちらをご参照ください。

ブログ主

今回はこの下の記事の続きとしてやってるから要チェックだね

 

チャット機能を実装

Chatモデルの作成

$ rails g model chat
resources :chats

 

モデル間の関連付け

class Chat < ApplicationRecord
  belongs_to :user
end
class User < ApplicationRecord
  has_many :chats,dependent: :destroy
(以下略)

 

マイグレーション

class CreateChats < ActiveRecord::Migration[5.2]
  def change
    create_table :chats do |t|
      t.references :users, null: false
      t.integer :partner_id, null: false #チャット相手
      t.string :sentence, null: false
      t.timestamps
    end
  end
end
$ rails db:migrate

 

コントローラー

$ rails g controller chats index show
class ChatsController < ApplicationController
  def index
    @my_chats=current_user.chats
    @chat_partners=User.where.not(id:current_user.id)#自分以外
  end

  def show
    @partner=User.find(params[:id])
    @chats_by_myself=Chat.where(user_id: current_user.id,partner_id: @partner.id)
    @chats_by_other=Chat.where(user_id: @partner.id,partner_id: current_user.id)
    @chats=@chats_by_myself.or(@chats_by_other)#リレーションオブジェクト達を結合する
    @chats=@chats.order(:created_at)
  end
end
indexでトークルーム一覧、showでチャットルームのやりとりを表示しています。
9行目以降は「送信したのが自分 or 送信されたのが自分」なチャットを取得し、それらのモデルオブジェクト達を結合させています。

ビュー

<table class="rooms">
  <tbody>
    <tr>
      <td>
        <%@chat_partners.each do |chat_partner|%>
          <%=link_to "#{chat_partner.email}さんとのトークルーム","/chats/#{chat_partner.id}"%>
        <%end%>
      </td>
    </tr>
  </tbody>
</table>
<%=link_to("ログインページへ","/")%>
<%=link_to("ログアウトする","/users/sign_out",method: :delete)%>

これがindex.html.erb。トークルーム一覧です。

 

<h2 style="text-align:center"><%="#{@partner.email}さんとのチャット"%></h2>
<div id="chats">
  <% @chats.each do |chat|%>
    <%if chat.user_id==current_user.id%>
      <div class="mycomment">
        <p><%=chat.sentence%></p>
      </div>
    <%else%>
      <div class="fukidasi">
        <div class="faceicon">
          <img src="/assets/profile.png" alt="相手">
        </div>
        <div class="chatting">
          <div class="says">
            <p><%=chat.sentence%></p>
          </div>
        </div>
      </div>
    <%end%>
  <%end%>
</div>
<form id="send-form">
  <input type="text" id="sentence" placeholder="入力してね" style="width:30%;">
  <input type="submit" value="送信" id="send">
  <input id="current_user_id" type="hidden" value= "<%=current_user.id%>">
  <input id="partner_id" type="hidden" value= "<%=@partner.id%>">
</form>
<%=link_to("ログアウトする","/users/sign_out",method: :delete)%>
こっちがshow.html.erb。トークルームです。

LINE風の吹き出しは、以下のCSS含めサルワカさんの記事を参考にさせていただきました。

 

CSSでLINE風に

/************************************
** トークルーム一覧
************************************/
.rooms{
  margin:0 auto;
  background-color:#fdfdfd;
  border: 2px solid #eee;
  padding:1em 2em;
}
/************************************
** チャットの吹き出し
************************************/
#chats{
  padding: 20px 10px;
  max-width: 450px;
  margin: 15px auto;
  text-align: right;
  font-size: 14px;
  background: #7da4cd;
}
.fukidasi {
  width: 100%;
  margin: 10px 0;
  overflow: hidden;
}
.fukidasi .faceicon {
  float: left;
  margin-right: -50px;
  width: 40px;
}
.fukidasi .faceicon img{
  width: 90%;
  height: auto;
  border-radius: 50%;
}
.fukidasi .chatting {
  width: 100%;
  text-align: left;
}
.says {
  display: inline-block;
  position: relative;
  margin: 0 0 0 50px;
  padding: 10px;
  max-width: 250px;
  border-radius: 12px;
  background: #edf1ee;
}
.says:after {
  content: "";
  display: inline-block;
  position: absolute;
  top: 3px;
  left: -19px;
  border: 8px solid transparent;
  border-right: 18px solid #edf1ee;
  -webkit-transform: rotate(35deg);
  transform: rotate(35deg);
}
.says p {
  margin: 0;
  padding: 0;
}
.mycomment {
  margin: 10px 0;
}
.mycomment p {
  display: inline-block;
  position: relative;
  margin: 0 10px 0 0;
  padding: 8px;
  max-width: 250px;
  border-radius: 12px;
  background: #30e852;
  font-size: 15px;
}
.mycomment p:after {
  content: "";
  position: absolute;
  top: 3px;
  right: -19px;
  border: 8px solid transparent;
  border-left: 18px solid #30e852;
  -webkit-transform: rotate(-35deg);
  transform: rotate(-35deg);
}
/************************************
** 送信フォーム
************************************/
#send-form{
  text-align:center;
  margin-bottom:200px;
}
これでlocalhost:3000/users/showにアクセスし、
フォームに入力してボタンを押してリロードしてあげればこんな感じに。

 

見た目だけはもう完成ですね(っ´ω`c)

ブログ主

というわけでこっからが本番!

 

Action Cableでチャットをリアルタイムで行えるように

jQueryを使えるようにする

gem 'jquery-rails'
$ bundle install

まずgemをインストール。

 

//= require rails-ujs
//= require activestorage
//= require turbolinks
//= require jquery #追記
//= require jquery_ujs #追記
//= require_tree .

そして上のようにapplication.jsに記述してあげればjQueryが使えます。

 

Chatチャンネルを作成

$ rails g channel chat speak
これでchat.jsとchat_channel.rbが生成されます。
chat.jsにはフロント側の処理を、chat_channel.rbにはサーバー側の処理を書いていきます。

current_userを使えるようにする

module ApplicationCable
  class Connection < ActionCable::Connection::Base
  #channelでcurrent_userが使えるようにする
  identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected
    def find_verified_user
      verified_user = User.find_by(id: env['warden'].user.id)
      return reject_unauthorized_connection unless verified_user
      verified_user
    end

    def session
      cookies.encrypted[Rails.application.config.session_options[:key]]
    end
  end
end
channels/applicationcable/connection.rbに上のように記述。
これでChatChannelでcurrent_userが呼べるようになり、ログインしてるユーザーを取得することができます。

chat.js(フロント側の処理)

App.chat = App.cable.subscriptions.create("ChatChannel", {
  connected: function() {
    // Called when the subscription is ready for use on the server
  },
  disconnected: function() {
    // Called when the subscription has been terminated by the server
  },
  received: function(data) {
    //画面を開いているのがチャット送信者だった場合
    if (data["isCurrent_user"]==true){
      sentence=`<div class='mycomment'><p>${data["sentence"]}</p></div>`;
    }
    //画面を開いているのがチャット受信者だった場合
    else{
      sentence=`<div class='fukidasi'><div class='faceicon'>
      <img src='/assets/profile.png' alt='管理人'></div>
      <div class='chatting'><div class='says'><p>${data["sentence"]}</p>
      </div></div></div>`;
    }
    $('#chats').append(sentence);
  },
  speak: function(sentence) {
    current_user_id=$("#current_user_id").val();
    partner_id=$("#partner_id").val();
    return this.perform('speak',{sentence: sentence, current_user_id: current_user_id, partner_id: partner_id});
  }
});
$(function(){
  $("#send").on("click",function(e){
    sentence=$("#sentence").val();
    App.chat.speak(sentence);
    $("#sentence").val(""); //フォームを空に
    e.preventDefault();
  });
});

簡単にまとめると

送信ボタンがクリックされたらspeakメソッドを呼び出すように

speakメソッドでは色々値を取得してサーバ側のspeakメソッドに渡している

recieved〜内ではサーバから値を受けとり、要素を追加している(チャットの内容を表示している)

こんな感じです。

ブログ主

慣れるまでは難しいよね

recived〜内ではサーバから受け取った値に応じて追加する要素を変えています。

今画面を開いているのがチャットを送った人なら右に、受け取った人なら左に吹き出しを追加する…ってな具合です。

次にこのフロント側と色々やり取りをするサーバ側だよ

ソロモン

 

chat_channel.rb(サーバー側の処理)

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_channel"
    stream_for current_user.id
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    if data["sentence"]
      Chat.create!(
        user_id: data["current_user_id"].to_i,
        partner_id: data["partner_id"].to_i ,
        sentence: data["sentence"]
      )
      #画面を開いているのがチャット送信者だった場合
      ChatChannel.broadcast_to data["current_user_id"].to_i,
        sentence: data["sentence"],
        partner_id: data["partner_id"],
        isCurrent_user: true

      #画面を開いているのがチャット受信者だった場合
      ChatChannel.broadcast_to data["partner_id"].to_i,
        sentence: data["sentence"],
        partner_id: data["partner_id"],
        isCurrent_user: false
    end
  end
end

subscribedメソッド内では接続が確立された時の処理を書いています。

stream_forでcurrent_user.idを指定することで今画面開いているユーザーに関連するストリームを作成。

ブログ主

専用の部屋を作る的な感じかな?

そしてフロント側から呼ばれるspeakメソッドの中で行っていることとして、

フロント側から送られてきたデータを元にチャットのレコードを作成&保存

broadcast_toで今画面開いているユーザーがチャット送信者なのか否かに応じて送るデータ(isCurrent_userの値)を変えている。

これでフロント側が、今画面を開いているのがチャットを送った人なのか否か判断できるわけですね。

以上です。これで完成!

お疲れ様!

ソロモン

 

最後に:Action Cable難しくて草

以上になります。

作った後に「チャットルームごとに番号を振って配信する」という手もあったことに気がつきました…

まあ不格好ながらカタチになったからいいか(›´ω`‹ )

ブログ主

まだ分からないことだらけ。

参考になれば幸いです!では⸝⸝- ̫ -⸝⸝

コメントを残す

メールアドレスが公開されることはありません。

CAPTCHA