LaravelとVue.jsでDM機能をつけてみた1(非同期)

2021.01.18

見出し画像

今回は前回のフォロー機能で実装した、フォローしているユーザーとDMをやりとり出来るよう実装しました。

次の記事

vue.jsを使用して非同期で処理しています。今回はroomを作成し、移動できるところまでやります。laravel7.x系です。

メッセージする場所はroomという部屋を作るようにします。

見た目は以下の感じです。codepenを参考にしました。

スクリーンショット 2020-09-08 13.41.10

目次

  1. 大まかな流れ
  2. マイグレーション 、モデルファイルの作成、設定
  3. ルーティング設定
  4. コントローラーアクション設定
  5. blade側の設定(ボタン)
  6. store/indexの設定
  7. vue側の設定
  8. blade側の設定(リストとメッセージ)
  9. vue側
  10. 学んだ事

すべて表示

大まかな流れ

マイグレーション 、モデルファイルの作成、設定

ルーティング設定

コントローラーアクション設定

vue側の設定(ボタン)

vue側の設定(メッセージ画面)

って感じで実装していきます。

マイグレーション 、モデルファイルの作成、設定

作成コマンド

php artisan make:model Room -m
php artisan make:model Room_User -m

それぞれファイルに書き加えていきます。

マイグレーションファイル​

public function up()
   {
       Schema::create('rooms', function (Blueprint $table) {
           $table->id();
           $table->string('name');
           $table->timestamps();
       });
   }
   
   
   
public function up()
   {
       Schema::create('room_user', function (Blueprint $table) {
           $table->id();
           $table->foreignId('user_id')->constrained('users')->onDelete('cascade');
           $table->foreignId('room_id')->constrained('rooms')->onDelete('cascade');
           $table->timestamps();
       });
   }

モデル

class Room extends Model
{
   protected $fillable = ['name'];

   public function users()
   {
       return $this->belongsToMany('App\User');
   }

}



class RoomUser extends Pivot
{
   protected $table = 'room_user';

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



class RoomUser extends Pivot
{
   protected $table = 'room_user';

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

Userモデルだけ多いので別にします。

   public function rooms()
   {
       return $this->belongsToMany('App\Room');
   }

   public function getRoom($login_user_id, $user_id)
   {
       $room_user_rocords = RoomUser::where('user_id', $login_user_id)->get();
       // $room_user_rocordsが存在するかどうか
       if(isset($room_user_rocords)) {
           // 自分のidが含まれているレコードをforeachで回す
           foreach($room_user_rocords as $room_user) {
               // $room_userのroom_idと一致するレコードを取得。
               $records = RoomUser::where('room_id', $room_user->room_id)->get();
               // $recordsをforeachで回す
               foreach($records as $record) {
                   // $recordが存在するか且つ、そのレコードに$user_idが含まれているものがあるかどうか
                   if(isset($record) && $record->user_id == $user_id) {
                       // 存在する場合はtrue
                       return $record;
                   }
               }
           }
       } else {
           return false;
       }
   }

   public function getRoom_byid($login_user_id, $room_id) 
   {
       $ids = [$login_user_id];
       $room_user_rocords = RoomUser::where('room_id', $room_id)->get();
       
       if(isset($room_user_rocords)) {
           // whereNotInは返り値が配列になる。だからfirstを使ってオブジェクトで取得している。
           $room_user = $room_user_rocords->whereNotIn('user_id', $ids)->first();
           $user = User::find($room_user->user_id);
           return $user;
       } else {
           return false;
       }
   }

ルーティング設定

Route::post('/user/{user}/room', 'RoomController@create')->name('create_room');

Route::get('/user/{user}/room_main', 'RoomController@index')->name('message_room_list');
Route::get('/user/{user}/room_redirect/{another_user}', 'RoomController@room_redirect')->name('message_room_redirect');
Route::get('/user/{user}/room_get/{room}', 'RoomController@room_get')->name('message_room_get');
Route::get('/user/{user}/room_list_get', 'RoomController@room_list_get')->name('room_list_get');​

コントローラーアクション設定

class RoomController extends Controller
{
   public function index(User $user, Request $request)
   {
       $all = Session::all();
       
       // リダイレクトされている場合かチェックしている
       if(isset($all['room'])) {
           $go_room = $all['room'];
           // 一度使うと消えるように設定
           Session::forget('room');
           return view('rooms.index')->with('room', $go_room);
       } else {
           return view('rooms.index');
       }
   }

