Chat Pagination with Infinite Scrolling

@lukeocodes 🕹👨‍💻 - Feb 4 '20 - - Dev Community

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 Nexmo Conversation Client SDK, which is now delivered paginated from the Conversation API.

Prerequisites

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
Enter fullscreen mode Exit fullscreen mode

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.

Nexmo CLI

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

npm install -g nexmo-cli@beta
Enter fullscreen mode Exit fullscreen mode

Sign up for a free Nexmo account and set up the Nexmo CLI with the API key and secret found on the dashboard.

nexmo setup <your_api_key> <your_api_secret>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Install the npm dependencies.

npm install
Enter fullscreen mode Exit fullscreen mode

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

cp .env.example .env
Enter fullscreen mode Exit fullscreen mode

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

# app config
PORT=3000
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

npm run dev
Enter fullscreen mode Exit fullscreen mode

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 Nexmo, and send or receive messages from the service, you need to configure the demo application.

Create a Nexmo Application

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

nexmo app:create "Nexmo RTC Chat" --capabilities=rtc --rtc-event-url=http://example.com --keyfile=private.key
# Application created: 4556dbae-bf...f6e33350d8
# Credentials written to .nexmo-app
# Private Key saved to: private.key
Enter fullscreen mode Exit fullscreen mode

Create a Nexmo Conversation

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

nexmo conversation:create display_name="Infinite Scrolling"
# Conversation created: CON-a57b0...11e57f56d
Enter fullscreen mode Exit fullscreen mode

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 focusses on styling your message UI in a simple, yet appealing, way.

nexmo user:create name=<USER_NAME> display_name=<DISPLAY_NAME>
# User created: USR-6eaa4...e36b8a47f
Enter fullscreen mode Exit fullscreen mode

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.

nexmo member:add <CONVERSATION_ID> action=join channel='{"type":"app"}' user_id=<USER_ID>
# Member added: MEM-df772...1ad7fa06
Enter fullscreen mode Exit fullscreen mode

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.

nexmo jwt:generate ./private.key sub=<USER_NAME> exp=$(($(date +%s)+86400)) acl='{"paths":{"/*/users/**":{},"/*/conversations/**":{},"/*/sessions/**":{},"/*/devices/**":{},"/*/image/**":{},"/*/media/**":{},"/*/applications/**":{},"/*/push/**":{},"/*/knocking/**":{}}}' application_id=<APPLICATION_ID>
# eyJhbGciOi...XVCJ9.eyJpYXQiOjE1NzM5M...In0.qn7J6...efWBpemaCDC7HtqA
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Nexmo 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 Nexmo 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!');
    }
  }

// ...
Enter fullscreen mode Exit fullscreen mode

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!');
      }
    }
  }

// ...
Enter fullscreen mode Exit fullscreen mode

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.

Nexmo 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;
    // ...
  }

// ...
Enter fullscreen mode Exit fullscreen mode

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 = "";
    // ...
  }

// ...
Enter fullscreen mode Exit fullscreen mode

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

// ...
Enter fullscreen mode Exit fullscreen mode

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!');
      }
    }
  }

// ...
Enter fullscreen mode Exit fullscreen mode

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

// ...
Enter fullscreen mode Exit fullscreen mode

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;

        // ...
// ...
Enter fullscreen mode Exit fullscreen mode

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);
// ...
Enter fullscreen mode Exit fullscreen mode

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

// ...
Enter fullscreen mode Exit fullscreen mode

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.

Nexmo 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 👇

The post Chat Pagination with Infinite Scrolling appeared first on Nexmo Developer Blog.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .