[rails7]turbo stream × Action cableでリアルタイムチャット機能を実装する

## はじめに
前回の記事で記載した、turbo streamを用いたコメント投稿にAction cableを導入しました。
あくまでturbo streamを導入している前提で記載しているので、Action cableのみ導入したいよ！という方はご注意ください。

https://qiita.com/sh10n/items/84aa844c3e06a810943f

### 開発環境
- ruby 3.2.0
- Rails 7.0.7.2
- MySQL

## Action cableとは
railsでリアルタイム配信機能実装できるフレームワークのこと。
Action cableを用いることで、チャットやSNSのようにリアルタイムでメッセージのやり取りをすることができる。

## 用語
- `チャネル`
チャネルとは、リアルタイム通信におけるサーバのようなもの。
サーバーとクライアントを円滑に繋ぐための部屋のようなものだとイメージするとよい。
1つのアプリケーションに対して、複数のチャネルを持たせることが可能。


- `サブスクリプション`
チャネルを購読すること。

- `コンシューマー`
チャネルからデータを受け取る側のこと。
コンシューマーはチャネルをサブスクリプション(購読)することで、メッセージ(データ)を受け取ることができる。

- `ブロードキャスト`
サーバーからクライアント側へデータを送信すること。


## 実装手順

### 1.チャンネルの作成
以下のコマンドでチャンネルを作成する。
```terminal:
rails g channel comments
```
コマンド実行後はチャネルである`comment_channel.rb`と関連するファイルが生成される。

:::note info
**comment_channel.rb**
サーバーとクライアントを繋ぐためのチャネルを設定するファイル。
:::


### 2.コントローラーにブロードキャストの記述を追記。
[`broadcast_prepend_later_to`](https://github.com/hotwired/turbo-rails/blob/v1.0.1/app/models/concerns/turbo/broadcastable.rb#L205-L207)を使用して、非同期的にHTMLを追加する。
先ほど作成した、comment_channelを経由してブロードキャストするように指定。

```ruby: app/controllers/comments_controller.rb
class CommentsController < ApplicationController
  before_action :authenticate_user!
  before_action :set_plan, only: [:index, :create, :edit, :update, :destroy]
  before_action :set_comment, only: [:edit, :update, :destroy]


  def create
    @comment = Comment.new(comment_params)

      respond_to do |format|
        if @comment.save
          reset_form
          
          #comment_channelを経由して、@commentのデータをブロードキャストする。
          @comment.broadcast_prepend_later_to("comment_channel")
          
          format.html { redirect_to plan_comments_path }
          format.turbo_stream
        else
          format.html { render :index, status: :unprocessable_entity }
        end
      end
  end
```


### 3.ビューの修正
**①create.turbo_stream.erb**
ブロードキャストでコメントの内容を受け取るようにしたので、`create.turbo_stream.erb`内でコメント内容の差し替えは不要になる。そのため入力フォームの更新のみ行えばよいので、以下の記述は削除する。

```erb:app/views/comments/create.turbo_stream.erb
<%= turbo_stream.replace "new_comment" do %>
  <%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<% end %>

<!-- 追記：以下の記述を削除-- >
<%#= turbo_stream.prepend "comments" do %>
  <%#= render @comments %>
<%# end %>
```

**②comments/index.html.erb**
```erb:app/views/comments/index.html.erb
<div class="text-center mx-auto py-6 sm:py-8 lg:py-12 w-1/2">
  <%= render partial:"form", locals: {plan: @plan, comment: @comment} %>

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

<!-- 追記：comments_channel経由でデータを受け取る記述を追加。-- >
<%= turbo_stream_from "comments_channel" %>
```

ここまでの実装でリアルタイムコメント追加ができました！

しかし、現在の実装だと1つ問題があります。それは別のplanに対してもコメントが非同期で更新されてしまうという点です。

動画の左はidが14、右はidが13の投稿ですが、plan14で投稿したコメント内容がplan13の投稿欄にも反映してしまっています。
![挙動確認用](https://gyazo.com/cdb273b54a33c86c7040605d1f3d924d/raw)

これは、1つのチャネルを全ブラウザで共通して使用しているためです。
1つのチャネルを使用している場合、全部のコメントが同一チャネルを通じて画面に表示されてしまいます。

次の実装で、関連のないコメントは表示されないように各planごとにチャネルを作成していきます。


## 個別チャネルの実装
### 1.comments_chanel.rbで一意のidを割り振る。
comment_chanel.rb内でコメントを追記したいplanのid情報を取得し、channelの末尾に一意のidを割り振ることで、plan毎にチャネルを分けることができる。

```ruby:app/channels/comments_channel.rb
class CommentsChannel < ApplicationCable::Channel
  def subscribed
    #以下の記述を追加
    @plan = Plan.find(params[:plan_id])
    stream_from "comments_channel_#{@plan.id}"
  end
end
```

### 2.コントローラー内で一意のチャネルを指定する。
```ruby: app/controllers/comments_controller.rb
  def create
    @comment = Comment.new(comment_params)

      respond_to do |format|
        # binding.pry
        if @comment.save
          #こちらの記述に追記。末尾にチャネルの末尾に@planを渡して、一意のチャネルにブロードキャストするように指定。
          @comment.broadcast_prepend_later_to("comments_channel_#{@plan}")
          
          reset_form
          format.html { redirect_to plan_comments_path }
          format.turbo_stream
        else
          format.html { render :index, status: :unprocessable_entity }
        end
      end
  end
```

### 3.ビュー内で一意のチャネル指定
ビューでも同じく、`turbo_stream_from`で連動しているチャネルを変更する。
```erb: app/views/comments/index.html.erb
<div class="text-center mx-auto py-6 sm:py-8 lg:py-12 w-1/2">
  <%= render partial:"form", locals: {plan: @plan, comment: @comment} %>

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

<!-- 以下の記述を編集。同じく@planの値を渡すことで一意のチャネルとの連動を作る -->
<%= turbo_stream_from "comments_channel_#{@plan.id}" %>
```

ここまでの実装で、関連のないコメントは反映されないように実装ができました！

![挙動確認用](https://gyazo.com/7be3b157de2b5cc9e524e48a5cfd384d/raw)


## 編集・削除機能の実装
### 1. コントローラー内の編集
updateアクションと、destroyアクション内の記述を変更。
updateの場合は、編集した内容に更新をかけたいので、[`broadcast_update_later_to`](https://github.com/hotwired/turbo-rails/blob/v1.0.1/app/models/concerns/turbo/broadcastable.rb#L240 )を使用して、channel経由でデータを返す。

destroyの場合は、投稿を削除したいので、[`broadcast_update_later_to`](https://github.com/hotwired/turbo-rails/blob/v1.0.1/app/models/concerns/turbo/broadcastable.rb#L93)を使用する。
```ruby: app/controllers/comments_controller.rb
~省略~
  def update
    respond_to do |format|
      if @comment.update(comment_params)
        @comment.broadcast_update_later_to("comments_channel_#{@plan.id}")
        reset_form
        format.html { redirect_to plan_comments_path }
        format.turbo_stream
      else
        format.html { render :index, status: :unprocessable_entity }
      end
    end
  end

  def destroy
    respond_to do |format|
      if @comment.destroy
        # 追記：broadcast_remove_toを使用して、削除した投稿を返す。
        @comment.broadcast_remove_to("comments_channel_#{@plan.id}")
        reset_form
        format.html { redirect_to plan_comments_path }
        format.turbo_stream
      else
        format.html { render :index, status: :unprocessable_entity }
      end
    end
  end
```

### 2. ビューの編集
`update.turbo_stream.erb`と`destroy.turbo_stream.erb`両方から、以下の記述を削除。
index.html.erb内でchannelを通してデータを受け取るようにする。

```erb:app/views/comments/update.turbo_stream.erb
<%= turbo_stream.replace "new_comment" do %>
  <%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<% end %>

<!-- 以下の記述を削除する-->
<%= turbo_stream.replace "comments" do %>
  <%= render @comments %>
<% end %>
```

```erb: app/views/comments/destroy.turbo_stream.erb
<%= turbo_stream.replace "new_comment" do %>
  <%= render partial:"form", locals: {plan: @plan, comment: @comment} %>
<% end %>

<!-- 以下の記述を削除する-->
<%= turbo_stream.update "comments" do %>
  <%= render @comments %>
<% end %>
```

ここまで実装ができたら、編集・削除の実装も完了です。

### 編集の挙動
![挙動確認用](https://gyazo.com/3694b1eef1cabec2296571d4900a8df9/raw)

### 削除の挙動
![挙動確認用](https://gyazo.com/25cb9ccc39b74a3955b4b61c8f09770f/raw)