   public function room_redirect(User $user, User $another_user)
   {
       $room = $user->getRoom($user->id, $another_user->id);
       // リダイレクトでsessionに渡している。withだとうまくいかなかった。
       Session::put('room', $room);
       return redirect()->route('message_room_list', ['user' => $user->id]);
   }

   public function room_list_get(User $user)
   {
       $rooms = $user->rooms()->get();
       foreach($rooms as $room) {
           $ids = [$user->id];
           $room_user = $room->users()->whereNotIn('user_id', $ids)->first();
           $room->user = $room_user;
           $room_last_message = $room->messages()->latest()->first();
           $room->last_message = $room_last_message;
       }
       return $rooms;
   }

   public function room_get(User $user, Room $room)
   {
       $room_user = $user->getRoom_byid($user->id, $room->id);
       return $room_user;
       
   }


   public function create(User $user)
   {
       $login_user = Auth::user();
       if(!$login_user->isMessaged(Auth::id(), $user->id)) {
           $room = new Room();
           $hash = $login_user->id + $user->id;
           $room->name = Hash::make($hash);
           $room->save();
           $room->users()->attach([$login_user->id, $user->id]);
           return $user->id;
           // return redirect()->route('message_room_redirect', ['user' => $login_user->id, 'another_user' => $user->id]);
       }
   }
}

blade側の設定(ボタン)

ユーザーのshowページに以下を表示できるようにします。一緒に複数の情報も渡しています。

<follow-button login_user_id="{{Auth::id()}}" user_id="{{$user->id}}" messaged="{{Auth::user()->isMessaged(Auth::id(), $user->id)}}"></follow-button>

app.js

Vue.component('follow-button', require('./components/FollowButton.vue').default);

store/indexの設定

vuexの導入についてはこちらを参考

mutations: {
    build_room(state, {login_user_id, id}) {
     var array = ["/user/", id, "/room"];
     const t = array.join('')
     axios.post(t).then(res => {
       let user_id = res.data
       var array = ["/user/", login_user_id, "/room_redirect/", user_id];
       // 作成したroomにそのまま移動
       const url = array.join('')
       window.location.href = url
     }).catch(function(error) {
       console.log(error)
     })
   },
}
actions: {
    build_room({commit}, {login_user_id, id}) {
     commit('build_room', {login_user_id, id})
   }
}

vue側の設定

#1 <div v-if="following_check && messaged_check">
     <a :href="room_link(login_user_id,user_id)">
       <button type="submit" class="btn btn-danger btn-sm">done</button>
     </a>
   </div>
   <div v-else-if="following_check && !messaged_check">
     <form @submit.prevent="build_room(login_user_id,user_id)">
       <button type="submit" class="btn btn-danger btn-sm">Message</button>
     </form>
   </div>
   
   ...省略
 props: ['login_user_id', 'user_id','messaged'],
   
 created() {
     if(this.messaged == 1) {
       this.messaged_check = true
     } else {
       this.messaged_check = false
     }
 },
 
 methods: {
#2 room_link(login_user_id, user_id) {
     var array = ["/user/", login_user_id, "/room_redirect/", user_id];
     const url = array.join('')
     return url
   },
#3 build_room(login_user_id,id) {
     this.$store.dispatch('follow/build_room', {login_user_id: login_user_id, id: id})
   }
 
 }

#1, DMしたことがあるか判別して、表示を変えている。

#2, DMしたことがある場合はurlを作成し、メッセージ画面へ飛ばす。

#3, DMしたことがない場合はvuexを通してルームを作成している。引数が複数の場合は書き方が少し変わるので注意。こちらもroom作成後にメッセージ画面に飛ばしている。

blade側の設定(リストとメッセージ)

メッセージ画面とDM一覧が表示されているページをを作ります。

<div class="messaging">
   @if(isset($room))
     <message-list login_user_id="{{Auth::id()}}" redirect_room_id="{{$room->room_id}}"></message-list>
   @else
     <message-list login_user_id="{{Auth::id()}}"></message-list>
   @endif
 </div>

リダイレクトで飛んできている場合はsessionに情報を入れるようにコントローラー側で設定しているので条件分岐している。

vue側

メッセージ送信部分については子コンポーネントととして設定しています。メッセージ部分については次の記事で紹介します。

