ActionCableでリアルタイムDMを実装する

#初めに
Ajaxとか勉強していたらActionCableという自分以外の人にもリアルタイムで投稿を表示することができる機能があることを知ったので勉強してみた。

正直中身に関してはいまいちよくわかってないのでいろいろ間違っているかもしれませんがその時は教えてください…

#とりあえず

とりあえずrails newしてmodelとcontrollerをつくる

```
rails new action_dm
rails g model user name:string
rails g model room name:string
rails g model comment body:text user_id:integer room_id:integer

rails db:migrate

rails g controller users index 
rails g controller rooms show
rails g controller sessions
rails g controller comments
```

gem "jquery-rails'
bundle install

してそのあと

assets/javascripts/application.jsに

```
//= require jquery
```
を
//= require rails-ujs
の上に書く

これでOK
userは名前だけ持っていてログインも名前で行う。
roomはダイレクトメッセージを行うところですべてのユーザー間にroomが作られるようにする
コメントを表示する際に名前も表示したいのでcommentにuser_idをいれた

#モデルにアソシエーションを書いとく

```ruby:comment.rb
belongs_to :room
```

```ruby:room.rb
has_many :comments
```

```ruby:user.rb
has_many :comments
```

userとroomはたくさんのcommentを持つのでhas_manyした
commentはとりあえずルームに紐づけていればよい


#User作成できるようにする

userを作成してログインできるようにcontrollerとviewsをいじっていく

まずusers_controller

```ruby:users_controller.rb
class UsersController < ApplicationController
  before_action :set_current_user

  def index
    @user = User.new
    @rooms = Room.all
    @users = User.all
  end

  def create 
    @user = User.create(user_params)
    if @user.save 
      redirect_to users_path 
    else
      render :new
    end 
  end 

  private 

  def user_params 
    params.require(:user).permit(:name)
  end 
end
```


indexには@user,@rooms,@usersがあってuser作成やroomへのリンクuser一覧、ログインすべてindex.html.erbで行うため必要。


before_actionのset_current_userはapplication_controller.rbに書いてある

application_controller.rbはこんな感じ

```ruby:application_controller.rb
class ApplicationController < ActionController::Base

    def set_current_user
        if session[:user_id]
            @current_user ||= User.find session[:user_id]
        end 
    end 

    helper_method :set_current_user
end
```

@current_userがあれば@current_userを代入してなければUser.find session[:user_id]で見つかったuserを@current_userに代入する。

次にviewをつくる

index.html.erbはこんな感じ

```erb:index.html.erb

<p>ユーザー一覧</p>

<% @users.each do |user| %>
    <%= user.name %>
<% end %>

<p>現在のアカ</p>
<%= @current_user&.name %>

<p>アカ作成</p>
<%= form_with model: @user, local: true do |f| %>
    <%= f.text_field :name %>
    <%= f.submit %>
<% end %>

<p>ログイン</p>
<%= form_with url: sessions_new_path do |f| %>
    <%= f.text_field :name %>
    <%= f.submit %>
<% end %>

<p>ログアウト</p>
<%= link_to "ログアウト", "sessions/destroy", method: :delete %>


<p>DM</p>


<% @users.each do |user| %>
    <% unless user.id == session[:user_id] %>
        <%= link_to "#{user.name}", "/rooms/create/#{user.id}", method: :post %>
    <% end %>
<% end %>
```

現在のアカに関しては@current_userがnilの時に@current_user.nameがエラーを出すので&.を使っています。これを使うと@current_userがnilでもエラーが出ない

cssに

```css
p {
  border-bottom: 1px solid gray;
}
```

と書いておく

とりあえずこれでユーザーを作成しログインすることができるようになった


#roomを作る

このroomがDMを行う場所

まずrooms/show.html.erbを

```erb:show.html.erb
<h1>DM</h1>

<h2>comments</h2>

<div id="comments">
    <%= render @room.comments %>
</div>

<%= render 'comments/new', room: @room %>
```

この<%= render @room.comments %>に関していまいちよくわかってないが

```erb:comment
<% @room.comments.each do |comment| %>
   <%= render 'comments/comment', comment: comment %>
<% end %>
```


の省略した書き方？だと思う

comments/_comment.html.erbは

```erb:_comment.html.erb
<p><%= username(comment.user_id) %>:<%= comment.body %> -- <%= comment.created_at.to_s(:long) %></p>
```

コメントを作成するための _new.html.erbは

```erb:_new.html.erb
<%= form_for([ @room, Comment.new ], remote: true) do |form| %>
    Your comment:<br>
    <%= form.text_field :body, size: '50x20' %>
    <%= form.submit %>
<% end %>
```


