🎉 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

Implement self-destructing messages in a React chat app

  • Ayooluwa Isaiah
February 22nd, 2019
You will need Node 8+ installed on your machine.

In this tutorial, I’ll demonstrate how to implement self-destructing messages in a chatroom built using Chatkit and React. For those unaware, the messages will self-destruct after a particular duration set by the user, leaving no trace of it in the chatroom or on Chatkit servers.

Here’s how the final result will look like:

Prerequisites

You need to have Node.js (v8 or later) and npm installed to complete this tutorial. You can check out this page to view instructions on how to install or upgrade Node.js on your machine. Prior experience with building React and Node applications is also necessary to grasp the concepts discussed in this tutorial.

Sign up for Chatkit

Open this link to create a new Chatkit account or sign in to your existing account. This is a necessary step so that you can create a new Chatkit instance for your application and manage your credentials. Once you’re logged in, create a new Chatkit instance for your application, then locate the Credentials tab on your instance’s dashboard and take note of the Instance Locator and Secret Key as we’ll be using them later on.

Head over to the Console tab, and create a new user and a new room. You can follow the instructions on this page to learn how to do so. Take note of the room ID as we’ll be using it later on.

Finally, head over to the ROLES tab and update the permissions under the default role to include room:update. This role needs to be enabled for users to be able to update the room as you’ll see later on as we progress.

Set up the server

Create a new directory for this tutorial in your filesystem and cd into it. Then run the following command to initialize the project with a package.json file:

    npm init -y

