How to build a modern React chatroom with React.js

Slash commands have long existed in chat since the IRC days, but they have been reimagined by modern chat apps such as Slack. They act as shortcuts for simple or complex workflows within an application.

Prerequisites

To follow along with this tutorial, you need need to:

  • Have prior experience with working with React and Chatkit.
  • Have Node.js (version 8 or later), and npm installed on your machine. Installation instructions can be found on this page.

Create a new Chatkit instance

Go to your Chatkit dashboard, create a new Chatkit instance for this tutorial and take note of your Instance Locator and Secret Key in the Credentials tab.

Also make sure your Test Token Provider is enabled as shown below. Once enabled, the endpoint from which the test token will be generated will be displayed for you to copy and paste. Note that the test token is not to be used in production. More information on the authentication process for production apps can be found here.

Next, click the Console tab and create a new user and a brand new room for your instance. Take note of the room ID as we’ll be needing it later on.

Create a new React app

Run the command below to install create-react-app globally on your machine, then use it to bootstrap a new react React application:

    npm install -g create-react-app
    create-react-app chatkit-slash-cmd

Once the app has been created, cd into the new chatkit-slash-cmd directory and install the following additional dependencies that we’ll be needing in the course of building the chat app:

    npm install @pusher/chatkit-client prop-types skeleton-css --save

You can now start your development server by running npm start then navigate to http://localhost:3000 in your browser to view the app.

Add the styles for the app

Open up src/App.css in your code editor, and change it to look like this:

    // src/App.css

    .App {
      width: 100vw;
      height: 100vh;
      display: flex;
      overflow: hidden;
    }

    .sidebar {
      height: 100%;
      width: 20%;
      background-color: darkcyan;
    }

    .login {
      padding: 5px 20px;
    }

    .sidebar input {
      width: 100%;
    }

    .chat-rooms .active {
      background-color: whitesmoke;
      color: #181919;
    }

    .chat-rooms li {
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: 15px 20px;
      font-size: 18px;
      color: #181919;
      cursor: pointer;
      border-bottom: 1px solid #eee;
      margin-bottom: 0;
    }

    .room-list h3 {
      padding-left: 20px;
      padding-right: 20px;
    }

    .room-unread {
      display: inline-block;
      width: 20px;
      height: 20px;
      line-height: 20px;
      border-radius: 50%;
      font-size: 16px;
      text-align: center;
      padding: 5px;
      background-color: greenyellow;
      color: #222;
    }

    .chat-rooms li:hover {
      background-color: #D8D1D1;
    }

    .chat-screen {
      display: flex;
      flex-direction: column;
      height: 100vh;
      width: calc(100vw - 20%);
    }

    .chat-header {
      height: 70px;
      flex-shrink: 0;
      border-bottom: 1px solid #ccc;
      padding-left: 10px;
      padding-right: 20px;
      display: flex;
      flex-direction: column;
      justify-content: center;
    }

    .chat-header h3 {
      margin-bottom: 0;
      text-align: center;
    }

    .chat-messages {
      flex-grow: 1;
      overflow-y: scroll;
      display: flex;
      flex-direction: column;
      justify-content: flex-end;
      margin-bottom: 0;
      min-height: min-content;
    }

    .message {
      padding-left: 20px;
      padding-right: 20px;
      margin-bottom: 10px;
      display: flex;
      justify-content: space-between;
      align-items: flex-start;
    }

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

    .message .user-id {
      font-weight: bold;
    }

    .message-form {
      border-top: 1px solid #ccc;
    }

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

    input[type="text"].message-input {
      height: 50px;
      border: none;
      padding-left: 20px;
    }

Create a basic chat application

Next, let’s set up a basic chat interface so we can add the slash command functionality.

    // src/App.js

    import React, { Component } from "react";
    import {
      handleInput,
      connectToChatkit,
      connectToRoom,
      sendMessage,
    } from "./methods";

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

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

        this.handleInput = handleInput.bind(this);
        this.connectToChatkit = connectToChatkit.bind(this);
        this.connectToRoom = connectToRoom.bind(this);
        this.sendMessage = sendMessage.bind(this);
      }

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

        const roomList = rooms.map(room => {
          const isRoomActive = room.id === currentRoom.id ? 'active' : '';
          return (
            <li
              className={isRoomActive}
              key={room.id}
              onClick={() => this.connectToRoom(room.id)}
            >
              <span className="room-name">{room.name}</span>
            </li>
          );
        });

        const messageList = messages.map(message => {
          const arr = message.parts.map(p => {
              return (
                <span className="message-text">{p.payload.content}</span>
              );
          });

          return (
            <li className="message" key={message.id}>
              <div>
                <span className="user-id">{message.senderId}</span>
                {arr}
              </div>
            </li>
          )
        });

        return (
          <div className="App">
            <aside className="sidebar left-sidebar">
              {!currentUser ? (
                  <div className="login">
                    <h3>Join Chat</h3>
                    <form id="login" onSubmit={this.connectToChatkit}>
                      <input
                        onChange={this.handleInput}
                        className="userId"
                        type="text"
                        name="userId"
                        placeholder="Enter your username"
                      />
                    </form>
                  </div>
                ) : null
              }
              {currentRoom ? (
                <div className="room-list">
                  <h3>Rooms</h3>
                  <ul className="chat-rooms">
                    {roomList}
                  </ul>
                </div>
                ) : null
              }
            </aside>
            {
              currentUser ? (
                <section className="chat-screen">
                  <ul className="chat-messages">
                    {messageList}
                  </ul>
                  <footer className="chat-footer">
                    <form onSubmit={this.sendMessage} className="message-form">
                      <input
                        type="text"
                        value={newMessage}
                        name="newMessage"
                        className="message-input"
                        placeholder="Type your message and hit ENTER to send"
                        onChange={this.handleInput}
                      />
                    </form>
                  </footer>
                </section>
              ) : null
            }
          </div>
        );
      }
    }

    export default App;

