🎉 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 Vue.js Chat app

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

In this tutorial, I’ll show you how to build a chat app with Vue.js and Chatkit. You’ll learn how self-destructing messages work in Chatkit, and how to implement it in your application.Self-destructing messages are automatically erased from the chatroom after a short period of time as set by the user. This deletion happens on the all connected device, and on the Chatkit servers. No lasting record of the conversation is kept.

Here’s how the completed application will look like:

If you want to grab the complete code used in this article, head to this GitHub repository and follow the instructions in the README.md file.

Prerequisites

Basic knowledge of Vue.js and Node is required to complete this tutorial. You also need to have Node.js (version 8 or later) and npm installed on your computer. This page contains instructions on the installation procedure for Node.js.

Sign up for Chatkit

Create a new Chatkit account or sign into your existing account here. Once you are logged in, create a new Chatkit instance, then navigate to the Credentials tab to grab your Instance Locator and Secret Key.

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. Once the room has been created, 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. I’ll explain why this step is necessary later on in the tutorial.

Set up the server

Create a new directory for this project, cd into it then run npm init -y from the root of the new directory to initialize your project with a package.json file.

Next, install all the dependencies we’ll be needing on the server with the following command:

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

As soon as the dependencies have been installed, create a new .env file at the root of your project and add your Chatkit credentials into it.

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

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

First, we import Chatkit from @pusher/chatkit-server, then we instantiate our Chatkit instance using the instance locator and secret key we added to .env in the previous step.

In the /users route, we take a userId and create a Chatkit user through our instance. The /authenticate route authenticates anyone who tries to connect to our instance, though it should be noted that we don’t actually try to do this in the above example.

The /delete-message route is where the destruction of messages will occur. It takes the message ID and a timeout in seconds, and deletes the message after the timeout elapses.

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

Set up the Vue.js application

We’ll be making use of Vue CLI to bootstrap the application frontend. Install it globally on your machine with the following command:

    npm install -g @vue/cli

Then use it to create a new Vue.js app in the root of your project directory. When prompted, use the default preset (babel, eslint).

    vue create client

Following that, cd into the client folder, and install the additional dependencies that we’ll be making use of on the frontend of the application including the Chatkit client SDK.

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

Once the dependencies have been installed, run npm run serve to start the development server on http://localhost:8080.

Add the application styles

Create a new App.css file in the client/src folder and 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: darkcyan;
      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 and send messages

Open up client/src/main.js in your text editor and change it to look like this:

    // client/src/main.js

    import Vue from 'vue';
    import App from './App.vue';

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

    Vue.config.productionTip = false;

    new Vue({
      render: h => h(App),
    }).$mount('#app');

Then update client/src/App.vue as follows:

    <template>
      <div class="App">
        <aside class="sidebar left-sidebar">
          <section v-if="!currentUser" class="join-chat">
            <h3>Join Chat</h3>
            <form @submit.prevent="addUser">
              <input
                placeholder="Enter your username"
                type="text"
                name="userId"
                v-model="userId"
                />
            </form>
          </section>

          <section v-if="currentUser" class="room-users">
            <h3>Room Users</h3>
            <ul>
              <li v-for="user in roomUsers" :key="user.id" class="user">
                <span class="presence" :class="user.presence.state" />
                <span>{{ user.name }}</span>
              </li>
            </ul>
          </section>
        </aside>

        <section class="chat-window">
          <header class="room-name">
            <h3 v-if="currentRoom">{{ currentRoom.name }}</h3>
            <h3 v-else>Chat</h3>
          </header>
          <section class="chat-session">
            <div v-for="message in messages" :key="message.id">
              <div v-if="message.type" class="status-message">
                <span>{{ message.text }}</span>
              </div>
              <div v-else class="message">
                <span class="user-id">{{ message.senderId }}</span>
                <span>{{ message.text }}</span>
              </div>
            </div>
          </section>
          <form @submit.prevent="sendMessage" class="message-form">
            <input
              class="message-input"
              autofocus
              placeholder="Compose your message and hit ENTER to send"
              v-model="newMessage"
              :disabled="!currentUser"
              name="newMessage"
              />
          </form>
        </section>

        <aside class="sidebar right-sidebar"></aside>
      </div>
    </template>

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

    export default {
      name: 'app',
      data () {
        return {
          title: 'Chatkit',
          userId: '',
          currentUser: null,
          currentRoom: null,
          newMessage: '',
          messages: [],
          roomUsers: [],
          messageTimer: '0',
        }
      },
      methods: {
        addUser() {
          const { userId } = this;
          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 id>',
                userId,
                tokenProvider,
              });

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

        sendMessage() {
          const { newMessage, currentUser, currentRoom } = this;

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

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

          this.newMessage = '';
        },

        connectToRoom() {
          const { currentUser } = this;

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

                    return 1;
                  });
                },
              },
            })
            .then(currentRoom => {
              this.currentRoom = currentRoom;
              this.roomUsers = currentRoom.users;
            });
        },
      }
    }
    </script>

