Chat

How to Build a Group Chat App with Vue.js

Oscar Castro on May 15, 2019
How to Build a Group Chat App with Vue.js

Chat is everywhere and has ascended to one of the most important communication mediums across our daily lives. The number of chat app use cases is vast and continues to grow. And with today's technological advancements, we expect our messages to send and receive in real time, instantaneously. It's not a nice-to-have, it's a requirement.

There are thousands of ways to build a chat app, and there are many things to consider: infrastructure, scalability, reliability, and security to name a few. With a vast amount of services, frameworks, and technologies to choose from, making that decision can be overwhelming!

In this tutorial, we're going to build a real-time group chat app in Vue.js. We'll power our app using PubNub, which takes care of the heavy work for us; all we have to worry about is developing the app, not the underlying infrastructure.

Vue Group Chat App

Tutorial Overview

Our application will send messages to all connected users in the group chat using publish, and receive messages using subscribe. Our messages will be stored using history, so users can see past or missed messages. To do all this, we will use the PubNub Vue.js SDK.

This tutorial is broken into two sections: Publish/Subscribe (Pub/Sub) and History.


GitHub LogoAll of the code in this tutorial is available in the Vue Chat App Respository.

Clone and run it with your free Pub/Sub keys!


Setting up Pub/Sub Messaging

Before we start working on the app, we need to sign up for a free PubNub account to create your Pub/Sub API keys. Sign up and log in using the form below:

Next, we need to install two dependencies: vuex and pubnub-vue. We can use NPM to do so.

  • npm install vuex –save
  • npm install pubnub-vue –save

Since the PubNub Vue.js SDK is a wrapper of the PubNub JavaScript SDK, it offers all the same features. A few extra features, such as Trigger Events, are added to simplify the integration with Vue. One trigger event we'll be using is $pnGetMessage. We use $pnGetMessage with a reactive property so messages display as soon as they are received. To subscribe to a channel we use $pnSubscribe and to publish to a channel we use $pnPublish.

Initialize the PubNub Client API

In the main.js file, create a unique UUID for each user and replace the pub/sub keys with your keys. We include two plugins: PubNubVue and Vuex.

import Vue from 'vue'
import App from './App'
import store from './store';
import PubNubVue from 'pubnub-vue'
Vue.config.productionTip = false;
 
const publish_Key = 'YOUR_PUBLISH_KEY_HERE';
const subscribe_Key = 'YOUR_SUBSCRIBE_KEY_HERE';
// Make a unique uuid for each client
const myUuid = fourCharID();
const me = {
  uuid: myUuid,
};
try{
  if(!publish_Key || !subscribe_Key){
    throw 'PubNub Keys are missing.';
  }
}
catch(err){
  console.log(err);
}
Vue.use(PubNubVue, {
  subscribeKey: subscribe_Key,
  publishKey: publish_Key,
  ssl: true
}, store);
/* eslint-disable no-new */
new Vue({
  el: '#app',
  store,
  components: { App },
  template: '<App/>',
  created
})

Next, we generate a random 4 character UUID for the user by making a call to the function fourCharID.

