React Chat Tutorial 4 Typing Indicators

React JS Chat Tutorial: Typing Indicators (4/4)

This is the final part 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 typing indicators to show when a user is typing in the chat app.

The steps we’ve already taken are as follows:

  • 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).

Presence Actions

When PubNub’s Presence feature is enabled, subscribing or unsubscribing to a channel generates a join, leave or timeout event, respectively. These events are broadcast and you will be notified in your app. There is also another event called “state-change.” This event is emitted when the custom state attribute is modified.

Setting this up is very similar to the online users component we did in the last blog. We’ll create constants for actions, define the actions for setting and clearing typing status for a user, then we’ll update the reducer to handle those actions.

We will store an array of User IDs of the users who are currently typing. Then, we’ll build out a component to display the current typing users. Of course, we’ll also have to add some code to trigger the actions.

Open the constants file in src/constants/index.js and append these lines:

    export const ADD_TYPING_USER = 'App/ADD_TYPING_USER';
    export const REMOVE_TYPING_USER = 'App/REMOVE_TYPING_USER';

Now that we have the constants defined for our actions, we can build the actions.

Open the actions file in src/actions/index.js and add the two new constants to the import at the top of the file. Next, add the following functions to the end of the file:

    export function addTypingUser(userID) {
      return {
        type: ADD_TYPING_USER,
        payload: userID,
      };
    }

    export function removeTypingUser(userID) {
      return {
        type: REMOVE_TYPING_USER,
        payload: userID,
      };
    }

Extend Application State

Now we’re ready to implement the desired behaviour in the reducer. Open the reducer file in src/reducers/app.js and append the new constants to the import statement at the top.

Next, we’ll need a new field in our store to hold the list of users typing.  We’ll do this by adding a new array to the INITIAL_STATE:

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

Finally, append these two case statements to the appReducer:

    case ADD_TYPING_USER:
      return state
        .update('usersTyping', (users) => (users.indexOf(action.payload) >= 0 ? users : users.concat(action.payload)));
    case REMOVE_TYPING_USER:
      return state
        .update('usersTyping', (users) => users.filter((userID) => userID !== action.payload));

That’s it for the redux part of the application. Now we just need to hook up the two actions and the usersTyping array to the container.

Reacting to Presence Changes

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

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

Next, we’ll add the usersTyping and the actions to the two map functions.

Append the following line to the object returned from mapStateToProps:

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

and add these lines to mapDispatchToProps:

    addTypingUser: (userID) => dispatch(addTypingUser(userID)),
    removeTypingUser: (userID) => dispatch(removeTypingUser(userID)),

Next, we’ll need to add these new mappings to the propTypes in the App class:

    usersTyping: React.PropTypes.array,
    addTypingUser: React.PropTypes.func,
    removeTypingUser: React.PropTypes.func,

Now that we have our container hooked up to the store and actions, we’ll need to pass these to components.

Before we go further, let’s first explain how we’re going to pass the typing indicator to other clients. In PubNub, you have the option of assigning additional state data to a given UUID.  When the state is changed for a user, an event is fired to all users that the state has changed, providing the new state. We’ll leverage this to pass in a boolean when the user is typing and when the user has stopped typing.

Since the PubNub instance for our application lies in the container, we’ll need to add a function that will set the state for us in PubNub. This function can then be passed down to the ChatInput component to call.

Below the render function, add this function to the App class:

    setTypingState = (isTyping) => {
      this.PubNub.state({
        channel: 'ReactChat',
        uuid: this.props.userID,
        state: { isTyping },
      });
    };

This will set the state for the current user based on what is passed into the function. Now we need to give this to the ChatInput component.

In the render function, add setTypingState function to the first line, and append the following attribute to the ChatInput component:

    setTypingState={ setTypingState }

Now that we know what to look for when the state changes, we can implement the state-change event handler. This is done in the onPresenceChange event handler. Append the following case statement to the switch in onPresenceChange:

    case 'state-change':
      if (presenceData.data) {
        if (presenceData.data.isTyping === true) {
          this.props.addTypingUser(presenceData.uuid);
        } else {
          this.props.removeTypingUser(presenceData.uuid);
        }
      }
      break;