Next, run the command below to install the dependencies we’ll be needing to build the server. . The [@pusher/chatkit-server](https://www.npmjs.com/package/@pusher/chatkit-server) package is the Node.js SDK that grants us the ability to make use of Chatkit features on the server.

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

Once all the dependencies have been installed, create a new .env file at the root of your project directory and populate it with your Chatkit credentials:

    // .env

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

Next, create a new server.js file and paste in 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}`);
    });

At the moment, we have two routes on the server. The /users endpoint is where user creation takes place, while the /authenticate endpoint is used to authenticate any user before they can gain access. For the sake of brevity, I have opted not to include a proper authentication flow in this example.

That’s all we need to do on the server for now. You can start the server by running node server.js to make it available on port 5200.

Set up the React application

We’ll be making use of create-react-app to bootstrap the React application. You can install it globally via npm:

    npm install -g create-react-app

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

    create-react-app client

Following that, cd into the client directory and install the other dependencies that we’ll be needing on the application frontend. The [@pusher/chatkit-client](https://www.npmjs.com/package/@pusher/chatkit-client) package is the JavaScript SDK that grants us the ability to make use of Chatkit features on the frontend.

    npm install skeleton-css @pusher/chatkit-client axios --save

Once all the dependencies have been installed, run npm start from within the client folder to launch the development server on http://localhost:3000.

Add the application styles

Before we get into the nitty-gritty aspects of this project, let’s update the styles for the app as follows:

    // 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-form, .message-input {
      width: 100%;
      margin-bottom: 0;
    }

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

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

    .preferences {
      padding-left: 20px;
      padding-right: 20px;
    }

    .right-sidebar select {
      width: 100%;
    }

    .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-window {
      width: 60%;
      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;
      height: 100%;
      overflow-y: auto;
      padding: 10px;
      list-style: none;
      display: flex;
      flex-direction: column;
      justify-content: flex-end;
      margin-bottom: 0;
    }

    .status-message {
      text-align: center;
      font-style: italic;
    }
    .message {
      margin-bottom: 5px;
      padding-left: 10px;
    }

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

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

Connect to Chatkit

Locate client/src/App.js and open it up in your text editor. This is where the majority of the code for this project will be written in. Add the following code into App.js:

    // 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 {
      state = {
        currentUser: null,
        currentRoom: null,
        newMessage: "",
        messages: [],
        roomUsers: [],
        messageTimer: "0"
      };

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

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

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

      connectToRoom = () => {

        const { currentUser } = this.state;

        return currentUser
          .subscribeToRoom({
            roomId: "<your room id>",
            messageLimit: 100,
            hooks: {
              onMessage: message => {
                const messages = [...this.state.messages, message];

                this.setState({
                  messages
                });
              },
              onPresenceChanged: () => {
                const { currentRoom } = this.state;
                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,
          messageTimer,
        } = this.state;

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

        const ChatSession = messages.map(message => {
          return message.type ? (
            <li className="status-message" key={message.id}>
              <span>{message.text}</span>
            </li>
          ) : (
            <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 left-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-window">
              <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>

            <aside className="sidebar right-sidebar" />
          </div>
        );
      }
    }

    export default App;

Don’t forget to update <your instance locator> and <your room id> with the appropriate details from your Chatkit instance dashboard.

When a user tries to join the chat, the addUser() method connects to your Chatkit instance and returns a currentUser object that represents the current connected user. This object is the primary means of interacting with Chatkit.

After this, the connectToRoom() method is invoked. This method connects the user to the room whose ID was provided, and loads the last 100 messages from that room into the application. The messageLimit property controls how many messages can be loaded at once.

When we subscribe to a room, we can take advantage of the room subscription hooks to perform actions whenever something happens in the room. These hooks are defined in the aptly named hooks object.

The onMessage hook is triggered when a new message is sent to the chatroom. We simply append the message to the messages array and render it in the chat window. Similarly, the onPresenceChanged hook is fired when a member of the room comes online or goes offline so that we can show the correct status of the users in the sidebar and update it immediately a change happens.

At this point, you should be able to add a user to the room and view the user’s online or offline status in the sidebar.

Sending messages

To send a message to the chatroom, all we need to do is call currentUser.sendMessage() and pass in the message as well as the id of the room where the message is to be sent.

Create a new sendMessage() method below connectToRoom() as shown below:

    // client/src/App.js

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

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

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

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

Self-destructing messages

And now to the headline feature of this project. We will create an interface for the user to opt into self-destruct mode, and also select the amount of time before messages are destroyed. We’ll also show a status message across all connected clients whenever the self-destruct status is changed.

Change up .right-sidebar in the render() method of App.js as follows:

      render() {

        return (
          <div className="App">

           // rest of the code    

            <aside className="sidebar right-sidebar">
              {currentUser ? (
                <section className="preferences">
                  <h3>Disappearing Messages</h3>
                  <select
                    id="timeout"
                    name="timeout"
                    value={messageTimer}
                    onChange={this.updateMessageTimer}
                  >
                    <option value="0">Off</option>
                    <option value="10000">10 seconds</option>
                    <option value="20000">20 seconds</option>
                    <option value="30000">30 seconds</option>
                    <option value="60000">1 minute</option>
                  </select>
                </section>
              ) : null}
            </aside>
          </div>
        );
      }

Next, update the addUser() method like this:

    // client/src/App.js

      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({
                onRoomUpdated: room => {
                  const { messageTimer } = room.customData;
                  this.setState(
                    {
                      messageTimer
                    },
                    () => this.showStatusMessage()
                  );
                }
              })
              .then(currentUser => {
                this.setState(
                  {
                    currentUser
                  },
                  () => this.connectToRoom()
                );
              });
          })
          .catch(console.error);
      };

Then add the following methods below sendMessage():

    // client/src/App.js

      updateMessageTimer = event => {
        const { value } = event.target;
        const { currentRoom, currentUser } = this.state;
        this.setState({
          messageTimer: value,
        });

        currentUser.updateRoom({
          roomId: currentRoom.id,
          customData: { messageTimer: value },
        });
      };

      showStatusMessage = () => {
        const { messageTimer, messages } = this.state;
        const text = `The disappearing message timeout has been set to ${messageTimer /
          1000} seconds`;

        const statusMessage = {
          id: `${Date.now() + Math.random()}`,
          text,
          type: 'status',
        };
        messages.push(statusMessage);

        this.setState({
          messages,
        });
      };

Whenever the self-destruct timer is updated, we update the room in updateMessageTime() and pass the current value of messageTimer to using the customData property which allows us to associate custom data with a room.

In addUser(), we updated the connect() method to include the connection hooks object which allows us to be notified when various events happen on the Chatkit instance. The onRoomUpdated hook is fired when a room is updated. In this case, we’re simply displaying a message in the chat window that indicates the current self-destruct status.

To actually delete a message, we need to set up a route on the server that takes the ID of the message to delete. Then we will use setTimeout() to execute the deletion after the specified period of time.

Add a new /delete-message route in server.js as follows:

    // server.js

    app.post('/delete-message', (req, res) => {
      const { messageId, timer } = req.body;
      setTimeout(() => {
        chatkit.deleteMessage({
          id: messageId
        })
          .then(() => console.log('gone forever'))
          .catch(err => console.error(err))
      }, timer);
    });

Then add a new deleteMessage() method below showStatusMessage() in App.js:

    // client/src/App.js
      deleteMessage = id => {
        const { messageTimer } = this.state;
        axios
          .post('http://localhost:5200/delete-message', {
            messageId: id,
            timer: Number(messageTimer),
          })
          .catch(console.error);
      };

Finally, update the onMessage hook as follows:

    // client/src/App.js

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

        return currentUser
          .subscribeToRoom({
            roomId: "<your room id>",
            messageLimit: 100,
            hooks: {
              onMessage: message => {
                const messages = [...this.state.messages];
                const index = messages.findIndex(item => item.id === message.id);
                if (index !== -1) {
                  messages.splice(index, 1, message);
                } else {
                  messages.push(message);
                }

                const { messageTimer } = this.state;
                if (message.text !== "DELETED" && messageTimer !== "0") {
                  this.deleteMessage(message.id);
                }

                this.setState({
                  messages
                });
              },
              onPresenceChanged: () => {
                const { currentRoom } = this.state;
                this.setState({
                  roomUsers: currentRoom.users.sort(a => {
                    if (a.presence.state === "online") return -1;

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

Once the message has been deleted from the room, Chatkit updates the message with a generic ‘DELETED’ text, and the onMessage hook will be triggered again. Instead of adding the deleted message again, we’ll just find the message in the messages array and simply update it to it’s latest value.

Test the app

Before you test the app, make sure to kill the server with Ctrl-C and start it again using node server.js. And don’t forget to update <your instance locator> and <your room id> in App.js with the appropriate details from your Chatkit instance dashboard.

Open the app in a few different tabs and join the chatroom under different usernames. Set the disappearing message timeout to 10 seconds and exchange few messages between the users. Messages sent after the timeout has been set will be deleted, similar to the GIF below:

Wrap up

That concludes my tutorial. 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.

Clone the project repository
  • Chat
  • 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.