7
9

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 1 year has passed since last update.

相互フォローしている人限定でDMを送れるようにする。

Last updated at Posted at 2022-02-10

地球の皆さんこんにちは。
今回は、この私が理解に苦しんだ、相互フォローしている人限定で、DMのやりとりをできる方法を解説します。
私は、これまでプログラミングというものをやってこなかった.....そう、やってこなかったのです。
この苦しみ....あなたにわかりますか?わからないというなら......!くらえ!!!!

それでは、さっそく始めていきましょう。

Rails 6.1.4
Ruby 2.6.3
Bootstrap, devise導入済み
Bookの投稿サイトを作っています

前提条件

Userモデル、コントローラ relationshipsモデル、コントローラー(フォロー機能実装のためのもの) 以上が作成済みであること。

ちなみに私の好きな食べ物はオムライスです。

モデルを作ろう

さあ!ここで問題です。今から何を作るでしょうか!?
チッチッチッ........
ヒントは好きな食べ物です!!!
さあ!答えをどうぞ!!!

そう!!モデルを作ります!!

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モデル

ユーザーが会話するチャットルームの番号(ID)を管理

Chatモデル

チャットの内容を管理(あとでコントローラーも作ります)

UserRoomモデル

その名の通りユーザーとルームを結ぶ中間テーブル。

中間テーブルについては、まあ....うん....他の記事見てください

rails db:migrate

忘れずにやっときましょう。

アソシエーションを設定しよう!

モデルが作れたらアソシエーションです。
ちなみに業界の方はアソシエーションを設定することを「アソシえる」というらしいです。

それではアソシえっていきましょうか!!

user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable
  
  has_many :user_rooms ここから!
  has_many :chats
  has_many :rooms, through: :user_rooms ここを追記!
  
  has_many :reverse_of_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy

  has_many :followers, through: :reverse_of_relationships, source: :follower
  
 
  has_many :relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy
  
  has_many :followings, through: :relationships, source: :followed
  
  has_one_attached :profile_image

  validates :name, length: { minimum: 2, maximum: 20 }, uniqueness: true
  validates :introduction, length: { maximum: 50 }
  
  
  def get_profile_image
    (profile_image.attached?) ? profile_image : 'no_image.jpg'
  end
  
  def follow(user)
    relationships.create(followed_id: user.id)
  end

  def unfollow(user)
    relationships.find_by(followed_id: user.id).destroy
  end

  def following?(user)
    followings.include?(user)
  end
end

chat.rb
class Chat < ApplicationRecord
  belongs_to :user
  belongs_to :room

  validates :message, presence: true, length: { maximum: 140 }
end

ついでにバリデーションもつけたよ♥

room.rb
class Room < ApplicationRecord
  has_many :user_rooms
  has_many :chats
end

user_room.rb
class UserRoom < ApplicationRecord
  belongs_to :user
  belongs_to :room
end

ユーザーモデルはなんかガチャガチャかいてますが、アソシエーションに関して追記するのは三行だけです。

では、関係性を見てみましょう。

ユーザー 対 チャットルーム
・ユーザーは多くの部屋に所属できる。
・チャットルームは多くのユーザー(今回はDMなので二人ですが)が所属している。

これは多対多ですね。
先ほども申しましたが今回はUser_Roomテーブルを中間テーブルとしています。
UserRoomテーブルの情報を参照してデータのやり取りをしていると思います。
正直理解はしていません。こんな僕をどうか嫌いにならないでください。

ユーザー 対 チャット
・一人のユーザーはたくさんチャットを投稿できる。
・一つのチャットに対するのは投稿した一人のユーザーのみ。

これは多対一です。
思い出しますね。僕もあの時一人対大勢での大暴動がありましてね。このチャットもあの時の僕と同じ気持ちなんでしょうね。

ルーティングを設定しよう

昨日まで何もなかったのに、こんなところにルーティングができてるな。興奮してきたな。

routes.rb
resources :chats, only: [:show, :create]

ネストも何もさせていません。これでいいんです。そう....あれで....よかったんだ.............

コントローラーを作ろう

「コントローラー作るって言ったじゃん!嘘つき!」

おいおい。まってくれよアンジェリカ。これでいいんだろ?

rails g controller chats

皆さん念願のコントローラーを作りました。
でもこれで終わりじゃありませんよ。

コントローラーの中身を記述していきましょう!
いくぜ!俺について来い!!

chats_controller.rb

class ChatsController < ApplicationController
  before_action :reject_non_related, only: [:show]
  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
    else
      @room = Room.new
      @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)
    render :validater unless @chat.save
  end

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

  def reject_non_related
    user = User.find(params[:id])
    unless current_user.following?(user) && user.following?(current_user)
      redirect_to books_path
    end
  end
