I made a team chat function with Action Cable in the portfolio, so I will output it.
I referred to the following article. Thank you very much. ・ Make with Rails 5 + Action Cable! Simple chat app (from DHH's demo video) ・ [Rails 6.0] Create real-time chat with Action Cable and Devise's greedy set (revised version)
Tweak the code in the reference article to send chats to each team. This time, we will create a chat action on the team controller so that we can send chats. Create the part other than Action Cable according to the MVC structure.
model The model describes the many-to-many relationship between the team model and the user model. In addition, 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 #Add a user icon in 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 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 I applied bootstrap, I am using container, alert, etc. Basically, the received chat is displayed on the screen by rendering a message partial. We also have 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, implement Action Cable. First, create a team channel. Directories and files are created for the channel.
$ rails g channel team
coffeescript First, describe the settings for monitoring the server from the browser. Use jQuery to get the team ID from 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, describe the settings for monitoring the browser from the server. The browser's @perform speak calls the server-side speak action. 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']}" #Show which team you are monitoring
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
Makes 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 } #Postscript
end
job Describe the setting to send the created message from job to the browser. First, create a MessageBroadcastJob from the rails command.
$ rails g job MessageBroadcast
When using ActiveStorage, if http_host of ApplicationController.renderer is not specified, the reference destination of the image will be an error. If you want to use a URL that starts with https: // instead of http: // in production, you need to specify https: true.
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, localhost:3000. Changed depending on the production environment
#https: true https://For URLs starting with, you need to add this code
)
renderer.render(partial: 'messages/message', locals: { message: message })
end
end
By this job, the partial of the message is passed to the receive of coffeescript and reflected in real time.
I thought ActionCable was complicated, but I was able to follow and understand the passing of data by reference. From this experience, I realized again that it is important to follow the data flow one by one even if it looks complicated, and it is the best shortcut to use binding.pry on the server side and debugger on the browser side. ..
Recommended Posts