🎉 New! Web Push Notifications for Chatkit. Learn more in our latest blog post.
Hide
Products
chatkit_full-logo

Extensible API for in-app chat

channels_full-logo

Build scalable realtime features

beams_full-logo

Programmatic push notifications

Developers

Docs

Read the docs to learn how to use our products

Tutorials

Explore our tutorials to build apps with Pusher products

Support

Reach out to our support team for help and advice

Sign in
Sign up

Add user mentions to a React chatroom

  • Ayooluwa Isaiah

February 4th, 2019
You will need Node 8+ installed on your machine.

During conversation, it’s often useful to call the attention of other members in the chat room using an @mention. To mention a user, you’d need to type in the @ symbol followed by a member’s username. If the person you mention is a member of the room, the mention will be highlighted in blue. On the other hand, mentions of the current user will be highlighted in yellow.

In this tutorial, I’ll show you how to add mentions to your Chatkit powered application. At the end of it all, you will have an application that looks like this:

Prerequisites

Prior experience with building React and Node.js applications is required to be able to follow through with this tutorial. You also need to have Node.js (version 8 ) and npm installed on your machine. You can view this page for instructions on how to install Node and npm on your machine.

Sign up for Chatkit

The first thing you need to do is create a free Chatkit account, or sign in to your existing account. Next, create a new Chatkit instance and take note of your Instance Locator and Secret Key in the Credentials tab.

Following that, switch over to the Console tab and create a new user and room for your instance. You can follow the instructions on this page to learn how to do so. Once the room has been created, take note of the room ID as we’ll be using it later on.

Set up the server

Let’s go ahead and set up a simple Node server to power our application. Create a new directory for this project, cd into it, then run npm init -y to initialize the project with a package.json file.

Next, install all the dependencies which we’’ll be needing on the server with the command below. The @pusher/chatkit-server package is the Node.js server SDK for the Chatkit service.

    npm install express dotenv body-parser cors @pusher/chatkit-server --save

Once the installation is complete, create a .env file in the root of your project directory and paste in the credentials retrieved from your Chatkit instance dashboard:

    // .env

    PORT=5200
    CHATKIT_INSTANCE_LOCATOR=<your chatkit instance locator>
    CHATKIT_SECRET_KEY=<your chatkit secret key>

Create another file, server.js in the root of your project directory, and add the following code into it to set up the Node server:

    // server.js

    require('dotenv').config({ path: '.env' });

    const express = require('express');
    const bodyParser = require('body-parser');
    const cors = require('cors');
    const Chatkit = require('@pusher/chatkit-server');

    const app = express();

    const chatkit = new Chatkit.default({
      instanceLocator: process.env.CHATKIT_INSTANCE_LOCATOR,
      key: process.env.CHATKIT_SECRET_KEY,
    });

    app.use(cors());
    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({ extended: true }));

    app.post('/users', (req, res) => {
      const { userId } = req.body;

      chatkit
        .createUser({
          id: userId,
          name: userId,
        })
        .then(() => {
          res.sendStatus(201);
        })
        .catch(err => {
          if (err.error === 'services/chatkit/user_already_exists') {
            console.log(`User already exists: ${userId}`);
            res.sendStatus(200);
          } else {
            res.status(err.status).json(err);
          }
        });
    });

    app.post('/authenticate', (req, res) => {
      const authData = chatkit.authenticate({
        userId: req.query.user_id,
      });
      res.status(authData.status).send(authData.body);
    });

    app.set('port', process.env.PORT || 5200);
    const server = app.listen(app.get('port'), () => {
      console.log(`Express running → PORT ${server.address().port}`);
    });

The /users route takes in a username and creates a Chatkit user through the chatkit instance, while /authenticate tries to validate anyone who tries to connect to our Chatkit instance by responding with a token (returned by chatkit.authenticate) if the request is valid. For the purpose of this tutorial, we don’t actually try to validate the request before returning the token, but you need to do so in your production code.

That’s all we need to do on the server. You can start it on port 5200 by running node server.js in the terminal.

Set up the React application

