vuejs-chatengine-pubnub-chat-app

How to Build a Vue.js Chat App with ChatEngine

Vue.js continues to grow in popularity and is guaranteed to be one of the most popular front-end frameworks of 2018. In looking at the graphs about download rates, it’s conclusive to say the Vue.js developer following is growing rapidly.

Following up our recent announcement of our Vue.js ChatEngine plugin, in this tutorial, we’ll walk you through building a fully-featured Vue.js chat app using Vuex, the state management pattern and library for Vue (comparable to React with Redux). During development, we were inspired to build a Vue.js plugin for ChatEngine, so ChatEngine can be integrated using the same patterns as the Vue-Socket.io plugin.

 

Tutorial Assets

You can try out the finished version of the application here. Create private chat rooms, try out the chat bot, or chat in the main global feed.

And all the code you need is available in our GitHub repo. Just a heads up, to run it, you need to register for PubNub (it’s free), and add your API keys to the app.

Amazon Lex Overview

In early 2018, PubNub announced the arrival of new, open source BLOCKS for each of the AWS Machine Learning APIs in the PubNub BLOCKS Catalog. This allows developers to fuse together their realtime applications with the powerful AI resources offered by AWS. Amazon Lex is a service that enables developers to build conversational interfaces with intelligent text and also voice audio.

The Vue.js demo that we built has the code to easily integrate a chatbot powered by Lex. The live demo features a chat interface with a digital version of PubNub founder Stephen Blum, which took just a few minutes to configure and deploy, thanks to AWS.

 

Building the UI

If you haven’t already, check out the live demo of this app hosted on GitHub Pages. First, we’ll walk through the steps taken to develop this app from scratch while learning how to use Vue.js and Lex at the same time.

Vue is a JavaScript framework, so naturally, it can be installed using NPM, the package manager for Node.js. To develop something as feature-full as thist tutorial, you will also need ChatEngine and some ChatEngine plugins.

## Get the Vue CLI
npm i vue -g

## Create a new Vue app
## Chose whichever settings you want using the Vue CLI
vue init webpack chat-app

cd chat-app
npm i vuex vue-chat-engine chat-engine chat-engine-typing-indicator

First, we’ll install the Vue CLI so we can create a new project using a template on the command line. We’re using Webpack, which allows all of the bundling to take place so when ready to deploy the app to production, all of our JS, CSS, and assets are as small and optimized as can be.

The CLI has neat on-screen options that you can select using the return and arrow keys (our GitHub repo uses the Google lint standard).

 

Once the Webpack starter template is ready, move into the project directory using the “cd” command. Next, install the NPM packages that will make our project code elegant and small.

  • vuex – Global state management pattern and library for Vue.js apps.
  • vue-chat-engine – A Vue.js plugin that allows Vuex to listen to ChatEngine events.
  • chat-engine – An opinionated JavaScript framework for rapid development of chat applications with PubNub.
  • chat-engine-typing-indicator – A plugin for ChatEngine that enables typing indicator functionality on a chat by chat basis.

There are a lot of default files and folders waiting in the chat-app directory. There’s no need to feel intimidated by all of that content. It isn’t important that you understand every line in each of the files. The most important stuff for a Vue.js app is located in the src folder.

./src

 

Here we have JavaScript, images, and Vue files. The Vue files (*.vue) will contain JavaScript, CSS, and HTML. Modern web frameworks like Vue break the front-end up into components in order to encapsulate event handling, encourage code reuse, and more.

Here is a diagram that will represent the components in the chat demo:

 

In the ./src/components/ directory, make new .vue files that will make up the components in my app.

./src

 

This is what an empty .vue file will look like after you clear our the default contents from the template copy.

<template>
  <div></div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data () {
    return {}
  }
}
</script>

<style scoped></style>

If you’ve done some front-end web development before, this should look a bit familiar. There are spots in the file for HTML, CSS, and JS. You can begin filling these tags in with your own markup, styling, and code to fill the shape of your newest hack. As you can see in the diagram above, there are components within components. This means that in the app, some components will be parents, siblings, or children of others in the DOM.

The ChatContainer component is the highest parent node of the UI. The markup will include custom HTML tags for other components and also some Vue specific markup to reactively render data.

<template>
  <div class="chat-container">
    <div class="heading">
      <h1>{{ title + uuid }}</h1>
    </div>
    <div class="body">
      <friend-list></friend-list>
      <div class="right-body">
        <div class="table">
          <chat-log></chat-log>
          <message-input></message-input>
        </div>
      </div>
    </div>
  </div>
