【Rails】 Action Cableを用いてチャット機能を作成

Active Cableを使って、Userログイン有り/チャットRoom有りのリアルタイムで投稿が反映されるチャット機能を作る事ができたので、
備忘録の意味も込めて投稿します。
↓以下、コピペでリアルタイムで表示が反映される、非同期的なchatが作れます。


#下準備
```bash:bash
$ rails new chat_app
```

``` ruby:Gemfile
gem 'devise'
gem 'jquery-rails'
```

```bash:bash
$ bundle
```

jQueryを読み込み

```javascript:app/assets/javascripts/application.js
//= require jquery
```

# DeviseでUser認証

```bash:bash
$ rails g devise:install
$ rails g devise user
$ rails db:migrate
```

# チャット機能

##RoomとMessageの作成
```bash:bash
$ rails g controller rooms show
$ rails g model message content:text
$ rails g model room
$ rails g migration AddRefsToMessages user:references room:references
$ rails db: migrate
```

モデルのアソシエーションのため
`user.rb`, `room.rb`に**has_many**を、`message.rb`に**belongs_to**を追記。
↓

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


###room.rb
```room.rb
class Room < ApplicationRecord
  has_many :messages
end
```
### message.rb
```messasge.rb
class Message < ApplicationRecord
  belongs_to :user
  belongs_to :room
  validates :content, presence: true
  scope :recent, -> { order(created_at: :desc)}
end
```

### rooms_controller.rb
```rooms_controller.rb
class RoomsController < ApplicationController
  before_action :authenticate_user!

  def index
    @rooms = Room.all
  end

  def create
    @room = Room.new
    @room.save
    redirect_to rooms_path
  end

  def show
    #binding.pry
    @room = Room.find(params[:id])
    @messages = @room.messages.recent.limit(5).reverse
  end
end

```

### app/veiws/rooms/index.html.erb
```erb:app/veiws/rooms/index.html.erb
<h1>Index</h1>

<% @rooms.each do |room| %>
  <div>
    <%= link_to "Room#{room.id}", room_path(room) %>
  </div>
<% end %>

<%= link_to "create new Room", rooms_path, method: :post, class: "btn btn-primary" %>

```

### app/veiws/rooms/show.html.erb
```erb:app/veiws/rooms/show.html.erb
<div class="container">
  <%= link_to "<<back index", rooms_path %>
  <h1><%= "Chat Room #{@room.id}" %></h1>
  <h4 class="border"><%= "Your Mail Address: #{current_user&.email}" %></h4>
  <div id="messages", data-room_id="<%= @room.id %>">
    <%= render @messages %>
  </div>
  <div class="input">
    <form>
      <label>post</label>
      <input type="text" data-behavior="room_speaker">
    </form>
  </div>
</div>
```

###app/veiws/messages/_message.html.erb
```html:app/veiws/messages/_message.html.erb
<div class="messages list-group-item">
  <p><%= "#{message.user.email}: #{message.content}" %></p>
</div>
```

##Channel作成

```bash:bash
$ rails g channel room speak
```

```app/channel/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.create!(content: data['message'],
                              user_id: current_user.id,
                              room_id: params['room_id'])
    render_message = ApplicationController.renderer.render(partial: 'messages/message',
                                                            locals: {message: message})
    ActionCable.server.broadcast 'room_channel',
                                  message: render_message,
                                  room_id: params['room_id']
  end
end
```

###app/assets/javascripts/channels/room.coffee
```app/assets/javascripts/channels/room.coffee
document.addEventListener 'turbolinks:load', ->
  room_id = $('#messages').data('room_id')
  App.room = App.cable.subscriptions.create { channel: "RoomChannel", room_id: room_id },
    connected: ->
      # Called when the subscription is ready for use on the server

    disconnected: ->
      # Called when the subscription has been terminated by the server

    received: (data) ->
      if data['room_id'] is room_id
        $('#messages').append data['message']
        # Called when there's incoming data on the websocket for this channel

    speak: (message) ->
      @perform 'speak', message: message

    $(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
      if event.keyCode is 13
        App.room.speak event.target.value
        event.target.value = ''
        event.preventDefault()

```

###app/channels/application_cable/connection.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

    private

      def find_verified_user
        session_key = cookies.encrypted[Rails.application.config.session_options[:key]]
        verified_id = session_key['warden.user.user.key'][0][0]
        verified_user = User.find_by(id: verified_id)
        return reject_unauthorized_connection unless verified_user
        verified_user
      end
  end
end
```
↑の`connection.rb`によって、
**class RoomChannel** 内で `current_user`が使用できるようになります。


## debugしてみる
def sepak(data) 内にbinding.pryを仕込んでdebugしてみると、こんな感じのパラメータが引き渡されているのが分かる

```
[3] pry(#<RoomChannel>)> params
=> {"channel"=>"RoomChannel", "room_id"=>1}
[4] pry(#<RoomChannel>)> data
=> {"message"=>"test", "action"=>"speak"}
```
↑params[:id] = 1 で formにtestと入力した時の例


## 参考元
https://qiita.com/eRy-sk/items/4c4e983e34a44c5ace27
（参考というより、ほぼ引用させて頂いております）