LaravelとVue.jsでコメント機能に@機能をつけてみた

2021.01.18

見出し画像

今回作成したのは、コメント機能のちょっと進化版みたいに思ってますw

インスタとかでコメントに対して返信出来たりっていう特定のユーザーに対してコメント出来たりするやつです。

以前にコメント機能については実装しているので参考にしてください。

備忘録として書いています。

                       ユーザークリックで指定出来る

                                        ※あばれる君がコメントしてます

画像1

              複数にも対応

           ※サンシャイン池崎君がコメントしてます

画像2

            ユーザー再クリックで指定解除

画像3

実装すること

コメント機能で特定のユーザーに対して返信出来る。

複数のユーザーを指定出来る。

ユーザーを再度クリックすれば指定解除出来る。

指定されて返信されたユーザーには通知が行く。

前提

アイテムの詳細ページでコメント機能が実装されている事。

通知機能が実装されている事。通知はしますが、通知の表示については今回省略しているので表示も実装したい方はこちらを参考にしてください。

参考記事

大まかな流れ

マイグレーションファイル書き換え

モデル設定

通知ファイル作成

コンポーネント書き換え

コントローラー書き換え

って感じで進めていきます。

マイグレーションファイル書き換え

特定されたユーザーのidを格納するカラムを作成します。ここに配列の形で紐付くユーザーのidを保存していく形になります。

text型にしてるのは配列の形で数字を保存するためです。[“1”, “4”]みたいな感じです。

public function up()
{
   Schema::create('comments', function (Blueprint $table) {
       $table->id();
       $table->string('text');
       $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
       $table->foreignId('item_id')->constrained('items')->onDelete('cascade');
 追加   $table->text('reply_user_ids')->nullable();
       $table->timestamps();
   });
}

上記の変更を加えたら下記コマンドで反映させます。

php artisan migrate
上コマンドでエラーが出たら
php artisan migrate:fresh    (リセットコマンドです)

モデル設定

先ほど追加したカラムを$fillableに追加していきます。

protected $fillable = ['text', 'user_id', 'item_id', 'reply_user_ids'];
                                   追加↑

通知ファイル作成

通知ファイルをコマンドで作成していきます。公式

php artisan make:notifications ReplyCommented

ReplyCommentedの部分はファイル名ですね。

通知にはコメントしたユーザーのid,nameと指定されたユーザーのid,nameの情報を入れます。

<?php

namespace App\Notifications;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
use App\User;
use App\Item;

class ReplyCommented extends Notification
{
   use Queueable;


   /**
    * Create a new notification instance.
    *
    * @return void
    */
   public function __construct($user, $select_user, $item)
   {
1      $this->user = $user;
       $this->select_user = $select_user;
       $this->item = $item;
   }

   /**
    * Get the notification's delivery channels.
    *
    * @param  mixed  $notifiable
    * @return array
    */
   public function via($notifiable)
   {
       return ['database'];
   }

   /**
    * Get the mail representation of the notification.
    *
    * @param  mixed  $notifiable
    * @return \Illuminate\Notifications\Messages\MailMessage
    */
   public function toDatabase($notifiable)
   {
       return [
 2         'commented_id' => $this->user->id,
           'commented_name' => $this->user->name,
 3         'select_user_id' => $this->select_user->id,
           'select_user_name' => $this->select_user->name,
 4         'item_id' => $this->item->id,
 5         'status' => false,
       ];
   }

   /**
    * Get the array representation of the notification.
    *
    * @param  mixed  $notifiable
    * @return array
    */
   public function toArray($notifiable)
   {
       return [
           //
       ];
   }
}

1,  この処理をする上で必要な情報を定義しています。ユーザーだったり指定されたユーザー、itemなどですね。

2,  コメントをしたユーザーのidと情報を定義しています。

3,  指定されたユーザーのidとnameを定義しています。

4,  どの商品に対してのコメントなのかを知りたいので、item_idも定義します。

5, statusは未読、既読状態を判定するために使います。unreadNotificationsで実装する方法もあります。公式

Bladeファイル

ログイン状態によってコメント出来る部分の表示を変えています。コメント機能実装についてはこちらの記事で紹介しています。

@if(Auth::check())
 <comment-list :item_id="{{$item->id}}" :current_user_id="{{Auth::id()}}" :item_user_id="{{$item->user_id}}" class="mt-5"></comment-list>
 <comment :item_id="{{$item->id}}" class="mt-5"></comment>
@else
 <comment-list :item_id="{{$item->id}}" :item_user_id="{{$item->user_id}}" class="mt-5"></comment-list>
@endif

Vuex側の記載

