ActionCable + jQueryによるDM機能の実装

##概要
【ActionCable】+【jQuery】を用いたチャットアプリの作成について、説明します。

##目的
Javascriptやcoffeescriptを用いてチャット機能を実装する記事は多く見受けられたのですが、
jQueryしか分からない私は、jQueryを用いてチャット機能を実装する記事を見つけられず、
jQueryでの実装に苦労しました。。。
同じ境遇の方への技術共有と、自分の勉強のために、jQueryを用いたチャット機能の実装方法を説明します！
<font color="DarkGray">
＊一部jQueryではなく、javasctiptも使用していますが、
　ActionCableを使う上で必要なものになりますので、なんとか理解して頂ければと思います。
</font><font color="DarkGray">
＊プログラミング学習を初めて3ヶ月ちょっとの人間が書いた記事です。
　誤った理解や、もっとスマートに実装出来るであろう箇所があるかと思いますので、
　もし気になる箇所が御座いましたら、ご指摘頂けますと幸いです。
</font>

##この記事のゴール
DM(ダイレクトメッセージ)が出来るチャットアプリの完成がゴールになります。

##目次
1. この記事で説明する、DM機能とは？
2. deviseの準備
3. データベース設計
4. チャットルーム作成機能の実装
5. DMでのメッセージ投稿機能の実装
6. 最後に
7. 参考

##環境
rails (5.2.4.2)
jquery-rails (4.3.5)
devise (4.7.1)

