railsでブロック機能実装してみた

# 概要
ブロック機能を実装しようとしたところ、記事としてまとめられているものがなかったのでアドベントカレンダーの企画に便乗して記事の執筆に挑戦しました！！

初めて記事を書くので拙いコード、文章になってしまっていたらごめんなさい
この記事を参考にしてブロック機能を実装して頂けたらうれしいです
改善点などあればぜひコメントお願いします

## 具体的にできること
ユーザーをブロックした場合そのユーザーから
- マイページを見られない
- フォローされない
- DMが送られてこない
- 投稿一覧、いいねした投稿一覧を見られない

マイページのみにフォローボタン、DMのリンク、投稿一覧、いいねした投稿一覧を置く
➡ブロックされている場合は通常のマイページを見られなくする
➡不審なユーザーからアクセスされないっていう感じです

# 完成図
![Geektwitter12 - Google Chrome 2023-12-10 16-38-32.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634732/16081a89-862c-9062-18a7-3db726c50ae1.gif)

そのユーザーにブロックされていた場合の表示

![スクリーンショット (443).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3634732/75c1235a-6e86-d338-438e-8188941fc724.png)



# 前提
- deviseでユーザーモデル作成、マイページ実装済み
- フォロー、DMも実装すればブロック機能に対応できます
筆者が参考にさせていただいた記事を以下に置いときます
[railsでフォロー機能をつける。 #Ruby - Qiit](https://qiita.com/kitaokeita/items/59b625e0c43a62f5fe6a)
[railsでDM機能を作成する](https://zenn.dev/kazu1/articles/ca4e2286ec821d)


# 実装スタート！！
### ①モデル
基本的にフォロー機能と同じように進めていきます

Blockモデル作成
```shell:コマンドプロンプト
$ rails generate model Block
```

マイグレーションファイルを編集
```Ruby:db/migrate/xxxxxxxxxxxxxx_create_blocks
classCreateBlocks < ActiveRecord::Migration[6.1]
  def change
    create_table :blocks do |t|
      t.integer :blocker_id　# 追加
      t.integer :blocking_id　#　追加
      t.timestamps
    end

    add_index :blocks, :blocker_id　# 追加
    add_index :blocks, :blocking_id　# 追加
    add_index :blocks, [:blocker_id, :blocking_id], unique: true　# 追加
  end
end
```

```shell:コマンドプロンプト
$ rails db:migrate
```


Blockモデルに追記
```Ruby:app/models/block.rb
class Block < ApplicationRecord
    validates :blocker_id, presence: true
    validates :blocking_id, presence: true
    belongs_to :blocker, class_name: 'User'
    belongs_to :blocking, class_name: 'User'
    validate :self_block
    
    private
    # 自分自身をブロックしていないか検証する
    def self_block
        return unless blocker_id && blocking_id
        errors.add(:blocking_id, '自分自身をブロックすることはできません') if blocker_id == blocking_id
    end
end
```
<br>

Userモデルはブロック機能で新しく追加するやつだけ書いてます
```Ruby:app/models/user.rb
  has_many :active_blocks, class_name:  "Block",
                                  foreign_key: "blocker_id",
                                  dependent:   :destroy
  has_many :blockings, through: :active_blocks, source: :blocking
  has_many :passive_blocks, class_name:  "Block",
                                   foreign_key: "blocking_id",
                                   dependent:   :destroy
  has_many :blockers, through: :passive_blocks, source: :blocker


  def block!(other_user)
    active_blocks.create!(blocking_id: other_user.id)
  end


  def unblock!(other_user)
    active_blocks.find_by(blocking_id: other_user.id).destroy
  end


  def blocking?(other_user)
    active_blocks.find_by(blocking_id: other_user.id)
  end


  def blocked?(other_user)
    passive_blocks.find_by(blocker_id: other_user.id)
  end
```




### ②コントローラー
blocksコントローラー作成
```shell:コマンドプロンプト
$ rails g controller blocks
```
```Ruby:app/controllers/blocks_controller.rb
class BlocksController < ApplicationController
  before_action :authenticate_user!
    def create
        @user = User.find(params[:block][:blocking_id])
        current_user.block!(@user)
   
        @user.unfollow!(current_user) if @user.following?(current_user)
        current_user.unfollow!(@user) if current_user.following?(@user)


       
   
        redirect_to @user, notice: "ユーザーをブロックしました。"
      end
   
      def destroy
        user = Block.find(params[:id]).blocking
        current_user.unblock!(user)
        redirect_to user, notice: "ユーザーのブロックを解除しました。"
      end
end
```


<br>

usersコントローラーは追加するやつだけ書いてます
```Ruby:app/controllers/users_controller.rb
def blocking
    @title = 'blocking'
    @user = User.find(params[:id])
    @users = @user.blockings
    render 'show_blocking'
end
```

      
### ③ルーティング
これも追加するやつだけ書いてます
```Ruby:config/routes.rb
  resources :users do
    member do
      get :following, :followers
      get :blocking, :blockers
    end
  end  
  resources :blocks, only: [:create, :destroy], controller: 'blocks'
```


### ④helper
```Ruby:app/helpers/users_helper.rb
def current_user?(user)
    user == current_user
end
```


### ⑤view
マイページは最初のif文とそれに対応するelseとendさえあればこの記事でやりたいことは実装できるので自由にカスタマイズしてみてください
```erb:app/view/users/show.html.erb
<% if current_user.blocked?(@user) %>
    <h3>あなたはこのユーザーにブロックされています</h3>
    <%= link_to "Tweet一覧に戻る", tweets_path %>
    <%= link_to "ユーザー一覧に戻る", users_path %>
<% else %>
    <h1>マイページ</h1>
    <p>名前 : <%= @user.name %></p>
    <p>メールアドレス : <%= @user.email %></p>
    <p>プロフィール : <%= @user.profile %></p>


    <% if user_signed_in? %>
        <%= render 'stats' %>
        <%= render 'follow_form' %> # フォロー機能を実装する場合は必要
        <%= render  'block_form' %>
    <% end %>
    <% if current_user.id == @user.id %>
        <%= link_to "編集する", edit_user_registration_path %>
    <% else %> # 以下DM機能を実装する場合は必要
        <% if @isRoom == true %>
            <p><%= link_to 'DMへ', room_path(@roomId) %></p>
        <% else %>
            <%= form_for @room do |f| %>
                <%= fields_for @entry do |e|%>
                    <% e.hidden_field :user_id, value: @user.id %>
                <% end %>
            <%= f.submit "DMを開始する"%>
            <% end %>
        <% end %> # DM機能ここまで
    <% end %>


    <h2>ユーザーの投稿一覧</h2>
    <% @user.tweets.each do |t| %>
        <%= t.user.name %>
        <%= t.body %>
    <% end %>
    <br>
    <%= link_to "Tweet一覧に戻る", tweets_path %>
    <%= link_to "ユーザー一覧に戻る", users_path %>
<% end %>
```


<br>

このページは何人ブロックしてるかっていう情報を表示するファイルです
フォローとフォロワーの記述はフォロー機能つけてない場合は必要ないです
```erb:app/view/users/_stats.html.erb
<% @user ||= current_user %>
<div class="stats">
    <a href="<%= following_user_path(@user) %>">
        フォロー
        <strong id="following" class="stat">
        <%= @user.followings.count %>
        </strong>
    </a>
    <a href="<%= followers_user_path(@user) %>">
        フォロワー
        <strong id="followers" class="stat">
        <%= @user.followers.count %>
        </strong>
    </a>
    <% if @user == current_user %>
        <p>
            <a href="<%= blocking_user_path(@user) %>">
                <strong id="blocking" class="stat">
                <%= @user.blockings.count %>
                </strong>
                人ブロック中
            </a>
        </p>
    <% end %>
</div>
```




<br>

自分が誰をブロックしてるか表示するページです
```erb:app/view/users/show_blocking.html.erb
<h1>ブロックしているユーザー</h1>
<% if @user.blockings.any? %>
  <ol>
    <% @user.blockings.each do |user| %>
      <li>
        <%= "#{user.name} - " %>
        <%= link_to '詳細', user_path(user) %>
      </li>
    <% end %>
  </ol>
<% else %>
  <p>ブロックしているユーザーはいません。</p>
<% end %>
```


<br>

今見てるユーザーをブロックしてるかどうかで表示するボタンを変えるためのファイルです
```erb:app/view/users/_block_form.html.erb
<% unless current_user?(@user) %>
<div id="block_form">
    <% if current_user.blocking?(@user) %>
        <%= render "unblock" %>
    <% else %>
        <%= render "block" %>
    <% end %>
</div>
<% end %>
```
<br>

ブロックボタンを表示するファイル
```erb:app/view/users/_block.html.erb
<%= form_for(current_user.active_blocks.build(blocking_id: @user.id)) do |f| %>
    <div><%= f.hidden_field :blocking_id %></div>
    <%= f.submit "Block" %>
<% end %>
```


<br>

ブロック解除ボタンを表示するファイル
```erb:app/view/users/_unblock.html.erb
<% block = current_user.active_blocks.find_by(blocking_id: @user.id) %>
<% if block %>
  <%= form_for(block, html: { method: :delete }) do |f| %>
    <%= f.submit "Unblock" %>
  <% end %>
<% end %>
```



# 参考記事
[服選びの時間短縮‼︎ スピードウェア #Ruby - Qiita](https://qiita.com/House-lovers7/items/c74c584d9c9dcfba18a8#8-%E3%83%96%E3%83%AD%E3%83%83%E3%82%AF%E6%A9%9F%E8%83%BD)
[railsでフォロー機能をつける。 #Ruby - Qiit](https://qiita.com/kitaokeita/items/59b625e0c43a62f5fe6a)
[railsでDM機能を作成する](https://zenn.dev/kazu1/articles/ca4e2286ec821d)


# まとめ
この記事を参考にしていただける方が少しでもいらっしゃったら幸いです。
ご覧いただきありがとうございました。