12. Chat

In this chapter, we will be creating a real-time chat module.

Creating a Chat Page

First, create a link to the chat page. Open your “ContactComponent.vue” file and add the following router link to the 3rd <td> tag:

<router-link v-bind:to="'/chat/' + contact.email" class="btn btn-primary" style="margin-left: 10px;">Chat</router-link>

Then create a file named “ChatComponent.vue” in your components folder. Then add it to your routes array in the “main.js” file:

import ChatComponent from "./components/ChatComponent.vue"

const routes = [
    ...
    
    { path: "/chat/:email", component: ChatComponent }
];

Following will be the code of the “ChatComponent.vue” file:

<template>
	<div class="container">
		<div class="row clearfix">
			<div class="col-lg-12">
				<div class="card chat-app">
					<div class="chat">
						<div class="chat-header clearfix">
							<div class="row">
								<div class="col-lg-6">
									<a href="javascript:void(0);" data-toggle="modal" data-target="#view_info">
										<img src="https://bootdey.com/img/Content/avatar/avatar2.png" alt="avatar">
									</a>

									<div class="chat-about">
										<!-- receiver name goes here -->
									</div>
								</div>

								<div class="col-lg-6 hidden-sm text-right text-white">
									<!-- attachment goes here -->
								</div>
							</div>
						</div>

						<div class="chat-history">
							<ul class="m-b-0">
								<!-- all messages gone here -->
							</ul>
						</div>

						<div class="chat-message clearfix">
							<div class="input-group mb-0">
								<div class="input-group mb-3">
									<input type="text" class="form-control" placeholder="Enter text here..." />
									<button class="btn btn-primary" type="button">Send</button>
								</div>
							</div>
						</div>
					</div>
				</div>
			</div>
		</div>
	</div>
</template>

<script>

	import "../../public/assets/css/chat.css"

	export default {
		//
	}
</script>

Create a file named “chat.css” in your web/public/assets/css folder. Following will be the code of the “chat.css” file:

