How you can use Vue.js and Chatkit to build a realtime chat app

How you can use Vue.js and Chatkit to build a realtime chat app

  • 2019-05-24 01:42 AM
  • 185

Build a customer service chat app Vuejs, In this tutorial, I’ll describe how you can use Vue.js and Chatkit to build a realtime customer service chat app.

Good customer service plays an important role in the growth of any business. In today’s world, it is important to offer some sort of live chat interface so that you can offer prompt responses to the customers who want to ask questions about your business.

If you’re looking to get a live chat system up and running for your customers, Chatkit makes it easy to do with just a few lines of code.

Here’s what the finished application will look like:

Vue.js Chat ui Prerequisites

Before you continue with this tutorial, make sure you have Node.js (version 8 or later) and npm installed on your computer. If not, you can find out how to install it for your operating system here. In addition, you need to have prior experience with building Vue.js applications, but no knowledge of Chatkit is assumed.

Sign up for Chatkit

Head over to the Chatkit page and create a free account or sign in to your existing account. By doing so, you’ll be able to create a new 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 later on. vue chat room

Next, click the Console tab and create a new user on your Chatkit instance. This user will be the support staff assigned to each customer when a new chat session is initialized. The user identifier for this user should be support as shown below:

You can also create users programmatically, but creating users from the dashboard inspector is useful for testing purposes.

Set up the application server

Open up the terminal app on your computer, and create a new customer-service directory for this project. Next cd into it, and run npm init -y to initialize the project with a package.json file. Following that, run the command below to install all the dependencies we’ll be making use of for building the application server:

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

Once the dependencies have been installed, create a new .env file in your project root and add in the credentials retrieved from your Chatkit instance dashboard.

    // .env

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

