地球の皆さんこんにちは。
今回は、この私が理解に苦しんだ、相互フォローしている人限定で、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
忘れずにやっときましょう。
アソシエーションを設定しよう!
モデルが作れたらアソシエーションです。
ちなみに業界の方はアソシエーションを設定することを「アソシえる」というらしいです。
それではアソシえっていきましょうか!!
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
class Chat < ApplicationRecord
belongs_to :user
belongs_to :room
validates :message, presence: true, length: { maximum: 140 }
end
ついでにバリデーションもつけたよ♥
class Room < ApplicationRecord
has_many :user_rooms
has_many :chats
end
class UserRoom < ApplicationRecord
belongs_to :user
belongs_to :room
end
ユーザーモデルはなんかガチャガチャかいてますが、アソシエーションに関して追記するのは三行だけです。
では、関係性を見てみましょう。
ユーザー 対 チャットルーム
・ユーザーは多くの部屋に所属できる。
・チャットルームは多くのユーザー(今回はDMなので二人ですが)が所属している。
これは多対多ですね。
先ほども申しましたが今回はUser_Roomテーブルを中間テーブルとしています。
UserRoomテーブルの情報を参照してデータのやり取りをしていると思います。
正直理解はしていません。こんな僕をどうか嫌いにならないでください。
ユーザー 対 チャット
・一人のユーザーはたくさんチャットを投稿できる。
・一つのチャットに対するのは投稿した一人のユーザーのみ。
これは多対一です。
思い出しますね。僕もあの時一人対大勢での大暴動がありましてね。このチャットもあの時の僕と同じ気持ちなんでしょうね。
ルーティングを設定しよう
昨日まで何もなかったのに、こんなところにルーティングができてるな。興奮してきたな。
resources :chats, only: [:show, :create]
ネストも何もさせていません。これでいいんです。そう....あれで....よかったんだ.............
コントローラーを作ろう
「コントローラー作るって言ったじゃん!嘘つき!」
おいおい。まってくれよアンジェリカ。これでいいんだろ?
rails g controller chats
皆さん念願のコントローラーを作りました。
でもこれで終わりじゃありませんよ。
コントローラーの中身を記述していきましょう!
いくぜ!俺について来い!!
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
はい。わけわからん。やめまーす。
なんて思っていませんか?
やる気のない奴は帰れーーーー!!!!!
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のところに書いてます。
コントローラーの各アクションを行う前に相互フォローかどうかを確認してます。えらい。
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をつくろう
コントローラー作るときに一緒に作ればよかったじゃんといわれると思います。
そんなあなた方に一言、言いたい。
すまん
<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としてあります。
うんこぶりぶり
$('.message').append("<p style='text-align: right;'><%= @chat.message %></p>");
$('input[type=text]').val("")
$('.errors').html("<%= j(render 'layouts/errors', obj: @chat) %>");
先ほどはしょったcreateアクションをあえてここで解説します。
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関係のファイルも情報添付します。
アプリケーション
// 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;
<% 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 %>
おわりに
おい!何見てんだよ!見せもんじゃねえぞ!
ということで自分のために書いていました。
僕は地球に来て間もないですが、いつか来る災厄のためにプログラミング頑張ります!
ファイヤー
はい。