[Ruby] Make Team Chat with Rails Action Cable

4 minute read

Introduction

Since I made a team chat function with Action Cable in the portfolio, I will output it.

Reference article

I referred to the following article. Thank you very much. ・Create with Rails 5 + Action Cable! Simple chat app (from DHH’s demo video)

model

The model describes the many-to-many relationship between the team model and the user model. The message model, which is the content of the chat, is created as a model of the intermediate table between the user and the team.

app/models/team.rb


class Team <ApplicationRecord # team model
  belongs_to :owner, class_name:'User', foreign_key: :owner_id
  has_many :team_assigns, dependent: :destroy
  has_many :members, through: :team_assigns, source: :user
  has_many :messages, dependent: :destroy
end

app/models/user.rb


class User <ApplicationRecord
  # Include default devise modules.Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  has_one_attached :icon #Attach user icon with Active Storage
  has_many :team_assigns, dependent: :destroy
  has_many :teams, through: :team_assigns, source: :team
  has_one :owner_team, class_name:'Team', foreign_key: :owner_id, dependent: :destroy
  has_many :messages, dependent: :destroy
end

app/models/team_assign.rb


class TeamAssign <ApplicationRecord
  belongs_to :team
  belongs_to :user
end

app/models/message.rb


class Message <ApplicationRecord
  belongs_to :user
  belongs_to :team
end

controller

The controller has created a chat action within the team. Receive the team ID from the link, search from the Team class, get the chat that the team has and pass it to view.

app/controllers/teams_controller.rb


  def chat
    @team = Team.find(params[:id])
    messages = @team.messages
  end

view

Since bootstrap is applied, container, alert, etc. are used. Basically, the received chat is displayed on the screen by rendering a message partial. It also has a form for sending chats.

html:app/views/teams/chat.html.erb


<h1 id="chat" data-team_id="<%= @team.id %>">
  <%= @team.name %> chat room
</h1>

<div class="container">
  <% if @team.members.count == 1 %>
    <div class="alert alert-danger" role="alert">
      There are no team members to chat with. Add members from the team screen
    </div>
  <% end %>
  <form class="form-group">
    <label>Chat:</label>
    <input type="text" data-behavior="team_speaker", placeholder="contents", class="form-control">
  </form>
  <div id="messages_<%= @team.id %>">
    <ul class="list-group">
      <%= render @messages %>
    </ul>
  </div>
</div>

html:app/views/messages/_message.html.erb


<div class="message">
  <li class="list-group-item">
    <div class="row">
      <div class="col-md-2">
        <% if message.user.icon.attached? %>
          <%= image_tag message.user.icon.variant(resize:'50x50').processed %>
        <% end %>
        <p><%= message.user.name %></p>
      </div>
      <div class="col-md-10">
        <small class="text-muted"><%= l message.created_at, format: :short %></small><br>
        <%= message.content %>
      </div>
    </div>
  </li>
</div>

From here, install the Action Cable. First, create a team channel. Directories and files are created for the channel.

$ rails g channel team

coffeescript

First, write the settings to monitor the server from the browser. Use jQuery to get the team ID from the view and create a channel to subscribe to.

app/assets/javascripts/channels/team.coffee


App.team = App.cable.subscriptions.create {
  channel: "TeamChannel",
  team_id: $("#chat").data("team_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) ->
    Receive the chat received by the speak method on the #Channel side
    $('#messages_'+data['team_id']).prepend data['message']

  speak: (message) ->
    team_id = $('#chat').data('team_id')
    @perform'speak', message: message, team_id: team_id

$(document).on'keypress','[data-behavior~=team_speaker]', (event) ->
  if event.keyCode is 13 # return = send
    App.team.speak event.target.value
    event.target.value = ``
    event.preventDefault()

channel.rb

Next, we will write the settings to monitor the browser from the server. The server’s speak action is invoked by @perform speak in the browser. The speak action creates an instance of the message class based on the information received from the browser.

app/channels/team_channel.rb


class TeamChannel <ApplicationCable::Channel
  def subscribed
    stream_from "team_channel_#{params['team_id']}" # Indicates which team is being monitored
  end

  def unsubscribed
# Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    message = Message.create!(
      content: data['message'],
      team_id: data['team_id'],
      user_id: current_user.id
    )
  end
end

I am using current_user in the channel, but for that I need to add the following code:

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
        verified_user = User.find_by(id: env['warden'].user.id)return reject_unauthorized_connection unless verified_user
        verified_user
      end
  end
end

Message model

Make the job run when an instance is created in the Message model (after commit).

app/models/message.rb


class Message <ApplicationRecord
  belongs_to :user
  belongs_to :team
  after_create_commit {MessageBroadcastJob.perform_later self }# Added
end

job

Describe the settings to send the created message from the job to the browser. First, create a MessageBroadcastJob from the rails command.

$ rails g job Message Broadcast

When using ActiveStorage, if the http_host of ApplicationController.renderer is not specified, the image reference destination will become an error. When using a URL starting with https:// instead of http:// in production, https: true must be specified.

app/jobs/message_broadcast_job.rb


class MessageBroadcastJob <ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast(
      "team_channel_#{message.team_id}",
      message: render_message(message),
      team_id: message.team_id
    )
  end

  private
    def render_message(message)
      renderer = ApplicationController.renderer.new(
        http_host:'http_host number', # local, locallhost:3000. Changed by production environment
*Https: true *If the URL starts with https://, this code needs to be added
      )
      renderer.render(partial:'messages/message', locals: {message: message })
    end
end

With this job, the message partial is passed to the coffeescript receive and reflected in real time.

Summary

I thought ActionCable was complicated, but the references helped me follow and understand the passing of data. From this experience, even if it looks complicated, it is important to follow the flow of data one by one, and I once again felt that using binding.pry on the server side and using debugger on the browser side is the best shortcut. ..