</template>

There are two instances of non-standard HTML in this markup. First is the curly braces within the h1. This binds some JavaScript variables that are part of the data exported in the code portion of the Vue file, reactively evaluates the expression, and displays the text output as the text content in the tag.

The friend-list, chat-log, and message-input tags are also different. These tags represent the components that are children of the ChatContainer. In order for these to render properly, they need to be imported in the script portion of the Vue file.

import FriendList from '@/components/FriendList';
import ChatLog from '@/components/ChatLog';
import MessageInput from '@/components/MessageInput';

This is the ES6 method of importing files. You can also see the used in the file path. To Vue, this means that the path is prepended with the src folder path.

As you continue to fill in the file with more JavaScript from the project GitHub repo, you can begin to see there is some more required code that allows the component to work.

import {mapGetters} from 'vuex';
import FriendList from '@/components/FriendList';
import ChatLog from '@/components/ChatLog';
import MessageInput from '@/components/MessageInput';

export default {
  name: 'chat-container',
  components: {
    FriendList,
    ChatLog,
    MessageInput,
  },
  data() {
    return {
      title: 'PubNub ChatEngine and Vue - User: ',
    };
  },
  computed: {
    ...mapGetters({
      uuid: 'getMyUuid',
    }),
  },
};

The component must have a name property that will coincide with the kebab-case HTML tag name. The data function returns some of the static string data that is rendered in the h1 tag. It’s also necessary to tell Vue which components are being referenced in the markup with the components property in the exports.

The computed properties are variables that need an entire function to generate their return value. In this component, there is a reference to the Vuex store, specifically a getter called getMyUuid.

src/store.js

import Vue from 'vue';
import Vuex from 'vuex';
import {EventBus} from './event-bus.js';

Vue.use(Vuex);

const state = {
  chats: {},
  chatMessages: {},
  currentChat: '',
  friends: [],
  me: {},
};

const mutations = {
  setMe(state, {me}) {
    state.me = me;
  },
  setCurrentChat(state, {chatKey}) {
    state.currentChat = chatKey;
  },
  setFriends(state, {friends}) {
    for (let friend of friends) {
      state.friends.push(friend);
    }
  },
  newChat(state, {chat}) {
    if (!chat.key) {
      throw Error('No chat.key defined on the new Chatengine chat Object');
    }
    state.chats[chat.key] = chat;
  },
  CHATENGINE_message(state, {event, sender, chat, data, timetoken}) {
    let key = chat.key || chat.chat.key;

    if (!state.chatMessages[key]) {
      Vue.set(state.chatMessages, key, []);
    }

    let message = data;
    message.who = sender.uuid;
    message.time = timetoken; // timetoken in ChatEngine 0.9.5 or later

    // Force stop the typing indicator
    if (chat.typingIndicator && sender.name !== 'Me') {
      // Handler in Chat Log Component (components/ChatLog.vue)
      EventBus.$emit('typing-stop', chat.key);
    }

    state.chatMessages[key].push(message);
    state.chatMessages[key].sort((msg1, msg2) => {
      return msg1.time > msg2.time;
    });
  },
};

const actions = {
  sendMessage(context, {chat, message}) {
    // emit the `message` event to everyone in the Chat
    context.state.chats[chat].emit('message', message);
  },
};

const getters = {
  getMyUuid: (state) => state.me.uuid,
  getFriends: (state) => state.friends,
};

export default new Vuex.Store({
  state,
  getters,
  actions,
  mutations,
});

The store file contains a global state object along with a collection of getter, action, and mutation functions. This gives the Vue application the ability to access these methods from anywhere. The getMyUuid method referenced in the ChatContainer is a global getter for the client’s current UUID for ChatEngine. Think of this as a globally unique ID for a user within ChatEngine.

The SDK references this property as ‘uuid’ however it doesn’t need to be a to-spec, 128-bit number. It can be a plain old string. In the case of this example app, it is a randomly generated 4 character string. Let’s see where that gets set for ChatEngine.

src/main.js

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue';
import App from './App';
import store from './store';
import VueChatEngine from 'vue-chat-engine';
import ChatEngineCore from 'chat-engine';
import DefaultChats from './default-chats';
import botInit from './bot';
import util from './util';

Vue.config.productionTip = false;

// Global chat settings are first in the friend list (default-chats.js)
const globalChatSettings = DefaultChats.friends[0];

