はしばみあきら blog

プログラミングアウトプットするブログ。202010スタート

【Rails】条件に応じて日付の表示を判定する

LINEのトーク表示って何時何分とか、何曜日とか、古い物だと2020.xx.xxみたいな感じで表示されます

あの部分を作ります

条件に応じて日付の表示を判定する

今日のメッセージなら xx:xx の表示
昨日〜1週間前までは x曜日 の表示 それ以前は 2020.xx.xx の表示

という形にします

ここはviewにガッツリ書くことにしまいた(本当はあんまりよろしくないのだけど...)

 p.latest-talk-show__data-time-notifiction
   span#data-time

pとspanを用意

この時点でcreated_atの値を確認

f:id:hashibamiakira:20201031220726p:plain

created_at: Sat, 31 Oct 2020 18:33:23 JST +09:00 という値が取れました

コードに記述を加えて分岐させていきます

p.latest-talk-show__data-time-notifiction
  span#data-time
    / もしトークが今日の物ならば
    - if Date.today == talk.talk.last.created_at.to_date
      = talk.talk.last.created_at.strftime('%H:%M')
    / もしトークが過去1週間ならば
    - elsif 1.week.ago.beginning_of_day.to_date..Time.zone.now.end_of_day.to_date == talk.talk.last.created_at.to_date
      - weeks = ["","","","","","",""]
      - num = talk.talk.last.created_at.strftime("%w").to_i
      - week = weeks[num]
      = week + "曜日"
    / それ以降であれば
    - else
      = talk.talk.last.created_at.strftime("%Y.%M.%D")

大分冗長です...すっきりさせたいけどそれは別の機会で...

少し読み解きます

  • if Date.today == talk.talk.last.created_at.to_date
    Date.todayをみます
=> Sat, 31 Oct 2020

== で今日の日付判定をcreated_atとする時、created_atにto_dateをつけましょう
何もつけないままだと時間の部分まで含めて判定するので今日の日付でもfalseになります

