🎉 New release for Pusher Chatkit - Webhooks! Extend your in-app chat functionality
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

Build a React chatroom featuring direct messaging

  • Ayooluwa Isaiah
March 12th, 2019
You will need Node 8+ installed on your machine.

Direct messages (DMs) are one-to-one conversations between two members of a public room. They work well for conversations that need to be kept private between two members of a larger group. Popular apps like Gitter, Slack, and Discord all have this functionality.

In this tutorial, I’ll show you how to achieve the same using Pusher Chatkit. Here's a live demo of what we’ll be building:

Prerequisites

Before you continue, make sure you have Node.js (version 8 or later) and npm installed on your machine. Installation instructions can be found on this page. You also need to have reasonable experience with building React and Node.js applications as these are the technologies that are being used to build the app.

Sign up for Chatkit

Open this link in a new browser tab, and create a new Chatkit account or sign into your existing account. Once you are logged in, create a new Chatkit instance for this tutorial and take note of your Instance Locator and Secret Key in the Credentials tab.

Next, click 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

Create a new directory for this project in your filesystem and cd into it. Run npm init -y from the project root to initialize a new Node.js project. Following that, run the command below to install all the dependencies that are needed to build the server:

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

Next, create a new .env file in the project root and add the following environmental variables to it replacing <your chatkit instance locator> and <your chatkit secret key> with the appropriate values from your Chatkit instance dashboard.

    // .env

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

Next, create a new server.js file in the project root, and paste the following code into it:

    // 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 is where creation of users on our Chatkit instance takes place, while /authenticate validates all incoming connections 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

Install create-react-app globally on your machine then use it to bootstrap your React application using the commands below:

    npm install -g create-react-app
    create-react-app client

Once the app has been created, cd into the new client directory and install the following additional dependencies that we’ll be needing in the course of building the application frontend:

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

You can now start your 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

Before we write the logic for the application, let’s add all the custom styles that we need for the app layout in client/src/App.css.

    // client/src/App.css

    html {
      box-sizing: border-box;
    }

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

    ul {
      list-style: none;
    }

    h4 {
      padding-left: 20px;
      margin-bottom: 10px;
    }

    .App {
      width: 100vw;
      height: 100vh;
      display: grid;
      grid-template-columns: 1fr 4fr 1fr;
    }

    .right-sidebar {
      border-left: 1px solid #ccc;
    }

    .left-sidebar {
      border-right: 1px solid #ccc;
    }

    .user-profile {
      height: 70px;
      display: flex;
      align-items: flex-start;
      padding-right: 20px;
      padding-left: 20px;
      justify-content: center;
      flex-direction: column;
      border-bottom: 1px solid #ccc;
    }

    .user-profile span {
      display: block;
    }

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

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

    .room-member {
      justify-content: space-between;
      padding: 0 20px;
      height: 60px;
    }

    .send-dm {
      opacity: 0;
      pointer-events: none;
      font-size: 20px;
      border: 1px solid #eee;
      border-radius: 5px;
      margin-bottom: 0;
      padding: 0 10px;
      line-height: 1.4;
      height: auto;
    }

    .room-member:hover .send-dm {
      opacity: 1;
      pointer-events: all;
    }

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

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

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

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

    .room-icon {
      display: inline-block;
      margin-right: 10px;
    }

    .chat-screen {
      display: flex;
      flex-direction: column;
      height: 100vh;
    }

    .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;
      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: center;
    }

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

    /* Dialog
       ========================================================================== */

    .dialog-container {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background-color: rgba(0, 0, 0, 0.8);
      display: flex;
      justify-content:center;
      align-items: center;
    }

    .dialog {
      width: 500px;
      background-color: white;
      display: flex;
      align-items:  center;
    }

    .dialog-form {
      width: 100%;
      margin-bottom: 0;
      padding: 20px;
    }

    .dialog-form > * {
      display: block;
    }

    .username-label {
      text-align: left;
      font-size: 16px;
    }

    .username-input {
      width: 100%;
    }

    input[type="text"]:focus {
      border-color: #5C8436;
    }

    .submit-btn {
      color: #5C8436;
      background-color: #181919;
      width: 100%;
    }

    .submit-btn:hover {
      color: #5C8436;
      background-color: #222;
    }