We’ll be making use of create-react-app to bootstrap our React application. You can install it globally with the following command:

    npm install -g create-react-app

Then use it to create a new React app within the root of your project directory:

    create-react-app client

Once the command has finished running, cd into the new client directory and install the following additional dependencies that we’ll be needing in the course of building the React app:

    npm install axios skeleton-css @webscopeio/react-textarea-autocomplete @pusher/chatkit-client --save

Once the dependencies have been installed, you can start the development server by running npm start from within the client directory, then navigate to http://localhost:3000 in your browser to view the app.

Add the application styles

Open up client/src/App.css and change its contents to look like this:

    /* client/src/App.css */

    html {
      box-sizing: border-box;
    }

    *, *::before, *::after {
      box-sizing: inherit;
      margin: 0;
      padding: 0;
    }

    .App {
      text-align: center;
      overflow: hidden;
      width: 100vw;
      height: 100vh;
      display: flex;
    }

    svg {
      width: 28px;
      height: 28px;
    }

    input[type="text"]:focus {
      border: 1px solid #300d4f;
    }

    .message {
      padding-left: 10px;
    }

    .message span {
      display: block;
      text-align: left;
    }

    .user-id {
      font-weight: 700;
      font-size: 20px;
    }

    .message-form, .message-input {
      width: 100%;
      margin-bottom: 0;
    }

    .message-input {
      border-radius: 0;
      border: none;
      border-top: 1px solid #ccc;
      padding: 5px 20px;
      font-size: 16px;
      background-color: #f6f6f6;
      color: #333;
      min-height: auto;
      resize: none;
    }

    input.message-input {
      height: 50px;
    }

    .message-input:focus {
      border: none;
      border-top: 1px solid skyblue;
    }

    .sidebar {
      width: 20%;
      background-color: #300d4f;
      height: 100%;
    }

    .sidebar ul {
      list-style: none;
    }

    .sidebar h3 {
      color: white;
      margin-bottom: 10px;
      text-align: left;
      padding: 10px 20px;
    }

    .join-chat form {
      display: flex;
      padding-left: 20px;
      padding-right: 20px;
    }

    .user {
      font-size: 22px;
      color: white;
      cursor: pointer;
      text-align: left;
      padding: 5px 20px;
      margin-bottom: 0;
      display: flex;
      align-items: center;
    }

    .presence {
      display: inline-block;
      width: 10px;
      height: 10px;
      background-color: #fff;
      margin-right: 10px;
      border-radius: 50%;
    }

    .presence.online {
      background-color: green;
    }

    .chat-screen {
      width: 80%;
      height: 100%;
      display: flex;
      flex-direction: column;
    }

    .room-name {
      border-bottom: 1px solid #ccc;
      text-align: left;
      padding: 10px 20px;
      display: flex;
    }

    .room-name h3 {
      margin-bottom: 0;
    }

    .chat-session {
      flex-grow: 1;
      overflow-y: auto;
      padding: 10px;
      list-style: none;
      display: flex;
      flex-direction: column;
      justify-content: flex-end;
      margin-bottom: 0;
    }

    .mentioned-user {
      display: inline-block !important;
      color: #052b3b;
      padding: 0 2px;
      border-radius: 4px;
      background-color: lightblue;
    }

    .mentioned-user.is-current {
      background-color: goldenrod;
      font-weight: bold;
    }

    .rta {
      position: relative;
    }

    .rta__autocomplete {
      position: absolute;
      width: 200px;
      background-color: white;
      border: 1px solid #ccc;
      border-radius: 5px;
    }

    .rta__autocomplete ul {
      list-style: none;
      text-align: left;
      margin-bottom: 0;
    }

    .rta__autocomplete li {
      margin-bottom: 5px;
      padding: 3px 20px;
    }

    .rta__autocomplete li:hover {
      background-color: skyblue;
    }

Render the chat screen

