Chat Pagination with Infinite Scrolling
Published on February 3, 2020

Following on from the previous post Create a Simple Messaging UI with Bootstrap, this article will show you how to load older messages from the conversation using the Vonage Conversation Client SDK, which is now delivered paginated from the Conversation API.

Prerequisites

Vonage API Account

To complete this tutorial, you will need a Vonage API account. If you don’t have one already, you can sign up today and start building with free credit. Once you have an account, you can find your API Key and API Secret at the top of the Vonage API Dashboard.

Node & NPM

To get started, you're going to need Node and NPM installed. This guide uses Node 8 and NPM 6. Check they're installed and up-to-date.

node --version npm --version

Both Node and NPM need to be installed and at the correct version. Go to nodejs.org and install the correct version if you don't have it.

Vonage CLI

To set up your application, you'll need to install the Vonage CLI. Install it using NPM in the terminal.

npm install @vonage/cli -g

You can find your API key and secret on the Dashboard which are used to set up the Vonage CLI.

vonage config:set --apiKey=VONAGE_API_KEY --apiSecret=VONAGE_API_SECRET

The Vonage CLI has plugins that when installed, provide additional capabilities. In this tutorial, you will be working with Conversations, so here is the command to install its plugin:

vonage plugins:install @vonage/cli-plugin-conversations

Git (Optional)

You can use git to clone the demo application from GitHub.

For those uncomfortable with git commands, don't worry, I've you covered. This guide contains instructions on downloading the project as a ZIP file.

Follow this guide to install git.

Getting Started

Based on the finished app from the last tutorial, there is a new starting demo application. Clone and install it by following these steps.

Get the Demo App

git clone https://github.com/nexmo-community/infinite-scrolling-pagination.git

For those not comfortable with git commands, you can download the demo application as a zip file and unpack it locally.

Once cloned or unpacked, change into the new demo application directory.

cd infinite-scrolling-pagination

Install the npm dependencies.

npm install

Configure the application port using an environment file. Copy the example file:

cp .env.example .env

Now, edit the environment file .env and set the port to 3000 (or whichever port you require).

# app config PORT=3000

Among other packages installed by your last command, there is a package called nodemon, that allows you to reload your application if you edit any files automatically.

To start the application in the standard way, run:

npm start

To start the application, but with nodemon instead, run:

npm run dev

Tip: If you're running the application with nodemon for the remainder of this tutorial, whenever I suggest restarting the application, you won't need to do that because nodemon does it for you. However, if you need to reauthenticate with the application, you will still need to do that, as the session information is stored in memory and not configured to use any other storage.

Configure The Demo App

To connect to Vonage, and send or receive messages from the service, you need to configure the demo application.

Create a Vonage Application

Firstly, create a Vonage Application with RTC (real-time communication) capabilities. The event URL will be a live log of events happening on the Vonage service, like users joining/leaving, sending messages, enabling audio (if you felt like enabling it).

vonage apps:create "Vonage RTC Chat" --rtc_event_url=http://example.com

Create a Vonage Conversation

Secondly, create a Vonage Conversation, which acts like a chatroom. Or, a container for messages and events.

vonage apps:conversations:create "Infinite Scrolling"

Create Your User

Now, create a user for yourself.

Note: In this demo, you won't chat between two users. Other guides show you how to create conversations between multiple users. This guide focuses on styling your message UI in a simple, yet appealing, way.

vonage apps:users:create USER_NAME --display_name=DISPLAY_NAME

Add the User to a Conversation

Next, add your new user to the conversation. A user can be a member of an application, but they still need to join the conversation.

vonage apps:conversations:members:add CONVERSATION_ID USER_ID

Generate a User Token

Lastly, generate your new user a token. This token represents the user when accessing the application. This access token identifies them, so anyone using it will be assumed to be the correct user.

In practice, you'll configure the application with this token. In production, these should be guarded, kept secret, and very carefully exposed to the client application, if at all.

vonage jwt --key_file=./vonage_rtc_chat.key --acl='{"paths":{"/*/users/**":{},"/*/conversations/**":{},"/*/sessions/**":{},"/*/devices/**":{},"/*/image/**":{},"/*/media/**":{},"/*/applications/**":{},"/*/push/**":{},"/*/knocking/**":{},"/*/legs/**":{}}}' --subject=USER_NAME --app_id=APP_ID

Configure the Application

Having generated all the parts you'll need, edit the views/layout.hbs file and find the JavaScript shown here.

<script>
      var userName = '';
      var displayName = '';
      var conversationId = '';
      var clientToken = '';
    </script>

Edit the config with the values you've generated in the commands above.

<script>
      var userName = 'luke'; // <USER_NAME>
      var displayName = 'Luke Oliff'; // <DISPLAY_NAME>
      var conversationId = 'CON-123...y6346'; // <CONVERSATION_ID>
      var clientToken = 'eyJhbG9.eyJzdWIiO.Sfl5c'; // this will be much much longer
    </script>