// ChatBot REST endpoint powered by PubNub Functions and Amazon Lex
const chatBotURL = '__Your_PubNub_Function_Endpoint_For_Lex__';

// Init ChatEngine with PubNub
const publishKey = '__Your_PubNub_Publish_Key__';
const subscribeKey = '__Your_PubNub_Subscribe_Key__';

if (!publishKey || !subscribeKey) {
  console.error('ChatEngine: PubNub Keys are missing.');
}

const chatEngine = ChatEngineCore.create({
  publishKey,
  subscribeKey,
}, {
  globalChannel: globalChatSettings.chatKey,
});

const myUuid = util.fourCharID();
const me = {
  name: myUuid,
  uuid: myUuid,
};

// ChatEngine injected into every component instance with the plugin
Vue.use(VueChatEngine, {chatEngine, store});

/**
 * Execute this function when the Vue instance is created
 */
function created() {
  const ChatEngine = this.$chatEngine;
  const store = this.$store;

  ChatEngine.connect(me.uuid, me);

  document.addEventListener('beforeunload', function() {
    ChatEngine.disconnect();
  });

  ChatEngine.on('$.ready', function(data) {
    // store my new user as `me`
    let me = data.me;
    store.commit('setMe', {me});

    // Auto add a 1:1 chat to UI when invited
    // more invite code in (components/FriendList.vue)
    me.direct.on('$.invite', (event) => {
      let uuids = [event.sender.uuid, store.state.me.uuid].sort();
      let chatKey = uuids.join('-');

      // Don't make the same 1:1 chat if it already exists
      if (store.state.chats[chatKey]) {
        return;
      }

      // Make the new 1:1 private Chat
      util.newChatEngineChat(
        store,
        ChatEngine,
        {
          chatKey,
          uuid: event.sender.uuid,
        },
        true,
      );
    });

    ChatEngine.global.key = globalChatSettings.chatKey;

    // Make a Global Chat and add to the client's UI
    const globalChat = util.newChatEngineChat(
      store,
      ChatEngine,
      globalChatSettings,
    );

    // Get the message history in the global chat
    globalChat.search({
      event: 'message',
      limit: 6,
    });

    store.commit('setCurrentChat', {
      chatKey: globalChat.key,
    });

    // Create a new chat for each user in the friends list
    DefaultChats.friends.forEach(function(friend) {
      const uuids = [friend.uuid, store.state.me.uuid].sort();
      const chatKey = uuids.join('-');

      // Don't make a duplicate chat if it already exists
      if (
        store.state.chats[chatKey] ||
        friend.uuid === 'global'
      ) {
        return;
      }

      // Make a private chat key with the Stephen bot
      if (friend.isChatBot) {
        // Init ChatBot with its own ChatEngine client (bot.js)
        botInit(ChatEngine, friend, chatBotURL);
      }

      // Add the chat key to the Chat object for Vue UI use
      friend.chatKey = chatKey;

      // Make the new 1:1 private Chat
      const myChat = util.newChatEngineChat(
        store,
        ChatEngine,
        friend,
        true,
      );

      // when a user comes online
      myChat.on('$.online.*', (data) => {
        // console.log('New user', data.user.uuid);
      });

      // when a user goes offline
      myChat.on('$.offline.*', (data) => {
        // console.log('User left', data.user.uuid);
      });
    });
  });
}

/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  components: {App},
  template: '<App/>',
  created,
});

This is the main file for a Vue app. It is where the Vue instance is made and also the ChatEngine instance. At the top, grab the global chat settings and other default chat settings from my default-chats.js file which is just some static JSON.

This tells Vue to use the VueChatEngine plugin so ChatEngine’s events are automatically handled by Vuex handlers that are prepended with CHATENGINE_.

The created event handler fires when a new Vue instance is made. This is where we configured the ChatEngine logic.

  • Disconnect PubNub when the browser’s “beforeunload” event fires.
  • Set the Me object in the Vuex store when ChatEngine is ready
  • Configure a listener for ChatEngine invite events, when a 1:1 chat is made in another client.
  • Make a global chat and get the message history, on init, using ChatEngine search
  • Create the Chatbot’s own ChatEngine client (in ./src/bot.js)
  • Make any other chats that are listed in the default-chats.js file

If you want to make your own clone of this app you must register for PubNub (it’s free), press the ChatEngine Setup button, and grab the API keys from the setup.

ChatEngine Setup