[3] pry(#<#<Class:0x00007f390891b390>>)> Date.today == talk.talk.last.created_at.to_date
=> true

trueが帰ってきました

= talk.talk.last.created_at.strftime('%H:%M')

created_atの中から時間と分を引っ張って表示させます

次に、今日と一致しない場合

  • elsif 1.week.ago.beginning_of_day.to_date..Time.zone.now.end_of_day.to_date == talk.talk.last.created_at.to_date

結構長いです!
まず1.week.ago.beginning_of_day.to_date..Time.zone.now.end_of_day.to_dateの部分

1.week.agoで現在の時間から1週間前を呼ぶことができます

[7] pry(#<#<Class:0x00007f390891b390>>)> 1.week.ago
=> Sat, 24 Oct 2020 22:19:33 JST +09:00

さらにbegining_of_dayでその日の始まり(00:00)になります

[10] pry(#<#<Class:0x00007f390891b390>>)> 1.week.ago.beginning_of_day 
=> Sat, 24 Oct 2020 00:00:00 JST +09:00

逆にTime.zone.nowで現在の時刻、.end_of_dayで終わり(23:59:59)を呼びます

今日の日付は最初のifで判定されるのでジャスト1週間前までのメッセージを呼べます

上記2つを .. で繋ぐことで1週間前のスタートから今日の終わりまでを指定します

ただやはり時間まではあると判定できないので、.to_dateをつけて日付のみにします

そしてそれを曜日に

      - weeks = ["日","月","火","水","木","金","土"]
      - num = talk.talk.last.created_at.strftime("%w").to_i
      - week = weeks[num]
      = week + "曜日"

.strftime("%W")とすることで曜日を0~6の数字で取れます

[1] pry(#<#<Class:0x0000000002fd42e0>>)> talk.talk.last.created_at.strftime("%w")
=> "6"

つまり、weeksに一旦文字列として値を入れ、numには取得した値を.to_iで数字に

weekにweeks[num]で数字に対応した曜日を入れることで曜日を取得できます

最後の行は割とシンプルですね

= talk.talk.last.created_at.strftime("%Y.%M.%D")

これで判定を行うことができました

実際の画面はこんな感じで

f:id:hashibamiakira:20201031230036p:plain

なかなか大変!

次はJavaScriptも触っていきます

またボチボチ更新していきます

【Rails】groupメソッドとorderメソッドを一緒に使う

今回はSQLとのやりとりで沼った、groupとorderを一緒に使う時の注意をつらつらと書いていきます

現在の自身の環境です

sqlRailsのデフォルト

  • gem 'sqlite3'

あと、binding.pry大好き人間なので

  • gem 'pry-byebug', group: :development

を導入

やりたいこと

ユーザーモデルがあり、トークルームモデルがあります

中間テーブルとして、ルームユーザー(どのルームにどのユーザーがいるか)と、トーク(メッセージがどのユーザーのものか)があります

f:id:hashibamiakira:20201019122347p:plain

今回やりたいことは、ラインのトークを押すと、最近トークが追加されたトークルーム順で並べる、というものです

実際のラインの表示

f:id:hashibamiakira:20201031115525p:plain

ちょっとわかり辛いですが、もしみられる方は自身の物を参考にしてもらえればと思います...

始めに行ったこと

最初に、まずTalkの全てを作成順 (.order(created_at: :desc))で並び替え、重複する talk_room_id でまとめる、ということをシンプルに記述してみました

latest_talk = Talk.order(created_at: :desc).group(:talk_room_id)

f:id:hashibamiakira:20201031121105p:plain

一見取得できているように見えたのですが、実はgroupメソッドの方が先に働いてしまうようです

動きとしては、

グループで同じtalk_room_idでまとめる(この時点で一番古いidでまとまる)
 ⇩
まとめたレコードで、新しい順に並べる

なので、最初に送ったトークを基準にルームを並べるという動きになってしまいます

全く意味がなくなってしまうので色々調べてみました

サブクエリ(from)を使う

fromを使うと、その中の記述が先に動いてくれるようです

実際に書いてみます

Talk.from(Talk.order(created_at: :asc)).group(:talk_room_id)

f:id:hashibamiakira:20201031121834p:plain

#<Talk::ActiveRecord_Relation:0x2bff408> という結果が帰ってきました

こいつはなんぞや、という話ですが、他の方の記事を流用させてもらうと

  • データベースにクエリを発行する。
  • 様々なデータベースとの互換性がある。
  • データベースの種類にかかわらず同じ表記を使用できる。

ということらしいです

なるほど?

なので色々と追加してみます

    latest_talk = Talk.from(Talk.order(created_at: :asc))
                      .group(:talk_room_id)
                      .select(:talk_room_id)
                      .order(created_at: :desc)

f:id:hashibamiakira:20201031122339p:plain

トークを並び替えてルームidでまとめた後、ルームidを指定しorderで並び替えます

これで一旦は、自分が求めていた「トークが追加されたルーム順で並べる」ということができました

ただこの状態ではただidを引っ張っただけでどうにもならないので、eachで回し配列にデータを追加します

    # トークが新しい順にトークルームを並べる
    latest_talk = Talk.from(Talk.order(created_at: :asc))
                      .group(:talk_room_id)
                      .select(:talk_room_id)
                      .order(created_at: :desc)
    @latest_talks = Array.new
    latest_talk.each do |talk|
      room = TalkRoom.find_by(id: talk.talk_room_id)
      @latest_talks.push(room)
    end

変数に一旦新しく配列を持たせ、その中にidと一致するルームの情報を入れていきます

f:id:hashibamiakira:20201031122828p:plain

これでやっとview側でデータを使うことができます。やったね

もうちょっと簡潔にする方法があるとは思いますが、当初の目的は達成できました

まとめ

groupとorderを使うときのまとめです

  • groupとorderを使うと、groupの方が優先される
  • もし2つを使うのであれば、fromを使うことで解決出来る
  • 取得したデータはActiveRecord::Relationという形になる

こんな感じですかね〜

2日ほど沼って解決できたので忘備録意味合いも込めて残しておきます

またボチボチと更新していきます

【RailsでLINEを作る】トーク内容の画面表示速度改善

友だちをクリックすると内容一覧を表示させるのですが、速度が恐ろしくかかる問題が発生しました

結論からすると

  • N+1問題の改善
  • eachの中のrender

この2つの影響で読み込み速度がかなり遅くなっていました

今回修正した部分をつらつらと書いていきます

トーク内容の画面表示速度改善

まず、何もない状態での速度です

f:id:hashibamiakira:20201028162813p:plain

表示するまでに1134msとなっています

1秒でも、体感としては長く感じます

N+1を解決します

ルームに紐づくトークテーブルは現状全て取得できていたのですが、そのトークのユーザーを表示させるために毎回SQLにアクセスしていました

each文を変更します

_user-talk-index.html.slim

talks.includes(:user).each do |talk|

これでユーザーの情報も全て取得します

これでviewを読み込みます

f:id:hashibamiakira:20201028163257p:plain

533msと、約半分になりました

しかしトーク内容がどんどん増えると時間は伸びます

テストで150件ほどトークを送ってみました

f:id:hashibamiakira:20201028163354p:plain

1895msと、ほぼ2秒かかっています

ラインで友達からメッセージが来て開こうとするたびにこれだけ時間がかかるとユーザビリティが悪い...

部分テンプレートを呼びまくっているので、部分テンプレートをやめて直書きにします

= render partial: 'users/attachment', locals: { user: talk.user}
  .talk = talk.talk
  .time
    span = talk.created_at.strftime('%H:%M')

  .user-talk-index__talks--left
    - if user.profile_image.attached?
       = image_tag(user.profile_image)
    - else
       = image_tag('usagi.jpg')
    .talk = talk.talk
    .time
       span = talk.created_at.strftime('%H:%M')

これで読み込み

f:id:hashibamiakira:20201028164015p:plain

198ms!!

each文の中になるべくrenderは書かないようにしようという今日の教訓でした

またボチボチ更新していきます

【RailsでLINEを作る】トークの画面をいい感じにしていく

前回の更新から少し空きました...

というのも逆無限スクロールを実装しようとしてそれが終わったら更新しようと思ったのですが、想像以上にややこしかった

考え方はわかるんだけど、それをコードにする実力がまだない!悔しい!

今回はトークの画面をラインに近づけます

トークの画面をいい感じにしていく

ラインのトークは新しいものが一番下になり、上に増えていきます

そして自分のトークは右側、相手のトークは左側です

コードに起こしていきます

users/_user-talk-index.html.slim

.user-talk-index
  - unless user.blank?
    .user-talk-index__name
      p = "#{user.name}"
    .user-talk-index__talks
      div id="talk" class="talk-#{@user.id}" data-user_id="#{current_user.id}"
        - unless talks.blank?
          - talks.each do |talk|
            - if talk.user_id == current_user.id
              .user-talk-index__talks--right
                .time
                  span = talk.created_at.strftime('%H:%M')
                .talk = talk.talk
            - else
              .user-talk-index__talks--left
                = render partial: 'users/attachment', locals: { user: talk.user}
                .talk = talk.talk
                .time
                  span = talk.created_at.strftime('%H:%M')
          / = paginate talks
    .user-talk-index__form
      textarea[type="text"
                id="talk-form"
                data-behavior="room_speaker"
                data-user="#{current_user.id}"
                data-room="#{room.id}"
                placeholder="メッセージを入力"]
  - else
    .non-talk-case
      .non-talk-case__contents
        img src="https://img.icons8.com/fluent-systems-filled/96/000000/line-me.png"
        span トークを始めよう!
        a href="https://icons8.com/icon/i7393ie24LoV/line" Line icon by Icons8

controllerからuserの情報(user)が渡ってこなければ(誰もクリックされていない初期状態)初期画面を表示

f:id:hashibamiakira:20201027203905p:plain

友だちの部分はこんな感じ

  .talk-rooms__list--friends
    span.select-friends-list 友だち
    ul
      - current_user.followings.each do |follower|
        li class="follower"
          = link_to talk_rooms_path(follower: follower.id), class: "user-talk-show", remote: true do
            = render partial: 'users/attachment', locals: { user: follower}
            p = follower.name

link_toでindexを呼び、remote: trueでindex.js.erb呼び出し

params[:follower]にidを持たせます

talk_rooms/index.js.erb

$('#talks').html("<%= j(render partial: 'users/user-talk-index', locals: { user: @user, room: @room, talks: @talks}) %>");
$(document).ready(function(){
  let $talk = $('.user-talk-index__talks')
  // トーク内容の表示を一番下からに
  $talk.scrollTop(10000);
});

こはちょっと力技なんですが...部分テンプレートが呼ばれたらトーク一覧部分を一番下まで下げます (ホントは要素の高さを取って一番下を取得したい)

コントローラの中も少し変えました

talk_rooms_controller.rb

  def index
    unless params[:follower].blank?
      @user = User.find_by(id: params[:follower])
      @current_room = RoomUser.where(user_id: current_user.id)
      @another_room = RoomUser.where(user_id: @user.id)
      unless @user.id == current_user.id
        @current_room.each do |cr|
          @another_room.each do |ar|
            # ルームが存在する時
            if cr.talk_room_id == ar.talk_room_id
              @is_room = true
              @room = TalkRoom.find_by(id: cr.talk_room_id)
              @talks = @room.talk
            end
          end
        end
        # なければ新規作成
        unless @is_room
          @room = TalkRoom.create
          RoomUser.create(user_id: current_user.id, talk_room_id: @room.id)
          RoomUser.create(user_id: params[:follower].to_i, talk_room_id: @room.id)
        end
      end
    end
    @users = User.where.not(id: current_user.id)
  end

params[:follower]を判定

あれば中間モデルの判定

ルームがあればトークを表示

なければルームと中間モデルのルームユーザーを作成します

これで基本的なことはできたかと思います

次に、ActionCableに変更を加えてトークが送られた時の判定を作っていきます

まずはviewのフォーム

talk_rooms/_user-talk-index.html.slim

    .user-talk-index__form
      textarea[type="text"
                id="talk-form"
                data-behavior="room_speaker"
                data-user="#{current_user.id}"
                data-room="#{room.id}"
                placeholder="メッセージを入力"]

js側に自身のidとルームのidを送ります

textareaに入力された時の動きは以前作成したものと大差はありませんので割愛... (下で全部載せます)

送られたデータをチャンネルで処理します

channnels/talk_room_channnel.rb

  def speak(talk)
    # Talkモデル内、talkカラムに、(talk)に渡されたvalue['文字列']が保存される
    talk = Talk.new(talk: talk['talk'][0],
                    user_id: talk['talk'][1].to_i,
                    talk_room_id: talk['talk'][2].to_i)
    talk.save
    # 同じchannel名の全てにインプットタグに入力された文字を配信する
    ActionCable.server.broadcast "talk_room_channel_#{params['room_id']}", talk: talk['talk'], user_id: talk['user_id'],created_at: talk[:created_at].strftime("%H:%M")
  end

talkをセーブしたあと、データをjs側に返します

トーク内容、ユーザーid、作成日時です

talk[:created_at].strftime("%H:%M") これは何時何分に送ったかのデータです

js側で、viewに表示させるための処理を行います

javascripts/channels/talk_room.coffee

$ ->
  talkForm = $('#talk-form')

  App.talk_room = App.cable.subscriptions.create { channel: "TalkRoomChannel", room_id: talkForm.data('room')},

    # 通信が確立された時
    connected: ->

    # 通信が切断された時
    disconnected: ->

    # 値を受け取った時
    received: (data) ->
      # サーバーサイドから値を受け取りviewに追加する
      if data['user_id'] == $('#talk').data('user_id')
        $('#talk').append('<div class="user-talk-index__talks--right">'+
                            '<div class="time">'+'<span>'+data["created_at"]+'</span>'+'</div>'+
                            '<div class="talk">'+  data["talk"]+'</div>'+
                          '</div>')
      else
        $('#talk').append('<div class="user-talk-index__talks--left">'+
                            '<div class="talk">'+  data["talk"]+'</div>'+
                            '<div class="time">'+'<span>'+data["created_at"]+'</span>'+'</div>'+
                          '</div>')
      scroll = $('.user-talk-index__talks');
      scroll.scrollTop(10000);

    speak: (talk) ->
      # サーバーサイド(channel)のspeakアクションにtalkパラメータを渡す
      @perform 'speak', talk: talk

  $(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
    # return(Enter)が押された時
    if event.keyCode is 13
      # channel speakへ, event.target.valueを引数に
      App.talk_room.speak [event.target.value, $('[data-user]').attr('data-user'), $('[data-room]').attr('data-room')]
      event.target.value = ''
      event.preventDefault()

未熟の者で...appendで直書きする方法しか思いつきませんでした(汗)

view側には#="talk"の部分に自分のidをuser_idとしてあるので、ここでトークのidが自分と同じかどうか判定します

自身のjsに送られてきたトークのuser_idが自分と同じなら右に表示させるclass

異なるのであれば左に表示させるclassとします

そしてここも力技なんですが、高さを無理やり下にしています

実際の画面はこんな感じになります

チャットだけなら大分ラインっぽくなってきた!

ここから画像を添付したり動画を添付したり通話機能つけたりそれらを非同期で行い...ラインって無限に作り込めるな????

そろそろ就職しないと、と思う今日この頃です

またボチボチ更新していきます

【RailsでLINEを作る】インクリメンタルサーチ実装

前回のトークを非同期で書き換えする機能と同じタイミングでインクリメンタルサーチも実装したのでまた書いていきます

インクリメンタルサーチは、検索フォームに入力されたワードで瞬時に検索し表示させる...であってるのかな?そういう機能です

どんな感じになるかの動画

内容は

  • 一文字入力する毎に、検索結果を表示
  • 検索フォームが空なら友だち一覧を、文字があれば検索された友達を表示
  • 検索結果がなければ、その旨を表示
  • 検索された友だちをクリックでトーク画面を表示

これを作っていきます

流れとしては

検索フォームを実装 ↓ フォームにワードを入力でjsが動く ↓ 入力されたワードをajax通信でコントローラへ ↓ 受け取ったデータを元に友だちを検索 ↓ データをjsに戻し、そのデータを元にviewを書き換える

それでは作っていきます

参考にした記事

Rails + jQueryでインクリメンタルサーチ(基礎)

インクリメンタルサーチ

まずは検索フォーム。部分テンプレートになっています

talk_rooms/index.html.slim

      .talk-rooms.tab-content.is-show
        .talk-rooms__search
          = render partial: "layouts/search"
layouts/_search.heml.slim

i(name="search-icon" id="search-icon" class="fas fa-search")
=form_tag('/talk_rooms/search', method: :get) do
  =text_field_tag :name, :'', id: 'searching-form', placeholder: "名前で検索", autocomplate: 'off'
br

iの部分はフォントオーサムです

このフォームをターゲットにjsを動かしていきます

javascripts/talk_rooms.js

    // 名前で検索機能を作る
    $(document).ready(function(){
      const inputForm = $('#searching-form');
      const url = location.href;
      const searchResult = $('.result');

      function builtHTML(data){
        if(data.profile_image === null){
          let html = `<a class="user-talk-show" data-remote="true" href="/talk_rooms?follower=${data.id}">
                      <img src="${data.profile_image}">
                      <p>${data.name}</p>
                      `
          searchResult.append(html);
        }else{
          let html = `<a class="user-talk-show" data-remote="true" href="/talk_rooms?follower=${data.id}">
                      <img src="/assets/usagi.jpg">
                      <p>${data.name}</p>
                      `
          searchResult.append(html);
        }
      }
      function NoResult(message){
        let html = `<li>${message}</li>`
        searchResult.append(html);
      }
      // フォームに入力でイベント発火
      inputForm.on('keyup', function(e){
        e.preventDefault();
        let target = $(this).val();
        if(target !== ''){
          search(target);  // ajax通信はsearch()という関数に
          $('.talk-rooms-content').addClass("talk-rooms-content-close");
        }else{
          searchResult.empty();
          $('.talk-rooms-content').removeClass("talk-rooms-content-close");
        }
      });
      // ajax処理
      function search(target){
        $.ajax({
          type: 'GET',
          url: 'talkrooms/search',
          data: {keyword: target},
          dataType: 'json'
        })
        .done(function(data){
          searchResult.empty(); // 再度検索した際に前のデータを消す処理
          if (data.length !== 0){
            data.forEach(function(data){ // dataは配列型に格納されているのでeachぶんで回す
              builtHTML(data)
            });
          }else{
            NoResult('検索結果がありません')
          }
        })
        .fail(function(data){
          alert('通信に失敗しました')
        })
      }
    });

結構長くなってしまいました

検索フォームはこの部分

      // フォームに入力でイベント発火
      inputForm.on('keyup', function(e){
        e.preventDefault();
        let target = $(this).val();
        if(target !== ''){
          search(target);  // ajax通信はsearch()という関数に
          $('.talk-rooms-content').addClass("talk-rooms-content-close");
        }else{
          searchResult.empty();
          $('.talk-rooms-content').removeClass("talk-rooms-content-close");
        }
      });

キーが入力される度に発火させています

targetにはフォームの文字を代入

targetが空でなければ、後のsearch()を起動

かつ、友だち一覧の部分にクラスを付けて、cssで非表示にするようにしてあります

起動するのはこの部分

      // ajax処理
      function search(target){
        $.ajax({
          type: 'GET',
          url: 'talkrooms/search',
          data: {keyword: target},
          dataType: 'json'
        })

トークルームコントローラのサーチアクションにデータが渡ります

ルーティングを書いておきましょう

routes.rb

  #検索機能
  get 'talkrooms/search', to: 'talk_rooms#search'

ajaxの部分で、'GET'と 'talkrooms/search' があることでコントローラが動きます

次にサーチアクションを記述

talk_rooms_controller.rb

  def search
    @users = current_user.followers.where('name LIKE(?)', "%#{params[:keyword]}%")
    respond_to do |format|
      format.html #htmlがないとエラーが出る
      format.json
    end
  end

ajax部分のdata: {keyword: target}, がパラメータとして使えるようになっています

次に、views/talk_roomsにsearch.json.jbuilderというファイルを作ります

まだ理解が浅いですが、コントローラから渡された値をjson形式で出力するものらしいです

@usersに代入された友だちをjson形式に変えていきます

@usersには複数データが入るのでeachで回します

talk_rooms/search.json.jbuilder

json.array! @users do |user|
  json.user user
  json.name user.name
  json.id user.id
  json.profile_image user.profile_image
end

この左側がkey、右側がvalueになります

通信が行われた場合のajaxの処理がこの部分です

      function search(target){
        $.ajax({
          type: 'GET',
          url: 'talkrooms/search',
          data: {keyword: target},
          dataType: 'json'
        })
        .done(function(data){
          searchResult.empty(); // 再度検索した際に前のデータを消す処理
          if (data.length !== 0){
            data.forEach(function(data){ // dataは配列型に格納されているのでeachぶんで回す
              builtHTML(data)
            });
          }else{
            NoResult('検索結果がありません')
          }
        })
        .fail(function(data){
          alert('通信に失敗しました')
        })
      }

.doneは通信が成功した時、.failが失敗した時です

json形式にしたデータは(data)に入ります

そのdataを元にhtmlを作成し検索結果をjsで書き換えていきます

      function builtHTML(data){
        if(data.profile_image === null){
          let html = `<a class="user-talk-show" data-remote="true" href="/talk_rooms?follower=${data.id}">
                      <img src="${data.profile_image}">
                      <p>${data.name}</p>
                      `
          searchResult.append(html);
        }else{
          let html = `<a class="user-talk-show" data-remote="true" href="/talk_rooms?follower=${data.id}">
                      <img src="/assets/usagi.jpg">
                      <p>${data.name}</p>
                      `
          searchResult.append(html);
        }
      }
      function NoResult(message){
        let html = `<li>${message}</li>`
        searchResult.append(html);
      }

あんまり冗長なのでもっといい方法がないか模索中ですがとりあえず上記の形に

ユーザーのプロフィールイメージがある時とないときで表示の方法を変えています

書き換わるindexのviewはこんな感じで

talk_rooms/index.html.slim

    .talk-room-index__talk-rooms
      .talk-rooms.tab-content.is-show
        .talk-rooms__search
          = render partial: "layouts/search"
        .result
        .talk-rooms-content
          .talk-rooms__my-information
            .talk-rooms__my-information--image
              = render partial: "users/attachment", locals: { user: current_user}
            .talk-rooms__my-information--name
              p = current_user.name
          .talk-rooms__list
            .talk-rooms__list--groups
              span.select-group-list グループ
              ul
            .talk-rooms__list--friends
              span.select-friends-list 友だち
              ul
                - current_user.followings.each do |follower|
                  li class="follower"
                    = link_to talk_rooms_path(follower: follower.id), class: "user-talk-show", remote: true do
                      = render partial: 'users/attachment', locals: { user: follower}
                      p = follower.name

フォームに文字が入力された時点で .result にその内容を付け加え、.talk-rooms-content はjsとcssで非表示にします

stylesheets/talk_rooms.scss

.talk-rooms-content-close{
  display: none;
}

こんな感じで実装できました。

あとはCSSを触ったり、データの受け渡しをしてなんとかLINE風に左右でトークを表示したり、トークをしたから上に表示させたりとやっていこうと思います。

またボチボチ更新していきます。

【RailsでLINEを作る】クリックでトークを表示させる

今日は恐ろしくやる気が出ず、ブログの更新だけになります...

昨日、一昨日で友だちをクリックしたときにトーク内容をJSで更新させるようにしました。

CSSをあまりいじってないので雰囲気だけ...

流れとして

友だち一覧を取得しeachで表示 ↓ 友だち1つ1つにパラメータとしてIDを持たせる ↓ link_toを非同期対応させて、同じページを呼び出す ↓ パラメータを元にコントローラの動きを分ける ↓ 部分テンプレートにしてあるトークを書き換える

まずindexのviewに部分テンプレートを記述します

talk_rooms/index.html.slim

    .talk-room-index__talk
      .user-talks
        #talks.user-talks__talk
          = render partial: "users/user-talk-index", locals: { user: @user, room: @room, talks: @talks}

id="talks"をターゲットにするのでidはつけておきます

次に、each文で表示する友だちにパラメターを持たせます

talk_rooms/index.html.slim

 .talk-rooms__list--friends
     span.select-friends-list 友だち
        ul
           - current_user.followings.each do |follower|
             li class="follower"
               = link_to talk_rooms_path(follower: follower.id), class: "user-talk-show", remote: true do
                  = render partial: 'users/attachment', locals: { user: follower}
                  p = follower.name

link_toのパスの後に(follower: follower.id)と書くことで、リンク先にfollowerのidをparams[:follower]として渡すことができます

パスは同じviewなのでtalk_tooms_pathにしてあります

次にコントローラー

talk_rooms_controller.rb

  def index
    unless params[:follower].blank?
      @user = User.find_by(id: params[:follower])
      @current_room = RoomUser.where(user_id: current_user.id)
      @another_room = RoomUser.where(user_id: @user.id)
      unless @user.id == current_user.id
        @current_room.each do |cr|
          @another_room.each do |ar|
            # ルームが存在する時
            if cr.talk_room_id == ar.talk_room_id
              @is_room = true
              @room = TalkRoom.find_by(id: cr.talk_room_id)
              @talks = @room.talk
            end
          end
        end
        # なければ新規作成
        unless @is_room
          @room = TalkRoom.new
          @room_user = RoomUser.new
        end
      end
    end
    @users = User.where.not(id: current_user.id)
  end

普通にindexを表示した時点ではパラメータがないので@users = User.where.not(id: current_user.id)だけを渡します

友だちがクリックされた時に初めて unless params[:follower].blank?を実行し変数をviewに渡します

そして書き換えるjsファイルを作成

talk_rooms/index.js.erb

$('#talks').html("<%= j(render partial: 'users/user-talk-index', locals: { user: @user, room: @room, talks: @talks}) %>");

部分テンプレートの中身を作成

users/_user-talk-index.html.slim

.user-talk-index
  - unless user.blank?
    .user-talk-index__name
      p = "#{user.name}"
    .user-talk-index__talks
      #talk
        - talks.each do |talk|
          p = talk.talk
    .user-talk-index__form
      input[type="text"
            id="talk-form"
            data-behavior="room_speaker"
            data-user="#{current_user.id}"
            data-room="#{room.id}"
            placeholder="メッセージを入力"]
  - else
    .non-talk-case
      p トークをはじめよう!

-unless user.blank?でクリックされてパラメータが渡される前は、elseの部分を表示させるようにしてあります

今回かなり沼ったのは素直にlink_toにパラメータを持たせず、dataを使おうとして混乱してしまった所です

シンプルに部分テンプレートで書き換えるのが間違いなさそうです

次回はインクリメンタルサーチを実装した記事を書いていきます。

【RailsでLINEを作る】ActionCableで特定のユーザーとリアルタイムチャット機能を作る.その2

前回はアクションケーブルを使ったリアルタイムチャット機能を実装しました。

ただ、今の状態だと、送ったチャットが、同じチャンネルに接続されている全てのviewに配信されます。

ユーザーidやルームidは保存されるのでリロードを挟めば消えるのですが、かといってダダ漏れでは大変です。

こんな感じです。

これをちゃんと同じルームの相手にだけリアルタイムで配信するようにします。

前回のコードに追記していきます。

特定のユーザーとリアルタイムチャット機能

今回こちらのかたの記事を参考にさせて頂き実装しました。

Rails 5 Action Cable メッセージとルームを紐付ける。

チャットを送るinputタグにはユーザーidとトークルームidをつけてあります。

この部分ですね。

talk_rooms/show.html.slim

    input[type="text"
          data-behavior="room_speaker"
          data-user="#{current_user.id}"
          data-room="#{@room.id}"]

このinputにidを持たせます。

talk_rooms/show.html.slim

    input[type="text"
          id="talk-form"
          data-behavior="room_speaker"
          data-user="#{current_user.id}"
          data-room="#{@room.id}"]

この値はjs(coffee)に渡され、さらにチャンネルまで渡します。

javascript/channels/talk_room.coffee

$ ->
  talkForm = $('#talk-form')

  App.talk_room = App.cable.subscriptions.create { channel: "TalkRoomChannel", room_id: talkForm.data('room')},

    # 通信が確立された時
    connected: ->

    # 通信が切断された時
    disconnected: ->

    # 値を受け取った時
    received: (data) ->
      # サーバーサイドから値を受け取りviewに追加する
      $('#talk').append("<p>"+data["talk"]+"</p>");

    speak: (talk) ->
      # サーバーサイド(channel)のspeakアクションにtalkパラメータを渡す
      @perform 'speak', talk: talk

  $(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
    # return(Enter)が押された時
    if event.keyCode is 13
      # channel speakへ, event.target.valueを引数に
      App.talk_room.speak [event.target.value, $('[data-user]').attr('data-user'), $('[data-room]').attr('data-room')]
      event.target.value = ''
      event.preventDefault()

少し沼ったのですが、coffeeはインデントで閉じる判定をしている?のか、 App.talk_room = App... の部分をインデント空けずに記述したら変数を渡せていませんでした。インデント注意。

これでchannelにroom_idを流せるので、channelに記述していきます。

channels/talk_room_channnel.rb

class TalkRoomChannel < ApplicationCable::Channel

  # 接続された時
  def subscribed
    # フロントとバックが通信している時(お互いを監視している時)に実行される
    stream_from "talk_room_channel_#{params['room_id']}"
  end

  # 切断された時
  def unsubscribed
  end

  def speak(talk)
    # Talkモデル内、talkカラムに、(talk)に渡されたvalue['文字列']が保存される
    talk = Talk.new(talk: talk['talk'][0],
                    user_id: talk['talk'][1].to_i,
                    talk_room_id: talk['talk'][2].to_i)
    talk.save
    # 同じchannel名の全てにインプットタグに入力された文字を配信する
    ActionCable.server.broadcast "talk_room_channel_#{params['room_id']}", talk: talk['talk']
  end

end

talk_room_channelを動的に区別できるようになりました。

これでブラウザを立ち上げてチャットをしてみます。

ちょっと見づらいですが同じルームだけチャットが届くようになりました。

これで一旦action cableを使ったリアルタイムチャット機能が完成しました。

もっと色々と応用がありそうだ・・・jobっていうのはまだ分かっておらずです。

これをLINEの見た目にしていきたいけど今はちょっと頭がそこまで回りそうにないぞ...!

この記事が何かのお役に立てれば幸いです。

またボチボチ更新していきます。