##前提
この記事は、[ActionCable + jQueryによるチャット機能の実装](https://qiita.com/kappa_hoge/items/063e36f399b4e6b2a6a8)の続きになります。
ActionCableを実装して、オープンチャット機能の実装が出来ていることを前提として説明します。

##この記事で説明する、DM機能とは？
前回の記事では、全てのユーザが参加出来るオープンチャット機能の実装方法を説明しましたが、
例えば、自分とAさんだけでメッセージのやりとりをしたい場合には、
自分とAさんがチャットをするための「チャットルーム」を作成して、チャットメンバーを管理する必要があります。
この記事では、上記の機能をDM機能として、実装方法について説明します。

処理の流れとしては、下記になります。
![qiita_ActionCable-Page-2.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604002/d96b4366-bdb6-de10-4222-1a208ca6bb3f.jpeg)

#deviseの準備
本記事では、セッションを使用するために
deviseのcurrent_userを用いますので、deviseを使用する準備をしておきます。

```rb:Gemfile
gem 'devise'
```

```
$ bundle install
$ rails g devise:install
```
<font color="DarkGray">
＊deviseに関する細かい説明は、この記事では割愛します。
</font>
##データベース設計
まずは、ユーザとチャットルームを管理するためのmodelを生成しましょう。

```
$ rails g devise User
$ rails g model Room host_id:integer member_id:integer
```

今回は、ユーザの識別をするためにユーザの名前を使用しますので、
この時点で、ユーザのマイグレーションファイルにnameを追加して下さい。

```rb:xxxxxx_devise_create_users.rb
class DeviseCreateUsers < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      # ここを追加する
      t.string :name

# ----------以下、省略----------
```

追加したら、マイグレーションを実行します。

```
$ rails db:migrate
```

また、UserテーブルとRoomテーブルをMessageテーブルと関連づけるために
Messageモデルにforeign_keyを追加します。

```
$ rails g migration AddColumnToMessage user_id:integer room_id:integer
$ rails db:migrate
```

この時点でschemaファイルが以下のようになっているかと思います。

```rb:schema.rb
ActiveRecord::Schema.define(version: 2020_05_06_101819) do

  create_table "messages", force: :cascade do |t|
    t.string "content"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.integer "user_id"
    t.integer "room_id"
  end

  create_table "rooms", force: :cascade do |t|
    t.integer "host_id"
    t.integer "member_id"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
  end

  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.string "name"
    t.string "reset_password_token"
    t.datetime "reset_password_sent_at"
    t.datetime "remember_created_at"
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
    t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
  end

end
```

マイグレーションが出来たら、modelにアソシエーションとカスケードを追加します。

```rb:user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

  # ここを追加する
  has_many :messages, dependent: :destroy
end
```

```rb:room.rb
class Room < ApplicationRecord

  # ここを追加する
  has_many :messages, dependent: :destroy
end
```

```rb:message.rb
class Message < ApplicationRecord

  # ここを追加する
  belongs_to :user
  belongs_to :room
end
```
この時点で、データベース設計は下記のようになっています。

**ER図**
![qiita_ActionCable-Page-1.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604002/95d68a9f-0019-6466-3578-393f107c27a3.jpeg)


<font color="DarkGray">
classname等を用いることで、もっとスマートな設計が出来るかもしれませんが、
今回は、このデータベース設計で実装を進めます。
</font>

また今回は、チャットする相手を選択する機能を、usersのshow画面に表示しますので、
usersControllerも生成しておきます。

```
$ rails g controller users show index
```

ルーティングも設定しておきます。

```rb:routes.rb
Rails.application.routes.draw do
  devise_for :users

  # ここを修正
  # get 'users/show'
  # get 'users/index'
  resources :users, only: [:show, :index]

  # ここも修正
  # root 'rooms#show'
  resources :rooms, only: [:show]

  mount ActionCable.server => '/cable'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
```

##チャットルーム作成機能の実装
modelとcontrollerが生成できたので、チャットルームの作成機能を実装します。
まずは、チャットをするメンバーを選択するためにユーザのshow画面を表示できるようにします。
users_controllerに下記のコードを記述します。

```rb:users_controller.rb
class UsersController < ApplicationController

  def show
    @user = User.find(params[:id])
  end
end
```

viewに下記のコードを記述します。

```html.erb:users>show.html.erb
<h1><%= @user.name %>のプロフィール</h1>

<div><%= @user.name %>とチャットをする<div>
```

動作確認をするためにデータベースにユーザのデモデータを追加します。

```
$rails c
[1] pry(main)> User.create(name:"モブ太郎", email:"aa@aa", password:"password")
[2] pry(main)> User.create(name:"モブ子", email:"bb@bb", password:"password")
[3] pry(main)> User.create(name:"モブ次郎", email:"cc@cc", password:"password")
```

[http://localhost:3000/users/1](http://localhost:3000/users/1)にアクセスすると、下記のように表示されるはずです。
<img width="406" alt="スクリーンショット 2020-05-06 19.50.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604002/d8b61d3f-783d-ff47-a71d-3d4a72fbb95d.png">

ブラウザに表示されている「モブ太郎とチャットをする」をクリックすることで、
チャットルームを作成できるようにします。

ルーティングを下記のように修正して下さい。

```rb:routes.rb
Rails.application.routes.draw do
  devise_for :users

  # ここを編集する
  resources :users, only: [:show, :index] do
    resources :rooms, only: [:create]
  end

  resources :rooms, only: [:show]

  mount ActionCable.server => '/cable'
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
```

チャットする相手の情報(user_id)を取得するためにrooms#createをusersにネストさせています。

rooms_controllerに下記を追加して下さい。

```rb:users_controller.rb
class RoomsController < ApplicationController

  # ここを追加する
  def create
    room = Room.new(host_id: current_user.id, member_id: params[:user_id])
    room.save
    redirect_to room_path(room.id)
  end

  def show
    @messages = Message.all
  end
end
```

viewファイルも修正します。

```html.erb:users>show.html.erb
<h1><%= @user.name %>のプロフィール</h1>

# ここを編集する
<%= link_to user_rooms_path(@user), method: :post do %>
  <div><%= @user.name %>とチャットをする<div>
<% end %>
```

動作確認をするために、アプリケーションにログインします。
[http://localhost:3000/users/sign_in](http://localhost:3000/users/sign_in)にアクセスして、下記の内容でログインして下さい。

・email: bb@bb
・password: password

ログインしたら、[http://localhost:3000/users/1](http://localhost:3000/users/1)にアクセスします。
<img width="406" alt="スクリーンショット 2020-05-06 19.50.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/604002/d8b61d3f-783d-ff47-a71d-3d4a72fbb95d.png">
「モブ太郎とチャットをする」をクリックすると作成したチャットルームとしてrooms#showページが表示されるはずです。

コンソールからデータを確認するとRoomにレコードが追加されていることが、確認できます。

```
$ rails c
[1] pry(main)> Room.all
  Room Load (1.0ms)  SELECT "rooms".* FROM "rooms"
=> [#<Room:xxxxxxxxxxxxxxxx
  id: 1,
  host_id: 2,
  member_id: 1,
  created_at: Wed, 06 May 2020 13:01:31 UTC +00:00,
  updated_at: Wed, 06 May 2020 13:01:31 UTC +00:00>,
```

ここまでで、チャットルームの作成し、チャットルームに入ることができました。

##DMでのメッセージ投稿機能の実装

チャットルームの作成ができましたので、
チャットルームに紐づいたユーザ間のメッセージのみ表示する機能を実装していきます。

まずは、Messageテーブルのカラムにuser_idとroom_idを追加していましたので、
投稿したメッセージのデータにuser_idとroom_idをつけてサーバにデータを送信する処理を実装します。

viewファイルを下記のように修正します。

```html.erb:rooms>show.html.erb
<h1>Chat Room</h1>

<ul>
  <% @messages.each do |message| %>
    <li><%= message.content %></li>
  <% end %>
</ul>

<ul id="add"></ul>

<input type="text", class="chat-input", autofocus>
<button class="button">送信</button>

# ここを追加します
<input type="hidden" class="room_id" value="<%= @room.id %>">
```

viewファイルからroom.jsにroom_idを渡せるように、viewファイル上にroom_idをセットしました。
inputタグのtypeをhiddenしてデータを記述することで、
ブラウザ上には表示されずに、view上にデータをセットすることができます。
<font color="DarkGray">
＊検証ツールを使えばブラウザ上でもhiddenタイプのデータを見ることができますので、
　パスワードやメールアドレス等のデータはセキュリティ上この方法でviewに記述しない方が良いです。
</font>



現在入室しているroomの情報を@roomに代入できるように、rooms_controllerを修正します。

```rb:rooms_controller.rb
class RoomsController < ApplicationController

  def create
    room = Room.new(host_id: current_user.id, member_id: params[:user_id])
    room.save
    redirect_to room_path(room.id)
  end

  # ここを修正する
  def show
    @room = Room.find(params[:id])
    @messages = @room.messages.all
  end
end
```

ついでに、@messagesはroomに紐づいたデータのみを代入するように修正しました。

続いて、room.jsがview上の@room.idを受け取り、サーバにデータを渡せるように修正します。

```js:room.js
App.room = App.cable.subscriptions.create("RoomChannel", {

  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) {
    $('<li>',{
      class: "hoge",
      text: data,
    }).appendTo("#add");

    // Called when there's incoming data on the websocket for this channel
  },

  // ここも修正します
  speak: function(content, room_id) {
    return this.perform('speak', {content: content, room_id: room_id});
  }
});

$(function(){
  $(".button").on("click",function(){

    // ここを追加します
    var room_id = $(".room_id").val();
    var content = $(".chat-input").val();

    // ここを修正します
    App.room.speak(content, room_id);
    $(".chat-input").val("")
  });
});
```

サーバがデータを受け取って、メッセージを保存した後、
フロントにデータを渡せるようにroom_channelも修正します。

```rb:room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)

    # ここを修正します
    message = Message.new(content: data["content"], room_id: data["room_id"], user_id: current_user.id)
    message.save
    ActionCable.server.broadcast "room_channel", content: data["content"], room_id: data["room_id"]
  end
end
```

サーバから受け取ったデータを、viewに表示できるようにroom.jsを修正します。

```js:room.js
App.room = App.cable.subscriptions.create("RoomChannel", {

  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) {
      $('<li>',{
        class: "hoge",

        // ここを修正します
        text: data["content"],
      }).appendTo("#add");
    };
    // Called when there's incoming data on the websocket for this channel
  },

  speak: function(content, room_id) {
    return this.perform('speak', {content: content, room_id: room_id});
  }
});

$(function(){
  $(".button").on("click",function(){
    var room_id = $(".room_id").val();
    var content = $(".chat-input").val();
    App.room.speak(content, room_id);
    $(".chat-input").val("")
  });
});
```

dataの中身が増えましたので、表示したいデータを明示的に記述しました。

さらに、サーバから受け取ったデータが、どこのルーム宛てかを判断して、
viewに表示できるようにroom.jsを修正します。

```js:room.js
App.room = App.cable.subscriptions.create("RoomChannel", {

  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) {

    // ここを修正します
    var receiver_room_id = $(".room_id").val();
    if(data["room_id"] == receiver_room_id){
      $('<li>',{
        class: "hoge",
        text: data["content"],
      }).appendTo("#add");
    };
    // Called when there's incoming data on the websocket for this channel
  },

  speak: function(content, room_id) {
    return this.perform('speak', {content: content, room_id: room_id});
  }
});

$(function(){
  $(".button").on("click",function(){
    var room_id = $(".room_id").val();
    var content = $(".chat-input").val();
    App.room.speak(content, room_id);
    $(".chat-input").val("")
  });
});
```

viewから@room.idの値を取得して、データを送信した際の@room.idを等しければ
メッセージを表示する処理をしました。

ここまでで実装は、ほぼ終わったのですが、
このままメッセージを投稿してもメッセージは表示されません...

room_channel.rbファイルでcurrent_userを使用しているのですが、
room_channel.rbファイルではセッションを使用することができないのです。

current_userの値を使用するためには、コネクションというファイルを下記のように修正する必要があります。

```rb:app>channels>application_cable>connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    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
    
  end
end
```

内容としては、セッションであるcurrent_userの値を、変数として使用できるように処理しています。
<font color="DarkGray">
＊詳細なコードの意味までは把握できていないため、知りたい方は[公式ガイド](https://railsguides.jp/action_cable_overview.html)を参照下さい。
</font>

ここまで出来たら動作確認をして見て下さい。
下記の要件が満たせていれば、実装完了です。

・同じチャットルームに入室しているユーザは、ルーム内のメッセージが非同期で表示される。
・投稿したメッセージが、他のチャットルームに表示されない。

##最後に

最後まで見て頂き、有り難う御座いました。

ちなみに、今回の説明したコードだけでは、チャットルームが重複して生成できますし、
実際にアプリケーションとして運用するためには、不足している点が多々あるかと思います。
本記事では、その辺の説明はしませんので、皆さんで工夫して頂ければと思います。

#参考

[【Rails6.0】ActionCableとDeviseの欲張りセットでリアルタイムチャット作成(改正版)](https://qiita.com/eRy-sk/items/4c4e983e34a44c5ace27)