🎉 New release for Pusher Chatkit - Webhooks! Extend your in-app chat functionality
Hide

Create a whiteboard Electron app with React - Part 2: Adding the group chat

  • Wern Ancheta
April 16th, 2019
You will need Node and Yarn installed on your machine.

This is part two of the two-part series on creating a whiteboard app in React. In this part, we will add a group chat feature so the users in the room can talk about their ideas while drawing in the whiteboard.

Here’s what the final output for this part will look like:

Prerequisites

Basic knowledge of React is required to follow this tutorial.

You’ll also need to have a Chatkit app instance.

You need to have completed part one of the series.

Adding the group chat

Before anything else, we first need to install the dependencies we’ll be using to implement the group chat functionality:

    yarn add @pusher/chatkit-client axios react-custom-scrollbars string-hash

Update Login screen

Now we’re ready to update the Login screen. Start by importing a couple of the dependencies we just installed:

    // src/screens/Login.js
    import Pusher from "pusher-js";

    // add these:
    import axios from "axios"; // for making an HTTP request to the server
    import stringHash from "string-hash"; // for getting a hash from a string value

Next, update the login function to create a hash from the username. The stringHash function returns a number which represents the username. Later on, we’ll use it as a unique ID for the users in Chatkit:

    login = () => {
      const { myUsername, channelName } = this.state;
      const myUserID = stringHash(myUsername).toString(); // create a hash for the username

      // next: update function to execute when group channel subscription succeeds
    }