.card {
    transition: .5s;
    border: 0;
    margin-bottom: 30px;
    border-radius: .55rem;
    position: relative;
    width: 100%;
    box-shadow: 0 1px 2px 0 rgb(0 0 0 / 10%);
}
.chat-app .people-list {
    width: 280px;
    position: absolute;
    left: 0;
    top: 0;
    padding: 20px;
    z-index: 7
}
.people-list {
    -moz-transition: .5s;
    -o-transition: .5s;
    -webkit-transition: .5s;
    transition: .5s
}
.people-list .chat-list li {
    padding: 10px 15px;
    list-style: none;
    border-radius: 3px
}
.people-list .chat-list li:hover {
    background: #efefef;
    cursor: pointer
}
.people-list .chat-list li.active {
    background: #efefef
}
.people-list .chat-list li .name {
    font-size: 15px
}
.people-list .chat-list img {
    width: 45px;
    border-radius: 50%
}
.people-list img {
    float: left;
    border-radius: 50%
}
.people-list .about {
    float: left;
    padding-left: 8px
}
.people-list .status {
    color: #999;
    font-size: 13px
}
.chat .chat-header {
    padding: 15px 20px;
    border-bottom: 2px solid #f4f7f6
}
.chat .chat-header img {
    float: left;
    border-radius: 40px;
    width: 40px
}
.chat .chat-header .chat-about {
    float: left;
    padding-left: 10px
}
.chat .chat-history {
    padding: 20px;
    border-bottom: 2px solid #fff
}
.chat .chat-history ul {
    padding: 0
}
.chat .chat-history ul li {
    list-style: none;
    margin-bottom: 30px
}
.chat .chat-history ul li:last-child {
    margin-bottom: 0px
}
.chat .chat-history .message-data {
    margin-bottom: 15px
}
.chat .chat-history .message-data img {
    border-radius: 40px;
    width: 40px
}
.chat .chat-history .message-data-time {
    color: #434651;
    padding-left: 6px
}
.chat .chat-history .message {
    color: #444;
    padding: 18px 20px;
    line-height: 26px;
    font-size: 16px;
    border-radius: 7px;
    display: inline-block;
    position: relative
}
.chat .chat-history .message:after {
    bottom: 100%;
    left: 7%;
    border: solid transparent;
    content: " ";
    height: 0;
    width: 0;
    position: absolute;
    pointer-events: none;
    border-bottom-color: #fff;
    border-width: 10px;
    margin-left: -10px
}
.my-message, .other-message {
    margin-top: 10px;
    padding-left: 20px;
    padding-right: 20px;
    padding-top: 10px;
    padding-bottom: 10px;
    border-radius: 5px;
}
.chat .chat-history .my-message {
    background: #efefef
}
.chat .chat-history .my-message:after {
    bottom: 100%;
    left: 30px;
    border: solid transparent;
    content: " ";
    height: 0;
    width: 0;
    position: absolute;
    pointer-events: none;
    border-bottom-color: #efefef;
    border-width: 10px;
    margin-left: -10px
}
.chat .chat-history .other-message {
    background: #e8f1f3;
    text-align: right
}
.chat .chat-history .other-message:after {
    border-bottom-color: #e8f1f3;
    left: 85%
}
.chat .chat-message {
    padding: 20px
}
.online,
.offline,
.me {
    margin-right: 2px;
    font-size: 8px;
    vertical-align: middle
}
.online {
    color: #86c541
}
.offline {
    color: #e47297
}
.me {
    color: #1d8ecd
}
.float-right {
    float: right
}
.clearfix:after {
    visibility: hidden;
    display: block;
    font-size: 0;
    content: " ";
    clear: both;
    height: 0
}
@media only screen and (max-width: 767px) {
    .chat-app .people-list {
        height: 465px;
        width: 100%;
        overflow-x: auto;
        background: #fff;
        left: -400px;
        display: none
    }
    .chat-app .people-list.open {
        left: 0
    }
    .chat-app .chat {
        margin: 0
    }
    .chat-app .chat .chat-header {
        border-radius: 0.55rem 0.55rem 0 0
    }
    .chat-app .chat-history {
        height: 300px;
        overflow-x: auto
    }
}
@media only screen and (min-width: 768px) and (max-width: 992px) {
    .chat-app .chat-list {
        height: 650px;
        overflow-x: auto
    }
    .chat-app .chat-history {
        height: 600px;
        overflow-x: auto
    }
}
@media only screen and (min-device-width: 768px) and (max-device-width: 1024px) and (orientation: landscape) and (-webkit-min-device-pixel-ratio: 1) {
    .chat-app .chat-list {
        height: 480px;
        overflow-x: auto
    }
    .chat-app .chat-history {
        height: calc(100vh - 350px);
        overflow-x: auto
    }
}

You will see an input field and a send button. We can get the email from the URL using the following code:

import axios from "axios"
import swal from "sweetalert2"

export default {
	data() {
		return {
			email: this.$route.params.email,
		}
	}
}

Then we need our “Send” button to call an AJAX and save the message in Mongo DB when clicked.

AJAX to Send Message

First, we will attach a onclick listener to the button.

<button class="btn btn-primary" v-on:click="sendMessage" type="button">Send</button>

Then we will attach a model to the input field so we can get the value of that input field in Javascript.

<input type="text" class="form-control" placeholder="Enter text here..." v-model="message" />

Then we need to create that model in our data object:

message: "",
page: 0,

We will be needing the page variable while fetching the messages. Also, we need to create a function in our methods object that will send the AJAX request.

methods: {

	sendMessage: async function () {

		const formData = new FormData()
		formData.append("email", this.email)
		formData.append("message", this.message)

		const response = await axios.post(
			this.$apiURL + "/chat/send",
			formData,
			{
				headers: this.$headers
			}
		)
		console.log(response)

		if (response.data.status == "success") {
			this.message = ""
		} else {
			swal.fire("Error", response.data.message, "error")
		}
	},
},

Now we need to create an API that will handle this request.

API to Save Messages

Create a new file named “chat.js” in your api/modules folder. Following will be the code of this file:

const mongodb = require("mongodb");
const ObjectId = mongodb.ObjectId;

const auth = require("./auth");

module.exports = {

    init: function (app, express) {
        const self = this;
        const router = express.Router();

        app.use("/chat", router);
    }
};

We will be using end-to-end encryption on our messages. So even if someone hacks into your Mongo DB, he will still not be able to know the content of messages. We will encrypt the messages during saving, and during retrieving we will decrypt the messages.