Now configured, start the application and access it using the default application URL.

Note: This is only a demo and you should not be hard coding credentials into any application, especially one that exposes them to the client.

Vonage Chat Simple Messaging UIVonage Chat Simple Messaging UI

Prepare a Message History

Because you need more messages to scroll through, create some message history by sending multiple messages to the client. The default page size is 20 items, so create more than 20 messages. I recommend creating 60 test messages so you can load 2 whole pages of history.

Adding Pagination to the App

The default settings for the application only returns 20 items from the conversation's past events. Now, it's time to add pagination to the application so users can load older events.

What Is Pagination?

Pagination, or paging, is how an application divides the content into multiple pages. When implemented in an APIs design, it allows for the delivery of manageable collections of results, that can usually be navigated programmatically. SDKs like the Vonage Conversation Client SDK are no different, often extending the APIs pagination functionality into friendly methods that make pagination more straightforward.

The User Experience

Some applications offer links like 'next' or 'previous', or page numbers. But that isn't what you'll implement here. As the messages in a chat channel are a continuous stream of conversation, this app will allow users to just keep scrolling through historical messages. This is done using a concept known as infinite scrolling. As you scroll through older messages and get to the end, the app will request the next page of history and slot them in. In older channels with a lot of history, this will give the feeling of being able to scroll forever or infinite scrolling.

The Code

Now, you're going to write some code. Here, you'll make changes to detect the scroll position of your message list, and load more messages when you reach the oldest message. The oldest message will be shown at the very top of the window.

Scrolling to the Top

To detect when you scroll to the top, you need to add a new event. Edit the public/javascripts/chat.js file and add the following code under the setupUserEvents() method.

// public/javascripts/chat.js