Next, set up 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}`);
    });

We have two routes on the server: the /users route takes a userId, and creates a Chatkit user through our chatkit instance while the /authenticate route is meant to authenticate each user that tries to connect to our Chatkit instance and respond with a token (returned by chatkit.authenticate) if the request is valid.

That’s all we need to do on the server side. You can start the server on port 5200 by running node server.js in the terminal.

Bootstrap the Vue.js application

We’ll be making use of Vue CLI to bootstrap the application frontend. Install it globally on your machine, 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).

    npm install -g @vue/cli
    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 vue-router vue-spinkit @pusher/chatkit-client axios --save

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

Set up routing

Our application frontend will have two views: one for the customer and one for the support staff. To switch between the two views, we’ll be making use of vue-router which we already installed in the previous section.

First, create two new files for each view within the client/src directory:

    touch Customer.vue Support.vue

Next, open up client/src/main.js and change it to look like this:

    // client/src/main.js

    import Vue from 'vue';
    import VueRouter from 'vue-router';
    import Customer from './Customer.vue';
    import Support from './Support.vue';
    import App from './App.vue';

    Vue.config.productionTip = false;
    Vue.use(VueRouter);

    const routes = [
      { path: '/', component: Customer },
      { path: '/support', component: Support },
    ];

    const router = new VueRouter({
      routes,
    });

    new Vue({
      el: '#app',
      router,
      render: h => h(App),
    });

After importing the library, as you can see, we’ve set up the router to load the Customer view on the root route, and the Support view on the /support route. We also need to update App.vue so that each view is rendered there:

    // client/src/App.vue
    <template>
      <div id="app" class="App">
        <!-- component matched by the route will render here -->
        <router-view></router-view>
      </div>
    </template>

    <script>
    export default {
      name: 'app',
    }
    </script>

Add the application styles

Before we go further, let’s add some styles for our applications. We’re making use of the skeleton-css boilerplate to add some basic styling, and complementing it with our defined styles in client/src/App.css.

Update client/src/main.js to look like this:

    // client/src/main.js

    import Vue from 'vue';
    import VueRouter from 'vue-router';
    import Customer from './Customer.vue';
    import Support from './Support.vue';
    import App from './App.vue';

    // add the lines below
    import 'skeleton-css/css/normalize.css';
    import 'skeleton-css/css/skeleton.css';
    import './App.css';

    // rest of the code

Then create client/src/App.css and update it to look like this:

    // client/src/App.css

    html {
      box-sizing: border-box;
    }

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

    .App {
      text-align: center;
      overflow: hidden;
    }

    svg {
      width: 28px;
      height: 28px;
    }

    input[type="text"]:focus {
      border: 1px solid #300d4f;
    }

    button {
      color: white;
      font-size: 14px;
      border-radius: 2px;
      background-color: #331550;
      border: 1px solid #331550;
      cursor: pointer;
      box-sizing: border-box;
    }

    button:hover {
      color: white;
      background-color: rebeccapurple;
    }

    .chat-widget {
      position: absolute;
      bottom: 40px;
      right: 40px;
      width: 400px;
      border: 1px solid #ccc;
    }

    .chat-header {
      width: 100%;
      height: 60px;
      background-color: #00de72;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    .chat-header h2 {
      font-size: 18px;
      margin-bottom: 0;
    }

    .chat-body {
      height: 350px;
      overflow-y: auto;
      padding: 10px;
    }

    .status-messages {
      text-align: center;
      padding: 5px;
    }

    .message {
      background-color: #f6f6f6;
      clear: both;
      margin-bottom: 15px;
      padding: 10px;
      border-radius: 5px;
      max-width: 80%;
    }

    .message.user {
      float: right;
      background-color: peachpuff;
    }

    .message.support {
      float: left;
      background-color: #ddd;
    }

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

Set up the customer view

This is where the user will initialize a chat session with a customer service agent. Open up Customer.vue in your editor and update it to look like this:

    // client/src/Customer.vue

    <template>
      <div class="customer-chat">
        <h1>Customer Service</h1>
        <p>
          Customers can interact with support using the chat widget in the
          bottom right corner
        </p>

        <button class="contact-btn">
          Contact Support
        </button>
      </div>
    </template>

    <script>
    export default {
      name: 'Customer',
      data() {
        return {
          title: 'Customer Support',
          userId: '',
          currentUser: null,
          currentRoom: null,
          newMessage: '',
          messages: [],
          isDialogOpen: false,
          isLoading: false,
        }
      },
    }
    </script>

Once the user hits the CONTACT SUPPORT button, a dialog should appear requesting the name of the user. This name will serve as the userId for connecting to our Chatkit instance.

Create a new Dialog.vue file inside the client/src/components directory and populate it with the following contents:

    // client/src/components/Dialog.vue

    <template>
      <div class="dialog-container">
        <div class="dialog">
          <form class="dialog-form" @submit.prevent="handleSubmit">
            <label class="username-label" for="username">
              What is your name?
            </label>
            <input
              id="username"
              class="username-input"
              autofocus
              type="text"
              name="userId"
              :value="username"
              @input="handleInput"
              />
            <button type="submit" class="submit-btn">
              Submit
            </button>
          </form>
        </div>
      </div>
    </template>

    <script>
    export default {
      name: 'Dialog',
      props: {
        username: String,
      },
      methods: {
        handleSubmit() {
          this.$emit('submit-username', this.username);
        },
        handleInput(event) {
          const { name, value } = event.target;
          this.$emit('update-input', name, value);
        }
      }
    }
    </script>

    <style scoped>
    .dialog-container {
      position: absolute;
      top: 0;
      right: 0;
      bottom: 0;
      left: 0;
      background-color: rgba(0, 0, 0, 0.9);
      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%;
    }

    .submit-btn {
      width: 100%;
    }
    </style>

Next, import this component into Customer.vue as follows:

    // client/src/Customer.vue

    <template>
      <div class="customer-chat">
        <!-- [..] -->

        <!-- Show the dialog when clicked -->
        <button @click="showDialog" class="contact-btn">
          Contact Support
        </button>

        <!-- Loading indicator -->
        <Spinner v-if="isLoading" name="three-bounce" color="#300d4f" />

        <Dialog
          v-if="isDialogOpen"
          :username="userId"
          @update-input="handleInput"
          @submit-username="launchChat"
          />
      </div>
    </template>

    <script>
    import Dialog from './components/Dialog.vue';
    import Spinner from 'vue-spinkit';

    export default {
      name: 'Customer',
      components: {
        Dialog,
        Spinner,
      },
      data() {
        return {
          title: 'Customer Support',
          userId: '',
          currentUser: null,
          currentRoom: null,
          newMessage: '',
          messages: [],
          isDialogOpen: false,
          isLoading: false,
        }
      },
    }
    </script>

Notice that, we are listening for the update-input and submit-username events on Dialog and binding those events to the handleInput and launchChat methods respectively. We’ve also bound the click event on the CONTACT SUPPORT button to the showDialog method. These methods have not been created yet, so we must do so in a new methods.js file as shown below:

    // client/src/methods.js

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

    function showDialog() {
      this.isDialogOpen = !this.isDialogOpen;
    }

    function launchChat() {
      this.isDialogOpen = false;
      this.isLoading = true;

      const { userId } = this;

      if (userId === null || userId.trim() === '') {
        alert('Invalid userId');
      } else {
        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.currentUser = currentUser;
              this.isLoading = false;
            });
          })
          .catch(console.error);
      }
    }

    function handleInput(name, value) {
      console.log(name, value);
      this[name] = value;
    }

    export {
      handleInput,
      showDialog,
      launchChat
    };

Now we can import and use these methods in our Customer.vue file as follows:

    <template>
     <!-- [..] -->
    </template>

    <script>
    import Dialog from './components/Dialog.vue';
    import { showDialog, launchChat, handleInput } from './methods.js';

    export default {
      name: 'Customer',
      // [..]
      methods: {
        showDialog,
        handleInput,
        launchChat,
      }
    }
    </script>

Be sure to update the <your chatkit instance locator> placeholder within the launchChat() method before proceeding. Now, you should be able to launch the dialog by clicking the contact support button. If you enter a username and hit SUBMIT, we connect to our Chatkit instance and get a currentUser object which represents the current connected user. At this point, we need to show a chat widget so that the user can begin to chat with the customer service agent.

Let’s create a new component for the chat widget and import it into the Customer view. Create a new ChatWidget.vue file within the client/src/components directory and add the following code to it:

    // client/src/components/ChatWidget.vue

    <template>
      <div class="chat-widget">
        <header class="chat-header">
          <h2>Got Questions? Chat with us</h2>
        </header>
        <section class="chat-body">
          <div v-for="message in messages" :key="message.id">
            <span :class="[ message.senderId === currentUser.id ? 'user' :
            'support']" class="message">{{ message.text }}</span>
          </div>
        </section>

        <form @submit.prevent="handleSubmit" class="message-form">
          <input
            class="message-input"
            autofocus
            name="newMessage"
            placeholder="Compose your message and hit ENTER to send"
            :value="newMessage"
            @input="handleInput"
            />
        </form>
      </div>
    </template>

    <script>
    export default {
      name: 'ChatWidget',
      props: {
        newMessage: String,
        messages: Array,
        currentUser: {
          type: Object,
          required: true,
          default: null,
        },
      },
      methods: {
        handleInput(event) {
          const { name, value } = event.target;
          this.$emit('update-input', name, value);
        },
        handleSubmit() {
          this.$emit('send-message');
        }
      }
    }
    </script>

Next, import it in Customer.vue like this:

    // client/src/Customer.vue

    <template>
      <div class="customer-chat">
        <h1>Customer Service</h1>
        <p>
          Customers can interact with support using the chat widget in the
          bottom right corner
        </p>

        <ChatWidget
          v-if="currentUser"
          :newMessage="newMessage"
          :currentUser="currentUser"
          :messages="messages"
          @send-message="sendMessage"
          @update-input="handleInput"
          />

        <button v-else @click="showDialog" class="contact-btn">
          Contact Support
        </button>

        <Spinner v-if="isLoading" name="three-bounce" color="#300d4f" />

        <Dialog
          v-if="isDialogOpen"
          :username="userId"
          @update-input="handleInput"
          @submit-username="launchChat"
          />
      </div>
    </template>

    <script>
    import Dialog from './components/Dialog.vue';
    import Spinner from 'vue-spinkit';
    import ChatWidget from './components/ChatWidget.vue'
    import { sendMessage, connectToRoom, createRoom, addSupportStaffToRoom, showDialog, launchChat, handleInput } from './methods.js';

    export default {
      name: 'Customer',
      components: {
        Dialog,
        Spinner,
        ChatWidget,
      },
      data() {
        return {
          title: 'Customer Support',
          userId: '',
          currentUser: null,
          currentRoom: null,
          newMessage: '',
          messages: [],
          isDialogOpen: false,
          isLoading: false,
        }
      },
      methods: {
        sendMessage,
        connectToRoom,
        addSupportStaffToRoom,
        createRoom,
        showDialog,
        handleInput,
        launchChat,
      }
    }
    </script>

Notice that we imported a few new methods from methods.js but these haven’t been created yet, so let’s do just that:

    // client/src/methods.js

    // [..]

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

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

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

      this.newMessage = '';
    }

    function connectToRoom(id, messageLimit = 100) {
      this.messages = [];
      const { currentUser } = this;

      return currentUser
        .subscribeToRoom({
          roomId: `${id}`,
          messageLimit,
          hooks: {
            onMessage: message => {
              this.messages = [...this.messages, message];
            },
          },
        })
        .then(currentRoom => {
          this.currentRoom = currentRoom;
        });
    }

    function addSupportStaffToRoom() {
      const { currentRoom, currentUser } = this;

      return currentUser.addUserToRoom({
        userId: 'support',
        roomId: currentRoom.id,
      });
    }

    function createRoom() {
      const { currentUser } = this;

      return currentUser
        .createRoom({
          name: currentUser.name,
          private: true,
        })
        .then(room => this.connectToRoom(room.id, 0))
        .then(() => this.addSupportStaffToRoom());
    }

    // update launchChat
    function launchChat() {
      // [..]

      if (userId === null || userId.trim() === '') {
        alert('Invalid userId');
      } else {
        axios
          .post('http://localhost:5200/users', { userId })
          .then(() => {
            // [..]

            return chatManager.connect().then(currentUser => {
              this.currentUser = currentUser;
              this.isLoading = false;
              // add this line
              return this.createRoom();
            });

          })
          .catch(console.error);
      }
    }

    export {
      sendMessage,
      connectToRoom,
      addSupportStaffToRoom,
      createRoom,
      handleInput,
      showDialog,
      launchChat,
    };

Once the currentUser object is set in the application state, we create a new room for this chat session via the createRoom() method. We’ve opted to make the room private in this instance so that only the user and support staff are able to access it. Once the room is created, we have to connect to it before we can send any messages. This is done via the connectToRoom() method which takes the ID of the room that was created and adds the user to the room.

Chatkit’s room subscription hooks allow us to perform actions when some event occurs in the current room. In this instance, we’ve set up the onMessage hook to append new messages to the messages array so that the new message is displayed in the chat widget.

Finally, we add the support agent to the room via the addSupportStaffToRoom() method which means that the support agent will be able to login to their own interface and see all ongoing conversations instantly.

At this point, you should be able to use the chat widget to send and view messages seamlessly.

Set up the support view

The next phase is to set up the view where the support agent will be able to interact with all customers at once. Open up Support.vue in your code editor and paste the following code into it:

    // client/src/Support.vue

    <template>
      <div class="support-area">
        <aside class="support-sidebar">
          <h3>Users</h3>
          <ul v-for="room in rooms" :key="room.id">
            <li
              class="room"
              :class="[currentRoom && currentRoom.id === room.id ? 'active' : '']"
             @click="connectToRoom(room.id)"
            >
            {{ room.name }}
            </li>
          </ul>
        </aside>
        <section class="support-session">
          <header class="current-chat">
            <h3 v-if="currentRoom">{{ currentRoom.name }}</h3>
            <h3 v-else>Chat</h3>
          </header>
          <div class="chat-session">
            <div v-for="message in messages" :key="message.id">
              <span :class="[ message.senderId === currentUser.id ? 'support' :
              'user']" class="message">{{ message.text }}</span>
            </div>
          </div>
          <form @submit.prevent="sendMessage" class="message-form">
            <input
              class="message-input"
              autofocus
              placeholder="Compose your message and hit ENTER to send"
              v-model="newMessage"
              name="newMessage"
              />
          </form>
        </section>
      </div>
    </template>

    <script>
    import { sendMessage, connectToRoom } from './methods';
    import Chatkit from '@pusher/chatkit-client';
    import axios from 'axios';

    export default {
      name: 'Support',
      data() {
        return {
          newMessage: '',
          currentUser: null,
          currentRoom: null,
          rooms: [],
          messages: [],
        }
      },
      methods: {
        sendMessage,
        connectToRoom,
      },
      mounted() {
        const userId = 'support';

        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 => {
                  this.rooms = [...this.rooms, room];
                },
              })
              .then(currentUser => {
                this.currentUser = currentUser;
                this.rooms = currentUser.rooms;
                if (this.rooms.length >= 1) {
                  this.connectToRoom(this.rooms[0].id);
                }
              });
          })
          .catch(console.error);
      }
    }
    </script>

    <style>
    .support-area {
      width: 100vw;
      height: 100vh;
      display: flex;
    }

    .support-sidebar {
      width: 20%;
      background-color: #300d4f;
      height: 100%;
    }

    .support-sidebar ul {
      list-style: none;
    }

    .support-sidebar h3 {
      color: white;
      margin-bottom: 0;
      text-align: left;
      padding: 10px 20px;
    }

    .room {
      font-size: 22px;
      color: white;
      cursor: pointer;
      text-align: left;
      padding: 10px 20px;
      margin-bottom: 10px;
    }

    .room:hover {
      color: yellowgreen;
    }

    .room.active {
      background-color: yellowgreen;
      color: white;
    }

    .support-session {
      width: 80%;
      height: 100%;
      display: flex;
      flex-direction: column;
    }

    .current-chat {
      border-bottom: 1px solid #ccc;
      text-align: left;
      padding: 10px 20px;
      display: flex;
    }

    .current-chat h3 {
      margin-bottom: 0;
    }

    .chat-session {
      flex-grow: 1;
      overflow-y: auto;
      padding: 10px;
    }
    </style>

The support agent needs to be able to interact with multiple customers at once, so we have a sidebar where all the connected users are listed, and the main chat area for sending and viewing messages.

We’re immediately connecting to the Chatkit instance on page load (via mounted()) and listing all the connected customers in the sidebar. We can jump between chats by clicking on each room name via connectToRoom() . This method simply connects to the selected room and changes the value of currentRoom so that the screen is updated appropriately.

Thanks to the onAddedToRoom() connection hook, we do not need to refresh the support view to see new chats that have been initiated. Everything is updated in realtime without much effort on your part.

Before testing, make sure to update the <your chatkit instance locator> placeholder within the mounted() method.

Wrap up

In this tutorial, I’ve shown you how to set up a customer support application using Vue.js and Chatkit. You can checkout other things Chatkit can do by viewing its extensive documentation. Don’t forget to grab the full source code used for this tutorial in this GitHub repository.

30s ad

Ethereum and Solidity: Build Dapp with VueJS

Learn Web Development Using VueJS

Curso de VueJS 2

Vuejs 2 + Vuex con TypeScript Nivel PRO

Curso de Vuejs 2, Cognito y GraphQL