Build read receipts using Laravel

Introduction

Read receipts with realtime updates allow users to track the message without reloading the page. This paves a way to have better collaboration and conversation between friends and collaborators. It brings a dynamic feel to the application interface and improves usability.

Today, we will create a read receipts system that updates in realtime using Laravel and Pusher. With the release of Echo, Laravel has provided an out of the box solution for implementing realtime data synchronisation using event broadcasting. It is simple and we can get started in a matter of minutes.

Setup an app on Pusher

We need to sign up with Pusher (it's free) and create a new app.

read-receipts-laravel-create-app

Install Laravel, Pusher SDK and Echo

First, we will grab a fresh copy of Laravel:

laravel new message-delivery-status-laravel-pusher

This will install the latest version of the Laravel framework and download the necessary dependencies. Next, we will install the Pusher PHP SDK using Composer:

composer require pusher/pusher-php-server

Next, we will install the JavaScript dependencies:

npm install

Now, we need to install two Javascript libraries necessary for realtime event broadcasting: Laravel Echo and Pusher JS

npm install --save laravel-echo pusher-js

We require some form of user authentication mechanism to demonstrate the functionality. Let us use the default authentication scaffolding provided by Laravel:

php artisan make:auth

Configuration

First, we need to set the APP_ID, APP_KEY, APP_SECRET and APP_CLUSTER in the environment file. We can get these details in our Pusher app dashboard:

1# .env
2
3BROADCAST_DRIVER=pusher
4
5PUSHER_APP_ID=your-pusher-app-id
6PUSHER_APP_KEY=your-pusher-app-key
7PUSHER_APP_SECRET=your-pusher-app-secret
8PUSHER_APP_CLUSTER=your-pusher-app-cluster

Next, we need to create a fresh Echo instance in our applications's JavaScript. We can do so at the bottom of our resources/assets/js/bootstrap.js file:

1import Echo from "laravel-echo"
2
3window.Echo = new Echo({
4    broadcaster: 'pusher',
5    key: 'your-pusher-app-key',
6    cluster: 'ap2',
7    encrypted: true
8});

Our application

We will create read receipts in a chat box, similar to what Facebook does. The core feature is the realtime update of status for the messages. We will not cover anything relating to writing CRUD functionality using Laravel. We will concentrate on the code necessary for implementing the live commenting feature. The code is available on a Github repository for cloning and understanding purposes.

Migrations

Next, we need a conversations table where we can store all the messages for the conversation.

php artisan make:model Conversation -m

The conversations table will require the following fields:

  • A field to store the message
  • A field to link the message to the user that created it
  • A field to store the status of the message

Below is the migration for our conversations table:

1use Illuminate\Support\Facades\Schema;
2use Illuminate\Database\Schema\Blueprint;
3use Illuminate\Database\Migrations\Migration;
4
5class CreateConversationsTable extends Migration
6{
7    /**
8     * Run the migrations.
9     *
10     * @return void
11     */
12    public function up()
13    {
14        Schema::create('conversations', function (Blueprint $table) {
15            $table->increments('id');
16            $table->text('message');
17            $table->string('status');
18            $table->unsignedInteger('user_id');
19            $table->timestamps();
20        });
21    }
22
23    /**
24     * Reverse the migrations.
25     *
26     * @return void
27     */
28    public function down()
29    {
30        Schema::dropIfExists('conversations');
31    }
32}

Broadcasting new messages

Whenever a new message is created, we need to fire an event which will be broadcast over Pusher to a specific private channel. For broadcasting an event, it should implement the ShouldBroadcast interface. Let us first create the NewMessage event:

php artisan make:event NewMessage

broadcastWith method

The event should implement a broadcastWith method. This method should return the array of data which the event should broadcast.

1namespace App\Events;
2
3use App\Conversation;
4use Illuminate\Broadcasting\InteractsWithSockets;
5use Illuminate\Broadcasting\PrivateChannel;
6use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
7use Illuminate\Foundation\Events\Dispatchable;
8use Illuminate\Queue\SerializesModels;
9
10class NewMessage implements ShouldBroadcast
11{
12    use Dispatchable, InteractsWithSockets, SerializesModels;
13
14    private $conversation;
15
16    /**
17     * Create a new event instance.
18     *
19     * @return void
20     */
21    public function __construct(Conversation $conversation)
22    {
23        $this->conversation = $conversation;
24    }
25
26    public function broadcastWith()
27    {
28        return [
29            'id' => $this->conversation->id,
30            'message' => $this->conversation->message,
31            'status' => $this->conversation->status,
32            'user' => [
33                'name' => $this->conversation->user->name,
34                'id' => $this->conversation->user->id,
35            ],
36        ];
37    }
38
39    /**
40     * Get the channels the event should broadcast on.
41     *
42     * @return Channel|array
43     */
44    public function broadcastOn()
45    {
46        return new PrivateChannel('chat');
47    }
48}

Next, we need to start our queue to actually listen for jobs and broadcast any events that are recorded. We can use the database queue listener on our local environment:

php artisan queue:listen

Broadcast the event

Whenever a new message is created, we will broadcast the NewMessage event using the broadcast helper:

1public function store()
2{
3    $conversation = Conversation::create([
4        'message' => request('message'),
5        'status' => 'Sent',
6        'user_id' => auth()->user()->id
7    ]);
8
9    broadcast(new NewMessage($conversation))->toOthers();
10
11    return $conversation->load('user');
12}

If we do not use the toOthers method, then the event would also be broadcasted to the user who has created it. This would create a list of duplicate messages.

toOthers allows you to exclude the current user from the broadcast's recipients.

Listening for new messages on the private channel

Installation and configuration of Laravel Echo is a must before we can start listening to new messages. We have covered the process in detail in the above section of this article. Please go through it if you might have skipped it.

1Echo.private('chat')
2    .listen('NewMessage', (e) => {
3        this.conversations.push(e);
4    })

Broadcasting status updates

Whenever a new message is delivered to other users, we need to fire an event which will notify the sender that the message was delivered successfully. First, we will update the status of the message as delivered. Let us modify the above code in the Vue component to update the status, once the message is received.

1Echo.private('chat')
2    .listen('NewMessage', (e) => {
3        this.conversations.push(e);
4        axios.post('/conversations/'+ e.id +'/delivered');
5    })

Now, let us update the status in the database and broadcast the MessageDelivered event:

1class MessageDeliveredController extends Controller
2{
3    public function __invoke(Conversation $conversation)
4    {
5        $conversation->status = 'Delivered';
6        $conversation->save();
7
8        broadcast(new MessageDelivered($conversation));
9    }
10}

Following is the the event which is responsible for broadcasting the updated status data:

1namespace App\Events;
2
3use App\Conversation;
4use Illuminate\Broadcasting\InteractsWithSockets;
5use Illuminate\Broadcasting\PrivateChannel;
6use Illuminate\Contracts\Broadcasting\ShouldBroadcast;
7use Illuminate\Foundation\Events\Dispatchable;
8use Illuminate\Queue\SerializesModels;
9
10class MessageDelivered implements ShouldBroadcast
11{
12    use Dispatchable, InteractsWithSockets, SerializesModels;
13
14    private $conversation;
15
16    /**
17     * Create a new event instance.
18     *
19     * @return void
20     */
21    public function __construct(Conversation $conversation)
22    {
23        $this->conversation = $conversation;
24    }
25
26    public function broadcastWith()
27    {
28        return [
29            'id' => $this->conversation->id,
30            'message' => $this->conversation->message,
31            'status' => $this->conversation->status,
32            'user' => [
33                'name' => $this->conversation->user->name,
34                'id' => $this->conversation->user->id,
35            ],
36        ];
37    }
38
39    /**
40     * Get the channels the event should broadcast on.
41     *
42     * @return Channel|array
43     */
44    public function broadcastOn()
45    {
46        return new PrivateChannel('chat');
47    }
48}

Listening for updates on the private channel

Next we find and update the conversation object on Vue side to update the status:

1Echo.private('chat')
2    .listen('MessageDelivered', (e) => {
3        _.find(this.conversations, { 'id': e.id }).status = e.status;
4    });

Authorization

Every private channel needs to be authenticated. Laravel Echo will automatically call the specified authentication route but we still need to write the authentication logic which will actually authorize the user to listen to a particular channel.

Authorization logic is written in the routes/channels.php. We will return true but you are free to write your own authorization code here:

1Broadcast::channel('chat', function ($user) {
2    return true;
3});

Vue.js component

That's it! Now, whenever a new message is delivered, it will be broadcast and we can listen using our private channel to update the status in realtime.

Below is our Example component written using Vue.js

1<script>
2    export default {
3        props: ['user'],
4        data() {
5            return {
6                'message': '',
7                'conversations': []
8            }
9        },
10        mounted() {
11            this.getConversations();
12            this.listen();
13        },
14        methods: {
15            sendMessage() {
16                axios.post('/conversations', {message: this.message})
17                    .then(response => this.conversations.push(response.data));
18            },
19            getConversations() {
20                axios.get('/conversations').then((response) => this.conversations = response.data);  
21            },
22            listen() {
23                Echo.private('chat')
24                    .listen('NewMessage', (e) => {
25                        this.conversations.push(e);
26                        axios.post('/conversations/'+ e.id +'/delivered');
27                    })
28                    .listen('MessageDelivered', (e) => {
29                        _.find(this.conversations, { 'id': e.id }).status = e.status;
30                    });
31            }
32        }
33    }
34</script>

Below is the image demonstrating the workflow of our system:

Conclusion

In this article, we have covered how to create a read receipts feature in realtime using Laravel and Pusher. We have covered the configuration options necessary to get started, and the example above should help you fill in the gaps and give an overview of some of the other configuration options available to you.