This will simply call one of the actions we’ve defined based on the boolean value in the state. Notice that we’re accessing the data field of presenceData; this is where the state is passed down through onPresenceChange.

Triggering Presence Changes

Now we need to update the ChatInput component to call setTypingState. Open the component in src/components/ChatInput.js and add the new function to the propTypes:

    setTypingState: React.PropTypes.func,

We will need to add an onChange event handler for the input so we can add our business logic based on the state of the user interaction.

Inside the onChange event handler, we’re going to compare the empty state of the input to the current user’s isTyping status to determine what value to send to setTypingState.

Since we’re going to want to track the current user’s typing status, we’ll create a variable in the ChatInput class to hold this.  Append the following line after the render function:

    isTyping = false;

Since the onChange event is fired whenever the user types, we can easily know when to set isTyping, but how do we know when the user stopped? Well, it’s not easy, because the user could have just paused to think for a second before finishing the message. To solve this, we’ll say the user isn’t typing if no onChange event happens within 3 seconds. We can use a setTimeout to call setTypingState(false) after the 3 seconds. However, if the user starts typing within that 3 second window, we’ll need to reset the timeout for another 3 seconds from that moment. This means we’ll need to keep track of the timeout identifier so we can clear the timeout if needed.

Add the following variable below the isTyping:

    stopTypingTimeout = undefined;

Now, we’ll need a function to do the resetting of the timeout. Add this function below the variable stopTypingTimeout:

  resetStopTypingTimeout = () => {
    const { stopTypingTimeout } = this;
    if (stopTypingTimeout) {
      clearTimeout(stopTypingTimeout);
    }
    this.stopTypingTimeout = setTimeout(() => {
      this.isTyping = false;
      this.props.setTypingState(this.isTyping);
      this.stopTypingTimeout = undefined;
    }, 3000);
  };

Here you can see it will check if there is a timeout set, and if so, it will clear it before creating a new timeout.

Next, add the following onChange event above the render function:

    onChange = () => {
      const { resetStopTypingTimeout } = this;
      const { setTypingState } = this.props;
      const isInputEmpty = (this.refs.txtMessage.value.length === 0);

      // If the input isn't empty, and isTyping is false, update the state
      if (isInputEmpty === false) {
        // If the user wasn't typing before, and now the event has fired,
        // it means they are now typing
        if (this.isTyping === false) {
          this.isTyping = true;
          setTypingState(this.isTyping);
          // Start a 3 second countdown to see if they type within that window
          resetStopTypingTimeout();
        } else {
          // If the user typed another character, reset the timeout
          resetStopTypingTimeout();
        }
      } else {
        if (this.isTyping === true) {
          // If the user was typing, but now the input is empty,
          // it means they've deleted everything and that triggered
          // an onChange event.  For this, we state they have stopped typing
          this.isTyping = false;
          setTypingState(this.isTyping);
          // Stop the timeout, if there is one running
          if (this.stopTypingTimeout) {
            clearTimeout(this.stopTypingTimeout);
            this.stopTypingTimeout = undefined;
          }
        }
      }
    };

Finally, we need to hookup our onChange event handler to the onChange event on the input.  Inside the render function, add onChange to the first line, so it is obtained from this. Next, look for the input tag and add this attribute:

    onChange={ onChange }

That’s it for the component. Now when you type, and stop typing, you can see in the console, the actions being fired. The only thing left to do now is create a component to display the typing users.

Writing The UI Elements

Create new component called ChatUsersTyping in src/components/ChatUsersTyping.js by creating a skeleton like so:

    import * as React from 'react';

    class ChatUsersTyping extends React.Component {
      render() {
      }
    }

    export default ChatUsersTyping;

For this component, we’ll want to accept an array of users who are typing so we can display an icon for each one. This means we’ll need to add propTypes to our component:

    static propTypes = {
      usersTyping: React.PropTypes.array,
    };

Next, we’ll update the render method.  But first we’ll need to access usersTyping from this.props:

    const { usersTyping } = this.props;

Then, we’ll create a list of users with their avatar and the speech bubble:

    return (
      <div className="typing-indicator-box">
        <ul>{
          usersTyping.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" />
                <div className="typing-indicator">
                  <span></span>
                  <span></span>
                  <span></span>
                </div>
              </li>
            );
          })
        }</ul>
      </div>
    );

