【Ruby on rails6】SNSのような機能を作ってみる

【Ruby on rails6】SNSのような機能を作ってみる

2023年3月15日

はじめに

今回は既に作ってある簡単なアプリケーションに対してSNSのような機能を追加していこうと思います。
具体的には投稿一覧ページで過去一週間にいいね数が多い順に表示、相互フォロー同士でのDM機能、ページの閲覧数のカウントをする機能を作成していきます。

1.過去一週間にいいね数が多い順に表示

①コントローラへの記述

def index
  to  = Time.current.at_end_of_day
  from  = (to - 6.day).at_beginning_of_day
  @books = Book.includes(:favorites).sort_by {|x| x.favorites.where(created_at: from...to).size}.reverse
  @book = Book.new
end

ハイ。この記述をするだけです。toで今日の日付を獲得し、fromで今日から6日間さかのぼり合計7日間の範囲を作っています。あとはこれでsortをかけてあげれば並び替えが完了します。

2.DM機能

①モデルの作成

rails g model Chat user_id:integer room_id:integer message:text
rails g model Room 
rails g model UserRoom user_id:integer room_id:integer

RoomモデルはDMをするための部屋番号を管理し、ChatモデルはChatの内容を管理します。そしてUserRoomモデルはUserとRoomを結ぶ中間テーブルとなっています。

モデルを作成出来たらいつも通りmigrateしましょう。

②アソシエーションの設定

まずはuserから。

has_many :user_rooms
has_many :chats
has_many :rooms, through: :user_rooms

次にRoom

class Room < ApplicationRecord
  has_many :user_rooms
  has_many :chats
end

次はuserroom

class UserRoom < ApplicationRecord
  belongs_to :user
  belongs_to :room
end

最後にchat

class Chat < ApplicationRecord
  belongs_to :user
  belongs_to :room

  validates :message, length: { in: 1..140 }
end

今回は1~140文字内でないといけないというバリデーションをつけてみました。

ユーザーは多くの部屋に参加することができ、ルームは多くのユーザーを持つことができるため、多対多になります。ユーザーとチャットの関係性はユーザーは多くのチャットを投稿することができますが、一つのチャットにかかわるユーザーは一人なので多対一になります。

③ルーティングの設定

resources :chats, only: [:show, :create]

④コントローラの作成

rails g controller chats
class ChatsController < ApplicationController
  def show
    @user = User.find(params[:id]) #チャットする相手は誰?
    rooms = current_user.user_rooms.pluck(:room_id) #ログイン中のユーザーの部屋情報を全て取得
    user_rooms = UserRoom.find_by(user_id: @user.id, room_id: rooms)#その中にチャットする相手とのルームがあるか確認

    unless user_rooms.nil?#ユーザールームがある場合
      @room = user_rooms.room#変数@roomにユーザー(自分と相手)と紐づいているroomを代入
    else#ユーザールームが無かった場合
      @room = Room.new#新しくRoomを作る
      @room.save#そして保存
      UserRoom.create(user_id: current_user.id, room_id: @room.id)#自分の中間テーブルを作成
      UserRoom.create(user_id: @user.id, room_id: @room.id)#相手の中間テーブルを作成
    end
    @chats = @room.chats#チャットの一覧
    @chat = Chat.new(room_id: @room.id)#チャットの投稿
  end

  def create
    @chat = current_user.chats.new(chat_params)
    @room = @chat.room
    @chats = @room.chats
    render :validater unless @chat.save
  end

  private
  def chat_params
    params.require(:chat).permit(:message, :room_id)
  end
end

このように作ります。

⑤viewの作成

今回は非同期なのでチャットの部分テンプレートを作ります。

<div class="chat-messages">
  <% chats.each do |chat| %>
    <% if chat.user_id == current_user.id %>
      <div class="message self">
        <div class="message-body">
          <%= chat.message %>
        </div>
      </div>
    <% else %>
      <div class="message other">
        <div class="message-sender"><%= chat.user.name %></div>
        <div class="message-body">
          <%= chat.message %>
        </div>
      </div>
    <% end %>
  <% end %>
</div>
<div class="chat-container">
  <div class="chat-topbar">
    <h2><%= @user.name %> さんとのチャット</h2>
  </div>
  <div class="message">
    <%= render "chats/messages", chats: @chats %>
  </div>

  <div class="chat-form">
    <%= form_with model: @chat, data: {remote: true} do |f| %>
      <div class="errors">
        <%= render "layouts/errors", obj: f.object %>
      </div>
      <%= f.text_field :message, placeholder: "メッセージを入力してください", autocomplete: "off" %>
      <%= f.hidden_field :room_id, value: @room.id %>
      <%= f.submit "送信" %>
    <% end %>
  </div>