モジュール別に分けてCommentを読み込んでいます。今回の実装に関係な部分は省略します。

import Vue from 'vue';
import Vuex from 'vuex';

const Comment = {
 namespaced: true,          1
 state: {
   comments: [],            2
   select_users: []         3
 },
 mutations: {
4  comments(state, id) {
     const array = ["/items/",id,"/get_comments"];
     const path = array.join('')
     axios.get(path).then(res => {
       state.comments = res.data
     }).catch(function(err) {
       console.log(err)
     })
   },
5  select(state, user) {
6    if(user == 'done') {
       // doneが飛んで来た時は空にする
7      state.select_users = []
     } else {
       // すでに値が入っているか確認している
8      if(state.select_users != null) {
         // すでに同じ値が入っているか確認し、ある場合はtrueが返ってくる。
9        if(state.select_users.some(val => val.id == user.id)) {
           // filterで重複ユーザーを取り除き、新しい配列を生成している。
10          state.select_users = state.select_users.filter(val => val.id != user.id)
         } else {
11          state.select_users.push(user)
         }
       } else {
         state.select_users.push(user)
       }
     }
   }
 },
 actions: {
   get_comments({commit}, id) {
     commit('comments', id)
   },
12 get_select_user({commit}, user) {
     commit('select', user)
   }
 }
}

export default new Vuex.Store({
 modules: {
13 comment: Comment,
 }
})

1,  モジュール分けしているのでnamespaceでcommentに対してのコードという形にしています。

2,  コメントを格納する箱です。

3,  指定されるユーザーが複数の場合にも対応するため配列にしています。

4,  コメントを取得しています。詳しい内容はコメント機能の実装記事で書いています。

5,  2,3で設定した値を呼び出すためとactionsからの値を受け取るuserを引数に入れています。

6,  doneが飛んできた際(メッセージを送信した後)は値を初期値に戻しています。

7,   初期値に戻す処理。

8,  すでにselect_usersに値が入っているか確認している。

9,  some()メソッドで重複するユーザーがいないかチェックしている。Set()とか使ったやり方もありそう。今回はユーザーがそこまで大きくならないのでsome()で対応してます。

10,  filter()メソッドで重複したユーザーを削除して新しい配列を生成しています。

11,  重複しなかったユーザーはそのままpush()で配列に追加します。

12,  mutationsのselectを呼び出します。このアクションはコンポーネントで呼ばれた時に発火します。

13,  コメントモジュールを読み込んでいます。

コンポーネント書き換え

複数対応になっているので若干if文多めです。(めっちゃある)

<template>
 <div>
   <div class="row">
     <h6>コメント</h6>
1    <small v-if="select_users.length != 0" class="text-muted mr-4 ml-2">
2      <div v-if="select_users.length == 1">
3        {{select_users[0].name + 'に返信しています'}}
       </div>
       <div v-else>
4        {{select_users[0].name + 'と他' + select_num(select_users) + '名に返信しています'}}
       </div>
     </small>
   </div>
5  <div v-if="select_users.length != 0">
6    <div v-if="select_users.length == 1">
7      <input type="text" v-model="text" class="px-2 py-2" :placeholder="select_users[0].name + 'に返信しています'" />
8      <button v-show="text != ''" @click.prevent="send(select_users)" type="button" class="btn btn-sm btn-primary">{{select_users[0].name}}に返信する</button>
     </div>
     <div v-else>
9      <input type="text" v-model="text" class="px-2 py-2" :placeholder="select_users[0].name + 'と他' + select_num(select_users) + '名に返信しています'" />
10     <button v-show="text != ''" @click.prevent="send(select_users)" type="button" class="btn btn-sm btn-primary">{{select_users[0].name + 'と他' + select_num(select_users)}}名に返信する</button>
     </div>
   </div>
11 <div v-else>
     <input type="text" v-model="text" class="px-2 py-2" placeholder="Type a Comment" />
     <button v-show="text != ''" @click.prevent="send()" type="button" class="btn btn-sm btn-primary">送信する</button>
   </div>
 </div>
</template>

<script>
import {mapState} from 'vuex'
export default {
 props: ['item_id'],       12
 computed: {
   ...mapState("comment", ['select_users'])   13
 },
 data() {
   return {
     text: ''
   }
 },
 methods: {
14 send(select_users = '') {              
15   var ids = []
16   if(select_users.length != 0) {
17     select_users.forEach(element => {
18       ids.push(element.id)
       });
     }
     const text = {
       text: this.text,
19     select_user_ids: ids
     }
     const id = this.item_id
20   const array = ["/items/",id,"/comments"];
     const path = array.join('')
21   this.text = ''
22   axios.post(path, text).then(res => {
23     this.$store.dispatch('comment/get_comments', id)
     }).catch(function(err) {
       console.log(err)
     })
24   select_users = 'done'
25   this.$store.dispatch('comment/get_select_user', select_users)
   },
   select_num(select_users) {
     return select_users.length - 1
   }
 }
}
</script>