Open up client/src/App.js and change its contents to look like this:

    // client/src/App.js

    import React, { Component } from 'react';
    import axios from 'axios';
    import Chatkit from '@pusher/chatkit-client';

    import 'skeleton-css/css/normalize.css';
    import 'skeleton-css/css/skeleton.css';
    import './App.css';

    class App extends Component {
      constructor() {
        super();
        this.state = {
          currentUser: null,
          currentRoom: null,
          newMessage: '',
          messages: [],
          roomUsers: [],
        };
      }

      addUser = event => {
        event.preventDefault();
        const { userId } = this.state;
        axios
          .post('http://localhost:5200/users', { userId })
          .then(() => {
            const tokenProvider = new Chatkit.TokenProvider({
              url: 'http://localhost:5200/authenticate',
            });

            const chatManager = new Chatkit.ChatManager({
              instanceLocator: '<your chatkit instance locator>',
              userId,
              tokenProvider,
            });

            return chatManager.connect().then(currentUser => {
              this.setState(
                {
                  currentUser,
                },
                () => this.connectToRoom()
              );
            });
          })
          .catch(console.error);
      };

      sendMessage = (event) => {
        event.preventDefault();
        const { newMessage, currentUser, currentRoom } = this.state;

        if (newMessage.trim() === '') return;

        currentUser.sendMessage({
          text: newMessage,
          roomId: `${currentRoom.id}`,
        });

        this.setState({
          newMessage: '',
        });
      };

      handleInput = event => {
        const { value, name } = event.target;

        this.setState({
          [name]: value,
        });
      };

      connectToRoom = () => {
        const { currentUser } = this.state;

        return currentUser
          .subscribeToRoom({
            roomId: '<your room id>',
            messageLimit: 100,
            hooks: {
              onMessage: message => {
                this.setState({
                  messages: [...this.state.messages, message],
                });
              },
              onPresenceChanged: () => {
                const { currentRoom } = this.state;
                if (currentRoom) {
                  this.setState({
                    roomUsers: currentRoom.users.sort(a => {
                      if (a.presence.state === 'online') return -1;

                      return 1;
                    }),
                  });
                }
              },
            },
          })
          .then(currentRoom => {
            this.setState({
              currentRoom,
              roomUsers: currentRoom.users,
            });
          });
      };

      render() {
        const {
          newMessage,
          roomUsers,
          currentRoom,
          messages,
          currentUser,
        } = this.state;

        const UserList = roomUsers.map(user => {
          return (
            <li className="user" key={user.name}>
              <span className={`presence ${user.presence.state}`} />
              <span>{user.name}</span>
            </li>
          );
        });

        const ChatSession = messages.map(message => {
          return (
            <li className="message" key={message.id}>
              <span className="user-id">{message.senderId}</span>
              <span>{message.text}</span>
            </li>
          );
        });

        return (
          <div className="App">
            <aside className="sidebar">
              {!currentUser ? (
                <section className="join-chat">
                  <h3>Join Chat</h3>
                  <form onSubmit={this.addUser}>
                    <input
                      placeholder="Enter your username"
                      type="text"
                      name="userId"
                      onChange={this.handleInput}
                    />
                  </form>
                </section>
              ) : null}

              {currentUser ? (
                <section className="room-users">
                  <h3>Room Users</h3>
                  <ul>{UserList}</ul>
                </section>
              ) : null}
            </aside>

            <section className="chat-screen">
              <header className="room-name">
                <h3>{currentRoom ? currentRoom.name : 'Chat'}</h3>
              </header>
              <ul className="chat-session">{ChatSession}</ul>
              <form onSubmit={this.sendMessage} className="message-form">
                <input
                  className="message-input"
                  autoFocus
                  placeholder="Compose your message and hit ENTER to send"
                  onChange={this.handleInput}
                  value={newMessage}
                  name="newMessage"
                />
              </form>
            </section>
          </div>
        );
      }
    }

    export default App;

There's a lot to unpack here, starting from the Join Chat form on the sidebar. Once the user submits this form, the addUser() method is invoked and a POST request is sent to the /users route, which results in the creation of a new Chatkit user which can be accessed through the currentUser object. This object is the primary interface to interact with our Chatkit instance.

