Skip to main content

One post tagged with "Vue.js"

View All Tags

· 12 min read

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:

tip

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.

src/utils/dates.ts
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:

src/chatkitty/index.ts
import ChatKitty from '@chatkitty/core';

export const chatkitty = ChatKitty.getInstance('YOUR_CHATKITTY_API_KEY');
note

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()
};
note

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 include theme and username.
const props = defineProps<{
theme: string;
username: string;
}>();
  • Create reactive references for rooms, roomsLoaded, loadingRooms, messages, messagesLoaded, and currentRoom. 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 the setup function when the component is mounted. Also, use the watch function to watch for changes in the username 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 the vue-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>
note

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 the ChatComponent.
import ChatComponent from './components/ChatComponent.vue';
  • Create reactive references for theme and username. 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'
}
];
note

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 the ChatComponent.
<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>
note

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!