That’s it for the component! Now we just need to hook it up to the container and add the CSS.

Give The UI Some Style

Open the styles in src/styles/index.css and append the following styles:

First we’ll make some room for our typing indicators by turning their element into a horizontal box.

    .typing-indicator-box {
      flex: 1;
      margin:0;
      max-height:45px;
      background-color: #A2C4C9;
    }

    .typing-indicator-box ul {
      flex: 1;
      flex-direction: row;
      overflow-x:auto;
      flex-wrap: nowrap;
      margin:0;
    }

    .typing-indicator-box img{
      padding:4px;
      width:45px;
    }

    .typing-indicator-box li{
      display: inline-block;
    }

Then we’ll make the typing indicator into something that looks like a comic book dialog bubble. We’ll also add a touch of animation that will make the dialog bubble gently expand and contract.

    .typing-indicator {
      background-color: #E6E7ED;
      width: auto;
      border-radius: 33px;
      padding: 10px;
      display: inline-block;
      margin: 0 auto;
      margin-top:5px;
      position: relative;
      top: -10px;
      -webkit-animation: 2s bulge infinite ease-out;
              animation: 2s bulge infinite ease-out;
    }

    .typing-indicator:before, .typing-indicator:after {
      content: '';
      position: absolute;
      bottom: -2px;
      left: -2px;
      height: 10px;
      width: 10px;
      border-radius: 40%;
      background-color: #E6E7ED;
    }

    .typing-indicator:after {
      height: 5px;
      width: 5px;
      left: -5px;
      bottom: -5px;
    }

We’ll turn our typing indicator’s spans into ellipses that will appear animate in a sequence:

    .typing-indicator span {
      height: 6px;
      width: 6px;
      float: left;
      margin: 0 1px;
      background-color: #9E9EA1;
      display: block;
      border-radius: 50%;
      opacity: 0.4;
    }

    .typing-indicator span:nth-of-type(1) {
      -webkit-animation: 1s blink infinite 0.3333s;
              animation: 1s blink infinite 0.3333s;
    }

    .typing-indicator span:nth-of-type(2) {
      -webkit-animation: 1s blink infinite 0.6666s;
              animation: 1s blink infinite 0.6666s;
    }

    .typing-indicator span:nth-of-type(3) {
      -webkit-animation: 1s blink infinite 0.9999s;
              animation: 1s blink infinite 0.9999s;
    }

We need to give our animations some instructions so that they know what to look like during their sequence.  We use  @keyframes to do this.  Keyframes let us specify what our element should look like when it is “x” percent of the way through its animation.

Our typing indicator started off at 40% opacity. To animate the ellipses we’ll set them to full opacity when they’re halfway through their blink animation.  Since we staggered their times this will the dots in our ellipses light up in sequence.

    @-webkit-keyframes blink {
      50% {
        opacity: 1;
      }
    }

    @keyframes blink {
      50% {
        opacity: 1;
      }
    }

Scaling the typing indicator’s dialog bubble up at the halfway point in its animation will make it slightly expand and contract.

    @-webkit-keyframes bulge {
      50% {
        -webkit-transform: scale(1.05);
                transform: scale(1.05);
      }
    }
    @keyframes bulge {
      50% {
        -webkit-transform: scale(1.05);
                transform: scale(1.05);
      }
    }

Now, open the container in src/containers/app.js and add the following import for the new component, below the ChatUsers import:

    import ChatUsersTyping from '../components/ChatUsersTyping';

Now we just need to add the ChatUsersTyping component to the render function between the ChatHistory and ChatInput:

    <ChatUsersTyping usersTyping={ props.usersTyping } />

That’s it! You’re done and have a functional chat application with a typing indicator! The application should now show a typing indicator like this:

Screenshot of final chat application showing the typing indicator

Conclusion

In our final tutorial, we were able to leverage the PubNub Presence API and create notifications when users are typing. Redux provided us with a strong architecture for extending our state and React made adding the visual elements a snap.

That’s it! We hope you enjoyed our four part series, and please feel free to contact us if you have any questions or comments.

Language:

Use Cases:

Try PubNub Today