Note: For more details on encryption and decryption of messages using Node JS and Mongo DB follow this.

Encrypt the Messages

Go ahead and install the crypto module. You can install it by running the following command in your CMD opened in api folder.

> npm install crypto

Then add the following lines at the top of your “chat.js” file:

const crypto = require('crypto');
const algorithm = 'aes-256-cbc'; // Using AES encryption
const key = "adnan-tech-programming-computers"; // must be of 32 characters

Now, create a function that will accept the text as a plain string and convert it into an encrypted hash.

let encrypt = function (text) {
    const iv = crypto.randomBytes(16);
    
    // protected data
    const message = text;

    // the cipher function
    const cipher = crypto.createCipheriv(algorithm, key, iv);

    // encrypt the message
    // input encoding
    // output encoding
    let encryptedData = cipher.update(message, "utf-8", "hex");
    encryptedData += cipher.final("hex");

    const base64data = Buffer.from(iv, 'binary').toString('base64');
    return {
        iv: base64data,
        encryptedData: encryptedData
    };
};

This will return the initialization vector and encrypted string which we will both store in Mongo DB.

Save Message in Mongo DB

After that, create a POST route that will create a new collection named “messages” and inside it will create a new document for this message.

router.post("/send", auth, async function (request, result) {
    const user = request.user;
    const email = request.fields.email;
    const message = request.fields.message;
    const createdAt = new Date().getTime();

    if (!email || !message) {
        result.json({
            status: "error",
            message: "Please enter all fields."
        });
        return;
    }

    // Text send to encrypt function
    const hw = encrypt(message);

    const receiver = await db.collection("users").findOne({
        email: email
    });

    if (receiver == null) {
        result.json({
            status: "error",
            message: "The receiver is not a member of Chat Station."
        });
        return;
    }

    const object = {
        message: hw,
        sender: {
            _id: user._id,
            name: user.name,
            email: user.email
        },
        receiver: {
            _id: receiver._id,
            name: receiver.name,
            email: receiver.email
        },
        isRead: false,
        createdAt: createdAt
    };
    const document = await db.collection("messages").insertOne(object);

    await db.collection("users").findOneAndUpdate({
        $and: [{
            "_id": receiver._id
        }, {
            "contacts._id": user._id
        }]
    }, {
        $inc: {
            "contacts.$.unreadMessages": 1
        }
    });

    const messageObject = {
        _id: document.insertedId,
        message: message,
        sender: object.sender,
        receiver: object.receiver,
        isRead: false,
        createdAt: createdAt
    };

    result.json({
        status: "success",
        message: "Message has been sent.",
		messageObject: messageObject
    });
});

Let’s look at what the function does.

  1. First, it will get all the values from AJAX.
    1. Message from the input field.
    2. Email from URL.
    3. And user from headers.
  1. Then it gets the current timestamp and saves it in a variable.
  2. Then it checks if email and message values are not empty.
  3. It converts the plain text into an encrypted message.
  4. Check if the receiver is a registered member.
  5. Creates a message object and set isRead to false by default. This will be set to true when the receiver opens this message.
  6. It creates a new document in the messages collection.
  7. It increments the other person’s unreadMessages by 1.
  8. Finally, it sends the response back to the client with the new message object. The client should append the new message at the bottom using this object (we will do it later).

DO NOT test the app now, it will not work. Instead, it will throw an error. That is because we haven’t included this module in our Node JS server.

So open your “server.js” file and include it at the top:

const chat = require("./modules/chat");

Then after the database is connected, write the following line:

chat.init(app, express);

Now you are free to test. Make sure both users are on the contact list of each other. Write some text in the message input field and hit “Send”. Refresh your Mongo DB Compass, you will see a new collection has been created.

messages collection
messages collection

And you will also see that in the user’s collection, the unreadMessages counter of the receiver will be incremented to 1.

unread messages increment
unread messages increment

For example, if A is in the contact list of B, and B is in the contact list of A. And you are logged in as A and are sending messages to B. Then you will see in the contact list of B that it will have an unread message counter of A incremented by 1.

Unread Message Counter in HomeComponent

Now, we need to show the number of unread messages on the home component where all contacts are displayed. So when you open the app you will know if you have any unread messages from any of your contact.

Simply open ContactComponent.vue and add the following <span> tag in the first <td> after the contact name span is created:

<span v-if="(contact.unreadMessages > 0)" v-text="' (' + contact.unreadMessages + ')'" class="text-danger"></span>

If you check user B, you will notice that it now displays the number of unread messages along with the contact name.

unread messages counter view
unread messages counter

Show all Messages

When you open a chat, you should be able to view all messages of that chat. When the chat component is mounted, we should call a function that will call an AJAX request to fetch the messages in that chat.

Write the following code in your ChatComponent.vue export default object.

mounted() {
	this.getData()
}

Then we need to create this method in your methods object.

getData: async function () {
	if (this.email == null) {
		return
	}

	const formData = new FormData()
	formData.append("email", this.email)
	formData.append("page", this.page)

	const response = await axios.post(
		this.$apiURL + "/chat/fetch",
		formData,
		{
			headers: this.$headers
		}
	)
	console.log(response)

	if (response.data.status == "success") {
		//
	} else {
		swal.fire("Error", response.data.message, "error")
	}
}

Then we need to create a function that will decrypt the message. So, create the following function in your api/modules/chat.js file:

let decrypt = function (text) {
    const origionalData = Buffer.from(text.iv, 'base64') 

    const decipher = crypto.createDecipheriv(algorithm, key, origionalData);
    let decryptedData = decipher.update(text.encryptedData, "hex", "utf-8");
    decryptedData += decipher.final("utf8");
    return decryptedData;
};

It will receive an object as a parameter. The object contains an initialization vector iv and encrypted string as encryptedData.

After that, we need to create an API that will fetch the messages of that chat. Open the file api/modules/chat.js and create a new POST route in it:

router.post("/fetch", auth, async function (request, result) {
    const user = request.user;
    const email = request.fields.email;
	const page = request.fields.page ?? 0;
	const limit = 30;

    if (!email) {
        result.json({
            status: "error",
            message: "Please enter all fields."
        });
        return;
    }

    const receiver = await db.collection("users").findOne({
        email: email
    });

    if (receiver == null) {
        result.json({
            status: "error",
            message: "The receiver is not a member of Chat Station."
        });
        return;
    }

    const messages = await db.collection("messages").find({
        $or: [{
            "sender._id": user._id,
            "receiver._id": receiver._id
        }, {
            "sender._id": receiver._id,
            "receiver._id": user._id
        }]
    })
        .sort({"createdAt": -1})
		.skip(page * limit)
		.limit(limit)
        .toArray();

    const data = [];
    for (let a = 0; a < messages.length; a++) {
        data.push({
            _id: messages[a]._id.toString(),
            message: decrypt(messages[a].message),
            sender: {
                email: messages[a].sender.email,
                name: messages[a].sender.name
            },
            receiver: {
                email: messages[a].receiver.email,
                name: messages[a].receiver.name
            },
            isRead: messages[a].isRead,
            createdAt: messages[a].createdAt
        });
    }

    let unreadMessages = 0;
    for (let a = 0; a < data.length; a++) {
        if (data[a].receiver.email == user.email && !data[a].isRead) {
            await db.collection("messages").updateMany({
                _id: ObjectId(data[a]._id)
            }, {
                $set: {
                    "isRead": true
                }
            })

            unreadMessages++;
        }
    }

    await db.collection("users").findOneAndUpdate({
        $and: [{
            "_id": user._id
        }, {
            "contacts.email": email
        }]
    }, {
        $inc: {
            "contacts.$.unreadMessages": -unreadMessages
        }
    });

    result.json({
        status: "success",
        message: "Messages has been fetched.",
        messages: data,
        user: {
            email: user.email,
            name: user.name,
            contacts: user.contacts
        },
        receiver: {
            email: receiver.email,
            name: receiver.name
        }
    });
});

It does the following:

  1. Gets the logged-in user from headers.
  2. Gets an email from the URL.
  3. Set starting page to 0 if undefined. Otherwise, set the value of the page as received from the client. We will get on this later.
  4. Set the limit to 30 as we will be displaying 30 messages, after that user needs to scroll up and load previous messages.
  5. Check if the receiver is a valid registered member.
  6. Get all the messages where the logged-in users and the other person whose chat is opened, are either sender or receiver.
  7. Sort the messages by their creation date i.e. latest messages first.
  8. Start the index from 0 or the value of the page variable.
  9. Set the limit to 30.
  10. Update the isRead value as true and count the number of read messages.
  11. Decrement the value of unreadMessages the counter from the user’s collection of that contact.
  12. Send the response back to the client with all the messages fetched and the sender and receiver object.