<template>
<div class="inbox_msg">
 <div class="inbox_people">
   <div class="headind_srch">
     <div class="recent_heading">
       <h4>Recent</h4>
     </div>
     <div class="srch_bar">
       <div class="stylish-input-group">
         <input type="text" class="search-bar" placeholder="Search" v-model="search" @keyup="search_room">
         <span class="input-group-addon">
         <button type="button"> <i class="fa fa-search" aria-hidden="true"></i> </button>
         </span> </div>
     </div>
   </div>
   <div class="inbox_chat">
     <div v-for="dm_user in user_rooms" :key="dm_user.id">
         <div class="chat_list active_chat">
           <div class="chat_people">
             <div class="chat_img"> 
               <div v-if="dm_user.user.avatar">
                 <img :src="dm_user.user.avatar" alt="sunil">
               </div>
               <div v-else>
                 <img src="https://ptetutorials.com/images/user-profile.png" alt="sunil"> 
               </div>
             </div>
             <div class="chat_ib">
               <h5><a @click="make_url(dm_user.user.id)" :url="url">{{dm_user.user.name}}</a>
                 <span class="chat_date" v-if="dm_user.last_message">
                   {{dm_user.last_message.month}} {{dm_user.last_message.date}}
                 </span>
                 <span v-else>
                   no date
                 </span>
               </h5>
               <p v-if="dm_user.last_message">{{dm_user.last_message.text}}</p>
               <p v-else>まだ会話はありません</p>
             </div>
           </div>
         </div>
     </div>
   </div>
 </div>
 <message @last_message="get_room_info" :user="user" :room_id="room_id" :login_user_id="login_user_id"></message>
</div>
</template>

<script>
import {mapState} from 'vuex'
import message from './Message.vue'
export default {
 props: [
   'login_user_id', 'redirect_room_id',
 ],
 components: {
   message
 },
 data() {
   return {
     user_rooms: [],
     message_url: '',
     user: '',
     room_id: '',
     search: '',
     here_redirect_room_id: ''
   }
 },
 created() {
   this.here_redirect_room_id = this.redirect_room_id
   this.get_room_info()
 },
 methods: {
#1 message_link(room_id) {
     let user_id = this.login_user_id
     var array = ["/user/", user_id, "/room_get/", room_id];
     const get_room_path = array.join('')
     axios.get(get_room_path).then(res => {
       this.user = res.data
       this.room_id = room_id
     }).catch(function(error) {
       console.log(error)
     })
   },
#2 get_room_info() {
     // リダイレクトidを持っているか判断
     if(this.here_redirect_room_id) {
       this.message_link(this.here_redirect_room_id)
       // 1度アクセスしたらhere_redirect_room_idは消す。他のページに遷移できなくなる。
       this.here_redirect_room_id = ''
     }
     let id = this.login_user_id
     var array = ["/user/", id, "/room_list_get"];
     // パスをjoinで結合
     const t = array.join('')
     axios.get(t).then(res => {
       this.user_rooms = res.data
       this.user_rooms.forEach(element => {
         let d = new Date(element.created_at)
         var min = ("0"+d.getMinutes()).slice(-2)
 
         if(element.last_message) {
           element.last_message.month = d.toLocaleDateString('en-US', { month: 'long'}),
           element.last_message.month = element.last_message.month.slice(0, 3)
           element.last_message.date = d.getDate()
         }
       })
     }).catch(function(error) {
       console.log(error)
     })
   },
#3 search_room() {
     let text = this.search
     let rooms = this.user_rooms
     if(event.key === 'Backspace' || event.key === 'Enter') {
       return false
     } else {
       return rooms.forEach(function(room, i) {
         // 検索した文字と前方一致するものを取得。-1は配列番号を合わせるためにつけている。
         if(room.user.name.indexOf(text) > -1) {
           // もともと存在している配列の中から削除。
           rooms.splice(i, 1)
           // 前方一致したものを配列の最初の要素として追加
           rooms.unshift(room)
         }
       })
     }
   }
 }
}
</script>

#1, クリックしたユーザーとのメッセージ画面に移動する処理です。子コンポーネントが非同期に切り替わります。

#2, リダイレクトか判断している。idを持っている場合はそのまま遷移する。全てのルームのラストメッセージ取得、データ情報の書き換えを行なっている。

#3, リストの中からユーザーの名前で検索できるようにしている。spliceとunshiftを使ってリストの上に表示されるようにしている。

ここまで出来ればroomまでの移動は出来ると思います!cssについては最後に貼っておきます。

エラーが出た場合はでバックしながら進みましょう。

学んだ事

splice、unshiftを使って配列の情報を書き換えている。

まとめ

ここまででユーザーと1対1のDMするroomを作成する事が出来た。

