Building a Vue 3 Chat App with vue-advanced-chat

In this tutorial, we will learn how to build a Vue.js chat app using the ChatKitty API and vue-advanced-chat.
vue-advanced-chat is a feature-rich and highly customizable Vue chat component library designed to simplify the process of building modern, real-time chat applications. It provides a wide range of out-of-the-box features, including:
- Direct messaging and group chats
- Images, videos, files, voice messages, emojis and link previews
- Tag users & emojis shortcut suggestions
- Typing indicators
- Reactions with emojis and GIFs
- Markdown text formatting - bold, italic, strikethrough, underline, code, multiline, etc.
- Online presence indicators for online/offline users' status
- Delivery and read receipts
- Theming and customization options including light and dark theme modes
- Responsive design perfect for mobile
In addition, vue-advanced-chat is compatible with all Javascript frameworks (Vue, Angular, React, etc.) or no framework at all as a web component.
By using vue-advanced-chat alongside a chat API service like ChatKitty, developers can quickly create chat applications with minimal boilerplate code, while also benefiting from a user-friendly and customizable UI.
This library helps reduce the time and effort required to implement chat functionalities, allowing developers to focus on other aspects of their application. Moreover, its responsive design ensures the chat interface adapts to different screen sizes, making it suitable for web and mobile applications alike.
Prerequisites
Before you start, make sure you have the following installed:
You can check out our working demo any time on Stackblitz.
We'll begin by setting up the project and installing the necessary dependencies.
Setting up the project
Create a new Vue.js project using the Vue CLI by running the following command:
npm init vue@latest
This command will install and execute the official Vue project scaffolding tool.
Next, navigate to the newly created project folder:
cd chatkitty-vue-example
Install dependencies
Install the required packages using npm:
npm install @chatkitty/core vue-advanced-chat
Integrating the ChatKitty service
Implementing date utility functions
First, in the src create a new utils directory with a dates.ts file, add helper functions to parse
and process ISO date-times returned by ChatKitty. The zeroPad function pads a number with leading zeros,
while the isSameDay function checks if two dates are on the same day. The parseTimestamp function
formats the timestamp based on the specified format, and the formatTimestamp function formats the timestamp
based on whether it is on the same day as the current date.
const zeroPad = (num: number, pad: number) => {
return String(num).padStart(pad, '0')
}
const isSameDay = (d1: Date, d2: Date) => {
return (
d1.getFullYear() === d2.getFullYear() &&
d1.getMonth() === d2.getMonth() &&
d1.getDate() === d2.getDate()
)
}
export const parseTimestamp = (timestamp: string, format = '') => {
if (!timestamp) return
const date = new Date(timestamp)
if (format === 'HH:mm') {
return `${zeroPad(date.getHours(), 2)}:${zeroPad(date.getMinutes(), 2)}`
} else if (format === 'DD MMMM YYYY') {
const options: Intl.DateTimeFormatOptions = { month: 'long', year: 'numeric', day: 'numeric' }
return `${new Intl.DateTimeFormat('en-GB', options).format(date)}`
} else if (format === 'DD/MM/YY') {
const options: Intl.DateTimeFormatOptions = {
month: 'numeric',
year: 'numeric',
day: 'numeric'
}
return `${new Intl.DateTimeFormat('en-GB', options).format(date)}`
} else if (format === 'DD MMMM, HH:mm') {
const options: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric' }
return `${new Intl.DateTimeFormat('en-GB', options).format(date)}, ${zeroPad(
date.getHours(),
2
)}:${zeroPad(date.getMinutes(), 2)}`
}
return date
}
export const formatTimestamp = (date: Date, timestamp: string) => {
const timestampFormat = isSameDay(date, new Date()) ? 'HH:mm' : 'DD/MM/YY'
const result = parseTimestamp(timestamp, timestampFormat)
return timestampFormat === 'HH:mm' ? `Today, ${result}` : result
}
Implementing service functions
Next, create a folder named chatkitty inside the src folder and create an index.ts file within it. Import
the ChatKitty package and initialize the ChatKitty instance with your API key:
import ChatKitty from '@chatkitty/core';
export const chatkitty = ChatKitty.getInstance('YOUR_CHATKITTY_API_KEY');
Replace 'YOUR_CHATKITTY_API_KEY' with your actual API key.
In the src/chatkitty/index.ts file, we'll implement various functions for interacting with the ChatKitty
API. These functions will handle entering and exiting chat rooms, fetching messages and rooms, user
authentication, and sending messages.
Let's start by implementing functions to map ChatKitty users, channels, and messages into vue-advanced-chat objects.
- Implement the
mapUserfunction to map a ChatKitty user to a vue-advanced-chat user
const mapUser = (user: User) => ({
_id: user.name,
username: user.displayName,
avatar: user.displayPictureUrl,
status: {
state: user.presence.online ? 'online' : 'offline'
},
_user: user
})
- Implement the
mapMessagefunction to map a ChatKitty message to a vue-advanced-chat message
const mapMessage = (message: Message, timeFormat?: string) => ({
_id: message.id,
content: message.type === 'TEXT' || message.type === 'SYSTEM_TEXT' ? message.body : undefined,
senderId: message.type === 'TEXT' || message.type === 'FILE' ? message.user.name : 'system',
username:
message.type === 'TEXT' || message.type === 'FILE' ? message.user.displayName : 'System',
timestamp: parseTimestamp(message.createdTime, timeFormat || 'HH:mm'),
date: parseTimestamp(message.createdTime, 'DD MMMM YYYY'),
_message: message
})
- Implement the
mapChannelfunction to map a ChatKitty channel to a vue-advanced-chat room
const mapChannel = async (user: CurrentUser, channel: Channel) => ({
roomId: channel.id,
roomName:
channel.type === 'DIRECT'
? channel.members
.filter((member) => member.id !== user.id)
.map((member) => member.displayName)
.join(', ')
: channel.name,
users:
channel.type === 'DIRECT'
? channel.members.map((member) => mapUser(member))
: await (async () => {
const result = await chatkitty.listChannelMembers({ channel })
if (result.succeeded) {
return result.paginator.items.map((member) => mapUser(member))
}
return []
})(),
lastMessage:
channel.lastReceivedMessage && mapMessage(channel.lastReceivedMessage, 'DD MMMM, HH:mm'),
_channel: channel,
_messages_paginator: null,
_chat_session: null
})
- Implement the
loginfunction to authenticate a user and start a ChatKitty session.
export const login = async (username: string) => {
await chatkitty.startSession({ username })
};
- Implement the
enterRoomfunction to enter a chat room and start a chat session.
export const enterRoom = async ({
room,
onMessageReceived,
onRoomUpdated,
}: {
room: any;
onMessageReceived: (message: any) => void;
onRoomUpdated: (room: any) => void;
}) => {
const result = await chatkitty.startChatSession({
channel: room._channel,
onMessageReceived: (message) => {
const mapped = mapMessage(message)
room.lastMessage = mapped
onMessageReceived(mapped)
onRoomUpdated(room)
}
})
if (result.succeeded) {
room._chat_session = result.session
}
};
- Implement the
exitRoomfunction to exit a chat room and end the chat session.
export const exitRoom = (room: any) => {
room._chat_session?.end?.()
room._messages_paginator = null
room._chat_session = null
};
- Implement the
fetchMessagesfunction to fetch messages for a chat room.
export const fetchMessages = async (room: any) => {
if (room._messages_paginator) {
const items = room._messages_paginator.items
.map((message: Message) => mapMessage(message))
.reverse()
const hasMore = room._messages_paginator.hasNextPage
if (hasMore) {
room._messages_paginator = await room._messages_paginator.nextPage()
}
return { items, hasMore }
}
const result = await chatkitty.listMessages({ channel: room._channel })
if (result.succeeded) {
const items = result.paginator.items.map((message) => mapMessage(message)).reverse()
const hasMore = result.paginator.hasNextPage
if (hasMore) {
room._messages_paginator = await result.paginator.nextPage()
}
return { items, hasMore }
}
return { items: [], hasMore: false }
};
- Implement the
fetchRoomsfunction to fetch a list of chat rooms.
export const fetchRooms = async () => {
const user = chatkitty.currentUser
if (!user) {
return []
}
const result = await chatkitty.listChannels({ filter: { joined: true } })
if (result.succeeded) {
return await Promise.all(
result.paginator.items.map(async (channel) => await mapChannel(user, channel))
)
}
return []
};
- Implement the
sendMessagefunction to send a message to a chat room.
export const sendMessage = async ({ room, content }: any) => {
if (content) {
await chatkitty.sendMessage({ channel: room._channel, body: content })
}
};
- Implement the
logoutfunction to end the ChatKitty session.
export const logout = async () => {
await chatkitty.endSession()
};
Refer to the provided code example for the implementation details.
Creating the Chat component
Create a new component directory in src and create a Vue component called ChatComponent.vue inside.
In the <script> section of the component, import the ChatKitty service functions and the
vue-advanced-chat package. Register the vue-advanced-chat web component before using it in the <template> section.
- In the
src/components/ChatComponent.vuefile, import the ChatKitty service functions and the vue-advanced-chat package.
<script setup lang="ts">
import * as chatService from '@/chatkitty';
import { register } from 'vue-advanced-chat';
register();
// ... implementation details
</script>
- Define the props for the
ChatComponentcomponent. These props includethemeandusername.
const props = defineProps<{
theme: string;
username: string;
}>();
- Create reactive references for
rooms,roomsLoaded,loadingRooms,messages,messagesLoaded, andcurrentRoom. These references will be used to store and manage the state of the chat component.
const rooms: Ref<any[]> = ref([]);
const roomsLoaded = ref(false);
const loadingRooms = ref(false);
const messages: Ref<any[]> = ref([]);
const messagesLoaded = ref(false);
const currentRoom = ref(null);
- Implement the
setupfunction to authenticate the user and fetch chat rooms.
const setup = async (username: string) => {
await chatService.login(username);
loadingRooms.value = true;
rooms.value = await chatService.fetchRooms();
loadingRooms.value = false;
roomsLoaded.value = true;
};
- Implement the
fetchMessagesfunction to fetch messages for the selected chat room.
const fetchMessages = async ({ room, options = {} }: any) => {
// ... implementation details
};
- Implement the
sendMessagefunction to send a message to the current chat room.
const sendMessage = ({ content }: any) => {
chatService.sendMessage({ room: currentRoom.value, content });
};
- Implement the
tearDownfunction to log out the user and reset the state of the chat component.
const tearDown = async () => {
await chatService.logout();
rooms.value = [];
roomsLoaded.value = false;
loadingRooms.value = false;
messages.value = [];
messagesLoaded.value = false;
};
- Use the
onMountedlifecycle hook to call thesetupfunction when the component is mounted. Also, use thewatchfunction to watch for changes in theusernameprop and update the chat component accordingly.
onMounted(async () => {
await setup(props.username);
watch(
() => props.username,
async (username) => {
await tearDown();
await setup(username);
}
);
});
- Define the template for the
ChatComponentcomponent. Use thevue-advanced-chatcomponent and pass the necessary props and event listeners.
<template>
<vue-advanced-chat
height="calc(100vh - 100px)"
:current-user-id="username"
:theme="theme"
:loading-rooms="loadingRooms"
:rooms-loaded="roomsLoaded"
:messages-loaded="messagesLoaded"
:single-room="false"
:show-search="false"
:show-add-room="false"
:show-files="false"
:show-audio="false"
:show-emojis="false"
:show-reaction-emojis="false"
.rooms="rooms"
.messages="messages"
@fetch-messages="fetchMessages($event.detail[0])"
@send-message="sendMessage($event.detail[0])"
/>
</template>
Refer to the provided code example for the implementation details.
Integrating the chat component into the main app
Open the src/App.vue file and import the ChatComponent Vue component. Add the ChatComponent to the main app template
and pass the necessary props, such as the theme and username.
- Open the
src/App.vuefile and import theChatComponent.
import ChatComponent from './components/ChatComponent.vue';
- Create reactive references for
themeandusername. These references will be used to store the selected theme and username.
const theme = ref('light');
const username = ref(users[Math.floor(Math.random() * users.length
)].username);
- Define an array of users with their usernames and names. This array will be used to populate the user selection dropdown.
const users = [
{
username: 'b2a6da08-88bf-4778-b993-7234e6d8a3ff',
name: 'Joni'
},
{
username: 'abc4264d-f1b1-41c0-b4cc-1e9daadfc893',
name: 'Penelope'
},
{
username: 'c6f75947-af48-4893-a78e-0e0b9bd68580',
name: 'Julie'
},
{
username: '2989c53a-d0c5-4222-af8d-fbf7b0c74ec6',
name: 'Paxton'
},
{
username: '8fadc920-f3e6-49ff-9398-1e58b3dc44dd',
name: 'Zaria'
}
];
You can create chat users for your app using the ChatKitty Platform API
- Define the template for the
App.vuecomponent. Add the user selection dropdown, theme selection buttons, and theChatComponent.
<template>
<div class="app-container" :class="{ 'app-container-dark': theme === 'dark' }">
<span class="user-logged" :class="{ 'user-logged-dark': theme === 'dark' }">
Logged in as
</span>
<select v-model="username">
<option v-for="user in users" :key="user.username" :value="user.username">
{{ user.name }}
</option>
</select>
<div class="button-theme">
<button class="button-light" @click="theme = 'light'">Light</button>
<button class="button-dark" @click="theme = 'dark'">Dark</button>
</div>
<ChatComponent :theme="theme" :username="username" />
</div>
</template>
Refer to the provided code example for the implementation details.
Styling the app
Add custom styling to the src/App.vue file to make the app look more polished. You can use the provided
styling or create your own.
<style lang="css">
body {
margin: 0;
}
input {
-webkit-appearance: none;
}
.app-container {
font-family: 'Quicksand', sans-serif;
padding: 30px;
&.app-container-dark {
background: #131415;
}
}
.user-logged {
font-size: 12px;
margin-right: 5px;
margin-top: 10px;
&.user-logged-dark {
color: #fff;
}
}
select {
height: 20px;
outline: none;
border: 1px solid #e0e2e4;
border-radius: 4px;
background: #fff;
margin-bottom: 20px;
}
.button-theme {
float: right;
display: flex;
align-items: center;
.button-light {
background: #fff;
border: 1px solid #46484e;
color: #46484e;
}
.button-dark {
background: #1c1d21;
border: 1px solid #1c1d21;
}
button {
color: #fff;
outline: none;
cursor: pointer;
border-radius: 4px;
padding: 6px 12px;
margin-left: 10px;
border: none;
font-size: 14px;
transition: 0.3s;
vertical-align: middle;
&.button-github {
height: 30px;
background: none;
padding: 0;
margin-left: 20px;
img {
height: 30px;
}
}
&:hover {
opacity: 0.8;
}
&:active {
opacity: 0.6;
}
@media only screen and (max-width: 768px) {
padding: 3px 6px;
font-size: 13px;
}
}
}
</style>
Conclusion
In this tutorial, we built a simple Vue.js chat app using the ChatKitty API. We created a ChatKitty service to interact with the API, implemented a Chat component that uses the vue-advanced-chat package, and integrated the Chat component into the main app.
Now you have a fully functional chat app that supports direct messaging, group chat, message threads, and more. You can further customize the app by adding more features from the ChatKitty API or by modifying the UI components.
Happy chatting!