Run the app now and you will see that the messages have been received in your browser console:

messages array
messages array

If you open the chat from user B (receiver) and then go back home. You will see that the unread counter is now removed. Because it now turned to 0.

Display the Messages

Now that we have received all the messages in our local Javascript array, we can display them on the front end.

First, go to your “ChatComponent.vue” file and add the following 2 variables to the data object.

messages: [],
receiver: null,

Then write the following code when the response status is “success”:

if (response.data.status == "success") {
	for (let a = 0; a < response.data.messages.length; a++) {
		this.messages.unshift(response.data.messages[a])
	}
	this.receiver = response.data.receiver
	this.user = response.data.user
}

Now we need to create a UI where we will display the messages. My messages will be displayed on right and the receiver’s messages will be displayed on left.

Write the following lines where <!-- all messages gone here --> the comment was added:

<li v-for="msg in messages" class="clearfix" v-bind:key="msg._id">
	<div v-bind:class="'message-data ' + (user != null && user.email == msg.sender.email ? 'text-right' : '')">
		<span class="message-data-time text-white" v-text="getMessageTime(msg.createdAt)"></span>
		<img src="https://bootdey.com/img/Content/avatar/avatar7.png" alt="avatar" style="width: 50px;" />
	</div>

	<div v-bind:class="'message ' + (user != null && user.email == msg.sender.email ? 'my-message float-right' : 'other-message')">
		<p v-text="msg.message" v-bind:class="(user != null && user.email == msg.sender.email ? 'text-right' : '')" style="margin-bottom: 0px;"></p>
	</div>
</li>

We are looping through all the messages. And we are aligning text to the right if the sender is me.

Then we are displaying message time. We are calling a function getMessageTime(), we need to create this function in our methods object.

getMessageTime: function (time) {
	const dateObj = new Date(time)
	let timeStr = dateObj.getFullYear() + "-" + (dateObj.getMonth() + 1) + "-" + dateObj.getDate() + " " + dateObj.getHours() + ":" + dateObj.getMinutes() + ":" + dateObj.getSeconds()
	return timeStr
},

After that, we are displaying an avatar of a user. This could be any random image for now. Later on, we can allow users to set their own avatars.

Again, we are setting my messages on the right using float-right and the other person’s messages will be on left by default.

That is how messages will be displayed on the sender side:

messages sender
messages sender

And this is how it will be displayed on the receiver side:

message receiver
message receiver

Display Message After Send

You might have noticed something, When you send a new message, you have to refresh the page to see your own sent message. But it should display the newly sent message automatically.

Simply add the following line after the response status is “success” in the sendMessage function:

this.messages.push(response.data.messageObject)

This will append the new message at the end of messages the array.

Display Receiver Name

We are already displaying the receiver’s email on the URL. But it should be displayed inside the app so the user will know with whom he is chatting.

Write the following code in place of <!-- receiver name goes here --> comments:

<h6 class="m-b-0 text-white" v-if="receiver != null" v-text="receiver.name" style="margin-bottom: 0px; position: relative; top: 10px;"></h6>

This will display the user name on top along with the avatar.

receiver name
receiver name

Message with Attachment

At this point, you will be able to send text messages only. We can allow users to attach files as well.

First, we need to create a button that when clicked will ask the user to select a file from his/her computer.

The following code goes in the <!-- attachment goes here --> comments:

<span v-if="attachment != null" style="margin-right: 10px; position: relative; top: 7px;" v-text="attachment.name"></span>
<a href="javascript:void(0);" class="btn btn-outline-secondary pull-right text-white" v-on:click="selectFile"><i class="fa fa-paperclip"></i></a>
<input type="file" id="attachment" style="display: none;" v-on:change="fileSelected" />

This will create a hidden input field and an anchor tag on the top right having a paper-clip icon. But not at the moment, we need to create an object named attachment that will hold all the information about the selected file or null if no file is selected.

Create the following variable in data() return object:

attachment: null,

There create a new method named selectFile in your methods object. This will trigger the input type file to select the file from the user’s computer.