1,  select_usersに値が存在するかどうかで条件分岐しています。

2,  返信しているユーザーが1名以上いるかどうかで条件分岐しています。

3,  ユーザー名を表示し誰に返信しているのかを表示します。

4,  複数の場合は最初に選んだユーザーと他何名に対して返信しているかを表示します。

5,  1と同じでselect_usersに値が存在するかどうかで条件分岐しています。

6,  2と同じ考えです。

7,  3と同じで、placeholderの値を変化しています。

8, ボタンの表示の値も変化させています。vue.js便利や。。。

9,  4と同じで、こちらもplaceholderの値を変化しています。

10,  8と全く同じ考えです。

11,  select_usersの値が無かった場合は、ただのコメント機能です。

12,  アイテムidをviewから受け取っている。

13,  vuexで先ほど設定したcommentモジュールからコメントを取得する。

14,  受け取っている引数の初期値をselect_users = ”にする事で引数が無かった場合にも対応している。send()メソッドが呼ばれた時にユーザーが指定されているかいないか

15,  指定されたユーザーのidを格納する箱を用意します。

16,  指定されたユーザーが存在するかどうか確認しています。

17,  select_usersをforeachで回して処理を行なっていきます。

18,  ユーザーのidを15で設定した配列にpush()メソッドで追加していきます。

19,  idsをselect_user_idsに格納してコントローラーに送る情報を定義します。select_user_idsはカラム名で設定したものと同じです。

20,  コメント投稿処理までのパスを書きます。

21,  送信した後はtextの値を空にします。場所は適宜変更してください。

22,  コントローラーのコメント投稿アクションに定義したパスや情報を持って飛んでいきます。

23,  postした後はget_commentsを実行してコメントを再取得する。

24,  select_usersにdoneを挿入する。

25,  doneをvuex側に送ってselect_usersを空にする処理を行う。

コメント一覧書き換え(コンポーネント)

コメント一覧表示です。返信されたユーザーを表示したり、編集、削除ボタンがあったりします。後はユーザーを選択する部分の処理もしています。

<template>
 <div class="">
   <h5 class="mb-3">コメント一覧</h5>
   <div class="container">
1    <div v-for="comment in comments" :key="comment.id">
       <div class="row my-2">
2        <div v-if="comment.reply_users.length != 0">
3          <small class="text-muted mr-2" style="font-size: 90%;"><a @click="select_user(comment.user)">{{comment.user.name}}</a><span class="ml-2"><i class="fas fa-level-down-alt"></i></span></small>
4          <div v-for="reply_user in comment.reply_users" :key="reply_user.id">
5            <small class="text-muted mr-4"><a @click="select_user(reply_user)" style="font-size: 40%;">{{reply_user.name}}</a></small>
           </div>
         </div>
         <div v-else>
6          <small class="text-muted mr-4"><a @click="select_user(comment.user)">{{comment.user.name}}</a></small>
         </div>
         
         <div v-if="edit_time && comment.id == edit_comment.id">
           <input v-if="edit_time" type="text" v-model="edit_comment.text" class="px-2 py-2" placeholder="Type a Comment" />
           <button v-if="comment.user_id == current_user_id && edit_comment.text != ''" @click.prevent="update(edit_comment)" type="button" class=" btn btn-primary btn-sm">更新</button>
           <button v-if="edit_time" @click.prevent="back(comment)" type="button" class="btn btn-outline-dark btn-sm ml-1">戻る</button>
         </div>
         <div v-else>
           <p style="display: contents;">{{comment.text}}</p>
           <button v-if="comment.user_id == current_user_id" @click.prevent="edit(comment)" type="button" class="ml-4 btn btn-warning btn-sm">編集</button>
           <button v-if="comment.user_id == current_user_id || item_user_id == current_user_id" @click.prevent="destroy(comment.id)" type="button" class="ml-1 btn btn-danger btn-sm">削除</button>
         </div>
       </div>
     </div>
   </div>
 </div>
</template>