Click here to globally deploy your ChatEngine back-end in 10 seconds


ChatEngine Keys

 

The ChatEngine setup button globally deploys your ChatEngine backend in a few seconds, and then shows you the API keys for the app. To make the Chatbot connect to AWS Lex, you need to add an On Request event handler in the ChatEngine backend in PubNub Functions.

Connecting Lex

To make the connection between your own clone of the chat app and your own AWS Lex bot, start by going to the PubNub Admin Dashboard.

Click on your ChatEngine App, the Functions tab, the ChatEngine Function, and then the Create button, to make a new On Request event handler.

Create a new PubNub Function event handler

 

This will add a REST endpoint to your ChatEngine backend, which will be used to request a Chatbot reply from AWS Lex. You will need to create an AWS Account and also access keys for Lex, which we will explain later.

Next, grab the code from ./pubnub-functions/lex-text-on-request.js in my GitHub repo. Copy and paste this code, replacing the default On Request code in the PubNub Function Editor.

PubNub Functions Event Handler Editor

 

Configure the Path input on the left to chat-bot or something like that. Before we deploy this, we should input some AWS access keys with the Lex Chatbot.

Creating the Chatbot

To make a Lex Chatbot, you need to make an AWS account.

 

Create a Free AWS Account

 

After you have made an account or logged into an existing one, type Amazon Lex into the AWS Services search bar. Click the Create button on the top left and select Custom bot. Choose the onscreen bot settings however you wish, and click Create.

 

Make your own Chatbot with AWS Lex

 

You should now be in the Lex Chatbot editor, where you can create Intents, or use existing Intents to teach your Chatbot how to interact. Once you have configured the bot inputs and outputs to your liking, hit Build then Publish on the top right.

Next, we’ll get API keys. Click the AWS logo on the top left to navigate back to the AWS Services search bar. Type IAM to navigate to the Identity and Access Management panel. Click Users on the left and create a user. Next you need to add permissions for this user by clicking its name, and then the Add Permissions button. Create a group if need be, or click Attach existing policies directly and add AmazonLexFullAccess using the filter input.

Then click the User on the left and create an access key. You will need to copy the Access key ID and the Secret access key.

Now go back to the PubNub Functions event handler editor for your ChatEngine App. Click the MY SECRETS button on the left and input the AWS keys as AWS_access_key and AWS_secret_key.

functions keys

 

Click SAVE and then the Restart Module button in the top right corner of the panel to deploy the REST endpoint.

The final step is adding the PubNub API keys and the On Request endpoint URL to the Vue project. The endpoint URL can be copied by clicking the COPY URL button on the left side of the Function editor.  Go into the ./src/main.js file and put this URL in the chatBotURL variable. Fill in your unique pub/sub keys using the ChatEngine App’s keys from the PubNub Admin Dashboard.

// ChatBot REST endpoint powered by PubNub Functions and Amazon Lex
const chatBotURL = '__Your_PubNub_Function_Endpoint_For_Lex__';

// Init ChatEngine with PubNub
const publishKey = '__Your_PubNub_Publish_Key__';
const subscribeKey = '__Your_PubNub_Subscribe_Key__';

Also, you need to configure the request payload for the Chatbot in ./src/bot.js.

// Make a request to PubNub Functions which contacts Lex API
util.post(chatBotURL, {
  body: {
    data: {
      lex: {
        botAlias: 'StephenChatEngineExample',
        botName: 'StephenChatEngineExample',
        contentType: 'text/plain; charset=utf-8',
        inputText: payload.data.text,
        userId: 'ChatEngineVueDemo',
      },
    },
  },
})

Add your Bot Alias and Bot Name as you put it in the AWS console. The user ID can be whatever you’d like.

To test your Vue app on your local machine, run npm run dev in your Vue app directory. This creates a local server for you to test your app in the browser at localhost.

Vue ChatEngine App

 

You can see your user ID in the top menu bar, and you can see everyone else’s ID above their text messages in the global chat. Using the text box and plus button on the right, you can invite another user to a private, 1 to 1 chat. They will automatically see the new private chat in their friend’s list on the left when you add their ID by clicking on the + button.

 

In App Chat with Typing Indicator

 

The private chats are complete with a typing indicator that is made with pure CSS. The events are handled by ChatEngine using the typing indicator plugin. Try chatting with the Stephen Chatbot! Try asking him how he’s doing, what PubNub is, or what ChatEngine is. He’s very excited to answer you.

Use Cases:

Try PubNub Today