selectFile: function () {
	document.getElementById("attachment").click()
},

When the file is selected, the input type file’s onchange event will be triggered and it will call the fileSelected function. So we need to create that function as well.

fileSelected: function () {
	const files = event.target.files
	if (files.length > 0) {
		this.attachment = files[0]
	}
},

This will get the selected file object and save it in our attachment variable.

Run the app now and you will see a paper-clip icon on the top right.

message with attachment
message with attachment

Click on that and you will be able to select a file from your computer. After the file is selected, you will see its name.

Send file with AJAX

Now we need to attach this file with the form data when the message is sent. So add the following lines when you are appending the email and message values in FormData object in sendMessage method.

if (this.attachment != null) {
	formData.append("attachment", this.attachment)
}

And when the response is successfully received, we need to set the attachment variable to null. Also, we need to clear the file from the input type file.

this.attachment = null
document.getElementById("attachment").value = null

After that, we need to handle it from the server-side too. Open your api/modules/chat.js file and go to the “/send” POST route. Where you are getting other input fields, get the attachment from the files objects too.

const attachment = request.files.attachment; // size (bytes), path, name, type, mtime

Save file on Node JS Server

To save the file on the server, we need to install a module named “fs” which stands for File System. So run the following command:

> npm install fs

Then, include it on top of your chat.js file:

const fileSystem = require("fs");

Also, we need to create a folder directly inside the “api” folder with the name “uploads”. All our uploaded files will be saved here.

Coming back to the “/send” POST route in chat.js, write the following lines before the insertOne statement:

if (attachment != null && attachment.size > 0) {
    if (!fileSystem.existsSync("uploads/" + user.email)){
        fileSystem.mkdirSync("uploads/" + user.email);
    }

    const dateObj = new Date();
    const datetimeStr = dateObj.getFullYear() + "-" + (dateObj.getMonth() + 1) + "-" + dateObj.getDate() + " " + dateObj.getHours() + ":" + dateObj.getMinutes() + ":" + dateObj.getSeconds();
    const fileName = "ChatStation-" + datetimeStr + "-" + attachment.name;
    const filePath = "uploads/" + user.email + "/" + fileName;

    object.attachment = {
        size: attachment.size,
        path: filePath,
        name: fileName,
        displayName: attachment.name,
        type: attachment.type
    };

    fileSystem.readFile(attachment.path, function (error, data) {
        if (error) {
            console.error(error);
        }

        fileSystem.writeFile(filePath, data, function (error) {
            if (error) {
                console.error(error);
            }
        });

        fileSystem.unlink(attachment.path, function (error) {
            if (error) {
                console.error(error);
            }
        });
    });
}

This will first check if the uploaded file is valid. You can send any type of file, image, video, PDF, or anything.

Then it will create a new folder inside the “uploads” folder dynamically with the user’s email, only if the folder is not already created.

Then it will create a variable with the current date-time string in YYYY-MM-DD hh:ii:ss format. And set the filename. Concatenating the current date-time string helps in making the file name unique.

After that, it creates a new property to the object object with a key attachment. It will hold the uploaded file data.

Run the app now, select a file and type the message and send. Then check your uploads folder, you will see that a folder with your email has been generated and your selected file will be saved in it.

attachment uploads
attachment uploads

And if you refresh your Mongo DB compass and check your “messages” collection. You will see that a new document has been inserted but this new document also has an attachment object with it.

attachment mongodb
attachment MongoDB

This is the benefit of Mongo DB that each document can have a different schema than the others. This greatly helps in scalability.

If you refresh the chat page now, you will see your sent message but you will not see your attached document. So now we need to show it with the message.

Show Attachment with Message

To display the selected file, first, create a simple file that will hold all the global variables. Create a file named “globals.js” in your api/modules folder.

For now, you only need to create a single variable that will hold the base URL address of the API.

const baseURL = "http://localhost:3000"
global.baseURL = baseURL

After that, you need to include that file in your api/modules/chat.js at the top:

require("./globals");

This will be used while accessing the attachment files from the server. Then go to the “/fetch” POST route. And inside the for loop, get the attachment file and save it in a separate variable.

for (let a = 0; a < messages.length; a++) {
    let attachment = null;
    if (messages[a].attachment != null) {
        attachment = messages[a].attachment;
        attachment.path = baseURL + "/chat/attachment/" + messages[a]._id;
    }

    ...
}

