LaravelとVue.jsでメッセージの未読、既読機能を付けてみた

2021.01.18

見出し画像

今回は前回書いたDM機能を実装した記事からの応用としてメッセージの未読、既読機能を実装していこうと思います。

目次

  1. どこまで実装するか?
  2. 大まかな流れ
  3. 前提
  4. 必要なテーブル、マイグレーションファイルの作成、編集。
  5. ルーティングの設定
  6. blade側
  7. コンポーネント側
  8. 既読にする処理
  9. pusherを追加してリアルタイム化
  10. まとめ

どこまで実装するか?

未読DM数を表示する機能

既読にする処理

未読DM数を表示する機能のリアルタイム化

ここまで実装します。

大まかな流れ

必要なテーブル、マイグレーションファイルの作成、編集。

ルーティングの設定

コンポーネント側

コントローラー側

pusherを追加してリアルタイム化

上記のような流れで、リアルタイム化まで実装する形で書いていきます。

前提

前回記事を参考に、メッセージの送信機能が実装できている。また、ルーム機能も備わっている。データ保存時に必要です。

最後のリアルタイム化していく部分はpusherの導入も終わっている前提です。

必要なテーブル、マイグレーションファイルの作成、編集。

メッセージとそのメッセージに紐付くユーザーを保存する中間テーブルを作成します。

命名規則的に作成するとMessage_Userとなるのでこの名前で作成します。-mオプションを付けてマイグレーションファイルも同時生成します。

php artisan make:model -m

マイグレーションファイル編集します。外部キーの他にis_readをboolean型で付けてデフォルト値はfalseにしています。このis_readを既読,未読状態の確認に使います。

public function up()
   {
       Schema::create('message_user', function (Blueprint $table) {
           $table->id();
           $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
           $table->foreignId('message_id')->constrained('messages')->onDelete('cascade');
           $table->boolean('is_read')->default(false);
           $table->timestamps();
       });
   }
php artisan migrate

上記うまくいかなかったら、下記

php artisan migrate:fresh

各モデルファイルの編集です。

中間テーブル
class Message_User extends Model
{
   protected $table = 'message_user';

   protected $fillable = ['user_id', 'message_id'];
}

中間テーブルはmessage_userと言う名前で使用するように明示しています。

ユーザーモデル
public function messages()
{
  return $this->belongsToMany('App\Message');
}

ユーザーが取得するメッセージはbelongsToManyで設定します。中間テーブル経由です。

ルーティングの設定

Vueからaxoisでアクセスするときのルートを設定します。putは未読から既読に変更する処理です。

Route::get('/user/{user}/dm_status', 'UserController@dm_status')->name('dm_status');
Route::put('/user/{room}/read_change', 'MessageController@read_change')->name('read_change');​

blade側

<dm-status :user="{{Auth::user()}}"></dm-status>

表示したい適当な場所に上記を記載します。

コンポーネント側

<template>
1 <p v-if="dm_count != 0" class="card-text"><small class="text-muted">未読: {{dm_count}}件</small></p>
</template>

<script>
export default {
 props: ['user'],       2
 data() {
   return {
     dm_count: ''
   }
 },
 created() {
   this.dm_status()     3
 },
 methods: {
   dm_status() {
     const id = this.user.id
     const array = ["/user/",id,"/dm_status"];
     const path = array.join('')
     axios.get(path).then(res => {
       this.dm_count = res.data            4
     }).catch(function(err) {
       console.log(err)
     })
   },
 }
}
</script>

1.  dm_countを表示しています。

2.  ログイン中のユーザーの情報を受け取っています。

3.  viewが読み込まれたタイミングでdm_status()メソッドが動くようにしています。

4. axiosでコントローラーに飛ばされ、取得した値をdm_countに代入しています。

public function dm_status(User $user)
{
1   $records = Message_User::where('user_id', $user->id)->where('is_read', false)->get();
2   return $records->count();
}

1.  Message_User中間テーブルから$user->idが一致し、is_readがfalseなレコードを取得します。ユーザーが未読のレコードです。

2. 返り値はcount()メソッドを使用して数値を返すようにしています。

