rails チャット通知

#[Rails]DM、チャット通知機能＋チャット一覧、チャット機能の実装


##目標
チャットの通知機能の実装に関する記事が少ないように感じたので投稿しました。
こんな感じでの画面です。チャットのページを開くと通知の青いマークが消えます。
チャット機能、チャットの一覧ページと通知の実装手順を紹介します。
（＊Deviseを用いたログイン機能を実装している前提でコードを書いています。）

<div>
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1287520/62a335af-93e1-0814-4386-ab267e3b9255.png" width="25%">　　
<img src="https://user-images.githubusercontent.com/79825084/125827778-46055e91-268a-4bba-8755-b91b6a1a079c.png" width="25%">　
</div>
##参考記事

チャット機能そのものについては、下記記事を参考にしました。
[【Ruby on Rails】DM、チャット機能](https://qiita.com/japwork/items/c2625e6f3c74b5260e60)

##モデル作成

```　:ターミナル
$ rails g model Room

$ rails g model Chat user:references room:references message:string

$ rails g model Room user:references room:references

```

```ruby:db/migrate/20210607031919_create_chats.rb
class CreateChats < ActiveRecord::Migration[5.2]
  def change
    create_table :chats do |t|
      t.references :user, foreign_key: true, null: false
      t.references :room, foreign_key: true, null: false
      t.string :message, null: false
      t.boolean :checked, default: false, null: false

      t.timestamps
    end
  end
end
```

```ruby:db/migrate/20210607031919_create_rooms.rb
class CreateUserRooms < ActiveRecord::Migration[5.2]
  def change
    create_table :user_rooms do |t|
      t.references :user, foreign_key: true, null: false
      t.references :room, foreign_key: true, null: false

      t.timestamps
    end
    add_index :user_rooms, [:user_id, :room_id], unique: true
  end
end
```
Roomのmigrationはそのままです。マイグレーションの内容は、本旨ではないので割愛しますが、外部キー制約等のDB単位での制限を設定しています。
上記変更後、ターミナルで、rails db:migrateを実行。

####アソシエーション(とバリデーション）
```ruby:models/user.rb
  has_many :user_rooms, dependent: :destroy
  has_many :chats, dependent: :destroy
```
```ruby:models/rooms.rb
  has_many :user_rooms, dependent: :destroy
  has_many :chats, dependent: :destroy
  has_many :users,  through: :user_rooms
```
```ruby:models/user_rooms.rb
  belongs_to :user
  belongs_to :room
  validates :user_id, presence: true
  validates :room_id, presence: true
  has_many :chats, through: :room
```
```ruby:models/chats.rb
  belongs_to :user
  belongs_to :room
  validates :user_id, presence: true
  validates :room_id, presence: true
  validates :message, presence: true, length: { maximum: 140 }
```
####ER図
![スクリーンショット 2021-07-16 4.44.04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1287520/7a893b3a-9c09-a8db-e3f8-09b0c8b1ecf6.png)


##　ルーティング
```ruby
  resources :rooms, only: %i[create index show]
  resource :chats, only: [:create]
```

## コントローラーの作成
```　:ターミナル
$ rails g controller rooms
$ rails g controller chats
```

```ruby:controllers/rooms_controller.rb
class RoomsController < ApplicationController
  before_action :authenticate_user!
  before_action :same_room_user!, only: [:show]

  # 取得している情報はUserRoomsだが、実質Roomのother_userとのリレーションを取得しているためRoomのindexに記述
  def index
  # ログインユーザーが持っているuser_roomのroom_idを取得。
    my_rooms_ids = current_user.user_rooms.select(:room_id)
  # ユーザールームから、room_idがmy_rooms_idsと一致し、user_idがcurrent_user_idではないものを取得。
    @user_rooms = UserRoom.includes(:chats, :user).where(room_id: my_rooms_ids)
                          .where.not(user_id: current_user.id).reverse_order
  end

  def create
    user_rooms = UserRoom.find_user_rooms(current_user, @user)
    if user_rooms.nil?
      room = Room.create
      UserRoom.create(user_id: current_user.id, room_id: room.id)
      UserRoom.create(user_id: @user.id, room_id: room.id)
    else
      room = user_rooms.room
    end
    redirect_to room_path(room.id)
  end

  def show
    room = Room.find(params[:id])
    room.check_chats_notification(current_user)
    user_id = room.user_rooms.where.not(user_id: current_user.id).select(:user_id)
    @user = User.where(id: user_id).first　　# グループチャットの実装を意識し、whereメソッドで取得。
    @chats = room.chats.includes(:user)
    @chat = Chat.new(room_id: room.id)
  end

  private

 # URLの直打ちによるアクセスを回避
  def same_room_user!
    return if Room.find(params[:id]).users.include?(current_user)

    flash[:danger] = 'ユーザーにはアクセスする権限がありません'
    redirect_to root_path
  end

end
```
```ruby:controllers/chats_controller.rb
class Public::ChatsController < ApplicationController
  before_action :authenticate_user!

  def create
    @chat = current_user.chats.new(chat_params)
    if @chat.save
      redirect_to request.referer
    else
      render 'shared/error'
    end
  end

  private

  def chat_params
    params.require(:chat).permit(:message, :room_id)
  end
end
```
エラー発生時のみ非同期通信にしています。

```:shared/error.js.erb
$("#flash_messages").html('<p class="alert alert-danger">１文字以上１４０文字以下で投稿してください</p>')
```

##モデルに記述するメソッド（下記をモデルに追記）
```ruby:models/user.rb
  # 未読の通知が存在するか確認(チャット)
  def unchecked_chats?
    my_rooms_ids = UserRoom.select(:room_id).where(user_id: id)
    other_user_ids = UserRoom.select(:user_id).where(room_id: my_rooms_ids).where.not(user_id: id)
    Chat.where(user_id: other_user_ids, room_id: my_rooms_ids).where.not(checked: true).any?
  end
```
```ruby:models/rooms.rb
  # チャット通知を既読にするためのメゾット
  def check_chats_notification(current_user)
    unchecked_chats = chats.includes(:user).where(checked: false).where.not(user_id: current_user.id)
 # &.を記述することで、uncheked_chatsがnilの場合エラーをNoMethodErrorを出さずにnilを返す。
    unchecked_chats&.each { |unchecked_chat| unchecked_chat.update(checked: true) }
  end
```
```ruby:models/user_rooms.rb
  # チャット相手とのルームを検索
  def self.find_user_rooms(current_user, other_user)
    rooms_ids = current_user.user_rooms.pluck(:room_id)
    UserRoom.find_by(user_id: other_user.id, room_id: rooms_ids)
  end

  # 個別のルームに未読の通知があるか確認（チャット）
  def massage_checked
    chats.where(user_id: user_id, checked: false).any?
  end
```
##viewの編集

```erb:users/show.html.erb
 # チャットルーム作成のリンク
<% if current_user != @user %>
  <% link_to 'chatを始める', rooms_path(user_id: @user), method: :post %>
<% end %>
```
＊user_showページを例にしているが、相手ユーザーのIDを取得できればどのページからでもroomの作成はできます。

```erb:footer.html.erb(もしくはheader.html.erb)
<%= link_to rooms_path, class: 'nav-link text-dark chat-notice' do %>
  <i class="far fa-envelope fa-lg.fa-stack-2x fa-backcolor" style="font-size: 1.5em;"></i>
 <% if current_user.unchecked_chats? %>
    <i class="fa fa-circle fa-xs n-circle-chat" style="color: blue;"></i>
  <% end %>
<% end %>
```
```erb:rooms/show.html.erb
<div class="partner text-center border-bottom border-secondary py-1">
  <%= link_to user_path(@user) do %>
    <%= attachment_image_tag @user, :image, :fill, 50, 50, format: 'jpg', fallback: "no_image.jpg", size:'40x40', class: "rounded-circle"%>
    <%= @user.name %>
  <% end %>
</div>
<div class="messaging">
  <div class="inbox_msg">
    <div class="mesgs  scroll">
      <div class="msg_history" id="scroll-inner">
          <% @chats.each do |chat| %>
            <% if chat.user == current_user %>
            <div class="outgoing_msg">
              <div class="sent_msg">
                <p><%= show_contents_with_uri(chat.message).html_safe %></p>
                <span class="time_date"><%= chat.created_at %></span></div>
            </div>
            <% else %>
          <div class="incoming_msg d-flex">
            <div class="incoming_msg_img">
              <%= attachment_image_tag @user, :image, :fill, 40, 40, format: 'jpg', fallback: "no_image.jpg", size:'40x40', class: "rounded-circle"%>
            </div>
            <div class="received_msg">
              <div class="received_withd_msg">
                <p><%= safe_join(chat.message.split("\n"),tag(:br)) %></p>
                <span class="time_date"><%= chat.created_at.strftime("%Y/%m/%d %H:%M") %></span></div>
            </div>
          </div>
            <% end %>
          <% end %>
      </div>
      <div class="type_msg">
          <%= form_with model: [@user, @chat] do |f| %>
            <%= f.text_area :message, class: "chat-form mt-3", :size=>"35x3" %>
            <%= f.hidden_field :room_id %>
            <div class="chatt-action">
              <%= f.submit "送信", class: "btn btn-dark-blues btn-rounded d-block ml-auto" %>
            </div>
          <% end %>
      </div>
    </div>
  </div>
</div>
```

＊画像の読み込みにrefileを使用しています。（チャットページは検索するとHTML形式ですが、テンプレがたくさん出てくるので好みのテンプレを参考にすると楽にレイアウトができると思います。

```erb:rooms/index.html.erb
<div class="row my-5">
  <% if @user_rooms.empty? %><p class="ml-4">メッセージはありません</p><% end %>
  <% @user_rooms.each do |room| %>
    <div class="col-8 mx-auto mb-3 border border-secondary rounded pt-2 px-2">
      <div class="user border-bottom border-secondary">
      <%= link_to user_path(room.user) do %>
        <%= attachment_image_tag room.user, :image, :fill, 35, 35, format: 'jpg', fallback: "no_image.jpg", size:'35x35', class: "rounded-circle" %>
        <span class="text-break"><%= room.user.name %></span>
      <% end %>
      </div>
      <%= link_to room_path(room.room_id), class: "text-decoration-none" do %>
      <div class=" chat-min-heiht">
        <% if room.chats != [] %>
          <p class="pt-3 ml-3 mb-0 text-dark text-break"><%= room.chats.last.message %></p>
          <div class="small text-muted text-right text-break">
            <span>
              <% if room.massage_checked %>
                <i class="fa fa-circle fa-x" style="color: blue;"></i>
              <% end %>
            </span>
            <%= (room.chats.last).created_at.strftime("%Y/%m/%d %H:%M") %>
          </div>
        <% end %>
      </div>
      <% end %>
    </div>
  <% end %>
</div>
```
```css
// ーーーーチャット通知マークーーーーー
.chat-notice {
  position: relative;
}
.n-circle-chat {
    position: absolute;
    top: 25px;
    left: 35px;
}


// ーーーーーーチャットルームーーーーーー
.scroll {
  overflow-y: scroll;
}
#scroll-inner{
  height: 350px;
}

.inbox_msg {
  border: 1px solid #c4c4c4;
  clear: both;
  overflow: hidden;
}

.chat_ib h5{ font-size:15px; color:#464646; margin:0 0 8px 0;}
.chat_ib h5 span{ font-size:13px; float:right;}
.chat_ib p{ font-size:14px; color:#989898; margin:auto}
.chat_img {
  float: left;
  width: 11%;
}
.chat_ib {
  float: left;
  padding: 0 0 0 15px;
  width: 88%;
}

.incoming_msg_img {
  display: inline-block;
  // width: 6%;
}
.received_msg {
  margin-top: 15px;
  display: inline-block;
  vertical-align: top;
  width: 92%;
}
 .received_withd_msg p {
  background: #ebebeb none repeat scroll 0 0;
  border-radius: 3px;
  color: #646464;
  font-size: 14px;
  margin: 0;
  padding: 5px 10px 5px 12px;
  width: 100%;
  word-wrap: break-word;
}
.time_date {
  color: #747474;
  display: block;
  font-size: 12px;
  margin: 8px 0 0;
}
.received_withd_msg { width: 57%;}
.mesgs {
  min-height: 350px;
  width: 100%;
  float: left;
  padding: 0 15px;
}

 .sent_msg p {
  background: #82bccc none repeat scroll 0 0;
  border-radius: 3px;
  font-size: 14px;
  margin: 0; color:#fff;
  padding: 5px 10px 5px 12px;
  width:100%;
  word-wrap: break-word;
}
.outgoing_msg{ overflow:hidden; margin-top :15px;}
.sent_msg {
  float: right;
  width: 46%;
}
.input_msg_write input {
  background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
  border: medium none;
  color: #4c4c4c;
  font-size: 15px;
  min-height: 48px;
  width: 100%;
}

.type_msg {border-top: 1px solid #c4c4c4;position: relative;}
.msg_send_btn {
  background: #05728f none repeat scroll 0 0;
  border: medium none;
  border-radius: 50%;
  color: #fff;
  cursor: pointer;
  font-size: 17px;
  height: 33px;
  position: absolute;
  right: 0;
  top: 11px;
  width: 33px;
}

.msg_history {
  height: 510px;
  overflow-y: auto;
}

.chat-form {
  display: block;
  padding: 0.375rem 0.75rem;
  font-size: 1rem;
  font-weight: 400;
  line-height: 1.5;
  color: #495057;
  background-color: #fff;
  background-clip: padding-box;
  border: 1px solid #ced4da;
  border-radius: 0.25rem;
  transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;
}
.chat-btn {
  margin-left: auto;
}

.chat-form:focus {
  color: #495057;
  background-color: #fff;
  border-color: #80bdff;
  outline: 0;
  box-shadow: 0 0 0 0.1rem rgba(0,0,0,0.2);
}

.chat-min-heiht {
  min-height: 30px;
}

.btn-rounded {
    border-radius: 35px;
}
.btn-dark-blues {
    width: 100px;
    background: #80b6cc;
    color: #fff;
    border: 3px solid #eee;
}
```

## 最後に
プログラミングの学習を始めて、３ヶ月程度で本記事を記述しています。
知識が浅く、間違っている部分もあるかと思いますが、不適切な記述があればご教授いただけると幸いです。
下記ポートフォリオで本記事で紹介したチャット機能を実装していますので、お時間あればご覧頂けると幸いです。

https://xn--n9jfc1f0c1grbyi7gxp.com/