Right after this, we have a data.push statement, so we need to add our attachment variable in that too.

attachment: attachment,

If the message has an attachment, then it will be the attachment object. Otherwise, it will be null.

Now come back to your ChatComponent.vue file inside the web/src/components folder. Inside the <li> tag, where you are displaying the content of the message <p v-text="msg.message" …></p>, write the following lines:

<template v-if="msg.attachment != null">
    <a href="javascript:void(0)" v-bind:data-id="msg._id" v-on:click.prevent="downloadAttachment" v-text="msg.attachment.displayName" class="text-info" target="_blank"></a>
</template>

This will create a <a> tag only if the message has an attachment. It will show the original name of the file.

Refresh your chat page and now you will see the name of a file with the message to which you have attached.

message with attachment name
a message with an attachment name

Now we need to download the file when the name of the file is clicked.

Download Attachment

We are already setting the message unique ID on the anchor tag. Now we need to create a Javascript function that will be called when the name of the file is clicked. So, create the following method in your ChatComponent.vue methods object.

downloadAttachment: async function () {
    const anchor = event.target
    const id = anchor.getAttribute("data-id")
    const originalHtml = anchor.innerHTML
    anchor.innerHTML = "Loading..."

    const formData = new FormData()
    formData.append("messageId", id)
    
    const response = await axios.post(
        this.$apiURL + "/chat/attachment",
        formData,
        {
            headers: this.$headers
        }
    )

    if (response.data.status == "success") {
        this.base64Str = response.data.base64Str
        this.downloadFileName = response.data.fileName
        
        const btnDownloadAttachment = this.$refs["btnDownloadAttachment"]
        setTimeout(function () {
            btnDownloadAttachment.click()
            anchor.innerHTML = originalHtml
        }, 500)
    } else {
        swal.fire("Error", response.data.message, "error")
    }
},

This will get the message ID from the anchor data-id attribute. Attach it to an FormData object and send it to an API endpoint.

The response received will be an object containing a base64 string and the name of the file. Then it will get the hidden download button DOM (we will create this hidden button in a moment). And after a slight delay of 0.5 seconds, it will trigger the download.

Create the following anchor tag anywhere in your ChatComponent.vue but inside the <template> tag.

<a v-bind:href="base64Str" ref="btnDownloadAttachment" v-bind:download="downloadFileName"></a>

Also, we need to create 2 more variables in our data object:

base64Str: "",
downloadFileName: ""

Then we need to create an API that will receive the message ID and send the attached file as a base64 string.

Open your api/modules/chat.js and create a function at the top.

// function to encode file data to base64 encoded string
let base64Encode = function(file) {
    // read binary data
    var bitmap = fileSystem.readFileSync(file);
    // convert binary data to base64 encoded string
    return new Buffer.from(bitmap).toString('base64');
};

This will receive the file relative path as a parameter. Read the file, get the buffer from the bitmap object and convert that into base64. They are all built-in functions provided by Javascript.

Route to get Base64 String

Then create the following route in it:

router.post("/attachment", auth, async function (request, result) {
    const messageId = request.fields.messageId;
    const user = request.user;

    const message = await db.collection("messages").findOne({
        _id: ObjectId(messageId)
    });

    if (message == null) {
        result.status(404).json({
            status: "error",
            message: "Message not found."
        });
        return;
    }

    const isSender = (message.sender._id == user._id.toString());
    const isReceiver = (message.receiver._id == user._id.toString());

    if (!(isSender || isReceiver)) {
        result.status(401).json({
            status: "error",
            message: "You are not authorized for viewing this attachment."
        });
        return;
    }

    let attachment = message.attachment;
    const base64Str = "data:" + attachment.type + ";base64," + base64Encode(attachment.path);
    result.json({
        status: "success",
        message: "Attachment has been fetched",
        base64Str: base64Str,
        fileName: attachment.displayName
    });
});

It is getting the message ID and checking if the message exists. Then it will check if the person who is sending the request to download the file, is either a sender or a receiver so that no one else will be able to access the file.

And finally, it will get the attachment object and convert that into a base64 string using it’s path and send the response back to the client.

Test the app now, click on the name of the file and it will be downloaded on your local computer.