function fourCharID() {
  const maxLength = 4;
  const possible = 'abcdef0123456789';
  let text = '';
  for (let i = 0; i < maxLength; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
}

It's recommended to use a standard 128-bit UUID in production apps, but the UUID can be a plain string as well, as is the case for this app. The constant ‘me’ holds the UUID. To commit the constant to the Vuex store, we add the function created.

function created(){
  this.$store.commit('setMe', {me});
}

This function will execute when the Vue instance is created.

Setting Up the Vuex Store

For the store.js file, we set up the centralized store that holds and manages the application state. The file will contain a global state object along with a collection of mutation and getter functions. Since outside components cannot access the state directly, we commit a mutation every time we need to update the state.

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
const state = {
  me: {},
};
const mutations = {
  setMe(state, {me}) {
    state.me = me;
  },
}
const getters = {
  getMyUuid: (state) => state.me.uuid,
};
  
export default new Vuex.Store({
  state,
  getters,
  mutations,
});

The getMyUuid getter is referenced in three of the components and is a global getter for the UUID.

ChatContainer Component

The ChatContainer component is the highest parent node of the UI. The markup includes custom HTML tags for the children components of ChatContainer, as well as Vue specific markup to reactively render data.

<template>
  <div class="chat-container">
    <div class="heading">
      <h1>{{title + '- User: ' + uuid }}</h1>
    </div>
    <div class="body">
      <div class="table">
        <chat-log></chat-log>
        <message-input></message-input>
      </div>
    </div>
  </div>
</template>

The h1 curly braces bind two JavaScript variables, title and uuid, reactively evaluates the expressions and displays the text output as the text content in the tag. The variable title gets its value from the function data.

data() {
  return {
    title: 'PubNub & Vue Global Chat',
  };
},

Before we discuss the variable uuid, let’s discuss the imports and the two properties above data.

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

Since we are using the chat-log and message-input tags in the markup, we need to import the ChatLog and MessageInput components so the tags are rendered properly. We also import mapGetters to get the UUID from the store.

export default {
  name: 'chat-container',
  components: {
     ChatLog,
     MessageInput,
  },

The name property is ‘chat-container’ and it coincides with the kebab-case HTML tag name in the markup. We include the components property to tell Vue which components are being referenced in the markup.

Now, getting back to the variable uuid in the h1 curly brace, we need to set up the computed property which uses mapGetters to map the getter ‘getMyUUID’ to uuid.

computed: {
 ...mapGetters({
     uuid: 'getMyUuid',
  ),
}, 

Since we are mapping the getter to a different name (uuid), we use an object. Finally, we include the function mounted to subscribe to the channel ‘vueChat'.

mounted() {
  this.$pnSubscribe({
      channels: ['vueChat'],
  });
},  

We subscribe, using $pnSubscribe, to the channel once the Vue instance is mounted to the DOM.

MessageInput Component

We break this component into 4 parts:

  1. First, we check that the message body is not empty.
  2. Then, we get the user UUID from the store and assigned it to the variable userUUID.
  3. We publish the message, along with userUUID, to the channel ‘vueChat'.
  4. Finally, we reset the text input.

Here is the template for the component.

<template>
  <div class="message-input">
    <textarea
      ref="messageInput"
      placeholder="message..."
      maxlength="20000"
      @keydown.enter="submitMessage"
    ></textarea>
  </div>
</template>

When the user types in the message body and presses enter, the function submitMessage is called to check that the message body is not empty. We include this function inside of methods. (Note: The rest of the code for MessageInput Component will be inside of submitMessage).

methods: {
  submitMessage(event) {
    if (!event.shiftKey) {
       event.preventDefault();
     } else {
        return;
     }
     // If the message body is empty, do not submit
     if (event.target.value.length === 0) {
       return;
     }

We access the getter, getMyUUID, and assign it to the variable userUUID.

const userUUID = this.$store.getters.getMyUuid;

If the user presses enter and the message body is not empty, we publish to ‘vueChat ‘ the text and the user’s UUID.

this.$pnPublish({ 
  channel: 'vueChat', 
  message: {
    text: event.target.value,
    uuid: userUUID,
    },
  })

We reset the text input once the user presses enter.

event.target.value = '';

MessageBubble Component

The chat log will display the messages sent and received in a message bubble. We'll get to the chat log component in a bit, but for now, let’s focus on the message bubble component. When you send a message, the message bubble is shown on the right side of the chat log without displaying your UUID. The messages received from other users are shown on the left side of the chat log with the users UUID shown above the bubble. This follows the design logic of many group chat apps.

<template>
  <div
    class="message-bubble"
    :class="me"
  >
    <span
      class="from"
      :class="me"
    >{{ uuid }}</span>
    <br :class="me">
    <span
      class="message-text"
    >{{ text }}</span>
  </div>
</template>

The class ‘me’ is bound to a class, such as  ‘message-bubble’ or ‘from’, only if you send the message. When ‘me’ is bound to a class, the positioning and styling of the message bubble will change, as mentioned above. A computed property is used to check if the user is yourself or someone else.

export default {
  name: 'message-bubble',
  props: ['uuid','text'],
  computed: {
    me() {
      let result = false;
      // Check if the client uuid of the message received is your client uuid
      if (this.$store.getters.getMyUuid === this.uuid) {
        result = true;
      }
      // Render the message bubble on the right side if it is from this client
      return result ? 'me' : '';
    },
  },
  data() {
    return {};
  },
};

Besides the computed property, another important part in the script are the prop attributes that are registered to the MessageBubble component. The two values in the prop, ‘uuid’ and ‘text’, will be passed to MessgeBubble's parent component, ChatLog.

ChatLog Component

The chat log displays the messages once it’s received in the channel. Before we work on the template, let’s do the script section first.

import MessageBubble from '@/components/MessageBubble';
import {mapGetters} from 'vuex';
function scrollBottom() {
  this.$el.scrollTo(0, this.$el.scrollHeight);
}

Since we'll be using the message-bubble tag in the markup, we need to import the MessageBubble component in the script so the tags are rendered properly. The scrollBottom function auto scrolls the chat log to the bottom whenever a message is received. This function is called in the watch property.

watch: {
  vueChatMsg: function(){
    this.$nextTick(scrollBottom);
  }
},

We use .$nextTick to make sure the scrollBottom function is called only after the DOM has been updated. Next, let’s add the name property, components and the data function.

name: 'chat-log',
components: {MessageBubble},
data() {
  return {
    vueChatMsg: this.$pnGetMessage('vueChat'),
  }
},

The data function returns vueChatMsg, which contains the new message payload from the channel. As mentioned before, since we are using $pnGetMessage, the messages will display as soon as they are received. We include the channel name as a parameter.

The vueChatMsg property holds an array of objects where every object of the array is the Subscribe Message Response. For every message published, a new message response is added to the array. The objects in the message response includes information such as the channel name, the publisher, the message payload, the subscriber, and so on. We only want the message payload which includes the ‘uuid’ and ‘text’. We will implement this logic in the template.

<template>
  <div
    class="chat-log"
    ref="chatLogContainer"
  >
  <message-bubble
    v-for="msg in vueChatMsg" 
    v-bind:key="msg.id"
    v-bind:uuid="msg.message.uuid"
    v-bind:text="msg.message.text"
  ></message-bubble>
  </div>
</template>

We use v-for to iterate vueChatMsg with ‘msg.id’ as the unique key. We use v-bind to bind the two prop values, ‘uuid’ and ‘text’. Remember that we declared the prop in the child component MessageBubble. So for every iteration of the for loop, we only iterate the message payload and bind ‘msg.message.uuid’ and ‘msg.message.text’ to its corresponding prop attribute.

Let’s quickly summarize the above. Every time a message response is received, it is added as a new element to the array of objects, vueChatMsg, which is returned by the data function. When this happens, inside the message-bubble tag we iterate, using v-for, the new element in the array. Since we only want the message payload, v-for only checks for ‘message’ which contains the payload. The payload values ‘uuid’ and ‘text’ are bind to its appropriate props. Both values are then sent back to the child component, MessageBubble.

That’s all for the Pub/Sub section of this tutorial. Make sure that the code is correct and that you have installed the appropriate plugins. Get the CSS section of the four components from the repo. Run your program by typing ‘npm install’ and ‘npm run dev’ in the terminal and your program should start on a localhost port. Type a message in the message input and you should see a blueish bubble on the right side of the chat log. Open another tab, or preferably window, and copy and paste the URL. Now type a new message in the message input and again, you should see the blueish bubble on the right side of the chat log. But now, you should also see the new message on the other chat log. This new message should be a greyish bubble on the left side of the chat log. Play around with both chat windows and see the messages appear in real time on both screens.

Storing Chat Messages with the History API

While everything is set up and ready to use, there is one problem. If you reload the page, you will notice that all the messages disappear. This occurs because the Storage & Playback feature is not turned on. To turn it on, go to the PubNub Admin Dashboard and click on your application. Click on Keyset and scroll down to Application add-ons. Keep scrolling down until you get to Storage & Playback and toggle the switch to on. Keep the default values the same.

Storage and Playback Toggle Switch

Now that it’s on, messages will persist in storage and can be retrieved later on. Messages can also be deleted from history to meet GDPR compliance.

If you cloned the repo, then reload the page for the chat app and the messages should appear in oldest to newest order. If you didn’t clone the repo, the messages won’t appear since the history function, which fetches historical messages of a channel, has not been added to the code. Either way, in the next section, we will implement the history function so messages can be stored and retrieved.

Setting up History

Fetching the historical messages of our channel is not difficult to do. We need to make small modifications to three files: store.js, ChatContainer.vue, and ChatLog.vue. Let’s start with store.js.

Modify the Vuex State

In the state, we need to add a new property, history, with an empty array as the value.

const state = {
  ...
  history: [],
};

In mutations, we add a new mutation, addHistory, with state as the first argument and history as the second argument.

const mutations = {
  ...
  addHistory(state, {history}){
    history.forEach(element => {
      state.history.push(element.entry);
    });
  },
}

We iterate the array history that contains the historical messages retrieved from the channel. Each element in the array contains two keys, timetoken and entry. We only want entry since it contains the text the user entered and their UUID. This is why in each iteration we push element.entry to the history array we added in state.

We will only add one line to getters.

const getters = {
  ...
  getHistoryMsgs: (state) => state.history,
};

Modify ChatContainer

Since we need to use the history function, import PubNubVue.

import PubNubVue from 'pubnub-vue';

Below the imports, we add a new function, fetchHistory, that will fetch 6 messages from the channel. You can change the number of messages to fetch with 100 being the max number.

function fetchHistory(store){
  PubNubVue.getInstance().history(
      {
        channel: 'vueChat',
        count: 6, // how many items to fetch
        stringifiedTimeToken: true, // false is the default
      },
      function(status, response) {
        const msgs = response.messages;
        // Iterate msgs array and save each element to history
        msgs.forEach(elem => {
          store.commit('addHistory', {history: [elem]});
        })
      }
   )   
}

To commit the history payload, save response.messages to the constant variable ‘msgs’. The constant contains an array of objects where each object contains two keys (timetoken and entry). We don’t want to commit the whole array to the Vuex Store, rather, we want to iterate the array and commit each element. This will make it easier to fetch the necessary keys in the addHistory function.

The last modification to include is in mounted which makes the call to fetchHistory.

mounted() {
  ...
  this.$nextTick(fetchHistory(this.$store));
},

We pass this.$store as a parameter so we can commit the changes to the store.

Modify ChatLog

This is the last file we need to update. We need to make changes to the template and the script. Let’s start with the script. We need to import mapGetters since we will be using it in the computed property.

import {mapGetters} from 'vuex';

In the computed property, we map the getter ‘getHistoryMsgs’ to history.

computed: {
  ...mapGetters({
    history: 'getHistoryMsgs',
  }),
},  

In the template, we add another message-bubble tag.

<template>
  ...
   <message-bubble
    v-for="historyMsg in history" 
    v-bind:key="historyMsg.id"
    v-bind:uuid="historyMsg.uuid"
    v-bind:text="historyMsg.text"
  ></message-bubble>
  ...
</template>

This looks very similar to what we did earlier. We use v-for to iterate history. At every iteration, we retrieve the ‘uuid’ and ‘text’ from the array and bind it to its appropriate prop attributes. The messages will show in the chat log as message bubbles.

That is all we need to do for history. Run the program again and you should see the last six messages from history in the chat log.

Historical Messages for the Group Chat App

There are two things to take note of. The first thing is that the messages will persist in storage for 1 day only. After 1 day, the messages are removed from storage. You can change the time period a message is stored by changing the retention time, which is located in the Storage & Playback add-on. For the purpose of this tutorial, we leave the default value of 1 day.

The second thing to note is that the history messages will show on the left side of the chat log, even if the messages are from you. This is because we generate a random 4 character UUID every time the app instantiates. So when you reload the page, a new UUID is assigned to you and the previous messages you sent before the reload will now be seen as messages sent from another user. This is fine for this tutorial, but for real production, each user should have a unique UUID that is persistent. For a persistent UUID, the history messages that are sent by you will show on the right side of the chat log.

What's Next?

Now that you've got your basic messaging functionality implemented, it's time to add more features! Head over to our Chat Resource Center to explore new tutorials, best practices, and design patterns for taking your chat app to the next level.

Have suggestions or questions about the content of this post? Reach out at devrel@pubnub.com.