2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Action Cableでグループチャット機能と削除機能を実装したい(その2)

Last updated at Posted at 2021-02-27

#1最終準備
今回からactioncableを実装していきます。actioncalbeではjqueryを使用し、bootstrapでレイアウトを調整したいので、bootstrapも導入します。
Gemに

Gemfile
gem 'bootstrap', '~> 4.5'
gem 'jquery-rails'

を追加し、bundleします。その後、application.cssをscssに変更し、bootstrapをインポートさせます。

Application.scss
@import "bootstrap";

最後にapplication.jsに読み込ませて完了です。

Application.js
//= require jquery3
//= require popper
//= require bootstrap-sprockets

#2いざ実装
準備が全て完了したので、実装していきます。
まずは、ルームの詳細画面にてコメントを送信するフォームと、コメントを表示するviewを作ります。

rooms/show.html.erb
<div id="chats">
  <%= render @chats %>
</div>

<textarea type="text" data-behavior="chat_speaker" class="from-control"></textarea>
<button class="chat_submit btn btn-info" >コメントを送信</button> 

これでコメントが送信できるので、次にコメントを表示できる様にしていきます。

chats/_chat.html.erb
<%= simple_format(chat.message) %>
<%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>

これでコメントと投稿日時が表示される様になります。
次に、チャネルを作成します。

 rails g channel room speak

これで、各種ファイルが生成されるので、App.room.speakとコンソールで入力し、trueと帰って来れば成功です。
今回はルーム事でのチャットの表示を行いたいので、ルームIDを付与して判別させていきます。

rooms/show.html.erb
<div id="messages" data-room_id="<%= @room.id %>">

これでルームIDを判断できる様になり、次にcoffeeで以下の記載をします