Following that, the user will be added to the room whose ID was provided in the subscribeToRoom method. We’ve also set up two room connection hooks here: onMessage is invoked anytime a new message arrives in the room, while onPresenceChanged is invoked when a user comes online or goes offline.

Because we’ve set messageLimit to 100, the most recent 100 messages in the room will be displayed on the screen. If you want to prevent any messages from appearing, you can set messageLimit to 0.

Finally, the sendMessage method is invoked whenever the form below is submitted. This method calls sendMessage() on the currentUser and sets the value of newMessage to an empty string so that the input is cleared.

At this point, you should be able to connect to the room and send a message. Don't forget to replace <your chatkit room id> and <your chatkit instance locator> with the appropriate values.

Mention users in the room

Sometimes you’ll need to call the attention of another member when having a conversation. In most applications, you can do so by typing @ followed by the username of the member. Mentions are usually highlighted in a different color so that they can be easily spotted in the text.

Let's go ahead and add the ability to mention users in our application. Make the following changes in App.js:

    // client/src/App.js

    class App extends Component {
      constructor() {
        super();
        this.state = {
          currentUser: null,
          currentRoom: null,
          newMessage: '',
          messages: [],
          roomUsers: [],
        };
      }


      [...]

      // add this below the other methods  
      insertTextAtIndices = (text, obj) => {
        return text.replace(/./g, function(character, index) {
          return obj[index] ? obj[index] + character : character;
        });
      }

      render() {
        const {
          newMessage,
          roomUsers,
          currentRoom,
          messages,
          currentUser,
        } = this.state;

        const UserList = roomUsers.map(user => {
          return (
            <li className="user" key={user.name}>
              <span className={`presence ${user.presence.state}`} />
              <span>{user.name}</span>
            </li>
          );
        });


        // update `ChatSession`
        const ChatSession = messages.map(message => {
          const messageText = message.text;
          const mentions = messageText.match(/@[a-zA-Z0-9]+/g) || [];
          const roomUserNames = this.state.roomUsers.map(user => `@${user.name}`);
          const mentionedUsers = mentions.filter(username =>
            roomUserNames.includes(username)
          );

          let text = messageText;
          mentionedUsers.forEach(user => {
            const startIndex = text.indexOf(user);
            const endIndex = startIndex + user.length;
            const isCurrent =
              currentUser.name === user.substring(1) ? 'is-current' : '';
            text = this.insertTextAtIndices(text, {
              [startIndex]: `<span class="mentioned-user ${isCurrent}">`,
              [endIndex]: '</span>',
            });
          });

          return (
            <li className="message" key={message.id}>
              <span className="user-id">{message.senderId}</span>
              <span
                dangerouslySetInnerHTML={{
                  __html: text,
                }}
              />
            </li>
          );
        });

        return (
          <div className="App">
            [...]
          </div>
        );
      }
    }

    export default App;

The ChatSession variable is where the majority of the logic to spot a mention is held. We search for mentions in each message and verify that the users that are mentioned are members of the current room. If so we wrap each mention inside a <span> tag so that we can highlight them in the message.

You can test it out by adding multiple users to the room, then mentioning any user in a message. @mentions of the current user will be highlighted in yellow, while mentions of other members are highlighted in blue.

Autocomplete mentions