</div>
$('.message').html("<%= j(render 'chats/messages', chats: @chats) %>");
$('input[type=text]').val("")

次にバリデーション用のjsファイルも作ります。

$('.errors').html("<%= j(render 'layouts/errors', obj: @chat) %>");

今回の非同期ではバリデーションに引っかからず、投稿が成功した場合はcreate.jsを探しに行き、成功しなかった場合にはvalidator.jsを探しに行くような仕様となっています。バリデーションのjsファイルではエラー文のテンプレートファイルを呼び出すように設定しています。

<% if current_user != user && current_user.following?(user) && user.following?(current_user) %>
  <%= link_to 'チャットを始める', chat_path(user.id), class: "ml-3" %>
<% end %>

今回は部分テンプレートにDMへのリンクを置いています。

⑥相互フォローしていないとDMできないようにする

linkは相互フォローの時しか出ませんがURLに打ち込むと別のDMに行けてしまうので、それを対策します。

コントローラに以下を追記します。

before_action :reject_non_related, only: [:show]
・
・
・
private
def reject_non_related
    user = User.find(params[:id])
    unless current_user.following?(user) && user.following?(current_user)
      redirect_to books_path
    end
  end

これでurlでの移動もできなくなりました。

⑦レイアウトを整える

せっかくなので、チャットっぽいレイアウトにしてみましょう

/*
 * This is a manifest file that'll be compiled into application.css, which will include all the files
 * listed below.
 *
 * Any CSS and SCSS file within this directory, lib/assets/stylesheets, or any plugin's
 * vendor/assets/stylesheets directory can be referenced here using a relative path.
 *
 * You're free to add application-wide styles to this file and they'll appear at the bottom of the
 * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
 * files in this directory. Styles in this file should be added after the last require_* statement.
 * It is generally better to create a new file per style scope.
 *
 *= require_tree .
 *= require_self
 */
 .chat-container {
  display: flex;
  flex-direction: column;
  height: 100%;
  max-width: 800px;
  margin: 0 auto;
}

.chat-topbar {
  display: flex;
  align-items: center;
  justify-content: center;
  background-color: #29b6f6;
  color: #fff;
  height: 50px;
  padding: 0 20px;
}

.chat-messages {
  flex: 1;
  overflow-y: scroll;
  padding: 20px;
}

.message {
  display: flex;
  flex-direction: column;
  margin-bottom: 10px;
}

.message-sender {
  margin-bottom: 5px;
  font-size: 0.8em;
  color: #666;
}

.message-body {
  padding: 10px;
  border-radius: 10px;
  max-width: 70%;
}

.self .message-body {
  align-self: flex-end;
  background-color: #29b6f6;
  color: #fff;
}

.other .message-body {
  align-self: flex-start;
  background-color: #f5f5f5;
  color: #333;
}

.chat-form {
  background-color: #f5f5f5;
  padding: 20px;
}

.chat-form form {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
}

.chat-form input[type="text"] {
  flex: 1;
  margin-right: 10px;
  padding: 10px;
  font-size: 1em;
  border: none;
  border-radius: 5px;
  background-color: #fff;
}

.chat-form input[type="submit"] {
  flex: 0 0 auto;
  padding: 10px;
  font-size: 1em;
  border: none;
  border-radius: 5px;
  background-color: #29b6f6;
  color: #fff;
  cursor: pointer;
}

このcssを適用したチャットの見た目はこんな感じになります。

はい。長かったですがDM機能はこれで完了です。

3.ページの閲覧数をカウント

①モデルの作成

rails g model ViewCount book_id:integer user_id:integer

閲覧数を数えるためのモデルを用意します。このコマンドを打ったらいつも通りmigrate!

②アソシエーション

belongs_to :user
belongs_to :book
has_many :view_counts, dependent: :destroy
has_many :view_counts, dependent: :destroy

③コントローラへの記述

閲覧数を表示したい場所のコントローラに記述を加えます。

def show
    @book = Book.find(params[:id])
    unless ViewCount.find_by(user_id: current_user.id, book_id: @book.id)
      current_user.view_counts.create(book_id: @book.id)
    end
    @user = @book.user
    @new_book = Book.new
    @book_comment = BookComment.new
  end

今回は閲覧数のカウントは一人一回のみにしてあります。

④viewへの記述

あとは閲覧数を出す記述を任意のviewに書くだけで完成です

<td>閲覧数: <%= @book.view_counts.count %></td>

おわり

今回はここまで!いやーすごく長い道のりでしたね。特にレイアウトで試行錯誤しましたw
レイアウトにこだわるといくら時間があっても足りませんね。ではまた別の記事で!