end

はい。わけわからん。やめまーす。

なんて思っていませんか?

やる気のない奴は帰れーーーー!!!!!

chats_controller.rb
class ChatsController < ApplicationController
  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
end

unlessは~じゃなかった場合、trueを返して処理を行います。

この場合、現在のユーザー(わたし)が対象のユーザー(あなた)をフォローしていてかつ、対象のユーザー(あなた)が現在のユーザー(わたし)をフォローしていなかった場合、一覧画面へリダイレクトさせるという記述です。
これが言うならば相互フォローかどうかをみているわけでございますね。

これをbefore_actionのところに書いてます。
コントローラーの各アクションを行う前に相互フォローかどうかを確認してます。えらい。

chats_controller.rb

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

 
end

showアクションはこんな感じです。
create()はnewとsaveを同時にできるやつだと聞きました。

まあ、つまり

ユーザーIDという名の箱の中に現在のユーザーのID(もしくは相手の)を入れて

ルームIDという箱の中に二人の共通のルームIDをいれて保存。

これで新しいチャットルームが完成するんですね。

それでは大詰め。ビューいきましょう。

Viewをつくろう

コントローラー作るときに一緒に作ればよかったじゃんといわれると思います。
そんなあなた方に一言、言いたい。

すまん

view/chats/show.html.erb
<h1 id="room" data-room="<%= @room.id %>" data-user="<%= current_user.id %>">
<%= @user.name %> さんとのチャット</h1>

<div class="message" style="width: 400px;">
  <% @chats.each do |chat| %>
    <% if chat.user_id == current_user.id %>
      <p style="text-align: right;"><%= chat.message %></p>
    <% else %>
      <p style="text-align: left;"><%= chat.message %></p>
    <% end %>
  <% end %>
</div>

<div class="errors">
  <%= render "layouts/errors", obj: @chat %>
</div>

<%= form_with model: @chat, data: {remote: true} do |f| %>
  <%= f.text_field :message %>
  <%= f.hidden_field :room_id %>
<% end %>

あとからエラ-文も設定します。
そしてコントローラーの時点でお気づきかもしれませんが非同期通信なのでremote trueとしてあります。
うんこぶりぶり

view/chats/create.js.erb
$('.message').append("<p style='text-align: right;'><%= @chat.message %></p>");
$('input[type=text]').val("")
view/chats/validater.js.erb
$('.errors').html("<%= j(render 'layouts/errors', obj: @chat) %>");

先ほどはしょったcreateアクションをあえてここで解説します。

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

もし新規投稿が保存されなかった場合validater.js.erbを探します。
逆に保存された場合create.js.erbを探します。
これが非同期の仕様です。そうです。正直わかりません。

リンクはこうする。
<% if current_user != user && current_user.following?(user) && user.following?(current_user) %>
  <%= link_to 'chatを始める', chat_path(user.id), class: "ml-3" %>
<% end %>

もしログイン中のユーザーが対象のユーザーではなく、対象のユーザーをフォローしていてかつ、ぞのユーザーからフォローされていた場合リンクを出現させます。

非同期が分からないので、関係ありませんが、JS関係のファイルも情報添付します。

アプリケーション
application.js
// This file is automatically compiled by Webpack, along with any other files
// present in this directory. You're encouraged to place your actual application logic in
// a relevant structure within app/javascript and only use these pack files to reference
// that code so it'll be compiled.

import Rails from "@rails/ujs"
import Turbolinks from "turbolinks"
import * as ActiveStorage from "@rails/activestorage"
import "channels"
import jQuery from "jquery"
import "popper.js"
import "bootstrap"

import '@fortawesome/fontawesome-free/js/all';
import "../stylesheets/application" 

Rails.start()
Turbolinks.start()
ActiveStorage.start()

global.$ = jQuery;
window.$ = jQuery;
Gemfile、environmentなどは特に何もなし。
エラーの部分テンプレート
<% if obj.errors.any? %>
  <div id="error_explanation">
    <h3><%= pluralize(obj.errors.count, "error") %> prohibited this obj from being saved:</h3>
    <ul>
      <% obj.errors.full_messages.each do |message| %>
        <li><%= message %></li>
      <% end %>
    </ul>
  </div>
<% end %>

おわりに

おい!何見てんだよ!見せもんじゃねえぞ!
ということで自分のために書いていました。

僕は地球に来て間もないですが、いつか来る災厄のためにプログラミング頑張ります!

ファイヤー

はい。

7
9
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
7
9

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?