Create a basic UI layout

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

    // client/src/App.js

    import React, { Component } from 'react';

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

    class App extends Component {
      constructor() {
        super();
        this.state = {
          userId: '',
          showLogin: true,
          isLoading: false,
          currentUser: null,
          currentRoom: null,
          rooms: [],
          roomUsers: [],
          roomName: null,
          messages: [],
          newMessage: '',
        };

      }

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

        return (
          <div className="App">
            <aside className="sidebar left-sidebar"></aside>
            <section className="chat-screen">
              <header className="chat-header"></header>
              <ul className="chat-messages"></ul>
              <footer className="chat-footer">
                <form className="message-form">
                  <input
                    type="text"
                    name="newMessage"
                    className="message-input"
                    placeholder="Type your message and hit ENTER to send"
                  />
                </form>
              </footer>
            </section>
            <aside className="sidebar right-sidebar"></aside>
          </div>
        );
      }
    }

    export default App;

Your app should look like this now:

The left sidebar is where the current user will be indicated, along with the rooms they belong to. On the right, the members of the current room will be listed along with their current status (online or offline). Finally, we’ll have the name of the current room at the top and the messages in the middle.

Connect a user to your Chatkit instance

Before connecting a user to your Chatkit instance, you need to identify the user first. We can do this when the app loads by prompting for a username and sending that username to the server (to the /users route) which will create a Chatkit user if one doesn't exist.

To collect the user's name, create a component called Dialog.js in a new components folder within the client/src directory:

    // client/src/components/Dialog.js

    import React from 'react';
    import Proptypes from 'prop-types';

    const Dialog = props => {
      const { userId, handleInput, connectToChatkit } = props;

      return (
        <div className="dialog-container">
          <div className="dialog">
            <form className="dialog-form" onSubmit={connectToChatkit}>
              <label className="username-label" htmlFor="username">
                Login with your username
              </label>
              <input
                id="username"
                className="username-input"
                autoFocus
                type="text"
                name="userId"
                value={userId}
                onChange={handleInput}
                placeholder="Enter your username"
              />
              <button type="submit" className="submit-btn">
                Submit
              </button>
            </form>
          </div>
        </div>
      );
    };

    Dialog.propTypes = {
      userId: Proptypes.string.isRequired,
      handleInput: Proptypes.func.isRequired,
      connectToChatkit: Proptypes.func.isRequired,
    };

    export default Dialog;

Next, create the a new methods.js in the client/src directory and paste in the code below. Make sure to update <your chatkit instance locator> with the appropriate value from your Chatkit instance dashboard.

    // client/src/methods.js

    import Chatkit from '@pusher/chatkit-client';
    import axios from 'axios';

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

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

    function connectToChatkit(event) {
      event.preventDefault();

      const { userId } = this.state;

      if (userId === null || userId.trim() === '') {
        alert('Invalid userId');
        return;
      }

      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({
              onAddedToRoom: room => {
                const { rooms } = this.state;
                this.setState({
                  rooms: [...rooms, room],
                });
              },
            })
            .then(currentUser => {
              this.setState(
                {
                  currentUser,
                  showLogin: false,
                  rooms: currentUser.rooms,
                }
              );
            });
        })
        .catch(console.error);
    }

    export { handleInput, connectToChatkit }

