React Chat Tutorial 3 Buddy List

React.js Chat Tutorial: Realtime User List with User Presence (3/4)

This is Part Three of our four part series on building a chat app with React and PubNub.

In this tutorial, we’ll use the PubNub Presence API to build a realtime user list (ie. a buddy list), that displays how many users are logged into your chat, who each user is, and their online/offline status.

Below is an overview for working with a Redux architecture:

  • Write any of the UI elements the feature might need.
  • Determine what state the feature requires and either extend an existing reducer or create a new one.
  • Determine if there are any actions that will trigger state change and create them.
  • Complete the data flow for the new features (react to UI events and store changes).

Configuring PubNub Presence

To show who’s online, you need to ensure that PubNub presence is activated. To do this, go to the admin dashboard, select your app, view application add-ons, and enable the presence feature.

When enabled, subscribing to a channel generates a join event. Unsubscribing generates a leave event along with a timeout. It’s that simple.

PubNub User ID’s

Next thing you need to do is update PubNub with the UserID. Until now, it’s been private and only available when sending a message. To see who is online, we need to pass our UserID.

Each connection to the PubNub network is assigned a universally unique ID to ensure data goes only to the desired recipients without accidentally sending anything to the wrong client. Additional data can be associated with a UUID using the PubNub’s state property on subscribe().

Open the container in src/containers/app.js and add this code to the init object in componentDidMount():

    uuid: ID,

This will set the UUID to be our UserID. We do this because the default UUID is shared amongst tabs and our ID changes during each load.

Reducers and Actions

Before we start collecting data from PubNub about users, we’ll need a place to store the data. Let’s build out the Redux part of our application that will handle the users.

First, let’s think about what we’ll need for user management. In regards to what data we’ll be saving, we’ll need their UserID (so we can show an avatar and name). All we need is an array that will hold the UserIDs. Open the reducer in src/reducers/app.js and add a users array to INITIAL_STATE:

    const INITIAL_STATE = fromJS({
      userID: 0,
      messages: [],
      lastMessageTimestamp: null,
      users: [],
    });

Next, for actions, we’ll need some way to add and remove users from the object. That means we’ll need a couple of constants for these actions.

Open the constants file in src/constants/index.js and add the following lines:

    export const ADD_USER = 'App/ADD_USER';
    export const REMOVE_USER = 'App/REMOVE_USER';

Next, open the actions file in src/actions/index.js and add the two constants to our constants list:

    import {
      ADD_MESSAGE,
      SET_CURRENT_USERID,
      ADD_HISTORY,
      ADD_USER,
      REMOVE_USER,
    } from '../constants';

Finally, add these two actions to the end of the file:

    export function addUser(userID) {
      return {
        type: ADD_USER,
        payload: userID,
      };
    }

    export function removeUser(userID) {
      return {
        type: REMOVE_USER,
        payload: userID,
      };
    }

Now we need to connect the actions to the reducer. Open the reducer in src/reducers/app.js and append the constants to the import at the top:

    import {
      ADD_MESSAGE,
      SET_CURRENT_USERID,
      ADD_HISTORY,
      ADD_USER,
      REMOVE_USER,
    } from '../constants';

Add the following lines of code before the default case:

  case ADD_USER:
    return state
      .update('users', (users) => (users.indexOf(action.payload) >= 0 ?
users :     
users.concat(action.payload)));
  case REMOVE_USER:
    return state
      .update('users', (users) => users.delete(action.payload));

That’s it, now we just need to provide the actions and user’s data to our container.

Open the container at src/containers/app.js and append our new actions to the import statement:

    import {
      setCurrentUserID,
      addMessage,
      addHistory,
      addUser,
      removeUser,
    } from '../actions';

Now update the mapStateToProps() to pass in our users array by appending this line:

    users: state.app.get('users').toJS(),

Next, update mapDispatchToProps() to add our two new actions:

    addUser: (userID) => dispatch(addUser(userID)),
    removeUser: (userID) => dispatch(removeUser(userID)),