次にrooms_controller.rb

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

  def show
    protect_room(params[:id])
    @room = Room.find params[:id]
  end

  def create
    @r = find_room(params[:id])
    if @r 
      redirect_to "/rooms/#{@r.id}}"
    else 
      @room = Room.new(name: "room@#{params[:id]}@#{session[:user_id]}")
      @room.save
      redirect_to "/rooms/#{@room.id}"
    end 
  end 


  private

  def protect_room(room_id)
    @room = Room.find room_id
    id = @room.name.split("@")
    unless id.find { |i| i.to_i == session[:user_id].to_i }
      redirect_to users_path 
    end 
  end 

  def find_room(id)
    @rooma = Room.find_by(name: "room@#{id}@#{session[:user_id]}")
    @roomb = Room.find_by(name: "room@#{session[:user_id]}@#{id}")
    @r = @rooma || @roomb 
    return @r 
  end 

end
```

まずroomは最初から作成されているわけではなくメッセージを送るためroomに入ろうとしたときに作成されるようにした。

例えばAとBがいた場合にroomがない状態でAがBにメッセージを送るためroomに入ろうとするとroomが"room@(Bのid)@(Aのid)"という形でroomが作られる。



BがAにメッセージを送ろうとしたときにまたroomが作成されないようにするためにfind_roomという確認メソッドを作った

```ruby
  def find_room(id)
    @rooma = Room.find_by(name: "room@#{id}@#{session[:user_id]}")
    @roomb = Room.find_by(name: "room@#{session[:user_id]}@#{id}")
    @r = @rooma || @roomb 
    return @r 
  end  
```

roomの名前

というのを作った
お互いの間にroomがない場合はnilを返す感じ


roomに他人を入れないようにするために
protect_room(room_id)メソッドを作った


```ruby
  def protect_room(room_id)
    @room = Room.find room_id
    id = @room.name.split("@")
    unless id.find { |i| i.to_i == session[:user_id].to_i }
      redirect_to users_path 
    end 
  end
```

名前にお互いのidが記入されているのでそれを取得してsession[:user_id:と比較している


ここまででuser作成とroom作成はおわり


#comment作成

action cableを使うのはこのcommentでcommentを作成したときに自動的にお互いに反映されるようにする


まずchannelを作る

```
rails g channel comments
```

このあと編集するのは二つのファイルで一つがサーバー用でもう一つがクライアントサイド用(多分..)

/assets/javascript/comments.coffeeがクライアントサイド用で

こんな感じにする

```channels/comments.coffee
App.comments = App.cable.subscriptions.create "CommentsChannel",
  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) ->
    $('#comments').append data.comment
    # Called when there's incoming data on the websocket for this channel

```


receivedでデータを受け取ったときに$('#comments').append data.commentが行われる。


サーバーサイドのファイルが/channels/comments_channel.rbにあり

```ruby:comments_channel.rb
class CommentsChannel < ApplicationCable::Channel
  def self.broadcast(comment)
    broadcast_to comment.room, comment: CommentsController.render(partial: 'comments/comment', locals: { comment: comment })
  end 

  def subscribed
    Room.all.each do |room|
      stream_for room 
    end 
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end
end
```

subscribedでaction cableを使うものを指定している。
なので例えばstream_for Room.last　とするそれだけaction cableを使える

stream_forとstream_fromの違いとしてはモデルに関連するストリームを作成する場合はstream_forでそうではない場合はstream_fromだそう


stream_forの場合CommentsChannel.broadcast_to(@room,@comment)と書けばブロードキャスト？できるそう


今回の場合はcomments#createでCommentsChannel.broadcast(comment)が行われている


さいごにrouteはこんな感じ

```routes.rb
Rails.application.routes.draw do
  resources :users
  resources :rooms, only: [:show] do 
    resources :comments 
  end 
  post 'sessions/new'
  delete 'sessions/destroy'

  post "rooms/create/:id", to: "rooms#create"
  # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end
```

これで
![room.png](https://qiita-image-store.s3.amazonaws.com/0/207921/4b1684c0-80db-12dd-a6ab-59e9c29ab0c0.png)


![room2.png](https://qiita-image-store.s3.amazonaws.com/0/207921/e43fd584-0991-dff6-4e3b-3f333c167873.png)


みたいな感じで使える。


#おわりに

正直なんで動いでいるのか全く分らない..

なにか間違っているところがあればご指摘いただけるとありがたいです。



だれもいらないだろうけどGitHubも上げておきます。
https://github.com/Sibakeny/action_dm


#参考文献

Rails Tour
https://www.youtube.com/watch?v=OaDhY_y8WTo&t=1089s

RailsGuides
https://railsguides.jp/action_cable_overview.html