Then update App.js like this:

    // client/src/App.js

    import React, { Component } from 'react';
    // [..]
    import {
      handleInput,
      connectToChatkit,
    } from './methods';
    import Dialog from './components/Dialog';

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

    class App extends Component {
      constructor() {
      // [..]

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

      render() {
        // [..]
        return (
          <div className="App">
            <aside className="sidebar left-sidebar">
              {currentUser ? (
                <div className="user-profile">
                  <span className="username">{currentUser.name}</span>
                  <span className="user-id">{`@${currentUser.id}`}</span>
                </div>
              ) : null}
            </aside>
            <section className="chat-screen">
             // [..]
            </section>
            <aside className="sidebar right-sidebar">
              {showLogin ? (
                <Dialog
                  userId={userId}
                  handleInput={this.handleInput}
                  connectToChatkit={this.connectToChatkit}
                />
              ) : null}
            </aside>
          </div>
        );
      }
    }

    export default App;

View your application in the browser. It should behave like the gif below:

Once the user submits the form, the connectToChatkit() method in methods.js will be executed. In this method, we instantiate the Chatkit ChatManager with our instanceLocator (replace <your chatkit instance locator>, the userId and a custom TokenProvider which points to the /authenticate route defined on the server.

Following that, we call the connect() method on ChatManager 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.

Add the user to a room

The next step is to add the user to a room and display all the rooms that the user belongs to on the left sidebar.

Create a new RoomList.js file in the client/src/components directory and add the following code into it:

    // client/src/components/RoomList.js

    import React from 'react';
    import Proptypes from 'prop-types';

    const RoomList = props => {
      const { rooms, currentRoom, connectToRoom, currentUser } = props;
      const roomList = rooms.map(room => {
        const roomIcon = !room.isPrivate ? '🌐' : '🔒';
        const isRoomActive = room.id === currentRoom.id ? 'active' : '';

        return (
          <li
            className={isRoomActive}
            key={room.id}
            onClick={() => connectToRoom(room.id)}
          >
            <span className="room-icon">{roomIcon}</span>
            {room.customData && room.customData.isDirectMessage ? (
              <span className="room-name">
                {room.customData.userIds.filter(id => id !== currentUser.id)[0]}
              </span>
            ) : (
              <span className="room-name">{room.name}</span>
            )}
          </li>
        );
      });
      return (
        <div className="rooms">
          <ul className="chat-rooms">{roomList}</ul>
        </div>
      );
    };

    RoomList.propTypes = {
      rooms: Proptypes.array.isRequired,
      currentRoom: Proptypes.object.isRequired,
      connectToRoom: Proptypes.func.isRequired,
      currentUser: Proptypes.object.isRequired,
    };

    export default RoomList;

Then update methods.js as with the code below. Replace <your room id> with the ID of the room you created earlier.

    // client/src/components/methods.js

    import Chatkit from '@pusher/chatkit-client';
    import axios from 'axios';

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

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

    // Add this function
    function connectToRoom(id = <your room id>) {
      const { currentUser } = this.state;

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

      return currentUser
        .subscribeToRoom({
          roomId: `${id}`,
        })
        .then(currentRoom => {
          const roomName =
            currentRoom.customData && currentRoom.customData.isDirectMessage
              ? currentRoom.customData.userIds.filter(
                  id => id !== currentUser.id
                )[0]
              : currentRoom.name;

          this.setState({
            currentRoom,
            roomUsers: currentRoom.users,
            rooms: currentUser.rooms,
            roomName,
          });
        })
        .catch(console.error);
    }

    function connectToChatkit(event) {
      event.preventDefault();

      const { userId } = this.state;

      if (userId === null || userId.trim() === '') {
        alert('Invalid userId');
        return;
      }

      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 instance locator>',
            userId,
            tokenProvider,
          });

          return chatManager
            .connect({
              onAddedToRoom: room => {
                const { rooms } = this.state;
                this.setState({
                  rooms: [...rooms, room],
                });
              },
            })
            .then(currentUser => {
              this.setState(
                {
                  currentUser,
                  showLogin: false,
                  rooms: currentUser.rooms,
                },
                // add this line
                () => connectToRoom.call(this)
              );
            });
        })
        .catch(console.error);
    }

    // update this line
    export { handleInput, connectToRoom, connectToChatkit }