An easy way to enhance the @mentions feature is to provide a way to autocomplete usernames when the @ symbols is entered. We’ll implement this functionality in our application using react-textarea-autocomplete. Make the following changes in App.js:

    // client/src/App.js

    import React, { Component } from 'react';
    import axios from 'axios';
    import Chatkit from '@pusher/chatkit-client';
    import ReactTextareaAutocomplete from '@webscopeio/react-textarea-autocomplete';

    import 'skeleton-css/css/normalize.css';
    import 'skeleton-css/css/skeleton.css';
    import './App.css';

    class App extends Component {
      constructor() {
        super();
        this.state = {
          currentUser: null,
          currentRoom: null,
          newMessage: '',
          messages: [],
          roomUsers: [],
        };
      }

      // update sendMessage
      sendMessage = () => {
        const { newMessage, currentUser, currentRoom } = this.state;

        if (newMessage.trim() === '') return;

        currentUser.sendMessage({
          text: newMessage,
          roomId: `${currentRoom.id}`,
        });

        this.setState({
          newMessage: '',
        });
      };

      // add handleKeyPress below the other methods
      handleKeyPress = event => {
        if (event.key === 'Enter') {
          event.preventDefault();
          this.sendMessage();
        }
      };

      render() {
        const {
          newMessage,
          roomUsers,
          currentRoom,
          messages,
          currentUser,
        } = this.state;

        const UserList = roomUsers.map(user => {
          return (
            <li className="user" key={user.name}>
              <span className={`presence ${user.presence.state}`} />
              <span>{user.name}</span>
            </li>
          );
        });

        // filter user names
        const filterUserNames = token =>
          roomUsers.filter(user => user.name.includes(token));

        const ChatSession = messages.map(message => {
          const messageText = message.text;
          const mentions = messageText.match(/@[a-zA-Z0-9]+/g) || [];
          const roomUserNames = this.state.roomUsers.map(user => `@${user.name}`);
          const mentionedUsers = mentions.filter(username =>
            roomUserNames.includes(username)
          );

          let text = messageText;
          mentionedUsers.forEach(user => {
            const startIndex = text.indexOf(user);
            const endIndex = startIndex + user.length;
            const isCurrent =
              currentUser.name === user.substring(1) ? 'is-current' : '';
            text = this.insertTextAtIndices(text, {
              [startIndex]: `<span class="mentioned-user ${isCurrent}">`,
              [endIndex]: '</span>',
            });
          });

          return (
            <li className="message" key={message.id}>
              <span className="user-id">{message.senderId}</span>
              <span
                dangerouslySetInnerHTML={{
                  __html: text,
                }}
              />
            </li>
          );
        });

        return (
          <div className="App">
            <aside className="sidebar">
              {!currentUser ? (
                <section className="join-chat">
                  <h3>Join Chat</h3>
                  <form onSubmit={this.addUser}>
                    <input
                      placeholder="Enter your username"
                      type="text"
                      name="userId"
                      onChange={this.handleInput}
                    />
                  </form>
                </section>
              ) : null}

              {currentUser ? (
                <section className="room-users">
                  <h3>Room Users</h3>
                  <ul>{UserList}</ul>
                </section>
              ) : null}
            </aside>

            <section className="chat-screen">
              <header className="room-name">
                <h3>{currentRoom ? currentRoom.name : 'Chat'}</h3>
              </header>
              <ul className="chat-session">{ChatSession}</ul>
              <form onSubmit={this.sendMessage} className="message-form">
                <ReactTextareaAutocomplete
                  className="message-input my-textarea"
                  name="newMessage"
                  value={newMessage}
                  loadingComponent={() => <span>Loading</span>}
                  onKeyPress={this.handleKeyPress}
                  onChange={this.handleInput}
                  placeholder="Compose your message and hit ENTER to send"
                  trigger={{
                    '@': {
                      dataProvider: token => {
                        return [...filterUserNames(token)];
                      },
                      component: ({ entity: { name } }) => <div>{`${name}`}</div>,
                      output: item => `@${item.name}`,
                    },
                  }}
                />
              </form>
            </section>
          </div>
        );
      }
    }

    export default App;

Typing the @ symbol followed by a member’s username will give you a narrowed down list of members to choose. When you find the member you want to mention, simply press the tab key to autocomplete and make your selection.

Wrap up

In this tutorial, we created a chatroom and added the ability to mention members of the room. We also covered how to autocomplete @mentions in the room.

You can checkout other things Chatkit can do by viewing its extensive documentation. Don't forget to grab the full source code used in this tutorial here.

Clone the project repository
  • Chat
  • JavaScript
  • Node.js
  • React
  • Social
  • chatroom
  • Chatkit

Products

  • Channels
  • Chatkit
  • Beams

© 2019 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 160 Old Street, London, EC1V 9BW.