Replace <your room id> and <your instance locator> as appropriate. Once a user joins our Chatkit instance, we get the currentUser object that represents the current connected user. Since Chatkit is "user-driven", almost all interactions happen on the currentUser object.

We then connect the user to the room whose ID was provided in the subscribeToRoom method. This method also takes an onMessage hook which is called each time a new message is sent to the room. Because we specified the messageLimit to be 100, onMessage is also called retroactively for up to 100 most recent messages. This means the user will be able to see up to the 100 most recent messages after connecting to the room.

Similarly, the onPresenceChanged hook is fired whenever 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 in realtime.

At this point, you should be able to connect to the chatroom and send messages to the room.

Self-destructing messages

We need to create a way for the user to opt into self destructing messages, and also select the amount of time before messages are destroyed.

Update your App.vue file as follows:

    // client/src/App.vue

    <template>
      <div class="App">
        <aside class="sidebar left-sidebar">
          <section v-if="!currentUser" class="join-chat">
            <h3>Join Chat</h3>
            <form @submit.prevent="addUser">
              <input
                placeholder="Enter your username"
                type="text"
                name="userId"
                v-model="userId"
                />
            </form>
          </section>

          <section v-if="currentUser" class="room-users">
            <h3>Room Users</h3>
            <ul>
              <li v-for="user in roomUsers" :key="user.id" class="user">
                <span class="presence" :class="user.presence.state" />
                <span>{{ user.name }}</span>
              </li>
            </ul>
          </section>
        </aside>

        <section class="chat-window">
          <header class="room-name">
            <h3 v-if="currentRoom">{{ currentRoom.name }}</h3>
            <h3 v-else>Chat</h3>
          </header>
          <section class="chat-session">
            <div v-for="message in messages" :key="message.id">
              <div v-if="message.type" class="status-message">
                <span>{{ message.text }}</span>
              </div>
              <div v-else class="message">
                <span class="user-id">{{ message.senderId }}</span>
                <span>{{ message.text }}</span>
              </div>
            </div>
          </section>
          <form @submit.prevent="sendMessage" class="message-form">
            <input
              class="message-input"
              autofocus
              placeholder="Compose your message and hit ENTER to send"
              v-model="newMessage"
              :disabled="!currentUser"
              name="newMessage"
              />
          </form>
        </section>

        <aside class="sidebar right-sidebar">
          <section v-if="currentUser" class="preferences">
            <h3>Self-destruct Timeout</h3>
            <select
              id="timeout"
              name="timeout"
              v-model="messageTimer"
              @change="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>
        </aside>
      </div>
    </template>

    // rest of the file

Next, update the addUser() method like this:

    // client/src/App.vue

        addUser() {
          const { userId } = this;
          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.messageTimer = messageTimer;
                    this.showStatusMessage();
                  },
                })
                .then(currentUser => {
                  this.currentUser = currentUser;
                  this.connectToRoom()
                });
            })
            .catch(console.error);
        },

Then add the following methods below sendMessage():

    // client/src/App.vue

        updateMessageTimer(event) {
          const { value } = event.target;
          const { currentRoom, currentUser } = this;

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

        showStatusMessage() {
          const { messageTimer, messages } = this;
          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.messages = messages;
        },

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

In the addUser() method, we add the onRoomUpdated connection hook which helps us display a message in the chat window that indicates the current self-destruct timeout.

When a self-destruct timer is set, all following messages will be deleted according to the set timeout leaving no trace in the chatroom or on Chatkit servers. Let’s make that happen by adding a new deleteMessage() method below showStatusMessage() in App.vue.

    // client/src/App.vue

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

Then update the onMessage hook as follow:

    // client/src/App.vue

        connectToRoom() {
          const { currentUser } = this;

          return currentUser
            .subscribeToRoom({
              roomId: '<your room id>',
              messageLimit: 100,
              hooks: {
                onMessage: message => {
                  const messages = [...this.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;
                  if (message.text !== 'DELETED' && messageTimer !== '0') {
                    this.deleteMessage(message.id);
                  }

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

                    return 1;
                  });
                },
              },
            })
            .then(currentRoom => {
              this.currentRoom = currentRoom;
              this.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.

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
  • chatroom
  • JavaScript
  • Node.js
  • Vue.js
  • 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.