Finally, update App.js like this:

    // client/src/App.js

    import React, { Component } from 'react';
    // import `connectToRoom`
    import {
      handleInput,
      connectToChatkit,
      connectToRoom,
    } from './methods';
    import Dialog from './components/Dialog';
    // import `RoomList`
    import RoomList from './components/RoomList';

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

    class App extends Component {
      constructor() {
        // [..]

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

      render() {
        // [..]

        return (
          <div className="App">
            <aside className="sidebar left-sidebar">
              {currentUser ? (
                <div className="user-profile">
                  <span className="user-id">{`@${currentUser.id}`}</span>
                </div>
              ) : null}

             // Add this
              {currentRoom ? (
                <RoomList
                  rooms={rooms}
                  currentRoom={currentRoom}
                  connectToRoom={this.connectToRoom}
                  currentUser={currentUser}
                />
              ) : null}
            </aside>
            <section className="chat-screen">
            // update `.chat-header`
             <header className="chat-header">
                {currentRoom ? <h3>{roomName}</h3> : null}
              </header>
            // [..]
            </section>
            // [..]
          </div>
        );
      }
    }

    export default App;

After the user is connected to our Chakit instance, connectToRoom() is invoked. This method subscribes the user to the room we created earlier using the subscribeToRoom() method on the currentUser. As you can see, we can reuse this function to connect to other rooms, but are using the id of the General room created earlier by default. Make sure to replace <your room id> before testing the changes in your browser.

Sending and viewing messages

Let's allow users send messages by creating a new sendMessage() function in methods.js:

    // client/src/components/methods.js

    // Add this below the other functions
    function sendMessage(event) {
      event.preventDefault();
      const { newMessage, currentUser, currentRoom } = this.state;

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

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

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

    // export `sendMessage`
    export { handleInput, connectToRoom, connectToChatkit, sendMessage }

Then use it in App.js like this:

    // client/src/App.js

    import React, { Component } from 'react';
    // import `sendMessage`
    import {
      handleInput,
      connectToChatkit,
      connectToRoom,
      sendMessage,
    } from './methods';
    import Dialog from './components/Dialog';
    import RoomList from './components/RoomList';

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

    class App extends Component {
      constructor() {
        // [..]

        this.connectToRoom = connectToRoom.bind(this);
        // add this line
        this.sendMessage = sendMessage.bind(this);
      }

      render() {
        // [..]
        return (
          <div className="App">
            <aside className="sidebar left-sidebar">
            // [..]
            </aside>
            <section className="chat-screen">
              <header className="chat-header">
                {currentRoom ? <h3>{roomName}</h3> : null}
              </header>
              <ul className="chat-messages"></ul>
              <footer className="chat-footer">
                // update `.message-form`
                <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>
            <aside className="sidebar right-sidebar">
            // [..]
            </aside>
          </div>
        );
      }
    }

    export default App;

When the form is submitted, we access the user's input via this.state.newMessage and call sendMessage on the currentUser, which sends the message to the current room that the user is subscribed to. To view messages in the room, we need to create a new component called ChatSession in the client/src/components directory:

    // client/src/components/ChatSession.js

    import React from 'react';
    import Proptypes from 'prop-types';
    import { format } from 'date-fns';

    const ChatSession = props => {
      const { messages } = props;
      return messages.map(message => {
        const time = format(new Date(`${message.updatedAt}`), 'HH:mm');

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

    ChatSession.propTypes = {
      messages: Proptypes.arrayOf(Proptypes.object).isRequired,
    };

    export default ChatSession;

Then update the connectToRoom() method as follows:

    // client/src/methods.js

    // beginning of the file

    function connectToRoom(id = <your room id>) {
      const { currentUser } = this.state;

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

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

                  return 1;
                }),
              });
            },
          },
        })
        .then(currentRoom => {
          const roomName =
            currentRoom.customData && currentRoom.customData.isDirectMessage
              ? currentRoom.customData.userIds.filter(
                  id => id !== currentUser.id
                )[0]
              : currentRoom.name;

          this.setState({
            currentRoom,
            roomUsers: currentRoom.users,
            rooms: currentUser.rooms,
            roomName,
          });
        })
        .catch(console.error);
    }

    // rest of the code

We’ve set up two room connection hooks here: onMessage is invoked anytime a new message arrives in the room, while onPresenceChanged is invoked when a room 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 when the user connects to the room.

Next, update App.js as follows:

    // client/src/App.js

    // [..]
    import RoomList from './components/RoomList';
    // import `ChatSession`
    import ChatSession from './components/ChatSession';

    // [..]

    class App extends Component {
      constructor() {
        // [..]
      }

      render() {
        // [..]

        return (
          <div className="App">
            // [..]
            <section className="chat-screen">
              <header className="chat-header">
                {currentRoom ? <h3>{roomName}</h3> : null}
              </header>
              // update `.chat-messages
              <ul className="chat-messages">
                <ChatSession messages={messages} />
              </ul>
              // [..]
          </div>
        );
      }
    }

    export default App;

Now, you should be able to send and view messages in the room seamlessly.

Display room users

The next step is to display the members of the room in the right sidebar and their current status (online or offline). To do so, we need to create a new component called RoomUsers and add the following code into it:

    // client/src/components/RoomUsers.js

    import React from 'react';
    import Proptypes from 'prop-types';

    const RoomUsers = props => {
      const { roomUsers, currentUser } = props;
      const users = roomUsers.map(user => {
        return (
          <li className="room-member" key={user.id}>
            <div>
              <span className={`presence ${user.presence.state}`} />
              <span>{user.name}</span>
            </div>
            {currentUser.id !== user.id ? (
              <button
                title={`Send ${user.name} a direct message`}
                className="send-dm"
              >
                +
              </button>
            ) : null}
          </li>
        );
      });

      return (
        <div className="room-users">
          <ul>{users}</ul>
        </div>
      );
    };

    RoomUsers.propTypes = {
      roomUsers: Proptypes.array.isRequired,
      currentUser: Proptypes.object.isRequired,
    };

    export default RoomUsers;