ここまでで機能の方は実装出来たと思います。挙動を確認して正しく動いたらリアルタイム化して行きましょう。

既読にする処理

コンポーネント側とコントローラー側の処理を書いて行きます。

コンポーネント側

read_info_change()メソッドが呼ばれるタイミング、場所などは適宜実装してください。以前作成したメッセージ機能を参考に。

read_info_change(room_id) {
 var array = ["/user/", room_id, "/read_change"];    1
 // パスをjoinで結合
 const t = array.join('')
 axios.put(t).then(res => {
 }).catch(function(error) {
   console.log(error)
 })
}

1. ルーティングに設定した通りのアクションにroom_idを持って飛ばされます。

コントローラー側

 public function read_change(Room $room) {
1      $room_messages = $room->messages()->get();
       foreach($room_messages as $message) {
           // 未読メッセージを取得する。
           // where()を使いroomのメッセージのidと一致し、且つis_readがfalseのものを取得する。
2          $read_records = Message_User::where('message_id', $message->id)->where('is_read', false)->get();
           // $read_recordsで取得した値のis_readをtrueに変更していく
3          foreach($read_records as $record) {
               // ログイン中のユーザーidと一致するレコードしか変更できない
4              if(Auth::id() == $record->user_id){
                   $record->is_read = true;
                   $record->save();
               }
           }
       }
   }

1.  ルームに紐付くメッセージを全て取得します。

2.  1で取得したメッセージ(配列)をeachで回し、Message_User中間テーブルから値を取得します。値は、$message->idに一致し、is_readがfalseのレコードのみを取得します。

3.  2で取得した値(配列)をeachで回して、1つ1つのレコードを取得します。

4.  ログイン中のユーザーidと一致したレコードのis_readをtrueに変換してきます。save()を実行し完了です。

pusherを追加してリアルタイム化

イベント作成

イベントを作成し、ファイルの編集をします。

php artisan make:event DmNotice
<?php

namespace App\Events;

use Illuminate\Broadcasting\Channel;
use Illuminate\Broadcasting\InteractsWithSockets;
use Illuminate\Broadcasting\PresenceChannel;
use Illuminate\Broadcasting\PrivateChannel;
use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

class DmNotice implements ShouldBroadcast   1
{
   use Dispatchable, InteractsWithSockets, SerializesModels;

   /**
    * Create a new event instance.
    *
    * @return void
    */
   public function __construct()
   {
       //
   }

   /**
    * Get the channels the event should broadcast on.
    *
    * @return \Illuminate\Broadcasting\Channel|array
    */
   public function broadcastOn()
   {
       return new Channel('dm-message');     2
   }
}

1.  ここにimplements ShouldBroadcastを追加します。これでリロードしなくても良くなる(簡単に言うと)。下記公式引用

ユーザーがページを再読込しなくてはならないなんてしたくありません。代わりにアップデートがあることをアプリケーションへブロードキャストしたいわけです

2.  呼び出して実行するときのチャンネル名を記載します。最初はprivateに設定されていますが、消しています。privateにするとユーザーの認証が必要になると。実装によって変わってくるかなと思います。下記公式引用

プライベートチャンネルをリッスンするには、ユーザーは認可されている必要があることを思い出してください

コントローラー側

use App\Events\DmNotice;           1

if(Auth::id() == $record->user_id){
   $record->is_read = true;
   $record->save();
   event(new DmNotice());    追加     2
}

1.  イベント呼び出せるように明示します。

2. $recordが保存された際に、DmNoticeを実行します。

ここまででリアルタイム化も実装出来ているかと思います。

まとめ

・未読DM数を表示する機能はaxiosでコントローラーから値を取得し、表示する。

・既読にする処理もaxiosでコントローラーにアクセスし値を変更している。コンポーネント側でいつ発火させるのか決める。

・未読DM数を表示する機能のリアルタイム化はイベントを作成し、コントローラーで呼び出せるようにする。コンポーネント側の処理は要らない。メッセージルームにいる状態でも発火させてしまうと既読したのに未読1件と表示されてしまうから。

また既読、未読判定が出来ると思うので未読の場合には色を変えるといった処理も実装できるかなと思います。

今回はこんな感じで!

以上!