How to Build a Realtime Spreadsheets Application with Pusher Channels - Part 3: Showing current viewers
Tags: pusher, realtime, php, laravel
Published: 2018-06-06 14:30:54

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:

![](https://d2mxuefqeaa7sj.cloudfront.net/s_E865783628DF374E07AD41774F75FC9C9CEBE398DCA5A3373AADB6CDBB8FE311_1524848803309_building-realtime-spreadsheets-app-laravel-whos-viewing.gif)

## Prerequisites

1. PHP 7.2 or higher, with the MongoDB driver installed. You can find installation instructions [here](http://php.net/manual/en/mongodb.installation.php).
2. Composer
3. MongoDB \(version 3.4 or higher\). Get it [here](https://docs.mongodb.com/manual/installation/).
4. A Pusher account. Create one [here](http://pusher.com/signup).

## 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
```

Or download the source directly from [this link.](https://github.com/shalvah/shoots/archive/part-2-realtime-collaboration.zip)

Then `cd` into the project folder and install dependencies:

```
composer install
```

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
```

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

```
let pusher = new Pusher('your-app-key', {
    cluster: 'your-app-cluster'
});
```

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
```

> 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](https://pusher.com/docs/client_api_guide/client_presence_channels#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**](https://pusher.com/docs/client_api_guide/client_presence_channels#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**](https://pusher.com/docs/client_api_guide/client_presence_channels#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>
```

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

```
<h2>{{ $sheet->name }}</h2>
<div id="sheet"></div>
```

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>
```

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>
```

## 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);
    });
```

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);
    });
```

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>
```

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
```

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.](https://github.com/shalvah/shoots/tree/part-3-whos-viewing)

_This article was originally published on [Pusher's blog](https://pusher.com/tutorials/realtime-spreadsheets-part-3)._