Then update App.js as follows::

    // client/src/App.js

    // [..]
    // import `RoomUsers
    import RoomUsers from './components/RoomUsers';

    // [..]

    class App extends Component {
      constructor() {
       // [..]
      }

      render() {
        // [..]

        return (
          <div className="App">
            // [..]
            // update `.right-sidebar`
            <aside className="sidebar right-sidebar">
              {currentRoom ? (
                <RoomUsers
                  currentUser={currentUser}
                  roomUsers={roomUsers}
                />
              ) : null}
            </aside>

            // [..]
          </div>
        );
      }
    }

    export default App;

At this point, the members in the room will be shown on the right sidebar:

Send direct messages

If you hover over each of the other users in the room, you will see a button that, once clicked, will create a private room for the current user and the user on which the button was clicked for the purpose of a one-on-one conversation between the users.

To achieve this, we need to add two new methods in methods.js:

    // client/src/methods.js

    // Add this below the other functions
    function createPrivateRoom(id) {
      const { currentUser, rooms } = this.state;
      const roomName = `${currentUser.id}_${id}`;

      const isPrivateChatCreated = rooms.filter(room => {
        if (room.customData && room.customData.isDirectMessage) {
          const arr = [currentUser.id, id];
          const { userIds } = room.customData;

          if (arr.sort().join('') === userIds.sort().join('')) {
            return {
              room,
            };
          }
        }

        return false;
      });

      if (isPrivateChatCreated.length > 0) {
        return Promise.resolve(isPrivateChatCreated[0]);
      }

      return currentUser.createRoom({
        name: `${roomName}`,
        private: true,
        addUserIds: [`${id}`],
        customData: {
          isDirectMessage: true,
          userIds: [currentUser.id, id],
        },
      });
    }

    function sendDM(id) {
      createPrivateRoom.call(this, id).then(room => {
        connectToRoom.call(this, room.id);
      });
    }

    // update the exports
    export { handleInput, connectToRoom, connectToChatkit, sendMessage, sendDM }

Then update App.js as follows::

    // client/src/App.js

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

    // [..]

    class App extends Component {
      constructor() {
        // [..]

        // Add this line
        this.sendDM = sendDM.bind(this);
      }

      render() {
       // [..]

        return (
          <div className="App">
            // [..]
            <aside className="sidebar right-sidebar">
              {currentRoom ? (
                // pass `sendDM` as props to `RoomUsers`
                <RoomUsers
                  currentUser={currentUser}
                  sendDM={this.sendDM}
                  roomUsers={roomUsers}
                />
    +          ) : null}
            </aside>

            // [..]
          </div>
        );
      }
    }

    export default App;

Finally, update the RoomUsers component as follows:

    import React from 'react';
    import Proptypes from 'prop-types';

    const RoomUsers = props => {
    // update this line
      const { roomUsers, sendDM, currentUser } = props;
      const users = roomUsers.map(user => {
        return (
          <li className="room-member" key={user.id}>
            <div>
              <span className={`presence ${user.presence.state}`} />
              <span>{user.name}</span>
            </div>
            {currentUser.id !== user.id ? (
              // add onClick to button
              <button
                onClick={() => sendDM(user.id)}
                title={`Send ${user.name} a direct message`}
                className="send-dm"
              >
                +
              </button>
            ) : null}
          </li>
        );
      });

      return (
        <div className="room-users">
          <ul>{users}</ul>
        </div>
      );
    };

    // update propTypes
    RoomUsers.propTypes = {
      roomUsers: Proptypes.array.isRequired,
      sendDM: Proptypes.func.isRequired,
      currentUser: Proptypes.object.isRequired,
    };

    export default RoomUsers; 

Once the + button is clicked, a new private room for the conversation is created instantly. Private conversations are annotated on the left sidebar with the lock icon and the name of the other user in the room.

Wrap up

In this tutorial, we created a chatroom and added the ability to send a private message to any member of the room. You can checkout other things Chatkit can do by viewing its extensive documentation. Don't forget to grab the full source code in this GitHub repository.

Clone the project repository
  • Chat
  • chatroom
  • JavaScript
  • Node.js
  • React
  • 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.