COMMIT - Powerful Single Chatroom with Laravel + React + Socket IO + Redis ( Intermediate )

Powerful Single Chatroom with Laravel + React + Socket IO + Redis ( Intermediate )

Programming

Bagi Kawan-kawan yang sudah melihat artikel tentang membaut Realtime Applications sebelumnya di website COMMIT, kali ini kita akan sedikit memasuki tahap menengah untuk membuat single chatroom , karena sebelumnya aplikasi yang kita buat tidak menerapkan autentikasi, kali ini kita pakai ya.

Bagi kalian yang belum mohon maaf sebelumnya harus kalian lewati terlebih dahulu, dikarenakan saya sedikit malas mengulang tulisan yang kemarin sudah dibuat. Namun tenang saja, kawan-kawan bisa melihat Tutorial Tahap pertama sebelum memasuki tahap intermediate.

Modifikasi Code Tahap Pertama

Ada beberapa Code yang akan kita modifikasi dan juga kita tambahkan untuk membuat chatroom yang saya beri nama room "bacotisme" ini, berikut dengan list users yang sedang tergabung di chatroom yang kita buat, yang perlu dimodifikasi antara lain adalah :

  1. Message.php ( Model )
  2. User.php ( Model )
  3. MessageSent.php (Event )
  4. MessageController ( Controller )
  5. channels.php (routes)
  6. Message.js ( React )
  7. message.blade.php ( Layouts )

Harap diperhatikan, sekarang kita buat dulu field tambahan untuk tabel messages, sekarang kita jalankan perintah artisan ini

php artisan make:migration add_user_id_to_messages_table --table=messages

gunanya untuk menambahkan field di tabel messages, lalu kita modifikasi sedikit  di method up.

public function up()
    {
        Schema::table('messages', function (Blueprint $table) {
            $table->unsignedInteger('user_id')->after('uuid');
        });
    }

lalu migrate dengan menjalankan php artisan migrate

User and Message

Pastikan tidak ada error setelah menjalankan perintah diatas ya, sekarang kita buat relasi antara Message dan User, pertama untuk User.php terlebih dahulu

    /**
     * User has many messages
     */
    public function messages()
    {
        return $this->hasMany('App\Message');
    }

Lalu Message.php , untuk ini kita tambahkan protected $with untuk melalukan eager load

    protected $with = ['user'];

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

What's Next ?

Kita Modifikasi event MessageSent.php dengan menghapus dontBroadcastToCurrentUser, dan mengganti tipe channelnya dengan PresenceChannel, yang pada akhirnya code MessageSent akan terlihat seperti ini.

<?php

namespace App\Events;

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

class MessageSent implements ShouldBroadcast
{
    use Dispatchable, InteractsWithSockets, SerializesModels;

    /**
     * Create a new event instance.
     *
     * @return void
     */
    public $message;
    public function __construct($message)
    {
        $this->message = $message;
    }

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

The Controller

MessageController akan kita tambahkan middleware agar setiap users yang masuk ke halaman /message memerlukan login terlebih dahulu agar dapat ikut dalam chatroom "bacotisme" ini, Oke kita ubah seperti ini.

<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Message;
use App\Events\MessageSent;
use Auth;

class MessageController extends Controller
{
    public function __construct()
    {
        $this->middleware('auth');
    }

    public function index()
    {
        $messages = Message::get();
        if(request()->wantsJson()){
            return $messages;
        }
        return view('message');
    }

