rails6 グループチャット機能の作成

# はじめに
railsでグループチャット機能の実装にはあらゆる手段がありますが、特に数ある機能の中でも難易度が非常に高く、実装したかったけど諦めてしまった。。という方を見かけましたので、railsの基礎とちょっとした応用で出来るグループチャット機能を実装してみました。

#### なお本機能実装には、usersコントローラー、Userモデルを作成していることが前提となります。また、現在メインで使用しているコントローラー/モデルとは別にこの記事通り作成を進めてください。

# 必要なモデルとテーブルを設計する
既にあるuserモデルに加え下記のモデルを作成していきます

### Groupモデル
・グループ名や説明を管理する
・保存するカラム
→name(string):グループ名

### Messageモデル
・チャットメッセージを管理
・保存するカラム
→content(text):メッセージの内容
→user_id(integer):投稿者(ユーザー)を関連付ける
→group_id(integer):メッセージが所属するグループを関連付ける

### GroupUserモデル
・ユーザーとグループの多対多の関係を管理
・保存するカラム
→user_id(integer):ユーザー
→group_id(integer):グループ


# モデルとアソシエーション
#### モデルの作成とマイグレーション
```terminal:コマンドプロンプト
rails g model Group name:string
rails g model Message content:text user:references group:references
rails g model GroupUser user:references group:references
rails db:migrate
```

#### モデルのアソシエーションを設定
Userモデル
```ruby:user.rb
has_many :group_users
has_many :groups, through: :group_users
has_many :messages
```

Groupモデル
```ruby:group.rb
has_many :group_users
has_many :users, through: :group_users
has_many :messages
validates :name, presence: true
```

Messageモデル
```ruby:Message.rb
belongs_to :user
belongs_to :group
validates :content, presence: true
```

GroupUserモデル
```ruby:user.rb
belongs_to :user
belongs_to :group
```

# コントローラーの作成と記述
### コントローラーの作成
```terminal:コマンドプロンプト
rails g controller Groups
rails g controller Messages
```


### コントローラーの記述
### ※アクションについて
#### GroupsController
・index:グループチャット一覧を表示
・new/create:新規グループ作成
・show:グループ内のメッセージを表示
#### MessageController
・create:メッセージを送信
```ruby:groups_controller.rb
class GroupsController < ApplicationController
  before_action :authenticate_user!

  def index
    @groups = current_user.groups
  end

  def show
    @group = Group.find(params[:id])
    @messages = @group.messages.includes(:user)
    @message = Message.new
  end

  def new
    @group = Group.new
  end

  def create
    @group = Group.new(group_params)
    if @group.save
      @group.users << current_user # 作成者をグループに追加
      redirect_to @group, notice: 'グループが作成されました'
    else
      render :new, status: :unprocessable_entity
    end
  end

  private

  def group_params
    params.require(:group).permit(:name, user_ids: [])
  end
end

```
```ruby:messages_controller.rb
class MessagesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_group

  def create
    @message = @group.messages.new(message_params)
    @message.user = current_user

    if @message.save
      # ActionCableブロードキャスト予定
      redirect_to @group, notice: 'メッセージが送信されました'
    else
      @messages = @group.messages.includes(:user)
      render 'groups/show', status: :unprocessable_entity
    end
  end

  private

  def set_group
    @group = Group.find(params[:group_id])
  end

  def message_params
    params.require(:message).permit(:content)
  end
end

```

# ルーティングの設定
```ruby:routes.rb
#省略
resources :groups do
  resources :messages, only: [:create]
end
#省略
```

# ビューの作成
#### index(グループ一覧を表示するページ)の作成
```ruby:groups/index.html.erb
<h1>グループチャット</h1>
<%= link_to 'グループ作成', new_group_path, class: 'btn btn-primary' %>

<ul>
  <% @groups.each do |group| %>
    <li><%= link_to group.name, group_path(group) %></li>
  <% end %>
</ul>
```
#### show(メッセージを作成するページ)の作成
```ruby:groups/show.html.erb
<h1><%= @group.name %></h1>

<div id="messages">
  <% @messages.each do |message| %>
    <p>
      <strong><%= message.user.email %>:</strong>
      <%= message.content %>
    </p>
  <% end %>
</div>

<%= form_with(model: [@group, @message], local: true) do |f| %>
  <div>
    <%= f.text_area :content, rows: 3, placeholder: 'メッセージを入力してください' %>
  </div>
  <div>
    <%= f.submit '送信', class: 'btn btn-success' %>
  </div>
<% end %>
```

#### new(グループを新規作成するページ)の作成
```ruby:groups/new.html.erb
<h1>グループ作成</h1>

<%= form_with(model: @group, local: true) do |f| %>
  <div>
    <%= f.label :name, 'グループ名' %>
    <%= f.text_field :name %>
  </div>
  <div>
    <%= f.submit '作成', class: 'btn btn-primary' %>
  </div>
<% end %>
```

#### お疲れ様です！今の段階でこの機能のほとんどが終了です！ここまで完了出来たら試しに、グループチャットを新規作成し、メッセージを投げてみましょう！上手く表示されれば続きを、うまくできなければ振り返ってみましょう！