// ...

  setupUserEvents() {

    // ...

    this.messageFeed.addEventListener("scroll", () => {
        alert('scrolling!');
    }
  }

// ...

You can test this in the browser, where you'll quickly discover why it's not very helpful. This code adds an event listener to the messageFeed element, meaning that every time you try to scroll it triggers a pop-up. Not what you want!

So, change it slightly. Add the following code above the setupUserEvents() method and modify your new event listener as shown.

// public/javascripts/chat.js

// ...

  isFeedAtTop() {
    return 0 === this.messageFeed.scrollTop;
  }

  setupUserEvents() {

    // ...

    this.messageFeed.addEventListener("scroll", () => {
      if (this.isFeedAtTop()) {
        alert('scrolling!');
      }
    }
  }

// ...

This new change creates a new method that detects where the scroll position of the messageFeed is at 0, zero, or the very start at the top of the message history. More useful! Now, you know when someone reaches the oldest message at the top of the message list.

Vonage Chat Alert When Scrolling to the TopVonage Chat Alert When Scrolling to the Top

Who Are You

To attribute new messages to a user when they're loaded from the conversation history, you should store. Editing the public/javascripts/chat.js file, add the following line after the line this.conversation = conversation;.

// public/javascripts/chat.js

// ...

  setupConversationEvents(conversation, user) {
    // ...
    this.user = user;
    // ...
  }

// ...

Store the Page Context

To load more messages from the message history, you need to know what page was last loaded. To do this, still editing the public/javascripts/chat.js file, change the existing showConversationHistory as shown below to store the most recent event page on the application.

// public/javascripts/chat.js

// ...

  showConversationHistory(conversation, user) {
    // ...
      .then((eventsPage) => {
        this.lastPage = eventsPage;
        var eventsHistory = "";
    // ...
  }

// ...

If it's not clear how the showConversationHistory method should look after the change, here is the entire method with the change applied.

// public/javascripts/chat.js

// ...

  showConversationHistory(conversation, user) {
    conversation
      .getEvents({ page_size: 20, order: 'desc' })
      .then((eventsPage) => {
        this.lastPage = eventsPage;
        var eventsHistory = "";

        eventsPage.items.forEach((value, key) => {
          if (conversation.members.get(value.from)) {
            switch (value.type) {
              case 'text':
                eventsHistory = this.senderMessage(user, conversation.members.get(value.from), value) + eventsHistory;
                break;
              case 'member:joined':
                eventsHistory = this.memberJoined(conversation.members.get(value.from), value) + eventsHistory;
                break;
            }
          }
        });

        this.messageFeed.innerHTML = eventsHistory + this.messageFeed.innerHTML;

        this.scrollFeedToBottom();
      })
      .catch(this.errorLogger);
  }

// ...

The idea of this method is to store the EventsPage returned from calling getEvents, so that the app can use it again later on.

With this change in place, the application is now aware of the most recent page.

Avoid Unnecessary Requests

One method on the EventsPage object is hasNext, which returns true if there are more events to load.

With the hasNext method, edit the scrolling event you added earlier to add this.lastPage.hasNext() to the condition around our alert.

// public/javascripts/chat.js

// ...

  setupUserEvents() {

    // ...

    this.messageFeed.addEventListener("scroll", () => {
      if (this.isFeedAtTop() && this.lastPage.hasNext()) {
        alert('scrolling!');
      }
    }
  }

// ...

Now, you'll only get an alert if there is another page of events to load.

Load the Next Page

To load the next page, replace the alert in your event listener with the code shown below:

// public/javascripts/chat.js

// ...

        this.lastPage
          .getNext()
          .then((eventsPage) => {
            this.lastPage = eventsPage;
            var moreEvents = "";

            eventsPage.items.forEach((value, key) => {
              if (this.conversation.members.get(value.from)) {
                switch (value.type) {
                  case 'text':
                    moreEvents = this.senderMessage(this.user, this.conversation.members.get(value.from), value) + moreEvents;
                    break;
                  case 'member:joined':
                    moreEvents = this.memberJoined(this.conversation.members.get(value.from), value) + moreEvents;
                    break;
                }
              }
            });

            this.messageFeed.innerHTML = moreEvents + this.messageFeed.innerHTML;
          })
          .catch(this.errorLogger);

// ...

This code uses this.lastPage that was stored on the application earlier in the article, and requests getNext which returns a new EventsPage.

The rest of the code seen here overwrites this.LastPage with the latest page, and performs near-enough the same function of the showConversationHistory method that renders historical messages when the page is loaded, adding them to the top of the messageFeed.

Fix the Scroll Position

With infinite scrolling in place, you'll notice that new messages get added to the top, but you're still looking at the top of the messageFeed, losing the position of where you were in the channel's message history. To fix this, you're going to reuse the scrollTo method already found inside the public/javascripts/chat.js file.

Previously, scrollTo was used to scroll to the bottom of the messages, which is achieved by any number larger than the height of the messageFeed. This team, you need to scroll to a specific point on the messageFeed.

If the position was when the application loaded new messages was 0 at the top, then it would make sense to scroll to the difference between the height before and after the messageFeed was updated.

Inside the condition that checks scroll position and hasNext, but before the the.lastPage.getNext() code is ran, add the code to store the scrollHeight, as shown here:

// public/javascripts/chat.js

// ...
      if (this.isFeedAtTop() && this.lastPage.hasNext()) {
        this.scrollHeight = this.messageFeed.scrollHeight;

        // ...
// ...

Now, in this same function, after the line that updates the messageFeed.innerHTML with moreEvents, add this line too:

// public/javascripts/chat.js

// ...
            // ...

            this.scrollTo(this.messageFeed.scrollHeight-this.scrollHeight);
// ...

If it's not clear how the "scroll" event listener should look after the change, here is the code in its entirety:

// public/javascripts/chat.js

// ...

    // ...

    this.messageFeed.addEventListener("scroll", () => {
      if (this.isFeedAtTop() && this.lastPage.hasNext()) {
        this.scrollHeight = this.messageFeed.scrollHeight;

        this.lastPage
          .getNext()
          .then((eventsPage) => {
            this.lastPage = eventsPage;
            var moreEvents = "";

            eventsPage.items.forEach((value, key) => {
              if (this.conversation.members.get(value.from)) {
                switch (value.type) {
                  case 'text':
                    moreEvents = this.senderMessage(this.user, this.conversation.members.get(value.from), value) + moreEvents;
                    break;
                  case 'member:joined':
                    moreEvents = this.memberJoined(this.conversation.members.get(value.from), value) + moreEvents;
                    break;
                }
              }
            });

            this.messageFeed.innerHTML = moreEvents + this.messageFeed.innerHTML;

            this.scrollTo(this.messageFeed.scrollHeight-this.scrollHeight);
          })
          .catch(this.errorLogger);
      }
    });

// ...

With any luck, when you try it out, you'll discover messages will seemingly load above your scroll position, allowing you to scroll 'to infinity', or the top.

Vonage Chat Infinite Scrolling to the TopVonage Chat Infinite Scrolling to the Top

The End

This article followed on from the previous post Create a Simple Messaging UI with Bootstrap, showing you how to load older messages as you scroll through the message history.

Don't forget, if you have any questions, feedback, advice, or ideas you'd like to share with the broader community, then please feel free to jump on our Community Slack workspace or pop a reply below 👇.

Luke OliffVonage Alumni

Friendly tech educator, family man, diversity champion, probably argue a bit too much. Formerly a backend engineer. Talk to me about JavaScript (frontend or backend), the amazing Vue.js, DevOps, DevSecOps, anything JamStack. Writer on DEV.to

Ready to start building?

Experience seamless connectivity, real-time messaging, and crystal-clear voice and video calls-all at your fingertips.

Subscribe to Our Developer Newsletter

Subscribe to our monthly newsletter to receive our latest updates on tutorials, releases, and events. No spam.