As always, we need to update the propTypes in the App class by appending these lines:

    users: React.PropTypes.array,
    addUser: React.PropTypes.func,
    removeUser: React.PropTypes.func,

Data Flow

Now that we have our actions and data, we can connect those events that call the actions to populate the data. Later, we’ll create a component for displaying the user’s data.

The events we are interested in include:

  • When you leave the chat
  • When someone else joins, leaves, or times out
  • When you join the chat.
  • Populating the user list when we join
  • Hot Module Reloader (HMR) events

Before discussing the event handling, we should quickly talk about hot module reloading. You may be familiar with the idea of live-reloading a web app after the code changes. HMR takes this concept a step further and live-reloads only the parts of a web application that have changed. This can be significantly faster than a full page refresh.

Let’s start with the first event – when you leave the chat. This happens when you close the browser window or tab. In addition, you could have the HMR unmount and mount a new container. This means there are two places we’ll need to add our code to leave the channel. To leave a channel, we

To leave a channel, we call unsubscribe(). You’ll notice we already have a subscribe() call, in componentDidMount. This means we’ll need to add a componentWillUnmount that will unsubscribe the user. However, while this will handle the case of the HMR, it won’t be called when the browser window or tab is closed. For this, we’ll use the onBeforeUnload event from the window.

Once we have subscribed, we should set up our event listener for unsubscribing. Create a function called leaveChat that will call unsubscribe by adding the following function after the render function:

leaveChat = () => {
  this.PubNub.unsubscribe({ channel: 'ReactChat' });
}

Next, add a componentWillUnmount above the render function that will call leaveChat:

componentWillUnmount() {
  this.leaveChat();
}

Finally, add the onBeforeUnload event handler to the end of componentDidMount:

window.addEventListener('beforeunload', this.leaveChat);

Now we can communicate to PubNub when leaving the chat. Next, we’ll need a way to detect when others join or leave. To do this, create an event handler which will be added to the subscribe() call.

Let’s a create a function that will handle all changes to the user’s presence. Add the following function before the render function:

    onPresenceChange = (presenceData) => {
      switch (presenceData.action) {
      case 'join':
        break;
      case 'leave':
      case 'timeout':
        break;
      default:
        console.error('unknown action: ' + presenceData.action);
      }
    }

Next, hookup this function to the subscribe() call by adding this to the object passed into subscribe:

    presence: this.onPresenceChange,

Your subscribe should look like this:

    this.PubNub.subscribe({
      channel: 'ReactChat',
      message: this.props.addMessage,
      presence: this.onPresenceChange,
    });

Note that this presence callback will fire a join event for the current user. We won’t need to add additional code to add ourselves, unlike the unsubscribe.

You’ll also notice in the onPresenceChanged event handler that a switch statement on the action field has been setup. Right now we’re only interested in: join, leave, and timeout. A timeout might occur if you were to lose your internet connection. Since we want to remove the user from our roster when they leave or time-out, we’ve grouped them together.

In the presenceData, we can find our state that we assigned in subscribe as data; this will be useful for getting the userID for adding a user.

Add the following line to the join case of onPresenceChange

    this.props.addUser(presenceData.uuid);

and add this line for the leave/timeout case:

    this.props.removeUser(presenceData.uuid);

The data flow is now successfully hooked up. The store contains a list of UserIDs in the users array. Next, we need to populate our roster component with the user’s data.

Displaying Connected Users

The final piece is to display the users who are connected by creating a component called ChatUsers.

To do this, create a new file called ChatUsers.js in src/components/ and add the import for React:

    import * as React from 'react';

Next, define a class for this component:

    class ChatUsers extends React.Component {
 
    }
    export default ChatUsers;

We know that our component will need to display a list of users and the data we’ll be passing in will be an array of User IDs. That means we’ll need to create a propType for that. Append this to the class:

    static propTypes = {
      users: React.PropTypes.array,
    }