# 参加・退出機能の追加
先ほどメッセージを送信してみたかと思いますが、チャットの参加/退出が現状できなくなっていますのでこちらを実装してより完璧にしていきましょう。
### GroupUsersControllerを作成
新しいコントローラーを作成して、参加と退出を管理する
```terminal:コマンドプロンプト
rails generate controller GroupUsers
```
### GroupUsersControllerの実装
```ruby:group_users_controller.rb
class GroupUsersController < ApplicationController
  before_action :authenticate_user!
  before_action :set_group

  # グループに参加
  def create
    @group.users << current_user unless @group.users.include?(current_user)
    redirect_to @group, notice: 'グループに参加しました。'
  end

  # グループから退出
  def destroy
    @group.users.delete(current_user)
    redirect_to groups_path, notice: 'グループから退出しました。'
  end

  private

  def set_group
    @group = Group.find(params[:group_id])
  end
end
```

# ルーティングの設定
```ruby:route.rb
resources :groups do
  resources :messages, only: [:create]
  resource :group_users, only: [:create, :destroy] # 参加と退出のルートを追記
end
```
# Viewの記述
groups/index.html.erbのグループそれぞれに参加/退出ボタンを追加する
```ruby:groups/index.html.erb
<h1>グループチャット</h1>
<%= link_to 'グループ作成', new_group_path, class: 'btn btn-primary' %>

<ul>
  <% Group.all.each do |group| %>
    <li>
      <%= link_to group.name, group_path(group) %>
      <% if current_user.groups.include?(group) %>
        <%= button_to '退出', group_group_users_path(group), method: :delete, class: 'btn btn-danger btn-sm' %>
      <% else %>
        <%= button_to '参加', group_group_users_path(group), class: 'btn btn-success btn-sm' %>
      <% end %>
    </li>
  <% end %>
</ul>
```
groups/show.html.erbに参加者一覧を追加する
```ruby:groups/show.html.erb
<h1><%= @group.name %></h1>

<p>現在の参加者:</p>
<ul>
  <% @group.users.each do |user| %>
    <li><%= user.email %></li>
  <% end %>
</ul>

<div id="messages">
  <% @messages.each do |message| %>
    <p>
      <strong><%= message.user.email %>:</strong>
      <%= message.content %>
    </p>
  <% end %>
</div>

<%= form_with(model: [@group, @message], local: true) do |f| %>
  <div>
    <%= f.text_area :content, rows: 3, placeholder: 'メッセージを入力してください' %>
  </div>
  <div>
    <%= f.submit '送信', class: 'btn btn-success' %>
  </div>
<% end %>
```

#### お疲れ様でした！これにて終了です。動かして確認してみましょう。下記は追加で実装したい人向けになっていますので、より高い完成度を目指したい方はチャレンジしてみてください！

# 追加機能
<details><summary>メッセージ送信時に自動でグループに参加する</summary>

#### MessagesController を修正
messages_controller.rbでメッセージ作成時に自動でグループに参加させる処理を追加する
```ruby:messages_controller.rb
class MessagesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_group

  def create
    # もし現在のユーザーがグループに参加していなければ参加させる
    @group.users << current_user unless @group.users.include?(current_user)

    @message = @group.messages.new(message_params)
    @message.user = current_user

    if @message.save
      # ActionCableなどリアルタイム更新処理を後で追加可能
      redirect_to @group, notice: 'メッセージが送信されました'
    else
      @messages = @group.messages.includes(:user)
      render 'groups/show', status: :unprocessable_entity
    end
  end

  private

  def set_group
    @group = Group.find(params[:group_id])
  end

  def message_params
    params.require(:message).permit(:content)
  end
end

```
これにより、ユーザーがメッセージを送信する際、自動的にそのグループに参加します。
</details>
<details><summary>グループに参加していないとメッセージが送信できない仕様にする</summary>

#### MessagesControllerに条件を追加
参加していないユーザーがメッセージを送信しようとした場合にリダイレクトするように変更

```ruby:messages_controller.rb
class MessagesController < ApplicationController
  before_action :authenticate_user!
  before_action :set_group
  before_action :ensure_group_membership

  def create
    @message = @group.messages.new(message_params)
    @message.user = current_user

    if @message.save
      redirect_to @group, notice: 'メッセージが送信されました'
    else
      @messages = @group.messages.includes(:user)
      render 'groups/show', status: :unprocessable_entity
    end
  end

  private

  def set_group
    @group = Group.find(params[:group_id])
  end

  def ensure_group_membership
    unless @group.users.include?(current_user)
      redirect_to @group, alert: 'グループに参加してください。'
    end
  end

  def message_params
    params.require(:message).permit(:content)
  end
end

```

#### Viewで「参加」ボタンを表示する
参加していないユーザーに対して「参加ボタン」を表示するようにする
groups/show.html.erbのフォームの前に、下記を追加

```ruby:groups/show.html.erb
<% unless @group.users.include?(current_user) %>
  <%= button_to '参加する', group_group_users_path(@group), class: 'btn btn-success', method: :post %>
  <p>グループに参加してからメッセージを送信してください。</p>
  <% return %>
<% end %>

```
このコードを追記することにより、ユーザーがグループに参加していない場合に「参加ボタン」を表示し、メッセージ入力フォームを非表示にする。

</details>