前回の記事を見ていない方はそちらから見ることをお勧めします。Laravel7.x系です。
前回の記事からの続きになります。
大まかな流れ
モデル、マイグレーションファイル作成
ルーティング設定
コントローラー設定
vuexの設定
コンポーネント設定
って感じの流れで実装します。
目次
モデル、マイグレーションファイル作成
php artisan make:model Message -m
public function up()
{
Schema::create('messages', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('room_id')->constrained('rooms')->onDelete('cascade');
$table->text('text');
$table->timestamps();
});
}
class Message extends Model
{
protected $fillable = ['user_id', 'room_id', 'text'];
public function user()
{
return $this->belongsTo('App\User');
}
public function room()
{
return $this->belongsTo('App\Room');
}
}
userモデルに以下を追加
public function messages()
{
return $this->hasMany('App\Message');
}
public function isMessaged($login_user_id, $user_id)
{
// 確認したいことは$login_user_id, $user_idがどちらも含まれているレコードが存在するかどうか
// ログイン中のユーザー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 true;
}
}
}
} else {
return false;
}
}
roomモデルに以下を追加
public function messages()
{
return $this->hasMany('App\Message');
}
アソシエーションを設定しています。isMessagedはルームに所属しているユーザー同士で過去にメッセージが交わされているかをチェックしています。
ルーティング設定
Route::get('/user/{room}/room_messages', 'MessageController@get_message')->name('room_messages');
Route::get('/user/{room}/get_latest_message', 'MessageController@get_latest_message')->name('get_latest_message');
Route::post('/user/{room}/message', 'MessageController@create')->name('message_create');
Route::put('/user/{room}/message_update', 'MessageController@update')->name('message_update');
Route::post('/user/{room}/message_delete', 'MessageController@destroy')->name('message_delete');
get2つはメッセージの取得、それ以外は作成、更新、削除に使います。
コントローラー設定
public function create(Room $room,Request $request)
{
$user = Auth::user();
$message = new Message();
$message->text = $request->text;
$message->user_id = $user->id;
$message->room_id = $room->id;
$message->save();
event(new MessageChange($message));
return $message;
}
public function get_message(Room $room)
{
$messages = $room->messages()->get();
return $messages;
}
public function update(Room $room, Request $request)
{
if(Auth::id() == $request->user_id){
$update_message = Message::find($request->id);
$update_message->text = $request->text;
$update_message->save();
event(new MessageChange($update_message));
return $update_message;
} else {
return back();
}
}
public function destroy(Room $room, Request $request)
{
if(Auth::id() == $request->user_id){
$delete_message = Message::find($request->id);
$delete_message->delete();
} else {
return back();
}
}
特に特殊なことはしていません。
pusherの設定
pusherの設定についてはこちらを参考に。通知イベントも作成しておいてください。
vuexの設定
const Message = {
namespaced: true,
state: {
},
mutations: {
create(state, {id, text}) {
const obj = {
text: text
}
const array = ["/user/", id, "/message"];
const path = array.join('')
axios.post(path, obj).then(res => {
}).catch(function(error) {
console.log(error)
})
},
update(state, {id, text}) {
const array = ["/user/", id, "/message_update"];
const path = array.join('')
axios.put(path, text).then(res => {
}).catch(function(error) {
console.log(error)
})
},
delete(state, {id, text}) {
const array = ["/user/", id, "/message_delete"];
const path = array.join('')
axios.post(path, text).then(res => {
}).catch(function(error) {
console.log(error)
})
}
},
actions: {
message_create({commit}, {id, text}) {
commit('create', {id, text})
},
message_update({commit}, {id, text}) {
commit('update', {id, text})
},
message_delete({commit}, {id, text}) {
commit('delete', {id, text})
}
}
}
export default new Vuex.Store({
modules: {
message: Message
}
})
axiosでコントローラーと通信して、非同期で処理をしています。
コンポーネント設定
<template>
<div class="mesgs" v-if="user && room_id">
<h6 class="text-center mb-2" >{{user.name}}</h6>
<div class="msg_history" ref="scroll_here">
<div v-for="message in room_messages" :key="message.id">
<div v-if="message.user_id != login_user_id">
<div class="incoming_msg">
<div class="incoming_msg_img">
<div v-if="user.avatar">
<img :src="user.avatar" alt="sunil">
</div>
<div v-else>
<img src="https://ptetutorials.com/images/user-profile.png" alt="sunil">
</div>
</div>
<div class="received_msg">
<div class="received_withd_msg">
<p>{{message.text}}</p>
<span class="time_date"> {{message.hour}}:{{message.min}} {{message.month}} | {{message.date}}</span>
</div>
</div>
</div>
</div>
<div v-else>
<div class="outgoing_msg">
<div class="sent_msg">
<p>{{message.text}}</p>
<div class="row">
<span class="time_date mr-1"> {{message.hour}}:{{message.min}} {{message.month}} | {{message.date}}</span>
<div class="message_action mt-1">
<span class="mt-1 user_active" @click="edit_message_modal(message)">編集</span>
<span class="mt-1 user_active" @click.prevent="delete_message(message)">削除</span>
</div>
</div>
</div>
<!-- モーダルエリア -->
<div id="edit" v-if="showmodal" @click="back(edit_message)">
<div id="i" @click.stop>
<div class="bg-primary rounded py-2 px-3 mb-2">
<input type="text" v-model="edit_message.text">
<button type="button" class="btn btn-primary btn-sm" @click="message_update(edit_message)">編集</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="type_msg">
<div class="input_msg_write">
<input type="text" v-model="text" class="write_msg px-2 py-2" placeholder="Type a message" />
<button class="msg_send_btn px-2" type="button" @click.prevent="send"><i class="far fa-paper-plane"></i></button>
</div>
</div>
</div>
</template>
<script>
export default {
props: ['login_user_id', 'user', 'room_id'],
data() {
return {
text: '',
room_messages: [],
showmodal: false,
edit_message: {}
}
},
created() {
if(this.room_id) {
this.get_messages()
}
// pusherからのデータを受け取る。コントローラーで発火したときは毎回動く。
// watchで見ると重複して動いてしまう。
#1 Echo.channel('message-change').listen('MessageChange', (e) => {
this.get_messages()
})
},
watch: { #2
room_id: function() {
this.get_messages()
},
// 削除のリアルタイム化。以下をすると永遠に更新され続けてしまう。
// room_messages: function() {
// this.get_messages()
// }
},
methods: {
send() {
this.$store.dispatch('message/message_create', {id: this.room_id, text: this.text})
this.text = null
},
edit_message_modal(message) {
this.edit_message = message
// 編集前のtextデータを入れておく。編集しないで戻ってきた時用
this.edit_message.back_text = message.text
this.showmodal = true
},
message_update(message) {
this.$store.dispatch('message/message_update', {id: this.room_id, text: message})
this.showmodal = false
},
delete_message(message) {
this.$store.dispatch('message/message_delete', {id: this.room_id, text: message})
this.get_messages()
},
back(message) {
this.showmodal = false
// 編集が無い時にback_textで編集前の状態に戻す
this.edit_message.text = message.back_text
},
get_messages(){
const id = this.room_id
const array = ["/user/", id, "/room_messages"];
const path = array.join('')
axios.get(path).then(res => {
res.data.forEach(element => {
let set_element = this.set_time(element)
})
this.room_messages = res.data
this.scrollToEnd()
this.$emit('last_message')
}).catch(function(error) {
console.log(error)
})
},
set_time(element) {
let d = new Date(element.created_at)
var min = ("0"+d.getMinutes()).slice(-2)
element.month = d.toLocaleDateString('en-US', { month: 'long'}),
element.month = element.month.slice(0, 3)
element.date = d.getDate(),
element.hour = d.getHours(),
element.min = min
return element
},
#3 scrollToEnd() {
this.$nextTick(() => {
const messageLog = this.$refs.scroll_here
if (!messageLog) return
messageLog.scrollTop = messageLog.scrollHeight
})
},
}
}
</script>
#1, Echoで変化があったらget_messages()を実行するようにしている。
#2, room_idが変化したら再度メッセージを取得するようにしている。
#3, $nextTrick()メソッドを使用して新しいメッセージが入力された際に自動でスクロールされるようにしています。参考記事
メッセージの作成、更新、削除についてはdispatchでvuexに飛ばして処理しています。ここら辺の処理はこちらの記事を参考に。
$emitで呼び出していますが、これは以前の記事の部分と繋がっているので是非合わせて確認してください。具体的にはラストメッセージをリストに表示されるものとしてラストメッセージをリスト側に送っています。
cssについては最後に貼っておきます。
学び
処理自体はそこまで難しいことをしていないが、vueのライフサイクルメソッドの理解がまだ薄い。事実watchで削除機能にもリアルタイムで対応できるが、コメントアウトを外すと無限ループが走ってしまう。
なので現状コメントアウトしてます。非同期じゃないだけで処理はコメントアウトのままでも動きます。
watch,computedあたりをうまく使いこなせるともっと効率よくデータの受け渡し、監視が出来そう。
課題
削除が非同期に出来ない問題。学びにも書いた通り。
メッセージ毎回全取得問題。get_messages()メソッドはコントローラーにアクセスしてるが、そこで毎回全メッセージを取得している。
フォローすれば誰でもメッセージを送信できる問題。インスタのDMみたいにフィルターみたいなのつけようか。。
まとめ
基本的にaxiosでやり取り、vueで変更をどう検知してリアルタイムに表示するか?の部分がメインで勉強になりました。
CSS
見た目部分の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 {
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>