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
mapUser
function 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
mapMessage
function 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
mapChannel
function 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
login
function to authenticate a user and start a ChatKitty session.
export const login = async (username: string) => {
await chatkitty.startSession({ username })
};
- Implement the
enterRoom
function 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
exitRoom
function 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
fetchMessages
function 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
fetchRooms
function 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
sendMessage
function 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
logout
function 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.vue
file, 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
ChatComponent
component. These props includetheme
andusername
.
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
setup
function 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
fetchMessages
function to fetch messages for the selected chat room.
const fetchMessages = async ({ room, options = {} }: any) => {
// ... implementation details
};
- Implement the
sendMessage
function to send a message to the current chat room.
const sendMessage = ({ content }: any) => {
chatService.sendMessage({ room: currentRoom.value, content });
};
- Implement the
tearDown
function 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
onMounted
lifecycle hook to call thesetup
function when the component is mounted. Also, use thewatch
function to watch for changes in theusername
prop 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
ChatComponent
component. Use thevue-advanced-chat
component 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.vue
file and import theChatComponent
.
import ChatComponent from './components/ChatComponent.vue';
- Create reactive references for
theme
andusername
. 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.vue
component. 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!