Next, we update the code to execute when the subscription to a group channel succeeds. So instead of immediately navigating to the Whiteboard screen, we first make a request to the /login endpoint of the server. As you’ll see later in the server code, this will allow us to register the users who are entering the channel so we can get the Chatkit room ID:

    this.group_channel.bind("pusher:subscription_succeeded", async () => {

      console.log("subscription to group succeeded");

      try {
        const response = await axios.post(`${BASE_URL}/login`, {
          user_id: myUserID,
          username: myUsername,
          channel: channelName
        });

        if (response.status === 200) {
          this.props.navigation.navigate("Whiteboard", {
            roomID: response.data.room_id, // Chatkit room ID
            channelName, 
            myUserID,
            myUsername,
            pusher: this.pusher,
            group_channel: this.group_channel
          });
        }
      } catch (e) {
        console.log("error occured logging in: ", e);
      }
    }

Update Whiteboard screen

Let’s proceed to update the Whiteboard screen. Start by importing the Chatkit package and the component for rendering the chat UI:

    // src/screens/Whiteboard.js
    import shortid from 'shortid';

    // add these:
    import Chatkit from '@pusher/chatkit-client';
    import ChatBox from '../components/ChatBox';

    const CHATKIT_TOKEN_PROVIDER_ENDPOINT = process.env.REACT_APP_CHATKIT_TEST_TOKEN_PROVIDER;
    const CHATKIT_INSTANCE_LOCATOR = process.env.REACT_APP_CHATKIT_INSTANCE_ID;

Initialize the state value for storing the messages to be rendered:

    state = {
      // <existing code here>
      tool: Tools.Pencil,
      messages: [] // add this
    }

Update componentDidMount to get the new nav params that were passed from the Login screen earlier:

    async componentDidMount() {

      const { navigation } = this.props;
      this.myUsername = navigation.getParam("myUsername");
      this.pusher = navigation.getParam("pusher");  
      this.group_channel = navigation.getParam("group_channel");

      // add these:
      this.roomID = navigation.getParam("roomID").toString(); 
      this.myUserID = navigation.getParam("myUserID"); 
      this.channelName = navigation.getParam("channelName"); 

      // next: initialize Chatkit
    }

Next, initialize Chatkit and subscribe to the room using the room ID returned from the server:


this.setState({
  myUsername: this.myUsername
});

try {

  // add these
  const chatManager = new ChatManager({
    instanceLocator: CHATKIT_INSTANCE_LOCATOR,
    userId: this.myUserID,
    tokenProvider: new TokenProvider({ url: CHATKIT_TOKEN_PROVIDER_ENDPOINT })
  });

  this.currentUser = await chatManager.connect();
  await this.currentUser.subscribeToRoom({
    roomId: this.roomID, 
    hooks: {
      onMessage: this.onReceive
    },
    messageLimit: 10
  });
} catch (err) {
  console.log("cannot connect user to chatkit: ", err);
}


let textGatherer = this._gatherText();

    /* 
      the rest of existing code...
    */

Next, update the render method so it also renders the ChatBox component. This component is used for rendering the messages sent in the room as well as the form for sending new messages:

    <Col lg={3} className="Sidebar">
      <div className="tools">

        {this.renderTools()}

        <div className="tool">
          ...
        </div>
      </div>

      <div>
        ...
      </div>

      {
        this.currentUser &&
        <ChatBox 
          userID={this.myUserID} 
          roomID={this.roomID}
          currentUser={this.currentUser} 
          messages={this.state.messages} />
      }
    </Col>  

Next, add the onReceive and _getMessage function. onReceive gets fired every time a new message is received from Chatkit. We simply create a new messages array by appending the new message to the end of the existing one:

    onReceive = async (data) => {
      let { message } = await this._getMessage(data);
      await this.setState(prevState => ({
        messages: [...prevState.messages, message]
      }));
    };

The _getMessage function is used for filtering the message data so it only returns what we need:

    _getMessage = async ({ id, senderId, sender, text }) => {
      const msg_data = {
        _id: id, // unique message ID
        text: text, // message inputted by the user
        user: {
          _id: senderId, // unique user ID
          name: sender.name // username
        }
      };

      return {
        message: msg_data
      };
    };

ChatBox component

Here’s the code for the ChatBox component. This component is responsible for rendering the messages as well as the form for sending a new message:

    // src/components/ChatBox.js
    import React, { Component } from 'react';
    import { Button, Input } from 'reactstrap';
    import { Scrollbars } from 'react-custom-scrollbars';
    import MessageBox from './MessageBox';

    class ChatBox extends Component {

      state = {
        is_sending: false, // whether the sending button is disabled or not
        message: '' // the message to be sent
      }

      render() {
        return (
          <div className="ChatBox">
            <Scrollbars
              style={{ height: 250, width: 300 }}
              autoHide={true}
            >
              <div className="MessageBoxes">{this._renderMessages()}</div>
            </Scrollbars>

            <div className="textInputContainer">
              <Input 
                type="textarea" 
                name="message" 
                id="message" 
                placeholder="Enter message here" 
                value={this.state.message}
                onChange={this.onUpdateMessage} />
            </div>

            <div className="buttonContainer">
              <Button
                variant="primary"
                onClick={this.sendMessage}
                disabled={this.state.is_sending}
                block
              >
                {this.state.is_sending ? "Sending…" : "Send"}
              </Button>
            </div>

          </div>
        );
      }

      // next: add _renderMessages function

    }

    export default ChatBox;

Here’s the function for rendering individual messages. This uses a MessageBox component which we’ll create later:

    _renderMessages = () => {
      return this.props.messages.map(msg => {
        return <MessageBox msg={msg} userID={this.props.userID} />
      });
    };

Next, here’s the function for updating the value of the text field for entering a new message:

    onUpdateMessage = evt => {
      this.setState({
        message: evt.target.value
      });
    };

    // next: add sendMessage function

Lastly, here’s the function for sending a message with Chatkit. All it requires is the text to be sent and the room ID which was passed from the ChatBox component:

    sendMessage = async () => {
      let msg = {
        text: this.state.message,
        roomId: this.props.roomID
      };

      this.setState({
        is_sending: true
      });

      try {
        await this.props.currentUser.sendMessage(msg);
        this.setState({
          is_sending: false,
          message: ""
        });
      } catch (err) {
        console.log("error sending message: ", err);
      }
    };

MessageBox component

Here’s the code for the MessageBox component. This renders the message and the username of the user who sent it:

    // src/components/MessageBox.js

    import React from 'react';

    const MessageBox = ({ msg, userID }) => {

      const className = (msg.user._id === userID) ? "MessageRow Me" : "MessageRow";

      return (
        <div className={className}>
          <div className="ChatBubble">
            <div className="username">{msg.user.name}</div>
            <div className="text">{msg.text}</div>
          </div>
        </div>
      );

    }

    export default MessageBox;

Update the styles

We used new class names in the ChatBox and MessageBox components. Here are the styles applied to them:

    // src/index.css
    .ChatBox {
      margin-top: 30px;
    }

    .MessageRow.Me {
      float: right;
      clear: both;
    }

    .MessageRow {
      float: left;
      clear: both;
    }

    .ChatBubble {
      background: #d1e7ff;
      margin-bottom: 10px;
      padding: 10px;
      border-radius: 10px;
      text-align: left;
      font-size: 14px;
      vertical-align: text-bottom;
      max-width: 250px;
    }

    .username {
      font-weight: bold;
    }

Update the server

Now we’re ready to update the server. Start by installing the Chatkit server module:

    yarn add @pusher/chatkit-server

Next, update your server/.env file to include the Chatkit credentials:

    CHATKIT_INSTANCE_ID="YOUR CHATKIT INSTANCE ID (WITHOUT V1:US1)"
    CHATKIT_SECRET_KEY="YOUR CHATKIT SECRET"

Next, update the server file to use Chatkit:

    // server/server.js
    // <existing code..>
    const cors = require("cors");

    // add these
    const Chatkit = require("@pusher/chatkit-server");

    var channels = []; // for storing channels and usernames within them

Next, initialize Chatkit:

    // <existing code..>
    var pusher = new Pusher({
     // ...
    });

    // add these:
    const chatkit = new Chatkit.default({
      instanceLocator: `v1:us1:${process.env.CHATKIT_INSTANCE_ID}`,
      key: process.env.CHATKIT_SECRET_KEY
    });

Add a createUser function. This is responsible for creating Chatkit users:

    const createUser = async (user_id, username) => {
      try {
        await chatkit.createUser({
          id: user_id,
          name: username
        });
      } catch (err) {
        if (err.error === "services/chatkit/user_already_exists") {
          console.log("user already exists: ", err);
        } else {
          console.log("error occurred: ", err);
        }
      }
    };

Next, create a new endpoint called login and add the following. As you’ve seen in the src/screens/Login.js file earlier, this endpoint receives the channel name, user ID, and username of the user who is logging in. We use it to determine whether the channel name (room name) already exists or not. If it doesn’t already exist then we create a Chatkit user as well as a room:

    app.post("/login", async (req, res) => {
      const { channel, user_id, username } = req.body;

      var channel_index = channels.findIndex(c => c.name === channel);
      if (channel_index === -1) {
        console.log("channel not yet created, so creating one now...");

        await createUser(user_id, username); // create a Chatkit user

        try {
          const room = await chatkit.createRoom({
            creatorId: user_id,
            name: channel
          });

          // push to an array so its existence can be checked when someone logs in
          channels.push({
            id: room.id.toString(),
            name: channel,
            users: [
              {
                id: user_id,
                name: username
              }
            ]
          });

          return res.json({
            room_id: room.id.toString() // return the unique room ID
          });
        } catch (err) {
          console.log("error creating room: ", err);
        }
      } else {
        // next: add code for when channel already exists
      }

      return res.status(500).send("invalid user");
    });

If the channel name already exists but the supplied username doesn’t exist, then we only create a Chatkit user and return the ID of the existing room:

    const user_index = channels[channel_index].users.findIndex(
      usr => usr.name === username
    );
    if (user_index === -1) {
      console.log("channel created, so pushing user...");

      await createUser(user_id, username);

      channels[channel_index].users.push({
        id: user_id,
        name: username
      });

      return res.json({
        room_id: channels\[channel_index\]["id"].toString()
      });
    }

    return res.json({
      room_id: channels[channel_index].id
    });

Running the app

At this point, you can now run the app. Start by running the server:

    cd server
    node server.js

Then the app itself:

    cd ..
    yarn start
    yarn electron-dev

Conclusion

In this tutorial, we added a group chat functionality to the whiteboard app so the users have a means of communicating with each other while they draw things in the canvas.

That also wraps up this series. In this series, we created a whiteboard app with chat capabilities using React, React Sketch and Chatkit.

You can find the source code for this tutorial on its GitHub repo.

Clone the project repository
  • Chat
  • Collaboration
  • JavaScript
  • Live UX
  • Node.js
  • React
  • Social
  • Channels
  • 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.