今回は前回のフォロー機能で実装した、フォローしているユーザーとDMをやりとり出来るよう実装しました。
vue.jsを使用して非同期で処理しています。今回はroomを作成し、移動できるところまでやります。laravel7.x系です。
メッセージする場所はroomという部屋を作るようにします。
見た目は以下の感じです。codepenを参考にしました。
目次
- 大まかな流れ
- マイグレーション 、モデルファイルの作成、設定
- ルーティング設定
- コントローラーアクション設定
- blade側の設定(ボタン)
- store/indexの設定
- vue側の設定
- blade側の設定(リストとメッセージ)
- vue側
- 学んだ事
すべて表示
大まかな流れ
マイグレーション 、モデルファイルの作成、設定
ルーティング設定
コントローラーアクション設定
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>