Create a new methods.js file within the src directory and add the following code into it:

    // src/methods.js

    import { ChatManager, TokenProvider } from "@pusher/chatkit-client";

    function handleInput(event) {
      const { value, name } = event.target;

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

    function connectToChatkit(event) {
      event.preventDefault();
      const { userId } = this.state;

      const tokenProvider = new TokenProvider({
        url:
          "<test token provider endpoint>"
      });

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

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

    function connectToRoom(roomId = "<your chatkit room id>") {
      const { currentUser } = this.state;
      this.setState({
        messages: []
      });

      return currentUser
        .subscribeToRoomMultipart({
          roomId,
          messageLimit: 10,
          hooks: {
            onMessage: message => {
              this.setState({
                messages: [...this.state.messages, message],
              });
            },
          }
        })
        .then(currentRoom => {
          this.setState({
            currentRoom,
            rooms: currentUser.rooms,
          });
        })
        .catch(console.error);
    }

    function sendMessage(event) {
      event.preventDefault();
      const { newMessage, currentUser, currentRoom } = this.state;
      const parts = [];

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

      parts.push({
        type: "text/plain",
        content: newMessage
      });

      currentUser.sendMultipartMessage({
        roomId: `${currentRoom.id}`,
        parts
      });

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

    export {
      handleInput,
      connectToRoom,
      connectToChatkit,
      sendMessage,
    }

Make sure to replace the <test token provider endpoint>, <your chatkit instance locator> and <your chatkit room id> placeholders above with the appropriate values from your Chatkit dashboard.

Create a slash command

At this point, users can converse in a room. What is left is to add slash commands to the room. To demonstrate this functionality, we’ll make a /news [topic] command that will look up news headlines on the provided topic, and post the first three results to the room.

Head over to https://newsapi.org and register for a free account. Once your account is created, your API key will be presented to you. Take note of it as we’ll be using it shortly.

Next, modify your methods.js file to look like this:

    // src/methods.js

    // [..]

    function handleSlashCommand(message) {
      const cmd = message.split(" ")[0];
      const query = message.slice(cmd.length)

      if (cmd !== "/news") {
        alert(`${cmd} is not a valid command`);
        return;
      }

       return sendNews.call(this, query);
    }

    function sendMessage(event) {
      event.preventDefault();
      const { newMessage, currentUser, currentRoom } = this.state;
      const parts = [];

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

      if (newMessage.startsWith("/")) {
        handleSlashCommand.call(this, newMessage);

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

        return;
      }

      parts.push({
        type: "text/plain",
        content: newMessage
      });

      currentUser.sendMultipartMessage({
        roomId: `${currentRoom.id}`,
        parts
      });

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

    // [..]

In sendMessage(), we check if the message starts with a slash character. If so, the message is passed to the handleSlashCommand() function where we retrieve the command name and the query after the command. If the command is not /news, a “Command not found” message will be displayed to the user. Otherwise, the query is passed off to the sendNews() function which you should create above handleSlashCommand():

    // src/methods.js

    function sendNews(query) {
      const { currentUser, currentRoom } = this.state;

      fetch(`https://newsapi.org/v2/everything?q=${query}&pageSize=3&apiKey=<your news api key>`)
        .then(res => res.json())
        .then(data => {
          const parts = [];
          data.articles.forEach(article => {
            parts.push({
              type: "text/plain",
              content: `${article.title} - ${article.source.name} - ${article.url}`
            });
          });

          currentUser.sendMultipartMessage({
            roomId: `${currentRoom.id}`,
            parts
          });
        })
        .catch(console.error);
    }

In sendNews(), a request for the top three headlines on the requested topic is made to newsapi.org, and the results are sent to the current room. You need to replace <your news api key> your API key in the URL endpoint above.

Since the news results will contain links, let’s account for that in the way we’re displaying the messages in the room so that the links don’t display as plain text:

    // src/App.js

    // [..]

    class App extends Component {
      // [..]

      render() {
        // [..]

        const insertTextAtIndices = (text, obj) => {
          return text.replace(/./g, function(character, index) {
            return obj[index] ? obj[index] + character : character;
          });
        };

        const messageList = messages.map(message => {
          const arr = message.parts.map(p => {
            const urlMatches = p.payload.content.match(/\b(http|https)?:\/\/\S+/gi) || [];
            let text = p.payload.content;
            urlMatches.forEach(link => {
              const startIndex = text.indexOf(link);
              const endIndex = startIndex + link.length;
              text = insertTextAtIndices(text, {
                [startIndex]: `<a href="${link}" target="_blank" rel="noopener noreferrer" class="embedded-link">`,
                [endIndex]: "</a>"
              });
            });

              return (
                <span className="message-text" dangerouslySetInnerHTML={{
                  __html: text
                }}></span>
              );
          });

          return (
            <li className="message" key={message.id}>
              <div>
                <span className="user-id">{message.senderId}</span>
                {arr}
              </div>
            </li>
          )
        });

        // [..]
      }
    }

    export default App;

Try it out! Test the app by using the /news command along with any topic of your choice. It should work similarly to the GIF below:

Wrap up

In this tutorial, you learned about how slash commands work and how to implement them in a React chat app. You can checkout other things Chatkit can do by viewing its extensive documentation.

Don’t forget to grab the complete source code in this GitHub repository.

#reactjs #javascript

How to build a modern React chatroom with React.js
12.75 GEEK