Then add the render function:

  render() {
    const { users } = this.props;
    return (
      <div className="online-user-list">
        <div className="online-users-number valign-wrapper">
          <i className="material-icons">people</i>
          <span className="valign">{ users.length } online</span>
        </div>
        <ul>{
          users.map((userID) => {
            const name = 'Anonymous Robot #' + userID;
            const imgURL = '//robohash.org/' + userID + '?set=set2&bgset=bg2&size=70x70';
            return (
              <li key={ userID }>
                <img title={ name } alt={ name } src={ imgURL } className="circle" />
              </li>
            );
          })
        }</ul>
      </div>
    );
  }

This may seem overly extensive but it’s not. Two things are displayed – a total count and a list of icons for the users. First the users array is plucked from props and then we use the length of the array for displaying the count. Finally, the users are iterated over to create a list item for each one, which consists of an image for their avatar. The array.map function was used so that we can transform the userIDs into JSX elements.

That’s it for our component, now we need to use it.

Bringing It All Together

As the user’s data is now in the app container and a ChatUsers component is defined, all that’s left is to join them.

To do this, open the container source in src/containers/app.js and add an import for the ChatUsers component we just built:

    import ChatUsers from '../components/ChatUsers';

Next, update the render function to include this line above ChatHistory:

    <ChatUsers users={ props.users } />

Finally, update the CSS. Open the stylesheet in src/styles/index.css.

Since the layout of the app has changed, we’ll need to modify the styles.

Replace all the styles in this file with these ones:

    .message-container {
      height: 100vh;
      flex-direction: column;
    }

    .message-container, .message-form {
      display: flex;
    }

    .message-form {
      flex: 1;
      max-height: 136px;
      background-color: #009688 !important;
      color: white;
    }

    .message-list {
      flex: 1;
      overflow: auto;
      margin-top: 0;
      margin-bottom: 0;
    }

    .message-form .input-field input {
      border-color: #FFFFFF;
    }

    .chip {
      font-style: italic;
    }

    .mdi-communication-chat {
      color: white;
    }

    .collection .collection-item:last-child {
      border-bottom: 0.1em solid #DCDCDC;
    }

    /* -------- Online user list ---------- */
    .online-user-list, .online-user-list ul {
      display: flex;
    }

    .online-user-list {
      margin: 0;
      height: 68px;
      background-color: #009688 !important;
    }

    .online-user-list ul {
      flex: 1;
      flex-direction: row;
      flex-wrap: nowrap;
      margin: 5px;
    }

    .online-user-list li{
      display: inline-block;
    }

    .online-user-list img{
      margin: 4px;
      width: 50px;
    }

    .online-users-number{
      color: white;
      margin-left: 10px;
      margin-right: 10px;
    }

    .online-users-number i{
      font-size: 50px;
      padding-right: 10px;
    }

To update the ChatInput to change a class, open src/components/ChatInput.js and find the footer tag in the render function. Replace the className="teal" with className="message-form".

Then update the ChatHistory component to add a new class by opening src/components/ChatHistory.js and locating <ul> in the render function. It should have a className of 'collection.' Change that to be collection message-list. Look for <li> with the className of 'collection avatar' and add 'message-item' so that it’s 'collection-item message-item avatar'.

Then open the app container in src/containers/app.js and add the className of ’message-container’ to the <div> in the render function.

These JSX changes complete the style changes we needed to make to accommodate our new markup. Using two web browsers you should be able to see yourself come and go from the chat application. The final application should look like the following:

screenshot of the final ReactJS chat application

Your chat application should be complete with an active user list being updated in realtime.

Next Steps

Thanks to the combination of React and Redux that we established in the first two posts, we were able to add new functionality to our application in an organized and scalable way. We extended our reducer to add a users list and we created some new views to show who is connected. In our next post, we’ll add a feature that lets us know when other users are typing.

In our next and final part, we’ll add typing indicators to show when a user is typing.

Language:

Use Cases:

Try PubNub Today

Share this on facebookShare this on TwitterShare this on Linkedin