room_coffee
 document.addEventListener 'turbolinks:load', ->
  App.room = App.cable.subscriptions.create {"RoomChannel", room_id: room_id: $('#messages').data('room_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) ->
      # Called when there's incoming data on the websocket for this channel


    speak: (message) ->
      @perform 'speak'

最後にルームチャネルの方でサブスクライブを判別してあげれば、ルームのチャットの準備が完了です。

room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel_#{params('room_id')}"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
  end
end

次は、実際にチャットをリアルタイムで反映させていきます。
room_coffeにチャットの内容を送り、その後room_channelにてDBに保存させます。

(省略)
   received: (data) ->
      # Called when there's incoming data on the websocket for this channel
     $('#chats').append data['chat']

    speak: (message) ->
      @perform 'speak', message: message

 $(document).on 'click', '.chat_submit', (event) ->
  App.room.speak $('[data-behavior~=chat_speaker]').val()
  $('[data-behavior~=chat_speaker]').val('')
  event.preventDefault()

これで、meesegeに入力したチャット内容が入るので、rubyの方で保存します。
そして、ブロードキャストでまたコーヒースクリプトの方にデータを渡して、実際に画面に反映させます。

room_chaneel.rb
(省略)
 def speak(data)
    chat = Chat.create! message: data['message'], user_id: current_user.id, room_id: params['room_id']
    ActionCable.server.broadcast "room_channel_#{params['room_id']}", chat: render_chat(chat)
 end

private
  def render_chat(chat)
    ApplicationController.renderer.render( partial: 'chats/chat', locals: { chat: chat})
  end

これで、入力した内容が即時に反映されるはずです。
ezgif.com-gif-maker.gif
出来ました!
#3削除機能を実装
チャット機能自体は完成したので、次に削除機能を作成していきます。
チャットのview画面に削除ボタンを追加します。

_chat.html.erb
<div class="chat" id="<%= chat.id %>">  #IDを判断してチャットを消すのでIDの追加
  
  <%= simple_format(chat.message) %>
  <%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
  
  <button id="<%= chat.id %>" class="delete-btn btn btn-danger mr-5">コメントを削除</button> #追加
</div>

ボタンが追加出来たので、coffeeの方でデーターを取得します。

room_coffee
    destroy: (id) ->  #新しく、destroyメソッドを追加
     @perform 'destroy', id: id
    speak: (message) ->
      @perform 'speak', message: message

 $(document).on 'click', '.chat_submit', (event) ->
  App.room.speak $('[data-behavior~=chat_speaker]').val()
  $('[data-behavior~=chat_speaker]').val('')
  event.preventDefault()

  $(document).on 'click', '.delete-btn', (event) ->
    App.chat.destroy event.target.id  #削除ボタンのイベント発火追加

これでidが取得出来たのでroom_channelの方で削除します。

room_channel.rb
(省略)
 def destroy(data) #追加
    Chat.find_by(id: data['id']).destroy
   ActionCable.server.broadcast "room_channel_#{params['room_id']}", id: data['id']
 end

最後にリアルタイムに削除する為にreceivedの方に処理を加えて完了です。

room_coffee
    received: (data) ->
     if data['id'] #取得したデータがIDだったらという条件分岐で判断
      id = ('#') + data['id']
      $(id).remove()
     else
      $('#chats').append data['chat']

これで削除機能も実装出来ました!
##3.5 LINE風とコメント時のスクロール機能を追加
これで、チャット機能と削除機能は完成ですが、削除機能は当然入力した本人のみがコメントを消せる様にしたいので、renderを用いて投稿したユーザーとそれ以外のユーザーと判別します。
その後、coffeeの方で条件分岐して反映させていきます。最後にスクロール機能とLINE風なcssを当てて完成です。
まずは、renderでコメントユーザとそれ以外で振り分けていきます。
新たにコメントユーザーとして_chatcurrent.html.erbを作成します

_chatcurrent.html.erb
<div class="chat" id="<%= chat.id %>">
      <div class="mycomment">
      <%= simple_format(chat.message) %><br>
      <%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
      </div>
     
      <div class="chat_delete">
         <button id="<%= chat.id %>" class="delete-btn btn btn-danger mr-5">コメントを削除</button>
      </div>
</div>

コメントユーザー以外のユーザーのパーシャルhtmlも_chatother.html.erbとして作成します。

_chatother.html.erb
<div class="chat" id=<%= chat.id %>>
  <div class="fukidasi">
     
      <div class="chatting">
       <div class="says">
        <%= simple_format(chat.message) %><br>
       </div>
       <%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
      </div>
   </div>
</div>

次にブロードキャストした際の、renderも変更します。

room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel_#{params['room_id']}"
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    chat = Chat.create! message: data['message'], user_id: current_user.id, room_id: params['room_id']
    ActionCable.server.broadcast "room_channel_#{params['room_id']}", chat: render_chat(chat), chatother: render_chatother(chat), chat_user: current_user.id #変更
   
  end
  
  def destroy(data)
    Chat.find_by(id: data['id']).destroy
   ActionCable.server.broadcast "room_channel_#{params['room_id']}", id: data['id']
  end
private
  def render_chat(chat)
    ApplicationController.renderer.render( partial: 'chats/chatcurrent', locals: { chat: chat, current_user: current_user})
    #コメントユーザー側のパーシャル
  end
  
  def render_chatother(chat)
    ApplicationController.renderer.render( partial: 'chats/chatother', locals: { chat: chat, current_user: current_user})
    #コメントユーザー以外のパーシャル 
  end
  
end

これでruby側での変更は完了したので,coffee側でも変更を加えます。
その後、jqueryのanimate関数でコメントした際の高さを取得し、最新のコメントが出るたびにスクロールさせます。


    received: (data) ->
     if data['id']
      id = ('#') + data['id']
      $(id).remove()
     else
     show_user = $('#show_user').data('show_user')
       if data['chat_user'] == show_user #ここでコメントユーザーを判断します
        $('#chats').append data['chat']
        $('.chat_box').animate scrollTop: $('.chat_box')[0].scrollHeight #これでコメントした際に一番下にスクロールします
       else
        $('#chats').append data['chatother']
        $('.chat_box').animate scrollTop: $('.chat_box')[0].scrollHeight

ここで出てきた、show_userをview の方に追記します。

show.html.erb
<div id="messages" data-room_id="<%= @room.id %>"></div>
<div id="show_user" data-show_user="<%= current_user.id %>"></div> #追加
<div class="chat_box"> #ここでスクロール用の高さを取得します
 <div id="chats">
   <%= render @chats %>
 </div>
</div>
<textarea type="text" data-behavior="chat_speaker" class="from-control"></textarea>
<button class="chat_submit btn btn-info" >コメントを送信</button>

最後に、_chat.html.erbにcurrent_userとそうでない場合の処理を加えて完了です。

_chat.html.erb
<div class="chat" id="<%= chat.id %>">
  <% if chat.user == current_user %>
    
    <div class="mycomment">
    <%= simple_format(chat.message) %><br>
    <%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
    </div>
   
    <div class="chat_delete">
       <button id="<%= chat.id %>" class="delete-btn btn btn-danger mr-5">コメントを削除</button>
    </div>
   <% else %>

   <div class="fukidasi">        
      <div class="chatting">
       <div class="says">
        <%= simple_format(chat.message) %><br>
       </div>
       <%= chat.created_at.strftime('%Y-%m-%d %H:%M') %>
      </div>
   </div>
   <% end %>
</div>

最後に、cssを当てて完成です。

room.scss
.chat_box{
  background-color: #fff;
  width: 50%;
  height: 500px;
  padding: 10px;
  margin-top: 10px;
  overflow: scroll;
  border: blue 1px solid;
  text-align: right;
  font-size: 14px;
  background: #7da4cd;
}


#outer_box {
  position: relative;
  height: 300px;
}

.mycomment {
  margin: 10px 0;
}
.mycomment p {
  display: inline-block;
  position: relative;
  margin: 0 10px 0 0;
  padding: 8px;
  max-width: 250px;
  border-radius: 12px;
  background: #30e852;
  font-size: 15px;
}
.mycomment p:after {
  content: "";
  position: absolute;
  top: 3px;
  right: -19px;
  border: 8px solid transparent;
  border-left: 18px solid #30e852;
  -webkit-transform: rotate(-35deg);
  transform: rotate(-35deg);
}
.fukidasi {
  width: 100%;
  margin: 10px 0;
  overflow: hidden;
}
.user_image-attachment {
  border-radius: 50%;
}
.fukidasi .faceicon {
  float: left;
  margin-right: -50px;
  width: 40px;
}
.fukidasi .chatting {
  width: 100%;
  text-align: left;
}
.says {
  display: inline-block;
  position: relative;
  margin: 0 10px 10px 50px;
  padding: 10px;
  max-width: 250px;
  border-radius: 12px;
  background: #edf1ee;
}
.says:after {
  content: "";
  display: inline-block;
  position: absolute;
  top: 3px;
  left: -19px;
  border: 8px solid transparent;
  border-right: 18px solid #edf1ee;
  -webkit-transform: rotate(35deg);
  transform: rotate(35deg);
}
.says p {
  margin: 0;
  padding: 0;
}

#4終わりに
以上で終了です。非常に長くなってしまいました。
所々雑な部分がありますが、ひとまずは自分用に復習として記入できたと思います。
又、turbolinksを使用していると、意図しない挙動をする場合があるのでその際はturbolinksを外すことをお勧めします。
#5参考
非常に参考にしたサイト
https://hajimeteblog.com/rails-actioncable/

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?