    public function store(Request $request)
    {
        $message = new Message;
        $content = $request->content;
        $message->content = $content;
        $message->user_id = auth()->id();
        $message->uuid = $request->uuid;
        $message->save();
        broadcast(new MessageSent($message))->toOthers();
        return $message;
    }
}
yang berubah pada Controller ini hanya lah menambahkan user_id untuk dimuat pada saat kita/user membuat pesan, dan menambahkan toOthers(), agar tidak ada duplikasi pesan pada saat membuat pesan ini. Maksud duplikasi disini adalah, karena dalam frontend javascript kita (react saat ini), kita menggunakan dua kali spread operator untuk memasukkan data baru yaitu pada method endpoint store dan broadcast. yang artinya dengan method toOthers(), user lain ( selain kita ) itu memuat perubahan yang berasal dari broadcast, dan kita tetap memuat perubahan data lewat endpoint, ya mungkin kurang lebih seperti itu ( CMIIW ).

The Channel Routes

Sekarang kita tambahkan otorisasi untuk channel chatroom ini , arahkan mouse anda ke routes/channels.php dan tambahkan perintah ini.

Broadcast::channel('chatroom',function($user){
    return $user;
});

Very Very easy.

Sharpen Our Blade Component

kita tambahkan sedikit baris codingan di message.blade.php seperti ini ya kawan-kawan.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Message</title>
    <meta name="csrf-token" content="{{ csrf_token() }}">
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO"
        crossorigin="anonymous">
    <script src="//{{ Request::getHost() }}:6001/socket.io/socket.io.js"></script>
    <script>
        window.Message = {!! json_encode([
                'csrfToken' => csrf_token(),
                'user' => Auth::user(),
                'signedIn' => Auth::check()
            ]) !!};
    </script>
</head>
<body>
    <div id="app">
        <div class="jumbotron">
            <h1 class="display-4">Hello, world!</h1>
            <p class="lead">This is a simple message.</p>
            <hr class="my-4">
        </div>
    </div>
    <div id="message"></div>
    <script src="{{ mix('/js/app.js')}}"></script>
</body>
</html>

Last but not Least

Yang terakhir tentu saja, yang kita tunggu-tunggu, ya benar sekali!! kita ubah sedikit file React yang bernama Message.js ini. sebelumnya jika kawan-kawan sedikit pusing dengan perintah-perintah diatas kawan-kawan bisa jalan-jalan atau ngopi dulu sebentar lalu lanjut membaca artikel ini. Tidak butuh ? Oke Lanjut kita ubah Message.js seperti ini.

import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import uuid from 'uuid';
import axios from 'axios';

export default class Message extends Component {
    constructor(props){
        super(props);
        this.state = {
            content:'',
            uuid:null,
            messages:[],
            users:[],//tambahan
        }
        this.handleChange = this.handleChange.bind(this);
        this.handleSubmit = this.handleSubmit.bind(this);
    }
    componentDidMount() {
        axios.get('/message').then(res => this.setState({
            messages:res.data
        }));
        //window.Echo.channel diganti
        window.Echo.join('chatroom')
            .here((users) => {
                this.setState({
                    users:users
                })
            })
            .joining((user) => {
                this.setState(state => ({
                    users:[...state.users,user]
                }))
            })
            .leaving(user => {
                this.setState(state => ({
                    users:state.users.filter(eUser => eUser.id !== user.id )
                }))
            }).listen('MessageSent',(e) => {
                this.setState(state => ({
                    messages:[...state.messages,e.message]
                }))
            })
        //berakhir disini
    }
    handleChange(e){
        this.setState({
            [e.target.name]:e.target.value
        })
    }
    handleSubmit(e){
        e.preventDefault();
        const data = {
            content:this.state.content,
            uuid:uuid.v4(),
            user:window.Message.user //tambah user dari window.Message yang tadi kita buat di message.blade.php
        }
        axios.post('/message',data).then(() => {
            this.setState(state => ({
                messages:[...state.messages,data],
                content:''
            }));
        }).catch(err => console.log(err.response))
    }
    
    render() {
        const {users} = this.state;//mau pake destruktur seperti ini atau this.state.users saja tidak masalah
        return (
            //tambahan
            <div className="container mb-4">
                <div className="row justify-content-center">
                <div className="col-md-8">
                    <div className="card">
                        <div className="card-header">Chats</div>
                        <div className="card-body">
                            {
                                this.state.messages && this.state.messages.map(message => (
                                    <a key={message.uuid} href="#" className="list-group-item mb-3 list-group-item-action flex-column align-items-start active">
                                        <div className="d-flex w-100 justify-content-between">
                                            <h5 className="mb-1">{message.user.username}</h5>
                                        </div>
                                        <p className="mb-1">
                                            {message.content}
                                        </p>
                                    </a>
                                ))
                            }
                        </div>
                    </div>
                </div>
                <div className="col-md-4">
                    <div className="card">
                        <div className="card-header">Connected Users</div>
                        <div className="card-body">
                            <ul className="list-group">
                               {
                                   users.length > 0 && users.map(user => (
                                    <li key={user.id} className="list-group-item d-flex justify-content-between align-items-center">
                                        {user.username}
                                    </li>
                                   ))
                               }     
                            </ul>
                        </div>
                    </div>
                </div>
            </div>
                <form onSubmit={this.handleSubmit}>
                    <div className="form-group">
                        <label htmlFor="exampleInputEmail1">Message</label>
                        <input value={this.state.content} onChange={this.handleChange} type="text" className="form-control" id="exampleInputEmail1" name="content" />
                        <small id="emailHelp" className="form-text text-muted">We'll Share your message!!</small>
                    </div>
                    <button type="submit" className="btn btn-primary">Send</button>
                </form>
            </div>
        );
    }
}
if(document.getElementById('message')){
    ReactDOM.render(<Message/>,document.getElementById('message'));
}

pada codingan diatas sudah saya beri comment jadi anda dapat membaca Why ada kode tambahan seperti itu, namun sedikit informasi method here() pada window.Echo itu maksudnya users yang berada pada chatroom "bacotisme", joining() yaitu user yang baru saja masuk ke channel chatroom , dan leaving adalah user yang pergi meninggalkan kita dari chatroom "bacotisme" ini :(.

Kesimpulan

Ada satu hal yang tidak saya tuliskan dan mungkin saja jika anda melewatkannya, anda akan mengalami error pada aplikasi yang anda buat, yaitu field "username" pada tabel users. Anda dapat menambahkan field ini sendiri ya, sekalian untuk menguji anda bahwa anda benar-benar memahami Framework Super Laravel ini. dan jangan lupa juga, setelah menambahkan code React pada file Message.js, jalankan npm run dev/prod ya, dan juga queue + laravel-echo-servernya dijalankan :).

Sekian Terima Kasih. Masih bingung atau ada error silahkan ke Forum

Enjoy this post? Share It!