DEV Community

Shalvah
Shalvah

Posted on • Updated on • Originally published at blog.shalvah.me

How to Build a Realtime Spreadsheets Application with Pusher Channels - Part 3: Showing current viewers

In the first two parts of this series, we built a spreadsheets app that supports simultaneous editing by multiple users, syncing changes across all clients in realtime.

In this article, we’ll add another useful piece of functionality present in Google Sheets, the ability to see who’s viewing the sheet in realtime. We’ll display avatars corresponding to each user currently viewing the sheet. Here’s how that will look in our app:

Prerequisites

  1. PHP 7.2 or higher, with the MongoDB driver installed. You can find installation instructions here.
  2. Composer
  3. MongoDB (version 3.4 or higher). Get it here.
  4. A Pusher account. Create one here.

Setting up

Note: if you followed through with the second part and still have your project code, you can skip this section.

Clone the project from GitHub:

git clone -b part-2-realtime-collaboration https://github.com/shalvah/shoots
Enter fullscreen mode Exit fullscreen mode

Or download the source directly from this link.

Then cd into the project folder and install dependencies:

composer install
Enter fullscreen mode Exit fullscreen mode

Lastly, copy the .env.example to a new file called .env. Add your Pusher app credentials to the .env file:

PUSHER_APP_ID=your-app-id
PUSHER_APP_KEY=your-app-key
PUSHER_APP_SECRET=your-app-secret
PUSHER_APP_CLUSTER=your-app-cluster
Enter fullscreen mode Exit fullscreen mode

Look for these lines of JavaScript in resources/views/spreadsheet.blade.php:

let pusher = new Pusher('your-app-key', {
    cluster: 'your-app-cluster'
});
Enter fullscreen mode Exit fullscreen mode

Insert your Pusher app key and cluster in the appropriate places.

Run the following command to generate an application encryption key:

php artisan key:generate
Enter fullscreen mode Exit fullscreen mode

Note: if your MongoDB server requires a username and password, add those in your .env file as the DB_USERNAME and DB_PASSWORD respectively.

Implementing presence with Pusher

In the previous part of this series, we subscribe each user to a presence channel when they open a sheet. The name of this channel is tied to the sheet’s ID. This presence channel will provide the core of our “Now Viewing” functionality, because of the information presence channels provide us with about who is subscribed to a channel. We’ll hook into certain presence events fired by Pusher on the channel.

Here’s how we’ll do this:

  • When the user successfully subscribes to the channel, the event [**pusher:subscription_succeeded**](https://pusher.com/docs/client_api_guide/client_presence_channels#pusher-subscription-succeeded) is triggered. The included payload contains a list of all the subscribed members. We’ll listen for this event and use the payload to display the avatars of everyone who was viewing the sheet when we opened it.
  • When a new user joins the channel (someone else opens the sheet), the event pusher:member_added ****is triggered. We’ll listen for this and add the avatar of the new member to our UI.
  • When a user leaves the channel (closes the browser window), the event pusher:member_removed ****is triggered. When this happens, we’ll remove the avatar of the member who left from our UI.

Let’s go!

Creating the markup

First, let’s create the markup for our avatars. The avatar is essentially the user’s initials on a colored circular background (similar to Google’s default user avatars). When we hover over it, it should display a tooltip containing the name of the user.

We’ll use Bootstrap and jQuery to style our avatars and tooltips. Open up your resources/views/spreadsheet.blade.php and include the Bootstrap and jQuery assets before the closing </head> tag:

<head>
  ...

  <link
    rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" 
    integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  <script src="https://code.jquery.com/jquery-3.3.1.min.js"
    integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
    crossorigin="anonymous">
  </script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"
    integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa"
    crossorigin="anonymous">
  </script>
</head>
Enter fullscreen mode Exit fullscreen mode

You should have code like this shortly after the closing </head> tag:

<h2>{{ $sheet->name }}</h2>
<div id="sheet"></div>
Enter fullscreen mode Exit fullscreen mode

Modify it so it looks like this:

<h2>{{ $sheet->name }}</h2>
<p>
  <span style="float: right; margin-right: 50px; margin-bottom: 40px; font-size: 16px;">Now viewing: <span id="viewers"></span>
  </span>
</p>
<br> <br>

<div id="sheet"></div>
Enter fullscreen mode Exit fullscreen mode

Now, add the styles for the avatar class just before the closing </head> tag:

<style>
    .avatar {
        color: rgb(255, 255, 255);
        background-color: #fc0093;
        display: inline-block;
        font-family: Arial, sans-serif;
        font-size: 20px;
        border-radius: 50%;
        width: 36px;
        height: 36px;
        text-align: center;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

Responding to presence events

Let’s modify our Pusher subscription code to respond to the events we considered above. In your resources/views/spreadsheet.blade.php, you should have code like this:

pusher.subscribe("{{ $sheet->channel_name }}")
    .bind('updated', function (message) {
        let [rowIndex, columnIndex, oldValue, newValue] = message.change;
        addCellValue(rowIndex, columnIndex, newValue);
        table.loadData(sheetContent);
    });
Enter fullscreen mode Exit fullscreen mode

Modify those lines to look like this:

pusher.subscribe("{{ $sheet->channel_name }}")
    .bind('pusher:subscription_succeeded', (data) => {
        Object.entries(data.members)
            .forEach(([id, member]) => addViewer(member));
    })
    .bind('pusher:member_added', (member) => addViewer(member.info))
    .bind('pusher:member_removed', (member) => removeViewer(member))
    .bind('updated', function (message) {
        let [rowIndex, columnIndex, oldValue, newValue] = message.change;
        addCellValue(rowIndex, columnIndex, newValue);
        table.loadData(sheetContent);
    });
Enter fullscreen mode Exit fullscreen mode

The last thing for us to do is to implement the addViewer and removeViewer functions. Add the following code to the end of your resources/views/spreadsheet.blade.php:

<script>
    function addViewer(viewer) {
        const userInitials = viewer.name.split(' ')
            .reduce((initials, name) => {
                initials.push(name[0]);
                return initials;
            }, []).join('');
        let $avatar = $('<span>')
            .addClass('avatar')
            .attr('data-toggle', 'tooltip')
            .attr('id', `avatar-${viewer._id}`)
            .attr('title', viewer.name)
            .text(userInitials);
        $('#viewers').append($avatar);
        // enable the tooltip
        $avatar.tooltip();
    }

    function removeViewer(viewer) {
        $(`#avatar-${viewer.id}`).remove();
    }
</script>
Enter fullscreen mode Exit fullscreen mode

The addViewer function creates the avatar element and adds it to the DOM using jQuery, while the removeViewer locates the avatar corresponding to the specified viewer and removes it.

All done! Let’s test our application. Start your MongoDB server by running mongod. (On Linux/macOS, you might need to run it as sudo).

Then start your app by running:

php artisan serve
Enter fullscreen mode Exit fullscreen mode

Sign in to your app at http://localhost:8000 and create a new spreadsheet. In a different browser, sign up as a new user at http://localhost:8000/register. Then copy the URL of the spreadsheet you created in the first browser and open it in the second browser. You should see that the user avatars are displayed at the top right corner as they open and close the sheet.

Conclusion

Thus far, we’ve been able to build a spreadsheets application with realtime collaboration and a “Now viewing” feature, thanks to Pusher. In the next part, we’ll see how we can use Pusher to add another nifty Google Sheets feature—automatically deleting empty documents when they’re closed. Stay tuned!

You can check out the source code of the app on GitHub.

This article was originally published on Pusher's blog.

Top comments (0)