所々つまづきながらだが、調べらながら自力で出来るようになってきたと思うw(主観ww)

この調子で次はメッセージをリアルタイムで実装する記事を書いていく!

それにしても最近また暑い。。。。

今回はこんな感じで。。。

以上!

CSS

vueのcssです

<style scoped>
.container{max-width:1170px; margin:auto;}
img{ max-width:100%;}
.inbox_people {
 background: #f8f8f8 none repeat scroll 0 0;
 float: left;
 overflow: hidden;
 width: 40%; border-right:1px solid #c4c4c4;
}
.inbox_msg {
 border: 1px solid #c4c4c4;
 clear: both;
 overflow: hidden;
}
.top_spac{ margin: 20px 0 0;}


.recent_heading {float: left; width:40%;}
.srch_bar {
 display: inline-block;
 text-align: right;
 width: 60%; padding:
}
.headind_srch{ padding:10px 29px 10px 20px; overflow:hidden; border-bottom:1px solid #c4c4c4;}

.recent_heading h4 {
 color: #05728f;
 font-size: 21px;
 margin: auto;
}
.srch_bar input{ border:1px solid #cdcdcd; border-width:0 0 1px 0; width:80%; padding:2px 0 4px 6px; background:none;}
.srch_bar .input-group-addon button {
 background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
 border: medium none;
 padding: 0;
 color: #707070;
 font-size: 18px;
}
.srch_bar .input-group-addon { margin: 0 0 0 -27px;}

.chat_ib h5{ font-size:15px; color:#464646; margin:0 0 8px 0;}
.chat_ib h5 span{ font-size:13px; float:right;}
.chat_ib p{ font-size:14px; color:#989898; margin:auto}
.chat_img {
 float: left;
 width: 11%;
}
.chat_ib {
 float: left;
 padding: 0 0 0 15px;
 width: 88%;
}

.chat_people{ overflow:hidden; clear:both;}
.chat_list {
 cursor: pointer;
 border-bottom: 1px solid #c4c4c4;
 margin: 0;
 padding: 18px 16px 10px;
}
.inbox_chat { height: 550px; overflow-y: scroll;}

.active_chat{ background:#ebebeb;}

.incoming_msg_img {
 display: inline-block;
 width: 6%;
}
.received_msg {
 display: inline-block;
 padding: 0 0 0 10px;
 vertical-align: top;
 width: 92%;
}
.received_withd_msg p {
 background: #ebebeb none repeat scroll 0 0;
 border-radius: 3px;
 color: #646464;
 font-size: 14px;
 margin: 0;
 padding: 5px 10px 5px 12px;
 width: 100%;
}
.time_date {
 color: #747474;
 display: block;
 font-size: 12px;
 margin: 8px 0 0;
}
.received_withd_msg { width: 57%;}
.mesgs {
 float: left;
 padding: 30px 15px 0 25px;
 width: 60%;
}

.sent_msg p {
 background: #05728f none repeat scroll 0 0;
 border-radius: 3px;
 font-size: 14px;
 margin: 0; color:#fff;
 padding: 5px 10px 5px 12px;
 width:100%;
}
.outgoing_msg{ overflow:hidden; margin:26px 0 26px;}
.sent_msg {
 float: right;
 width: 46%;
}
.input_msg_write input {
 background: rgba(0, 0, 0, 0) none repeat scroll 0 0;
 border: medium none;
 color: #4c4c4c;
 font-size: 15px;
 min-height: 48px;
 width: 100%;
}

.type_msg {border-top: 1px solid #c4c4c4;position: relative;}
.msg_send_btn {
 background: #05728f none repeat scroll 0 0;
 border: medium none;
 border-radius: 50%!important;
 color: #fff;
 cursor: pointer;
 font-size: 17px;
 height: 33px;
 position: absolute;
 right: 0;
 top: 11px;
 width: 33px;
 outline: none;
}

.msg_send_btn:active{
 outline: none;
 opacity: .5;
}
.messaging { padding: 0 0 50px 0;}
.msg_history {
 height: 516px;
 overflow-y: auto;
}

.user_active{
 cursor: pointer;
 font-size: .6rem;
}
#edit{
/*要素を重ねた時の順番*/
z-index:1;

/*画面全体を覆う設定*/
position:fixed;
top:0;
left:0;
width:100%;
height:100%;
background-color:rgba(0,0,0,0.5);

/*画面の中央に要素を表示させる設定*/
display: flex;
align-items: center;
justify-content: center;

}
</style>