<script>
import {mapState} from 'vuex'
export default {
 props: ['item_id', 'current_user_id', 'item_user_id'],     7
 data() {
   return {
     edit_time: false,
     edit_comment: {},
     users: []
   }
 },
 computed: {
   ...mapState("follow",['url']),
   ...mapState("comment", ['comments'])      8
 },
 created() {
   this.getComments()        9
 },
 methods: {
   getComments() {
     const id = this.item_id
     this.$store.dispatch('comment/get_comments', id)       10
   },
   edit(comment) {
     this.edit_time = true
     this.edit_comment = comment
     this.edit_comment.old_text = comment.text
   },
   update(comment) {
     const update_comment = {
       text: comment.text
     }
     const id = this.item_id
     const array = ["/items/",id,"/comments/", comment.id];
     const path = array.join('')
     axios.put(path, update_comment).then(res => {
       this.edit_time = false
       this.edit_comment = {}
       this.getComments()
     }).catch(function(err) {
       console.log(err)
     })
   },
   destroy(comment_id) {
     const id = this.item_id
     const array = ["/items/",id,"/comments/", comment_id];
     const path = array.join('')
     axios.delete(path).then(res => {
       this.getComments()
     }).catch(function(err) {
       console.log(err)
     })
   },
   back(comment) {
     comment.text = comment.old_text
     this.edit_time = false
     this.edit_comment = {}
   },
   make_url(id) {
     this.$store.dispatch('follow/get_link', id)
     window.location.href = this.url
   },
   select_user(user) {
     if(this.current_user_id != user.id) {       11
       this.$store.dispatch('comment/get_select_user', user)       12
     }
   }
 }
}
</script>

<style scoped>
.btn{
 height: 2rem;
}
.text-muted.mr-4{
 cursor: pointer;
}
.text-muted.mr-2{
 cursor: pointer;
}
</style>

1,  コメントをv-forで回しています。

2,  コメントに紐づくユーザーが存在するかどうかを確認しています。

3,  コメントしたユーザーの名前を表示します。

4,  コメントに紐づく(返信された)ユーザーをv-forで回します。

5,  4で回して取得したユーザー名を表示します。

6,  コメントに紐づくユーザーが存在しない場合は3の部分だけを表示します。

7,  view側からitemを投稿者とログイン中のユーザーの名前を取得しています。

8,  vuexのcommentモジュールからcommentsを受け取っています。

9,  getComments()を実行しています。

10,  vuexのget_commentsを呼び出してコメントを取得します。

11,  ユーザーを指定する際に自分をクリックしても選択できない出来ないようにしています。

12,  自分以外のユーザーであれば、vuexのget_select_userアクションを呼びだす。

コントローラー書き換え

コメントを保存して、返信されたユーザーがいた場合は通知を送る処理を行なっています。コメントの保存処理については深く書きません。

use App\Events\Notice;
use App\Notifications\ReplyCommented;

public function store(Item $item, CommentCheck $request)  1
{
   $comment = new Comment();
   $comment->text = $request->text;
   $comment->user_id = Auth::id();
2  if($request->select_user_ids) {
       // ユーザーを取得し個別に送信する
3      foreach($request->select_user_ids as $user_id) {
4          $select_user = User::find($user_id);
5          $select_user->notify(new ReplyCommented(Auth::user(), $select_user, $item));
6          event(new Notice());
       }
   }

   // アイテムが自分のものではない時に通知が処理されるように条件分岐をかけている
7  if($item->user_id != Auth::id()) {
8      $item->user->notify(new ItemCommented(Auth::user(), $item));
       event(new Notice());
   }

9  $json_num = json_encode($request->select_user_ids);
10 $comment->reply_user_ids = $json_num;
11 $item->comments()->save($comment);
}

1,  コメントのバリデーションを通過すると$requestとして取得しています。

2,  指定されたユーザーが存在するかどうかを確認しています。

3,  idの配列なので、foreachで回していきます。

4,  3で回して取得したidを元にユーザーを取得します。指定されたユーザーゲット。

5,  指定されたユーザーに通知を送信します。

6,  イベント発火で通知を非同期で受け取れるようにしています。

7,  自分が自分のアイテムにコメントしているときは通知処理が実装されないようにしている。

8,  コメント通知を送信します。

9,  json_encode()してからデータベースに保存します。これしないと保存できません。配列のままでは無理なので、文字列にする必要があります。

10,  9で定義したものをカラムに代入します。

11,  $itemに紐づくコメントとして保存していきます。

こんな感じです。ここまでで返信コメント機能が出来たと思うので挙動確認してみましょう。

まとめ

調べてもあまり出て来なかったので、我流で実装しています。参考程度に見て頂けると喜びます。w

実装していて思ったのは、指定されたユーザーを扱うのに中間テーブルを使うやり方もありそうだなと思いました。

今回はこんな感じで!

以上!