Skip to main content

· 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!

· 11 min read

Geometric pattern

In today's fast-changing world, businesses need to stay on top of their game when it comes to customer engagement. With the rise of AI and large language models, chatbots have become an indispensable tool for businesses looking to improve their customer engagement strategies. However, building and managing chat code can be time-consuming and complicated, which is why we are thrilled to announce our new product that allows businesses to integrate and customize complex chat experiences without any hassle.

ChatKitty, the easiest way to build deploy chat

When we founded ChatKitty in 2020, our goal was to create the easiest way to build chat for all types of businesses. We launched our REST API and JS SDKs to empower developers to build chat for online communication problems facing their users. However, as with all Chat API solutions currently in the market, it takes developers significant work and effort to integrate APIs and SDK function calls into their apps. Developers still need to read and understand deeply technical documentation, write a significant amount of code, and handle complex chat interactions. It also requires a lot of additional effort to integrate third-party functionality like emails, push notifications, SMS, and, yes, conversational AI chat -features that businesses building customer engagement platforms need.

We've begun building a comprehensive plug-and-play product that utilizes new technology to solve this problem in a unique and exciting way. We're building a server-side UI rendering engine that allows us to build UI and push it to user devices as HTML or native device views. Meaning we can build UI once, and our customers can use that UI in their apps without writing a single line of code. The "server-side frontend" product will produce UI for not only web apps but for native iOS and Android apps as well.

Opportunities for customer engagement conversational chat

ChatKitty is a chat company, and we recognize the opportunities AI-powered chat creates for our customers. One area where AI has proven to be particularly useful is in the development of AI assistants. AI assistants are computer programs that use natural language processing and machine learning algorithms to interact with users and provide assistance with various tasks. Here are some of the benefits of integrating AI assistants into applications:

Reading and answering questions

One of the most significant benefits of AI assistants is their ability to read and answer lead and customer inquiries. With advanced natural language processing capabilities, these assistants can understand and respond to user questions in seconds, providing instant solutions to their problems. This functionality is particularly useful in customer service and support, where timely and accurate responses are critical to customer satisfaction.

For example, by integrating a ChatGPT-powered chatbot into our demos, we have improved response times and provided potential customers with faster, more accurate solutions to their queries. Not only does this increase lead conversation, but it also reduces the workload on our lead generation team, allowing them to focus on more complex tasks.

Translation

Another significant benefit of AI assistants is their ability to translate languages. With the global nature of business today, it is essential to communicate with customers and clients in their language. By integrating translation functionality into our applications, we can communicate effectively with customers regardless of their location or language.

Summarization

Businesses can also use AI assistants to summarize large amounts of data quickly and efficiently. This functionality is handy for companies that deal with large volumes of data, such as market research or financial analysis. Automating the summarization process can save valuable time and resources, allowing us to focus on more critical tasks.

Customer support chatbots

Perhaps one of the most significant benefits of AI assistants is their ability to provide customer support through chatbots. Chatbots can handle a wide range of customer inquiries, from basic product information to complex technical support. By automating the customer support process, we can provide 24/7 support, improve response times, and reduce costs.

Because language models ChatGPT can remember and integrate new information without retraining, we can easily customize the models for our specific company and business domain.

Chatbots can provide 24/7 availability, allowing customers to get answers to their questions at any time of day or night, regardless of if human agents are available. By automating responses to common customer inquiries, conversational AI helps reduce the workload of human agents, allowing them to focus on more complex customer issues. Chatbots can provide a cost-effective alternative to hiring and training additional customer service representatives. They can handle multiple customer inquiries simultaneously, reducing the need for additional staff. Ultimately, businesses can provide quick and efficient answers to customer inquiries, increasing customer satisfaction and loyalty.

The current state of AI and large language models

I've been following the development of artificial neural networks for several years now. And the progress made by large language models like ChatGPT, GPT-3, and BERT over the past few years has been quite remarkable.

This breakthrough in the field of natural language processing opens up new opportunities for businesses building interactive user applications. By unlocking new abilities for language translation, chatbots, writing assistance, text summarization, and even code generation, language models like ChatGPT have the potential to transform the way we interact with non-human intelligent systems and reshape our society in ways not seen since the dawn of the internet, and the industrial revolution.

That ChatGPT can generate text so human-like may be unexpected given the way ChatGPT works. Like other language models in the Generative Pre-trained Transformer (GPT) family, ChatGPT uses a Transformer architecture. Transformer neural networks, first described by Google in 2017, are a type of deep learning architecture that has become increasingly popular in natural language processing tasks such as language translation and language modeling. Unlike traditional neural networks that process sequences of input data sequentially, Transformers are designed to process entire sequences of input data in parallel.

Before we can dive into the specifics of Transformers, we'll need to first discuss word embeddings.

Word embeddings

Word embedding is a very interesting concept; it's worth spending a bit of time understanding what word embeddings are and why they are so important for how current state-of-the-art language models work. Before a sequence of text can go through a neural network and be processed, its tokens must be converted to a representation the network is able to understand. Like all neural networks, transformer language models represent inputs, features, and outputs as vectors of numbers (more precisely as arrays of floats). A word embedding is simply an efficient representation of a word or token as a numerical vector of certain length that captures some "meaning" of that word relative to other words in a language. With word embeddings, words that are used in similar contexts are represented closer to each other, than words which tend to used in different contexts.

Each dimension of the vector represents a different feature or aspect of the word. Word embeddings are typically learned from large amounts of text data using algorithms like Word2Vec or GloVe. Once trained, these embeddings can be used as input to neural networks for a variety of natural language processing tasks.

Word embeddings can be used to compare the meaning of words in different contexts, and to identify relationships between words. For example, if we have a vector for "cat" and a vector for "dog", we can measure the distance between these two vectors to determine how similar they are semantically. We can expect that the vectors for "dog" and "cat" be very similar, near a vector for "pet", while the vector for "car" should be quite different. In addition to representing words, word embeddings can also represent phrases, and the relationships between them. This makes it easier for neural networks to learn patterns and relationships between words, and is particularly useful when it comes to natural language processing tasks such as sentiment analysis, question answering, and machine translation.

Note that strictly, GPT models does not deal with words, but rather with tokens, which approximate to words. A token here refers to common sequence of text characters that can easily be assigned a meaning. A token might be a whole word like "cat", or meaningful fragments like "ing", or "ly", or "ed". Tokens make it easier for neural networks to understand compound or non-standard words, and even other languages. After "tokenizing" the input text, an embedding algorithm like Word2Vec then converts these tokens into embeddings.

Transformers

The Transformer was introduced by Vaswani et al. in "Attention Is All You Need" and has quickly became one of the most popular architectures for NLP tasks. Language models are probability distributions over sequences of words (or tokens) in a language. This means their primary function is predicting the likelihood of a particular word being the next word for any sequence in the language.

For a language model to be good at predicting likely next words for a given sequence, it needs to be able to look back in the sequence to remember the context and the semantics of previously seen tokens, in other it needs memory. There are long-term dependencies of the beginning of meaningful sentences, at the end of a sentence. For example, to complete the sentence "Jack came to the pub to have a drink and I talk to ", we'd expect the next word in the sequence to be noun or a pronoun, "him", "her", etc.

However, the relevant piece of information is the word "Jack", all the way at the beginning of the sentence. A good language model needs to be able to remember "Jack", and return a probable next token associated with "Jack" in the language, "Jack came to the pub to have a drink and I talk to him". Without the ability to remember relevant context in a text sequence, the language model is likely to generate sentences that aren't meaningfully although syntactically valid (for example "Jack came to the pub to have a drink and I talk to John").

Attention

In other to learn from massive dataset efficiently, and generate sequences of text, language models need a way of deciding which parts of input sequences are more important, thereby assigning different weights to different parts of the input (as numeric vectors). By assigning greater weight to certain parts of the input, the model can be thought of as "paying attention" to those parts.

The key innovation of the Transformer architecture is its so-called "self-attention" mechanism. Self-attention works by computing a score between every pair of elements in the input sequence. The score is computed by multiplying the vectors representing the two elements. The model then uses the scores to determine which elements it should "pay attention".

In addition to self-attention, the Transformer architecture also uses an encoder-decoder architecture, which enables the model to learn from input sequences of varying lengths. The encoder reads in the input sequence one element at a time and produces a vector representation of the entire sequence. The decoder then uses this vector representation to generate the output.

Screenshot: Transformer architecture

Original Transformer Architecture.
Image credit: Kindra Cooper/"OpenAI GPT-3: Everything You Need to Know"

By using self-attention and an encoder-decoder architecture, Transformer models like ChatGPT are capable of learning from large datasets and generating coherent sequences of text.

Integrating ChatGPT and beyond into our chat platform

Last week, OpenAI introduced APIs for its ChatGPT and Whisper models, giving developers access to its state-of-the-art language and speech-to-text generative AI capabilities. The same day I was able to integrate ChatGPT as an AI assistant into an application powered by ChatKitty.

Screenshot: ChatGPT introduction

Screenshot: ChatGPT french poem

ChatKitty provides Chat Functions, an event-driven serverless framework for extending our platform to handles use-cases that even we can't imagine. With ChatGPT your AI assistant can help your users draft emails, write code, answer questions, translate messages, summarize chats, and much more.

I've written a guide to help developers get started integrating a ChatGPT powered AI assistant into apps.

Summary

I'm excited about the opportunities created by AI-powered chat, for customer engagement and communication. The progress made by large language models using the Transformer architecture like ChatGPT, GPT-3, and BERT is truly remarkable, unlocking new abilities for language translation, chatbots, writing assistance, text summarization, and code generation. Although current large language models are far from perfect, with the help of AI-powered chat conversation, businesses can communicate with customers in a more natural and personalized way, improving engagement and providing better customer service. Issues regard AI safety, potential biases, hallucinations and non-factual results need to be considered, but I'm confident that with continued advances in AI, machine learning and natural language processing, AI-powered chat will be a powerful tool for businesses to engage with customers and increase customer satisfaction.

· 20 min read

Building a Chat App with React Native and Gifted Chat (Part 4)

In this tutorial series, I'll be showing you how to build a functional and secure chat app using the latest React Native libraries, including Gifted Chat and the Expo framework, powered by the ChatKitty platform.


So far you learned how to use the Gifted Chat React Native library with ChatKitty's JavaScript SDK to build a full-featured chat screen with real-time messaging functionality into your app, adding screens for users to create public channels, discover new channels, and view their channels. In the third article of this series, you enhanced that chat experience by implementing in-app and push notifications to notify your users when chat events occur.

In this tutorial, you'll be building on the group chat experience you created adding direct messaging to allow users to communicate privately. You'll also be adding a few enhancements to the chat experience including typing indicators, and chat room presence notifications.

After reading this article, you will be able to:

  1. Create direct channels for users to chat privately

  2. Integrate Gifted Chat's in-built typing indicator and implement a custom more detailed indicator

  3. Notify chat users when a user enters or leaves the chat from a chat screen

  4. Allow users to leave chat channels they are no longer interested in

If you followed along the previous articles, you should already have the ChatKitty JavaScript SDK NPM package added to your Expo React Native project.


Before we begin, let's go over some terms we'll be using a lot in this article.

Direct channels

Previously, we learned about ChatKitty chat channels, specifically public channels that users can discover and join, or be invited to join. Direct channels, on the other hand, let users have private conversations between up to 9 other users. New users cannot be added to a direct channel and there can only exist one direct channel between a set of users. Direct channels are perfect for one-off conversations that don't require an entire channel to discuss.

Entering a chat

When a user starts a chat session and has no other active chat sessions in the session channel, the user has entered a chat. In other words, users who have entered a chat have at least one active chat session for that chat channel, and are active participants of the conversation. Active chat participants receive real-time messaging events, and are present to reply immediately.

Leaving a chat

After a user ends a chat session and has no other active chat sessions in the session channel, the user has left a chat. Users leave a chat when there is no longer at least one active chat session for that chat channel, and are no longer active participants of the conversation. After leaving a chat, users begin to get notifications of events that happened in the chat while they are away.

Leaving a channel

After a user joins a channel, the user becomes a channel member. Channel members can send messages in a channel, and receives messages and notifications related to the channel. If a user is no longer interested in a channel, the user can leave the channel and is no longer a channel member.

Okay, let's get started! 🏎️

Next, you'll be adding direct messaging functionality to your chat app.

Creating a direct messaging channel

Edit the chatScreen.js screen file you previous created to destructure its navigation prop:

export default function ChatScreen({ route, navigation /* Add this */ }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

// Unchanged
}

Now, you can customize your Gifted Chat message avatar to create or get a direct channel, and navigate the user to a new chat screen with the direct channel when it's pressed.

Define a new method renderAvatar to pass into your GiftedChat component:

import { Avatar, Bubble, GiftedChat } from 'react-native-gifted-chat';

// Unchanged

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(avatarUser) => {
chatkitty
.createChannel({
type: 'DIRECT',
members: [{ id: avatarUser._id }]
})
.then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
}}
/>
);
}

Set the GiftedChat renderAvatar prop to the method you defined:

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
renderBubble={renderBubble}
renderAvatar={renderAvatar} /* Add this */
/>
);

After these changes, chatScreen.js should look like this:

import React, { useContext, useEffect, useState } from 'react';
import { Avatar, Bubble, GiftedChat } from 'react-native-gifted-chat';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';
import { AuthContext } from '../context/authProvider';

export default function ChatScreen({ route, navigation }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [loadEarlier, setLoadEarlier] = useState(false);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [messagePaginator, setMessagePaginator] = useState(null);

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
channel: channel,
onMessageReceived: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
}
});

chatkitty
.listMessages({
channel: channel
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setMessagePaginator(result.paginator);
setLoadEarlier(result.paginator.hasNextPage);

setLoading(false);
});

return startChatSessionResult.session.end;
}, [user, channel]);

async function handleSend(pendingMessages) {
await chatkitty.sendMessage({
channel: channel,
body: pendingMessages[0].text
});
};

async function handleLoadEarlier() {
if (!messagePaginator.hasNextPage) {
setLoadEarlier(false);

return;
}

setIsLoadingEarlier(true);

const nextPaginator = await messagePaginator.nextPage();

setMessagePaginator(nextPaginator);

setMessages((currentMessages) =>
GiftedChat.prepend(currentMessages, nextPaginator.items.map(mapMessage))
);

setIsLoadingEarlier(false);
}

function renderBubble(props) {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: '#d3d3d3'
}
}}
/>
);
}

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(avatarUser) => {
chatkitty
.createChannel({
type: 'DIRECT',
members: [{ id: avatarUser._id }]
})
.then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
}}
/>
);
}

if (loading) {
return <Loading />;
}

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
renderBubble={renderBubble}
renderAvatar={renderAvatar}
/>
);
}

function mapMessage(message) {
return {
_id: message.id,
text: message.body,
createdAt: new Date(message.createdTime),
user: mapUser(message.user)
};
}

function mapUser(user) {
return {
_id: user.id,
name: user.displayName,
avatar: user.displayPictureUrl
};
}

If you run the app now and go to a public chat screen, you should see:

Screenshot: Public chat

Tapping a message avatar should take you to a new chat screen where you can have a direct private conversation.

ChatKitty doesn't expose unique names for direct channels, so we don't see a channel name in the app title bar. Let's create an appropriate name for the chat screen title.

Add a helper method channelDisplayName to the index.js file in the src/chatkitty/ directory:

export function channelDisplayName(channel) {
if (channel.type === 'DIRECT') {
return channel.members.map((member) => member.displayName).join(', ');
} else {
return channel.name;
}
}

After this change, index.js should look like this:

import ChatKitty from '@chatkitty/core';

export const chatkitty = ChatKitty.getInstance('YOUR CHATKITTY API KEY HERE');

export function channelDisplayName(channel) {
if (channel.type === 'DIRECT') {
return channel.members.map((member) => member.displayName).join(', ');
} else {
return channel.name;
}
}

You can now update your app to use a more readable channel display name.

Update homeStack.js in src/context to use the channelDisplayName method:

import { chatkitty, channelDisplayName } from '../chatkitty';

// Unchanged

<ChatStack.Screen
name='Chat'
component={ChatScreen}
options={({ route }) => ({
title: channelDisplayName(route.params.channel) /* Add this */
})}
/>;

Also update browseChannelsScreen.js and homeScreen.js in src/screens to use the helper method:

import { chatkitty, channelDisplayName } from '../chatkitty';

// Unchanged

<List.Item
title={channelDisplayName(item)} /* Add this */
// Unchanged
/>;

Running the app now shows the display names of a direct channel's members in the title bar

Screenshot: Direct chat name

Great! Now you can privately chat with other channel members. Now let's move on to enhancing your chat app's experience with a typing indicator.

Adding a typing indicator with Gifted Chat and ChatKitty

The Gifted Chat React Native library saves you a lot of time when creating a chat UI. By providing a bunch of component props, you can customize the chat UI and implement chat features like typing indicators, using a chat service like ChatKitty.

You'll be using the isTyping Gifted Chat prop to display a typing indicator when another user is typing.

Screenshot: Simple typing indicator partial

ChatKitty tracks the typing state of users sending typing keystrokes in a channel. You'll need to send typing keystrokes to ChatKitty to let it know when a user is typing. When starting a chat session, you can register handler methods with ChatKitty to handle chat events like when a user starts or stops typing, enters or leaves a chat, enters keystrokes, etc. You can track when a user starts and stops typing with ChatKitty handler methods, storing the current user typing in your component state. Using the typing user state, you can set Gifted Chat's isTyping prop to display the typing indicator.

Add a helper method handleInputTextChanged in chatScreen.js to send typing keystrokes to ChatKitty, so it can know when a user is typing:

function handleInputTextChanged(text) {
chatkitty.sendKeystrokes({
channel: channel,
keys: text
});
}

Next, on the GiftedChat component, set the onInputTextChanged prop to handleInputTextChanged.

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
onInputTextChanged={handleInputTextChanged} /* Add this */
renderBubble={renderBubble}
renderAvatar={renderAvatar}
/>
);

ChatKitty is now able to know when your users start and stop typing.

Next, define a new state variable to track the current typing user.

export default function ChatScreen({ route, navigation }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [loadEarlier, setLoadEarlier] = useState(false);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [messagePaginator, setMessagePaginator] = useState(null);
const [typing, setTyping] = useState(null); /* Add this */

// Unchanged
}

You can now register chat event handlers to control the typing state. Register both onTypingStarted and onTypingStopped in ChatKitty.startChatSession to set the typing state.

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
channel: channel,
onMessageReceived: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
onTypingStarted: (typingUser) => { /* Add this */
if (typingUser.id !== user.id) {
setTyping(typingUser);
}
},
onTypingStopped: (typingUser) => { /* Add this */
if (typingUser.id !== user.id) {
setTyping(null);
}
}
});

// Unchanged
}, [user, channel]);

typing now holds the current typing user, so you can set the GiftedChat component isTyping prop to typing != null.

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
onInputTextChanged={handleInputTextChanged}
isTyping={typing != null} /* Add this */
renderBubble={renderBubble}
renderAvatar={renderAvatar}
/>
);

After these changes, chatScreen.js should look like this:

import React, { useContext, useEffect, useState } from 'react';
import { Avatar, Bubble, GiftedChat } from 'react-native-gifted-chat';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';
import { AuthContext } from '../context/authProvider';

export default function ChatScreen({ route, navigation }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [loadEarlier, setLoadEarlier] = useState(false);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [messagePaginator, setMessagePaginator] = useState(null);
const [typing, setTyping] = useState(null);

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
channel: channel,
onMessageReceived: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
onTypingStarted: (typingUser) => {
if (typingUser.id !== user.id) {
setTyping(typingUser);
}
},
onTypingStopped: (typingUser) => {
if (typingUser.id !== user.id) {
setTyping(null);
}
}
});

chatkitty
.listMessages({
channel: channel
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setMessagePaginator(result.paginator);
setLoadEarlier(result.paginator.hasNextPage);

setLoading(false);
});

return startChatSessionResult.session.end;
}, [user, channel]);

async function handleSend(pendingMessages) {
await chatkitty.sendMessage({
channel: channel,
body: pendingMessages[0].text
});
}

async function handleLoadEarlier() {
if (!messagePaginator.hasNextPage) {
setLoadEarlier(false);

return;
}

setIsLoadingEarlier(true);

const nextPaginator = await messagePaginator.nextPage();

setMessagePaginator(nextPaginator);

setMessages((currentMessages) =>
GiftedChat.prepend(currentMessages, nextPaginator.items.map(mapMessage))
);

setIsLoadingEarlier(false);
}

function handleInputTextChanged(text) {
chatkitty.sendKeystrokes({
channel: channel,
keys: text
});
}

function renderBubble(props) {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: '#d3d3d3'
}
}}
/>
);
}

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(avatarUser) => {
chatkitty
.createChannel({
type: 'DIRECT',
members: [{ id: avatarUser._id }]
})
.then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
}}
/>
);
}

if (loading) {
return <Loading />;
}

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
onInputTextChanged={handleInputTextChanged}
isTyping={typing != null}
renderBubble={renderBubble}
renderAvatar={renderAvatar}
/>
);
}

function mapMessage(message) {
return {
_id: message.id,
text: message.body,
createdAt: new Date(message.createdTime),
user: mapUser(message.user)
};
}

function mapUser(user) {
return {
_id: user.id,
name: user.displayName,
avatar: user.displayPictureUrl
};
}

If you run your app now on a mobile, you should see a typing indicator when someone else starts typing.

Screenshot: Simple typing indicator

Pretty cool! However, the out-of-the-box Gifted Chat typing indicator has a few limitations. Firstly, the in-built indicator doesn't work on Web, so if you deploy your Expo app on the Web, your users won't see this amazing feature. Secondly, although the in-built indicator lets your user know someone is typing, it doesn't tell your users who is typing. It would be nice if we could see the name of the user typing, since a group chat might have multiple active members possibly typing at a time.

Adding a detailed typing indicator

Gifted Chat lets you add a custom footer to a chat using its renderFooter prop. Let's use this to render a detailed typing status message if a user is currently typing. This footer shows up on Web and gives your users more information.

Screenshot: Detailed typing indicator partial

  • Start by importing StyleSheet and View from react-native, and Text from react-native-paper.

  • Create a helper method renderFooter inside the chatScreen.js component.

  • Define a styles object with styling for the footer <View/> component.

  • Return a <View/> component nesting a <Text/> displaying the typing user if typing with the new styles or null otherwise.

  • Lastly, on the GiftedChat component, set its renderFooter prop to the renderFooter method.

After these changes chatScreen.js should look like this:

import React, { useContext, useEffect, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Avatar, Bubble, GiftedChat } from 'react-native-gifted-chat';
import { Text } from 'react-native-paper';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';
import { AuthContext } from '../context/authProvider';

export default function ChatScreen({ route, navigation }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [loadEarlier, setLoadEarlier] = useState(false);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [messagePaginator, setMessagePaginator] = useState(null);
const [typing, setTyping] = useState(null);

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
channel: channel,
onMessageReceived: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
onTypingStarted: (typingUser) => {
if (typingUser.id !== user.id) {
setTyping(typingUser);
}
},
onTypingStopped: (typingUser) => {
if (typingUser.id !== user.id) {
setTyping(null);
}
}
});

chatkitty
.listMessages({
channel: channel
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setMessagePaginator(result.paginator);
setLoadEarlier(result.paginator.hasNextPage);

setLoading(false);
});

return startChatSessionResult.session.end;
}, [user, channel]);

async function handleSend(pendingMessages) {
await chatkitty.sendMessage({
channel: channel,
body: pendingMessages[0].text
});
}

async function handleLoadEarlier() {
if (!messagePaginator.hasNextPage) {
setLoadEarlier(false);

return;
}

setIsLoadingEarlier(true);

const nextPaginator = await messagePaginator.nextPage();

setMessagePaginator(nextPaginator);

setMessages((currentMessages) =>
GiftedChat.prepend(currentMessages, nextPaginator.items.map(mapMessage))
);

setIsLoadingEarlier(false);
}

function handleInputTextChanged(text) {
chatkitty.sendKeystrokes({
channel: channel,
keys: text
});
}

function renderBubble(props) {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: '#d3d3d3'
}
}}
/>
);
}

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(avatarUser) => {
chatkitty
.createChannel({
type: 'DIRECT',
members: [{ id: avatarUser._id }]
})
.then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
}}
/>
);
}

function renderFooter() {
if (typing) {
return (
<View style={styles.footer}>
<Text>{typing.displayName} is typing</Text>
</View>
);
}

return null;
}

if (loading) {
return <Loading />;
}

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
onInputTextChanged={handleInputTextChanged}
renderBubble={renderBubble}
renderAvatar={renderAvatar}
renderFooter={renderFooter}
/>
);
}

function mapMessage(message) {
return {
_id: message.id,
text: message.body,
createdAt: new Date(message.createdTime),
user: mapUser(message.user)
};
}

function mapUser(user) {
return {
_id: user.id,
name: user.displayName,
avatar: user.displayPictureUrl
};
}

const styles = StyleSheet.create({
footer: {
paddingRight: 10,
paddingLeft: 10,
paddingBottom: 5
}
});

With that done, running the app and typing in a chat as another user shows:

Screenshot: Detailed typing indicator

You now have a typing indicator implemented, making your chat app more immersive, great job! Next, let's continue with the immersion by announcing when other users enter or leave a chat.

Adding chat presence notifications

ChatKitty provides chat session handler methods to handle when users enter and leave a chat. In the previous article, you added Expo notifications to provide push and in-app notifications for your chat app. Let's use this in your chat sessions' onParticipantEnteredChat and onParticipantLeftChat handler methods to notify users when users enter or leave a chat.

In chatScreen.js, let's register chat session handler methods using the notification context sendNotification function we created in part 3 to show a notification when a user enters or leaves the chat.

import { NotificationContext } from '../context/notificationProvider'; // Import notification context

export default function ChatScreen({ route, navigation }) {
const { sendNotification } = useContext(NotificationContext); // Add this

//...

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
//...
onParticipantEnteredChat: (participant) => { /* Add this */
sendNotification({
title: `${participant.displayName} entered the chat`
});
},
onParticipantLeftChat: (participant) => { /* Add this */
sendNotification({
title: `${participant.displayName} left the chat`
});
}
});

// Unchanged...
}

After these changes chatScreen.js should look like this:

import React, { useContext, useEffect, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Avatar, Bubble, GiftedChat } from 'react-native-gifted-chat';
import { Text } from 'react-native-paper';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';
import { AuthContext } from '../context/authProvider';
import { NotificationContext } from '../context/notificationProvider';

export default function ChatScreen({ route, navigation }) {
const { user } = useContext(AuthContext);
const { sendNotification } = useContext(NotificationContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [loadEarlier, setLoadEarlier] = useState(false);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [messagePaginator, setMessagePaginator] = useState(null);
const [typing, setTyping] = useState(null);

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
channel: channel,
onMessageReceived: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
onTypingStarted: (typingUser) => {
if (typingUser.id !== user.id) {
setTyping(typingUser);
}
},
onTypingStopped: (typingUser) => {
if (typingUser.id !== user.id) {
setTyping(null);
}
},
onParticipantEnteredChat: (participant) => {
sendNotification({
title: `${participant.displayName} entered the chat`
});
},
onParticipantLeftChat: (participant) => {
sendNotification({
title: `${participant.displayName} left the chat`
});
}
});

chatkitty
.listMessages({
channel: channel
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setMessagePaginator(result.paginator);
setLoadEarlier(result.paginator.hasNextPage);

setLoading(false);
});

return startChatSessionResult.session.end;
}, [user, channel]);

async function handleSend(pendingMessages) {
await chatkitty.sendMessage({
channel: channel,
body: pendingMessages[0].text
});
}

async function handleLoadEarlier() {
if (!messagePaginator.hasNextPage) {
setLoadEarlier(false);

return;
}

setIsLoadingEarlier(true);

const nextPaginator = await messagePaginator.nextPage();

setMessagePaginator(nextPaginator);

setMessages((currentMessages) =>
GiftedChat.prepend(currentMessages, nextPaginator.items.map(mapMessage))
);

setIsLoadingEarlier(false);
}

function handleInputTextChanged(text) {
chatkitty.sendKeystrokes({
channel: channel,
keys: text
});
}

function renderBubble(props) {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: '#d3d3d3'
}
}}
/>
);
}

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(avatarUser) => {
chatkitty
.createChannel({
type: 'DIRECT',
members: [{ id: avatarUser._id }]
})
.then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
}}
/>
);
}

function renderFooter() {
if (typing) {
return (
<View style={styles.footer}>
<Text>{typing.displayName} is typing</Text>
</View>
);
}

return null;
}

if (loading) {
return <Loading />;
}

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
onInputTextChanged={handleInputTextChanged}
renderBubble={renderBubble}
renderAvatar={renderAvatar}
renderFooter={renderFooter}
/>
);
}

function mapMessage(message) {
return {
_id: message.id,
text: message.body,
createdAt: new Date(message.createdTime),
user: mapUser(message.user)
};
}

function mapUser(user) {
return {
_id: user.id,
name: user.displayName,
avatar: user.displayPictureUrl
};
}

const styles = StyleSheet.create({
footer: {
paddingRight: 10,
paddingLeft: 10,
paddingBottom: 5
}
});

If you run your app now, you should see a notification when another user enters a chat. You should also see a notification when the user leaves the chat.

Pretty cool, right? With a typing indicator and presence notifications your users are now more aware of what other users are doing.

Leaving a channel

If a user is no longer interested in a channel and its discussions, let's give them a way to leave the channel and no longer be a member of that channel. Let's add a long press action to the home screen which when pressed, shows a "leave channel" dialog. React Native Paper provides dialog UI you can use to build the confirmation UI.

Screenshot: Leave channel partial

Edit the homeScreen.js you created earlier in src/screens/ with the following steps:

  • Import Button, Dialog and Portal from react-native-paper
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { Button, Dialog, Divider, List, Portal } from 'react-native-paper'; /* Add this */

  • Add a new state variable to track if the current user wants to leave a channel, storing the selected channel
export default function HomeScreen({ navigation }) {
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(true);
const [leaveChannel, setLeaveChannel] = useState(null); /* Add this */

// Unchanged
}

  • Next create helper methods to handle leaving a selected channel or dismissing the selected channel
function handleLeaveChannel() {
chatkitty.leaveChannel({ channel: leaveChannel }).then(() => {
setLeaveChannel(null);

chatkitty.listChannels({ filter: { joined: true } }).then((result) => {
setChannels(result.paginator.items);
});
});
}

function handleDismissLeaveChannel() {
setLeaveChannel(null);
}

  • Finally, create a <Dialog/> component to prompt the current user for confirmation when leaving a channel, and use the onLongPress flat list item prop to select a channel to leave by setting leaveChannel state
return (
<View style={styles.container}>
<FlatList
data={channels}
keyExtractor={(item) => item.id.toString()}
ItemSeparatorComponent={() => <Divider />}
renderItem={({ item }) => (
<List.Item
title={channelDisplayName(item)}
description={item.type}
titleNumberOfLines={1}
titleStyle={styles.listTitle}
descriptionStyle={styles.listDescription}
descriptionNumberOfLines={1}
onPress={() => navigation.navigate('Chat', { channel: item })}
onLongPress={() => { /* Add this */
setLeaveChannel(item);
}}
/>
)}
/>
// Add this
<Portal>
<Dialog visible={leaveChannel} onDismiss={handleDismissLeaveChannel}>
<Dialog.Title>Leave channel?</Dialog.Title>
<Dialog.Actions>
<Button onPress={handleDismissLeaveChannel}>Cancel</Button>
<Button onPress={handleLeaveChannel}>Confirm</Button>
</Dialog.Actions>
</Dialog>
</Portal>
</View>
);

If you try running your app now, and long press a channel on the home screen, you should see a confirmation dialog asking if you want to leave the channel.

Screenshot: Leave channel

Confirming the dialog prompt should remove the channel from your channels list.

Conclusion

Amazing! You've completed this tutorial series, and successfully created a robust and full-featured Expo React Native chat app using Gifted Chat powered by ChatKitty. By using Firebase and ChatKitty Chat Functions, you were able to provide a simple yet secure login and registration flow for your users. Using the ChatKitty real-time SDK, you saved time and effort building out real-time messaging complete with features like push notifications, typing indicators, and user presence. That's what I call easy development. 😉

What's Next?

In the next post, I'll be starting a new series of articles covering how to build chat for Web React projects using the bleeding edge chatscope chat UI kit. Stay tuned for more. 🔥

Like always, if you have any questions, comments or need help with any part of this article, join our Discord Server where you can ask questions, discuss what you're working on, and I'll be more than happy to help.

You can find the complete source code for this project inside this GitHub repository.

👉 Checkout the other blog posts in this series:


This article contains materials adapted from "Chat app with React Native" by Aman Mittal, originally published at Heartbeat.Fritz.Ai.

This article features an image by Volodymyr Hryshchenko.

· 11 min read

Building a Chat App with React Native and Expo (Part 3)

In this tutorial series, I'll be showing you how to build a functional and secure chat app using the latest React Native libraries, including Gifted Chat and the Expo framework powered by the ChatKitty platform.


In the second article of this series, you learned how to use the Gifted Chat React Native library with ChatKitty's JavaScript SDK to build a full featured chat screen with real-time messaging functionality into your app. You also added screens for users to create public channels, discover new channels, and view their channels.

In this tutorial, you'll be using Expo push notifications and ChatKitty Chat Functions to set up local notifications and push notifications to inform users when new messages are received or relevant actions happen inside a channel and across your app.

You can checkout our Expo React Native sample code any time on GitHub.

After reading this article, you will be able to:

  1. Implement local notifications for users to see what's happening from another screen

  2. Use ChatKitty user properties to store arbitrary data related to your users like expo push tokens

  3. Use Expo push notifications and ChatKitty Chat Functions to implement push notifications

If you followed along the last article, you should already have the ChatKitty JavaScript SDK NPM package added to your Expo React Native project.


Before we begin, let's go over some terms we'll be using a lot in this article.

What are local notifications?

Local notifications are messages that pop up while your app is in-use to inform a user of relevant actions related to another screen in your application from their current screen. ChatKitty sends notifications to your app through the ChatKitty JavaScript SDK. You can listen for these notifications and use them to build in-app notification views.

What are push notifications?

Push notifications are short messages sent to mobile devices to alert a user when something of interests happen, and provide information related to that event even when your app isn't currently in-use. Push notifications are a great way to engage your users and improve your customer experience. Push notifications are a critical part of most chat apps and have traditionally been difficult to implement. However, the Expo framework provides seamless support for push notifications, simplifying the process of send push notifications to your users.

Installing notification libraries

For this project, you'll be using Expo push notifications. So, install the Expo notifications, and other dependency modules you'll need to get expo push tokens which are needed to register user devices for push notifications:

npx expo install expo-device expo-notifications

Let's also install the EAS CLI. EAS Build is a hosted service for building app binaries for your Expo and React Native projects. You will be using it to set up and handle Expo push notification credentials.

npm install -g eas-cli

Setting up Expo push notification credentials

info

Check out the official Expo push notifications guide for more information on setting up Expo push notification as things change,

For iOS, the managed Expo workflow handles push notification credentials automatically when you register your device and run the eas build command. However, for Android you'll need to add an Android app to your Firebase project, update your project, and upload your FCM server credentials to Expo.

Adding Firebase credentials to the app

From the Firebase console side menu, go to your "Project settings".

Screenshot: Firebase project settings

Go to the "Your apps" section and click the Android icon:

Screenshot: Screenshot: Firebase add app

Fill out the application details and register your android app

Screenshot: Screenshot: Firebase create android app register

Download the google-services.json file and add it to your Expo React Native project's root directory

Screenshot: Screenshot: Firebase create android app download

In your app.json inside your project's root directory, add an android.googleServicesFile property with the relative path to the google-services.json file, as well as an android.package property with your app's Android package name:

{
"expo": {
...
"android": {
"package": "com.yourpackage.yourcoolapp",
"googleServicesFile": "./google-services.json"
}
...
}
}

Uploading FCM Server Credentials to Expo

To allow Expo to send push notifications to your Android app, you'll need to upload your FCM server key. Before you can upload your server key to Expo, you'll need to create an Expo account.

To get your FCM server key, go to "Project Settings" section of your Firebase project, then go to the "Cloud Messaging" tab. As you will see, Server Key is only available in Cloud Messaging API (Legacy), which is disabled by default, so you will need to enable it.

Screenshot: Enable Firebase Cloud Messaging API

Once you have enabled this, you can copy the server key listed next to the token.

Screenshot: Firebase Cloud Messaging server key

Now, go to the "Credentials" section under the "Account Settings" option from your Expo account's dashboard side menu, and upload the required credentials to Expo.

To run a local Android build, you will need to run this command as well:

eas build --platform android --local

Getting a user's expo push token

To send a push notification to a user using Expo, we'll need their expo push token. Once we get the expo push token, we can then store it as a ChatKitty user property, so we can access it later in a chat function or on a back-end.

We'll interface with Expo notifications using a new context provider. Inside the src/context/ directory, create a new file notificationProvider.js. We'll define a new context and provider component to register the user's device for push notifications and update the current ChatKitty user's properties to store their Expo device token.

src/context/notificationProvider.js
import { createContext, useEffect, useRef, useState } from 'react';
import { Platform } from 'react-native';

import * as Device from 'expo-device';
import * as Notifications from 'expo-notifications';
import { chatkitty } from '../chatkitty';

Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: false
})
});

export const NotificationContext = createContext({});

export const NotificationProvider = ({ children }) => {
const [notification, setNotification] = useState(null);
const notificationListener = useRef();
const responseListener = useRef();

useEffect(() => {
notificationListener.current =
Notifications.addNotificationReceivedListener((notification) => {
setNotification(notification);
});

responseListener.current =
Notifications.addNotificationResponseReceivedListener((response) => {
console.log(response);
});

return () => {
Notifications.removeNotificationSubscription(
notificationListener.current
);
Notifications.removeNotificationSubscription(responseListener.current);
};
}, []);

return (
<NotificationContext.Provider
value={{
notification,
registerForPushNotifications: async () => {
let token;
if (Device.isDevice) {
const { status: existingStatus } = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
alert('Failed to get push token for push notification!');
return;
}
token = (await Notifications.getExpoPushTokenAsync()).data;
console.log(token);
} else {
alert('Must use physical device for Push Notifications');
}

if (Platform.OS === 'android') {
await Notifications.setNotificationChannelAsync('default', {
name: 'default',
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: '#FF231F7C'
});
}

await chatkitty.updateCurrentUser((user) => {
user.properties = {
...user.properties,
'expo-push-token': token
};
return user;
});
}
}}
>
{children}
</NotificationContext.Provider>
);
};

Later, we'll be updating the notification provider to send local notifications.

To get the notification context inside your app components, wrap the app routes with notification provider.

Edit the src/context/index.js file to wrap the app routes with the notification provider.

The index.js file should now contain:

src/context/index.js
import React from 'react';
import { DefaultTheme, Provider as PaperProvider } from 'react-native-paper';

import { AuthProvider } from './authProvider';
import { NotificationProvider } from './notificationProvider';
import Routes from './routes';

export default function Providers() {
return (
<PaperProvider theme={theme}>
<AuthProvider>
<NotificationProvider>
<Routes />
</NotificationProvider>
</AuthProvider>
</PaperProvider>
);
}

const theme = {
...DefaultTheme,
roundness: 2,
colors: {
...DefaultTheme.colors,
primary: '#5b3a70',
accent: '#50c878',
background: '#f7f9fb'
}
};

Next, update homeStack.js to call registerForPushNotifications from the notification context.

src/context/homeStack.js
import React, { useContext, useEffect } from 'react';
import { NotificationContext } from './notificationProvider';

export default function HomeStack() {
const { registerForPushNotifications } = useContext(NotificationContext);

useEffect(() => {
registerForPushNotifications();
}, []);

// Unchanged
}

With that, you should have the user's expo push token as the expo-push-token user property. With Expo set up, let's create a ChatKitty chat function to use Expo to send a push notification when a ChatKitty notification event happens.

Adding Expo to your Chat Runtime

ChatKitty makes it easy to integrate your back-end and external services like Expo into a ChatKitty application using Chat Functions. Chat Functions let you write arbitrary code that runs any time a relevant event or action happens inside your app. We'll be using a chat function to send a push notification whenever an event occurs that a user should be notified about, and the user isn't online. With ChatKitty, you can use any NPM package inside your Chat Functions as a Chat Runtime dependency.

From your ChatKitty application dashboard, go to the "Functions" page:

Screenshot: ChatKitty side menu functions

Go to the "Runtime" tab and add a new dependency to the Expo Server SDK NPM package, expo-server-sdk. Version 3.7.0 was the latest version as of the time this article was written.

Screenshot: ChatKitty runtime add expo Remember to click the "Save" icon to confirm your chat runtime dependencies changes.

Now we're ready to define a chat function to send a push notification using Expo, whenever a user should be notified about an event, and the user is offline.

Sending push notifications using a chat function

From your ChatKitty application dashboard, go to the "Functions" page and select the "User Received Notification" event chat function:

Screenshot: ChatKitty chat functions

This chat function runs whenever an event a user can be notified about happens. Edit the chat function to send a push notification if the user isn't currently online.

const { Expo } = require('expo-server-sdk');

const expo = new Expo(); // create Expo client

async function handleEvent(
event: UserReceivedNotificationEvent,
context: Context
) {
if (event.userHasActiveSession) return; // skip if this user is online

const expoPushToken = event.user.properties['expo-push-token']; // get the expo push token registered

if (!expoPushToken || !Expo.isExpoPushToken(expoPushToken)) return; // check expo push token is present and valid

const notification = event.notification;

// send push notification with Expo
await expo.sendPushNotificationsAsync([
{
to: expoPushToken,
sound: 'default',
title: notification.title,
body: notification.body,
data: notification.data
}
]);
}

Screenshot: ChatKitty chat function user received notification Remember to click the "Save" icon to confirm your chat function changes.

If you close the app now, and send a message from another device as another user, you should see a push notification:

Screenshot: Push notification

Handling local notifications with Expo

Now that we have Expo push notifications set up, let's also handle local notifications with Expo.

To send local notifications, let's add a sendNotification function to notificationProvider.js that schedules a local Expo notification and export it in the notification context:

src/context/notificationProvider.js
export const NotificationProvider = ({ children }) => {
// Unchanged...

return (
<NotificationContext.Provider
value={{
notification,
sendNotification: async (content) => {
await Notifications.scheduleNotificationAsync({
content,
trigger: null
});
},
registerForPushNotifications: async () => { /* Unchanged */}
}}
>
{children}
</NotificationContext.Provider>
);
};

Next, in homeStack.js register a ChatKitty onNotificationReceived event listener in the useEffect React hook to show received in-app notifications:

src/context/homeStack.js
import {chatkitty} from '../chatkitty';

export default function HomeStack() {
const {registerForPushNotifications, sendNotification} = useContext(NotificationContext);

useEffect(()=> {
registerForPushNotifications();

chatkitty.onNotificationReceived(async (notification) => {
await sendNotification({
title: notification.title,
body: notification.body
});
});
}, []);

// Unchanged...
}

Conclusion

Pretty cool, you've completed the third part of this tutorial series and successfully implemented push notifications, using the Expo framework and ChatKitty Chat Functions. You've also implemented local notifications that seamlessly inform your users when something they care about happens. Your users are now always in the loop.

What's next?

In the next post of this series, we'll be enhancing your chat app's user experience with direct messaging, typing indicators, and chat presence notifications. Stay tuned for more. 🔥

If you have any questions, comments or need help with any part of this article, join our Discord Server where you can ask questions, discuss what you're working on, and I'll be more than happy to help.

You can find the complete source code for this project inside this GitHub repository.

👉 Checkout the other blog posts in this series:


This article contains materials adapted from "Chat app with React Native" by Aman Mittal, originally published at Heartbeat.Fritz.Ai.

This article features an image by Volodymyr Hryshchenko.

· 18 min read

Building a Chat App with React Native and Gifted Chat (Part 2)

In this tutorial series , I'll be showing you how to build a functional and secure chat app using the latest React Native libraries, including Gifted Chat and the Expo framework powered by the ChatKitty platform.


In the first article of this series, you learned how to use Firebase along with ChatKitty Chat Functions to implement a secure yet simple user login flow by proxying Firebase Authentication through ChatKitty. Along with that, you built a couple of screens with the react-native-paper UI library to allow users to register for your chat app and login into the app.

In this tutorial, you'll be using the Gifted Chat React Native library to create a full-featured chat screen with its out of the box features. You'll also use ChatKitty's JavaScript Chat SDK to add real-time messaging to your chat app.

After reading this article, you will be able to:

  1. Create public channels for users to join

  2. View all channels a user can join and discover channels created by other users

  3. Integrate the react-native-gifted-chat library to implement a group chat screen

You can check out our Expo React Native sample code any time on GitHub.

If you followed along the last article, you should already have the ChatKitty JavaScript SDK NPM package added to your Expo React Native project.


Before we begin, let's go over some terms we'll be using a lot in this article.

What are channels?

Channels are the backbone of the ChatKitty chat experience. Users can join channels and receive or send messages. ChatKitty broadcasts messages created in channels to channel member users with active chat sessions and sends notifications to offline members.

What are chat sessions?

Before a user can begin sending and receiving real-time messages and use in-app chat features like typing indicators, delivery and read receipts, live reactions, etc, their device needs to start a chat session. A user device can start up to 10 chat sessions at a time but usually have only a maximum of one active at a time. You can think of an active chat session as corresponding to being in a "chat room", when the user "leaves" the chat room, its chat session ends.

With that, you have all the information you need build to chat into your app.

Let's go! 🏎️

First, you'll start by creating a screen that shows a list of channels a user can chat in after logging in.

Displaying a user's channels

Start by changing the homeScreen.js you previously created to list the channels a logged-in user is a member of.

The homeScreen.js file should contain:

src/screens/homeScreen.js
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { Divider, List } from 'react-native-paper';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';

export default function HomeScreen() {
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(true);

const isFocused = useIsFocused();

useEffect(() => {
let isCancelled = false;

chatkitty.listChannels({ filter: { joined: true } }).then((result) => {
if (!isCancelled) {
setChannels(result.paginator.items);

if (loading) {
setLoading(false);
}
}
});

return () => {
isCancelled = true;
};
}, [isFocused, loading]);

if (loading) {
return <Loading />;
}

return (
<View style={styles.container}>
<FlatList
data={channels}
keyExtractor={(item) => item.id.toString()}
ItemSeparatorComponent={() => <Divider />}
renderItem={({ item }) => (
<List.Item
title={item.name}
description={item.type}
titleNumberOfLines={1}
titleStyle={styles.listTitle}
descriptionStyle={styles.listDescription}
descriptionNumberOfLines={1}
onPress={() => {
// TODO navigate to a chat screen.
}}
/>
)}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1
},
listTitle: {
fontSize: 22
},
listDescription: {
fontSize: 16
}
});

If you run the app and log in now, it shouldn't look like much.

Screenshot: Home screen empty

Pretty empty, huh? Soon you'll create a screen responsible for creating new channels, so the home screen can be populated.

Creating shared header components and a modal stack navigator

Before we create the CreateChannel screen, we should modify the app header bar to share options across different screens. The CreateChannel screen will be implemented as a modal, so we'll also need a separate stack navigator to wrap the home stack navigator and handle modals. Modals are screens that block interactions with the main view when displaying their content.

Modify your homeStack.js file in the src/context/ directory to apply header bar options across screens and define a stack navigator for app modal screens.

The homeStack.js file should contain:

src/context/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

import HomeScreen from '../screens/homeScreen';

const ChatStack = createStackNavigator();
const ModalStack = createStackNavigator();

export default function HomeStack() {
return (
<ModalStack.Navigator screenOptions={{
headerShown: false,
presentation: 'modal'
}}>
<ModalStack.Screen name='ChatApp' component={ChatComponent} />
</ModalStack.Navigator>
);
}

function ChatComponent() {
return (
<ChatStack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#5b3a70'
},
headerTintColor: '#ffffff',
headerTitleStyle: {
fontSize: 22
}
}}
>
<ChatStack.Screen name='Home' component={HomeScreen} />
</ChatStack.Navigator>
);
}

Creating a channel creation screen

Now we can create a new screen file createChannelScreen.js inside the src/screens/ directory. From this screen, users will create new public channels other users can join and chat in.

The createChannelScreen.js file should contain:

src/screens/createChannelScreen.js
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { IconButton, Title } from 'react-native-paper';

import { chatkitty } from '../chatkitty';
import FormButton from '../components/formButton';
import FormInput from '../components/formInput';

export default function CreateChannelScreen({ navigation }) {
const [channelName, setChannelName] = useState('');

function handleButtonPress() {
if (channelName.length > 0) {
chatkitty
.createChannel({
type: 'PUBLIC',
name: channelName
})
.then(() => navigation.navigate('Home'));
}
}

return (
<View style={styles.rootContainer}>
<View style={styles.closeButtonContainer}>
<IconButton
icon='close-circle'
size={36}
iconColor='#5b3a70'
onPress={() => navigation.goBack()}
/>
</View>
<View style={styles.innerContainer}>
<Title style={styles.title}>Create a new channel</Title>
<FormInput
labelName='Channel Name'
value={channelName}
onChangeText={(text) => setChannelName(text)}
clearButtonMode='while-editing'
/>
<FormButton
title='Create'
modeValue='contained'
labelStyle={styles.buttonLabel}
onPress={() => handleButtonPress()}
disabled={channelName.length === 0}
/>
</View>
</View>
);
}

const styles = StyleSheet.create({
rootContainer: {
flex: 1
},
closeButtonContainer: {
position: 'absolute',
top: 30,
right: 0,
zIndex: 1
},
innerContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
title: {
fontSize: 24,
marginBottom: 10
},
buttonLabel: {
fontSize: 22
}
});

Okay, let's test the CreateChannel screen by adding a temporary button to open the screen in our home screen header bar, and creating a new channel.

The homeStack.js file should contain:

src/context/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { IconButton } from 'react-native-paper';

import CreateChannelScreen from '../screens/createChannelScreen';
import HomeScreen from '../screens/homeScreen';

const ChatStack = createStackNavigator();
const ModalStack = createStackNavigator();

export default function HomeStack() {
return (
<ModalStack.Navigator screenOptions={{
headerShown: false,
presentation: 'modal'
}}>
<ModalStack.Screen name='ChatApp' component={ChatComponent} />
<ModalStack.Screen name='CreateChannel' component={CreateChannelScreen} />
</ModalStack.Navigator>
);
}

function ChatComponent() {
return (
<ChatStack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#5b3a70'
},
headerTintColor: '#ffffff',
headerTitleStyle: {
fontSize: 22
}
}}
>
<ChatStack.Screen
name='Home'
component={HomeScreen}
options={({ navigation }) => ({
headerRight: () => (
<IconButton
icon='plus'
size={28}
iconColor='#ffffff'
onPress={() => navigation.navigate('CreateChannel')}
/>
)
})}
/>
</ChatStack.Navigator>
);
}

If you run the app now, you should see a plus icon in the header bar:

Screenshot: Home screen add

Tap the button and create a new channel:

Screenshot: Create channel screen

Tap "Create", and you should be redirected back to the home screen with your new channel:

Screenshot: Home screen added

You now have a channel to send messages, receive messages and chat in. Next, let's get started building a channel chat screen with the react-native-gifted-chat library.

Creating a chat screen

Gifted Chat is an open-source React Native library that saves you tons of time and development effort building chat UIs. The library is extensible and customizable with a large online community making it a great option to build chat.

To use Gifted Chat, add its NPM package to your Expo React Native project:

npx expo install react-native-gifted-chat

Next, create a file chatScreen.js inside the src/screens/ directory. This screen will render a chat screen for users to send new messages and view messages they've sent and received. We'll be updating this screen throughout the rest of this tutorial series to add more advanced and sophisticated chat features.

chatScreen.js will need quite a few things, so let's break down what you'll be doing:

  • Import GiftedChat since we need a GiftedChat component to add the chat UI and functionality.

  • Retrieve the current user from our authentication context, so we can show messages as created by the current user and perform other current user specific functions.

  • Retrieve the channel to start this chat with using the route props.

  • Create a ChatScreen functional React component, and inside it define a messages state variable. This array will hold message data objects representing the chat message history. This variable is initially an empty array.

  • Define a couple of helper functions mapUser and mapMessage to map the current user and message objects we get from ChatKitty into a schema Gifted Chat recognizes.

  • Use an useEffect React hook to start a new chat session with the ChatScreen channel using the ChatKitty startChatSession function. Register an onMessageReceived function that appends new messages received from ChatKitty into the existing Gifted Chat managed messages, and replace the messages state when a new message is received. After starting the chat session, fetch the channel's last messages using listMessages then replace the messages state. As part of cleaning up when the component is about to be destroyed, return the ChatSession's end function to the useEffect function to end the chat session and free up ChatKitty resources.

  • Define a helper function handleSend, to send a new message using the ChatKitty sendMessage function.

  • Return to be rendered a GiftedChat with the messages state, a mapped GiftedChat current user chatUser, and the handleSend helper function.

The chatScreen.js file should contain:

src/screens/chatScreen.js
import React, { useContext, useEffect, useState } from 'react';
import { Bubble, GiftedChat } from 'react-native-gifted-chat';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';
import { AuthContext } from '../context/authProvider';

export default function ChatScreen({ route }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
channel: channel,
onMessageReceived: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
}
});

chatkitty
.listMessages({
channel: channel
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setLoading(false);
});

return startChatSessionResult.session.end;
}, [user, channel]);

async function handleSend(pendingMessages) {
await chatkitty.sendMessage({
channel: channel,
body: pendingMessages[0].text
});
}

function renderBubble(props) {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: '#d3d3d3'
}
}}
/>
);
}

if (loading) {
return <Loading />;
}

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
renderBubble={renderBubble}
/>
);
}

function mapMessage(message) {
return {
_id: message.id,
text: message.body,
createdAt: new Date(message.createdTime),
user: mapUser(message.user)
};
}

function mapUser(user) {
return {
_id: user.id,
name: user.displayName,
avatar: user.displayPictureUrl
};
}

Now, let's add the Chat screen to the home stack navigator. Edit homeStack.js in src/context/ with a new screen entry.

The homeStack.js file should contain:

src/context/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { IconButton } from 'react-native-paper';

import ChatScreen from '../screens/chatScreen';
import CreateChannelScreen from '../screens/createChannelScreen';
import HomeScreen from '../screens/homeScreen';

const ChatStack = createStackNavigator();
const ModalStack = createStackNavigator();

export default function HomeStack() {
return (
<ModalStack.Navigator screenOptions={{
headerShown: false,
presentation: 'modal'
}}>
<ModalStack.Screen name='ChatApp' component={ChatComponent} />
<ModalStack.Screen name='CreateChannel' component={CreateChannelScreen} />
</ModalStack.Navigator>
);
}

function ChatComponent() {
return (
<ChatStack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#5b3a70'
},
headerTintColor: '#ffffff',
headerTitleStyle: {
fontSize: 22
}
}}
>
<ChatStack.Screen
name='Home'
component={HomeScreen}
options={({ navigation }) => ({
headerRight: () => (
<IconButton
icon='plus'
size={28}
iconColor='#ffffff'
onPress={() => navigation.navigate('CreateChannel')}
/>
)
})}
/>
<ChatStack.Screen
name='Chat'
component={ChatScreen}
options={({ route }) => ({
title: route.params.channel.name
})}
/>
</ChatStack.Navigator>
);
}

Before we can begin chatting, you'll need to update the homeScreen.js component to redirect to a Channel screen.

The homeScreen.js file should contain:

src/screens/homeScreen.js
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { Divider, List } from 'react-native-paper';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';

export default function HomeScreen({ navigation }) {
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(true);

const isFocused = useIsFocused();

useEffect(() => {
let isCancelled = false;

chatkitty.listChannels({ filter: { joined: true } }).then((result) => {
if (!isCancelled) {
setChannels(result.paginator.items);

if (loading) {
setLoading(false);
}
}
});

return () => {
isCancelled = true;
};
}, [isFocused, loading]);

if (loading) {
return <Loading />;
}

return (
<View style={styles.container}>
<FlatList
data={channels}
keyExtractor={(item) => item.id.toString()}
ItemSeparatorComponent={() => <Divider />}
renderItem={({ item }) => (
<List.Item
title={item.name}
description={item.type}
titleNumberOfLines={1}
titleStyle={styles.listTitle}
descriptionStyle={styles.listDescription}
descriptionNumberOfLines={1}
onPress={() => navigation.navigate('Chat', { channel: item })}
/>
)}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1
},
listTitle: {
fontSize: 22
},
listDescription: {
fontSize: 16
}
});

If you run the app, you should now be able to send messages in the channel you created:

Screenshot: Channel chat screen sent message

Awesome! You've successfully added chat into your app. However, before we bring out the champagne there's a couple of things we should do. Firstly, we're currently only getting a slice of the channel message history, as the ChatKitty API paginates all collections. Currently, if the channel had more than the default messages page size (25 items) messages, then the messages response would be truncated and messages older than the 25th message would not be returned.

Screenshot: Channel chat screen no pagination

We can't see messages before 25.

Secondly, although it's nice we can chat with ourselves, it'll be really cool if users can find channels created by other users, or by your backend that they have permission to join. We can create a browse channel screen for users to see public channels created by other users.

Loading earlier messages

To load older messages in pages before the last channel page, we can use the paginator object returned with the ChatKitty listMessages result to fetch more pages and prepend the messages fetched into our messages collection.

// Fetching a next page:

const result = chatkitty.listMessages({ channel: channel });

const paginator = result.paginator; // paginator from result

if (paginator.hasNextPage) { // check if there are more pages
const nextPaginator = await messagePaginator.nextPage();

const nextMessages = nextPaginator.items;
}

Now, let's use paginators to load more messages into our chat. Edit the chatScreen.js file in src/screens/ to load more messages using Gifted Chat.

The chatScreen.js file should contain:

src/screens/chatScreen.js
import React, { useContext, useEffect, useState } from 'react';
import { Bubble, GiftedChat } from 'react-native-gifted-chat';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';
import { AuthContext } from '../context/authProvider';

export default function ChatScreen({ route }) {
const { user } = useContext(AuthContext);
const { channel } = route.params;

const [messages, setMessages] = useState([]);
const [loading, setLoading] = useState(true);
const [loadEarlier, setLoadEarlier] = useState(false);
const [isLoadingEarlier, setIsLoadingEarlier] = useState(false);
const [messagePaginator, setMessagePaginator] = useState(null);

useEffect(() => {
const startChatSessionResult = chatkitty.startChatSession({
channel: channel,
onMessageReceived: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
}
});

chatkitty
.listMessages({
channel: channel
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setMessagePaginator(result.paginator);
setLoadEarlier(result.paginator.hasNextPage);

setLoading(false);
});

return startChatSessionResult.session.end;
}, [user, channel]);

async function handleSend(pendingMessages) {
await chatkitty.sendMessage({
channel: channel,
body: pendingMessages[0].text
});
};

async function handleLoadEarlier() {
if (!messagePaginator.hasNextPage) {
setLoadEarlier(false);

return;
}

setIsLoadingEarlier(true);

const nextPaginator = await messagePaginator.nextPage();

setMessagePaginator(nextPaginator);

setMessages((currentMessages) =>
GiftedChat.prepend(currentMessages, nextPaginator.items.map(mapMessage))
);

setIsLoadingEarlier(false);
}

function renderBubble(props) {
return (
<Bubble
{...props}
wrapperStyle={{
left: {
backgroundColor: '#d3d3d3'
}
}}
/>
);
}

if (loading) {
return <Loading />;
}

return (
<GiftedChat
messages={messages}
onSend={handleSend}
user={mapUser(user)}
loadEarlier={loadEarlier}
isLoadingEarlier={isLoadingEarlier}
onLoadEarlier={handleLoadEarlier}
renderBubble={renderBubble}
/>
);
}

function mapMessage(message) {
return {
_id: message.id,
text: message.body,
createdAt: new Date(message.createdTime),
user: mapUser(message.user)
};
}

function mapUser(user) {
return {
_id: user.id,
name: user.displayName,
avatar: user.displayPictureUrl
};
}

If you run the app now, you should see an option to load more messages if you have a lot of messages.

Screenshot: Channel chat screen paginated

If you tap it, the next set of messages loads, repeatable until the beginning of the conversation.

Screenshot: Channel chat screen paginated loaded more

Much better. Now, let's provide a screen for users to discover new channels.

Creating a browse channels screen

The browse channels screen is going to be very similar to the home screen, but instead of listing channels a user is already a member of, it will list channels a user can join.

Create a new file browseChannelsScreen.js in src/screens/. This component will display a list of joinable channels from ChatKitty.

The browseChannelsScreen.js file should contain:

src/screens/browseChannelsScreen.js
import { useIsFocused } from '@react-navigation/native';
import React, { useEffect, useState } from 'react';
import { FlatList, StyleSheet, View } from 'react-native';
import { Divider, List } from 'react-native-paper';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';

export default function BrowseChannelsScreen({ navigation }) {
const [channels, setChannels] = useState([]);
const [loading, setLoading] = useState(true);

const isFocused = useIsFocused();

useEffect(() => {
chatkitty.listChannels({ filter: { joined: false } }).then((result) => {
setChannels(result.paginator.items);

if (loading) {
setLoading(false);
}
});
}, [isFocused, loading]);

async function handleJoinChannel(channel) {
const result = await chatkitty.joinChannel({ channel: channel });

navigation.navigate('Chat', { channel: result.channel });
}

if (loading) {
return <Loading />;
}

return (
<View style={styles.container}>
<FlatList
data={channels}
keyExtractor={(item) => item.id.toString()}
ItemSeparatorComponent={() => <Divider />}
renderItem={({ item }) => (
<List.Item
title={item.name}
description={item.type}
titleNumberOfLines={1}
titleStyle={styles.listTitle}
descriptionStyle={styles.listDescription}
descriptionNumberOfLines={1}
onPress={() => handleJoinChannel(item)}
/>
)}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1
},
listTitle: {
fontSize: 22
},
listDescription: {
fontSize: 16
}
});

Next, let's add our new screen to our home stack. Open the homeStack.js file in src/context/ and add the BrowseChannels screen. Also, let's make it so that clicking the "Add" icon button from the home screen takes us to the BrowseChannels screen. Then, let's create another "Add" icon button for the BrowseChannel screen that now opens the CreateChannel screen.

The homeStack.js file should contain:

src/context/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { IconButton } from 'react-native-paper';

import BrowseChannelsScreen from '../screens/browseChannelsScreen';
import ChatScreen from '../screens/chatScreen';
import CreateChannelScreen from '../screens/createChannelScreen';
import HomeScreen from '../screens/homeScreen';

const ChatStack = createStackNavigator();
const ModalStack = createStackNavigator();

export default function HomeStack() {
return (
<ModalStack.Navigator screenOptions={{ headerShown: false, presentation: 'modal' }}>
<ModalStack.Screen name='ChatApp' component={ChatComponent} />
<ModalStack.Screen name='CreateChannel' component={CreateChannelScreen} />
</ModalStack.Navigator>
);
}

function ChatComponent() {
return (
<ChatStack.Navigator
screenOptions={{
headerStyle: {
backgroundColor: '#5b3a70'
},
headerTintColor: '#ffffff',
headerTitleStyle: {
fontSize: 22
}
}}
>
<ChatStack.Screen
name='Home'
component={HomeScreen}
options={({ navigation }) => ({
headerRight: () => (
<IconButton
icon='plus'
size={28}
iconColor='#ffffff'
onPress={() => navigation.navigate('BrowseChannels')}
/>
)
})}
/>
<ChatStack.Screen
name='BrowseChannels'
component={BrowseChannelsScreen}
options={({ navigation }) => ({
headerRight: () => (
<IconButton
icon='plus'
size={28}
iconColor='#ffffff'
onPress={() => navigation.navigate('CreateChannel')}
/>
)
})}
/>
<ChatStack.Screen
name='Chat'
component={ChatScreen}
options={({ route }) => ({
title: route.params.channel.name
})}
/>
</ChatStack.Navigator>
);
}

Let's test the browse channels functionality by logging in as another user.

Screenshot: Login

After logging in, if you navigate to the browse screen using the "Add" icon button, you should see the channel created earlier by the other user.

Screenshot: Browse channels screen

Let's say hello!

Screenshot: Channel chat screen another user

Conclusion

Amazing, you've completed the second part of this tutorial series and made your chat app even better. By implementing screens and functionality to allow users to create, discover and join channels, your users can beginning having meaningful conversations. You also used the Gifted Chat React Native library and ChatKitty to build a chat screen to send and receive real-time messages in minutes. It doesn't get much easier than that. Congratulations! 🍾

What's next?

In the next post of this series, we'll be handling what happens when a user is away from a chat screen or offline. We'll be using in-app notifications to inform a user when a new message is received, or relevant action happens in a channel they've joined. We'll also be using Expo push notifications to inform users about new messages when they're not connected to your app and are offline. Stay tuned for more. 🔥

Like always, if you have any questions, comments or need help with any part of this article, join our Discord Server where you can ask questions, discuss what you're working on, and I'll be more than happy to help.

You can find the complete source code for this project inside this GitHub repository.

👉 Checkout the other blog posts in this series:


This article contains materials adapted from "Chat app with React Native" by Aman Mittal, originally published at Heartbeat.Fritz.Ai.

This article features an image by Volodymyr Hryshchenko.

· 26 min read

Building a Chat App with React Native and Firebase

In this tutorial series, I'll be showing you how to build a functional and secure chat app using the latest React Native libraries, the Expo framework, and Firebase, powered by the ChatKitty platform. Part 1 covers using Firebase Authentication and ChatKitty Chat Functions to securely implement user registration and login.


This is the first article of this series. After reading this article, you will be able to:

  1. Create an Expo React Native application

  2. Create a Firebase project for user authentication

  3. Create a ChatKitty project and connect to ChatKitty to provide real-time chat functionality

  4. Use Firebase Authentication and ChatKitty Chat Functions to securely implement user login

What is React Native?

React Native is a great way to develop both web and mobile applications very quickly, while sharing a lot of code when targeting multiple platforms. With a mature ecosystem of libraries and tooling, using React Native is not only fast but also reliable. Trusted by organizations like Facebook, Shopify, and Tesla - React Native is a stable framework for building both iOS and Android apps.

What is Expo?

The Expo framework builds on top of React Native to allow developers to build universal React applications in minutes. With Expo, you can develop, build, deploy and quickly iterate on iOS, Android and web apps from the same JavaScript code. Expo has made creating both web and mobile applications very accessible, handling would-be complex workflows like multi-platform deployment and advanced features like push notifications.

What is Firebase?

Firebase is a Backend-as-a-Service offering by Google. It provides developers a wide array of tools and services to develop quality apps without having to manage servers. Firebase provides key features like authentication, a real-time database, and hosting.

What are ChatKitty Chat Functions?

ChatKitty provides Chat Functions, serverless cloud functions that allow you to define custom logic for complex tasks like user authentication, and respond to chat events that happen within your application. With ChatKitty Chat Functions, there's no need for you to build a backend to develop chat apps. ChatKitty Chat Functions auto-scale for you, and only cost you when they run. Chat Functions lower the total cost of maintaining your chat app, enabling you to build more logic, faster.

Prerequisites

To develop apps with Expo and React Native, you should be able to write and understand JavaScript or TypeScript code. To define ChatKitty Chat Functions, you'll need to be familiar with basic JavaScript.

You'll need a version of Node.js above 10.x.x installed on your local machine to build this React Native app.

You'll use the Expo CLI tool through npx.

For a complete walk-through on how to set up a development environment for Expo, you can go through the official documentation here.

You can check out our Expo React Native sample code any time on GitHub.

Now that you have all the needed background information, let's get started.

Creating project and installing libraries

First, initialize a new Expo project with the blank managed workflow. To do so, you're going to need to open a terminal window and execute:

npx create-expo-app chatkitty-example-react-native

After creating the initial application. You can enter the app root directory and run the app:

# navigate inside the project directory
cd chatkitty-example-react-native

# for android
npx expo start --android

# for ios
npx expo start --ios

# for web
npx expo start --web

If you run your newly created React Native application using Expo, you should see:

Screenshot: Created Project

Now that we have our blank project, we can install the React Native libraries we'll need:

# install following libraries for React Native
npx expo install @react-navigation/native @react-navigation/stack react-native-reanimated react-native-gesture-handler react-native-screens react-native-safe-area-context @react-native-community/masked-view react-native-paper react-native-vector-icons @react-native-async-storage/async-storage firebase

Creating reusable form elements

We'll be creating Login and Signup screens soon which share similar logic. To prevent us from violating the DRY principle, let's create some reusable form components that we can share across these two screens. We'll also create a loading spinner component to provide a good user experience whenever a user waits for a long screen transition.

We'll create reusable FormInput, FormButton, and Loading UI components.

At the root of your Expo React Native app, create a src/ directory and inside it create another new components/ directory.

Inside the src/components/ directory, create a new JavaScript file formInput.js. In this file, we'll define a React component to provide a text input field for our Login and Signup screens to use for the user to enter their credentials.

The formInput.js file should contain the following code snippet:

src/components/formInput.js
import React from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import { TextInput } from 'react-native-paper';

const { width, height } = Dimensions.get('screen');

export default function FormInput({ labelName, ...rest }) {
return (
<TextInput
label={labelName}
style={styles.input}
numberOfLines={1}
{...rest}
/>
);
}

const styles = StyleSheet.create({
input: {
marginTop: 10,
marginBottom: 10,
width: width / 1.5,
height: height / 15
}
});

Our next reusable component is going to be in another file formButton.js. We'll use it to display a button for a user to confirm their credentials.

The formButton.js file should contain:

src/components/formButton.js
import React from 'react';
import { Dimensions, StyleSheet } from 'react-native';
import { Button } from 'react-native-paper';

const { width, height } = Dimensions.get('screen');

export default function FormButton({ title, modeValue, ...rest }) {
return (
<Button
mode={modeValue}
{...rest}
style={styles.button}
contentStyle={styles.buttonContainer}
>
{title}
</Button>
);
}

const styles = StyleSheet.create({
button: {
marginTop: 10
},
buttonContainer: {
width: width / 2,
height: height / 15
}
});

Finally, create a loading.js file. We'll use it to display a loading spinner when a user has to wait for a screen transition.

The loading.js file should contain:

src/components/loading.js
import React from 'react';
import { ActivityIndicator, StyleSheet, View } from 'react-native';

export default function Loading() {
return (
<View style={styles.loadingContainer}>
<ActivityIndicator size='large' color='#5b3a70' />
</View>
);
}

const styles = StyleSheet.create({
loadingContainer: {
flex: 1,
alignItems: 'center',
justifyContent: 'center'
}
});

Now that we have our reusable form components, we can create a login screen for users to enter our chat app.

Creating a login screen

The first screen we'll be creating is the login screen. We will ask an existing user for their email and password to authenticate, and provide a link to a sign-up form for new users to register with our app.

The login screen should look like this after you're done:

Screenshot: Login screen

Inside the src/, create a screens/ directory; inside this directory create a loginScreen.js file.

The loginScreen.js file should contain:

src/screens/loginScreen.js
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Title } from 'react-native-paper';

import FormButton from '../components/formButton.js';
import FormInput from '../components/formInput.js';

export default function LoginScreen({ navigation }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

return (
<View style={styles.container}>
<Title style={styles.titleText}>Welcome!</Title>
<FormInput
labelName='Email'
value={email}
autoCapitalize='none'
onChangeText={(userEmail) => setEmail(userEmail)}
/>
<FormInput
labelName='Password'
value={password}
secureTextEntry={true}
onChangeText={(userPassword) => setPassword(userPassword)}
/>
<FormButton
title='Login'
modeValue='contained'
labelStyle={styles.loginButtonLabel}
onPress={() => {
// TODO
}}
/>
<FormButton
title='Sign up here'
modeValue='text'
uppercase={false}
labelStyle={styles.navButtonText}
onPress={() => navigation.navigate('Signup')}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
titleText: {
fontSize: 24,
marginBottom: 10
},
loginButtonLabel: {
fontSize: 22
},
navButtonText: {
fontSize: 16
}
});

Later, you'll hook up this login screen to ChatKitty to log in users into your app. We've also configured the navigation prop to navigate the user to the signup screen you'll soon be creating. For now, let's add a stack navigator to direct users to the initial login route.

Create a new directory src/context/. This will contain all the routes and components needed to build the app's navigation as well as context providers using the React Context API. Inside this directory, create a authStack.js file that defines a stack navigator for our authentication screens.

The authStack.js file should contain:

src/context/authStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

import LoginScreen from '../screens/loginScreen';

const Stack = createStackNavigator();

export default function AuthStack() {
return (
<Stack.Navigator initialRouteName='Login' screenOptions={{ headerShown: false }}>
<Stack.Screen name='Login' component={LoginScreen} />
</Stack.Navigator>
);
}

Later, we'll be adding another route for the Signup screen to our navigator.

Next, you'll need a navigation container to hold the app's stacks, starting with the auth stack. Create a routes.js file inside the src/context/ directory.

The routes.js file should contain:

src/context/routes.js
import { NavigationContainer } from '@react-navigation/native';
import React from 'react';

import AuthStack from './authStack';

export default function Routes() {
return (
<NavigationContainer>
<AuthStack />
</NavigationContainer>
);
}

We'll also be wrapping the app's routes in a paper provider that provides a theme for all the app components.

Create an index.js inside src/context/. This file should contain:

src/context/index.js
import React from 'react';
import { DefaultTheme, Provider as PaperProvider } from 'react-native-paper';

import Routes from './routes';

export default function Providers() {
return (
<PaperProvider theme={theme}>
<Routes />
</PaperProvider>
);
}

const theme = {
...DefaultTheme,
roundness: 2,
colors: {
...DefaultTheme.colors,
primary: '#5b3a70',
accent: '#50c878',
background: '#f7f9fb'
}
};

Next, let's update the App.js file in the project root directory to use our providers.

The App.js file should now contain:

App.js
import React from 'react';

import Providers from './src/context';

export default function App() {
return <Providers />;
}

Now, if you run the app, you should see the login screen:

Screenshot: Login screen

Creating a sign-up screen

Now that you have a Login screen, you'll need a way for new users to sign up for your chat app, so let's build a Signup screen. We'll ask a new user for their email, a new password, as well as a display name that will be shown to other users in the app.

The sign-up screen should look like this after you're done:

Screenshot: Signup screen

First, inside the src/screens/ directory, create a signupScreen.js file to hold your Signup screen component.

Then, add the following to signupScreen.js:

src/screens/signupScreen.js
import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { IconButton, Title } from 'react-native-paper';

import FormButton from '../components/formButton';
import FormInput from '../components/formInput';

export default function SignupScreen({ navigation }) {
const [displayName, setDisplayName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

return (
<View style={styles.container}>
<Title style={styles.titleText}>Let's get started!</Title>
<FormInput
labelName='Display Name'
value={displayName}
autoCapitalize='none'
onChangeText={(userDisplayName) => setDisplayName(userDisplayName)}
/>
<FormInput
labelName='Email'
value={email}
autoCapitalize='none'
onChangeText={(userEmail) => setEmail(userEmail)}
/>
<FormInput
labelName='Password'
value={password}
secureTextEntry={true}
onChangeText={(userPassword) => setPassword(userPassword)}
/>
<FormButton
title='Signup'
modeValue='contained'
labelStyle={styles.loginButtonLabel}
onPress={() => {
// TODO
}}
/>
<IconButton
icon='keyboard-backspace'
size={30}
style={styles.navButton}
iconColor='#5b3a70'
onPress={() => navigation.goBack()}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
titleText: {
fontSize: 24,
marginBottom: 10
},
loginButtonLabel: {
fontSize: 22
},
navButtonText: {
fontSize: 18
},
navButton: {
marginTop: 10
}
});

Later, we'll hook up this screen to Firebase and ChatKitty to create a new user profile and begin a new chat session. Now, like with the login screen, let's add this screen to the auth stack navigator.

Edit the authStack.js file you created earlier in src/context/, adding the signup screen to its stack screen:

src/context/authStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

import LoginScreen from '../screens/loginScreen';
import SignupScreen from '../screens/signupScreen';

const Stack = createStackNavigator();

export default function AuthStack() {
return (
<Stack.Navigator initialRouteName='Login' screenOptions={{ headerShown: false }}>
<Stack.Screen name='Login' component={LoginScreen} />
<Stack.Screen name='Signup' component={SignupScreen} />
</Stack.Navigator>
);
}

Now, if you run the app and tap the "Sign up here" form button text on the login screen, you should see the sign-up screen:

Screenshot: Signup screen

With both the screens we need for user authentication set up, let's create a home screen to redirect users after they authenticate.

Creating a home screen

For now let's define a placeholder home screen to redirect users to after they log in or sign up. In future tutorials, we'll flesh out this screen with complete real-time chat functionality.

Inside the src/screens/ directory, create a homeScreen.js file.

The homeScreen.js file should contain:

src/screens/homeScreen.js
import React from 'react';
import { StyleSheet, View } from 'react-native';
import { Title } from 'react-native-paper';

import FormButton from '../components/formButton';

export default function HomeScreen() {
return (
<View style={styles.container}>
<Title>ChatKitty Example</Title>
<FormButton modeValue='contained' title='Logout' />
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
});

You'll need another navigation stack to handle screen routes after the user logs in. To do so, create a homeStack.js file inside the src/context directory.

The homeStack.js file should contain:

src/context/homeStack.js
import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

import HomeScreen from '../screens/homeScreen';

const Stack = createStackNavigator();

export default function HomeStack() {
return (
<Stack.Navigator>
<Stack.Screen name='Home' component={HomeScreen} />
</Stack.Navigator>
);
}

You will also need a context provider that is responsible for authentication. Inside the src/context directory create a authProvider.js file.

The authProvider.js file should contain the following:

src/context/authProvider.js
import React, { createContext, useState } from 'react';

export const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);

return (
<AuthContext.Provider
value={{
user,
setUser,
loading,
setLoading,
login: async (email, password) => {
// TODO
},
register: async (displayName, email, password) => {
// TODO
},
logout: async () => {
// TODO
}
}}
>
{children}
</AuthContext.Provider>
);
};

Later, we'll be updating the authentication provider to log in, register, and logout the user using Firebase and ChatKitty.

To get the authentication context inside your app components, you'll need to wrap the app routes with the authentication provider.

Edit the src/context/index.js file to wrap the app routes with the authentication provider.

The index.js file should now contain:

src/context/index.js
import React from 'react';
import { DefaultTheme, Provider as PaperProvider } from 'react-native-paper';

import { AuthProvider } from './authProvider';
import Routes from './routes';

export default function Providers() {
return (
<PaperProvider theme={theme}>
<AuthProvider>
<Routes />
</AuthProvider>
</PaperProvider>
);
}

const theme = {
...DefaultTheme,
roundness: 2,
colors: {
...DefaultTheme.colors,
primary: '#5b3a70',
accent: '#50c878',
background: '#f7f9fb'
}
};

Now, we have screens for user authentication, and a place to redirect users after they authenticate. Next, let's hook the app up to a Firebase and ChatKitty backend.

Creating a Firebase project

You'll be using Firebase to manage user passwords and authentication, so you'll need a Firebase project. If you don't already have one, create a new project using the Firebase console.

From the Firebase console home, create a project:

Screenshot: Firebase create project

Then, fill out the details of your Firebase project:

Screenshot: Firebase create project details

When you're done, click the "Continue" button, you'll be redirected to a dashboard screen for your new Firebase project.

Screenshot: Firebase create project complete

To support user email and password login, you'll need to enable the Firebase email sign-in method. From the Firebase console side menu, navigate to the Authentication section.

Screenshot: Firebase side menu authentication

Next, go to the "Sign-in method" tab, and enable the email sign-in provider.

Screenshot: Firebase select sign in method

Screenshot: Firebase enable email password sign in method

Adding Firebase credentials to the app

To add Firebase credentials to your app, go to your "Project settings" from the Firebase console menu.

Screenshot: Firebase project settings

Then, go to the "Your apps" section and click the Web icon

Screenshot: Firebase add app

Fill out the application details

Screenshot: Firebase create web app register

Continue to the console settings page

Screenshot: Firebase create web app complete

You should now be able to copy your Firebase web app config to add to your Expo React Native project.

From the "Your apps" section, select the "FirebaseConfig" Firebase SDK snippet for your web app and copy it:

Screenshot: Firebase web app config

Next, in your Expo React Native project, inside the src/ directory, create a firebase/ directory. Inside the src/firebase directory, create an index.js file to hold your Firebase web app credentials.

The index.js file should contain:

src/firebase/index.js
import AsyncStorage from '@react-native-async-storage/async-storage';
import { initializeApp } from 'firebase/app';
import {
initializeAuth,
getReactNativePersistence
} from 'firebase/auth/react-native';

// Replace this with your Firebase SDK config snippet
const firebaseConfig = {
/* YOUR FIREBASE CONFIG OBJECT PROPERTIES HERE */
};

// Initialize Firebase
const app = initializeApp(firebaseConfig);

// Initialize Auth
const auth = initializeAuth(app, {
persistence: getReactNativePersistence(AsyncStorage)
});

export { auth };

Replacing firebaseConfig with the value from your Firebase SDK config snippet.

That's it! You've now configured your Expo React Native app to use Firebase.

Hooking up user registration to Signup screen

You now have everything needed to create new users in Firebase. Let's partially implement the register function stub in the authentication provider to add new users to Firebase.

To do so, you'll edit the src/context/authProvider.js file to use Firebase to register new users.

The authProvider.js file should now contain:

src/context/authProvider.js
import React, { createContext, useState } from 'react';

import { createUserWithEmailAndPassword, updateProfile } from 'firebase/auth/react-native';
import { auth } from '../firebase';

export const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);

return (
<AuthContext.Provider
value={{
user,
setUser,
loading,
setLoading,
login: async (email, password) => {
// TODO
},
register: async (displayName, email, password) => {
setLoading(true);

try {
const userCredential = await createUserWithEmailAndPassword(
auth, email, password);

await updateProfile(auth.currentUser, {
displayName: displayName
});

// Signed-in Firebase user
const currentUser = userCredential.user;

console.log("Firebase user created: ", currentUser);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
},
logout: async () => {
// TODO
}
}}
>
{children}
</AuthContext.Provider>
);
};

Now, we can hook the register function to the Signup screen.

Edit the src/screens/signupScreen.js file to call the register function.

The signupScreen.js file should now contain:

src/screens/signupScreen.js
import React, { useContext, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { IconButton, Title } from 'react-native-paper';

import FormButton from '../components/formButton';
import FormInput from '../components/formInput';
import Loading from '../components/loading';
import { AuthContext } from '../context/authProvider';

export default function SignupScreen({ navigation }) {
const [displayName, setDisplayName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const { register, loading } = useContext(AuthContext);

if (loading) {
return <Loading />;
}

return (
<View style={styles.container}>
<Title style={styles.titleText}>Let's get started!</Title>
<FormInput
labelName='Display Name'
value={displayName}
autoCapitalize='none'
onChangeText={(userDisplayName) => setDisplayName(userDisplayName)}
/>
<FormInput
labelName='Email'
value={email}
autoCapitalize='none'
onChangeText={(userEmail) => setEmail(userEmail)}
/>
<FormInput
labelName='Password'
value={password}
secureTextEntry={true}
onChangeText={(userPassword) => setPassword(userPassword)}
/>
<FormButton
title='Signup'
modeValue='contained'
labelStyle={styles.loginButtonLabel}
onPress={() => register(displayName, email, password)}
/>
<IconButton
icon='keyboard-backspace'
size={30}
style={styles.navButton}
iconColor='#5b3a70'
onPress={() => navigation.goBack()}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
titleText: {
fontSize: 24,
marginBottom: 10
},
loginButtonLabel: {
fontSize: 22
},
navButtonText: {
fontSize: 18
},
navButton: {
marginTop: 10
}
});

Run the app now and submit a sign-up form:

Screenshot: sign-up screen filled

A new Firebase user should be created. You can verify this by checking the Firebase dashboard, Under the "Authentication" "Users" section, you should see your user:

Screenshot: Firebase created user

To complete the sign-up and login process, you'll need a ChatKitty project to connect to the ChatKitty JavaScript SDK and create chat sessions for your users. Let's create a ChatKitty project now.

Creating a ChatKitty project

You'll be using ChatKitty to provide real-time chat functionality for your chat app. You can create a free ChatKitty account here.

Once you've created a ChatKitty account, create an application for your Expo React Native project:

Screenshot: ChatKitty create application

Adding Firebase to your Chat Runtime

Now, let's integrate Firebase into your ChatKitty project. ChatKitty makes it easy to integrate any back-end into a ChatKitty application using Chat Functions. Chat Functions let you write arbitrary code that runs any time a relevant event or action happens inside your app. You can send push notifications, create emails, or make HTTP calls to your backend, as well as use a pre-initialized server-side ChatKitty SDK to make changes to your application in response to user actions. With ChatKitty, you can use any NPM package inside your Chat Functions as a Chat Runtime dependency.

Let's now add Firebase as a Chat Runtime dependency. From your ChatKitty application dashboard, go to the "Functions" page:

Screenshot: ChatKitty side menu functions

Go to the "Runtime" tab and add a new dependency to the Firebase JavaScript SDK NPM package, firebase-admin. Version 11.4.1 was the latest version as of the time this article was written.

Screenshot: ChatKitty runtime add firebase Remember to click the "Save" icon to confirm your chat runtime dependencies changes.

Before you can use Firebase within your chat functions, the Firebase SDK needs to be initialized. You can add arbitrary code that runs before each chat function using a chat runtime initialization script. Let's add an initialization script to initialize the Firebase SDK.

From the "Runtime" tab, click the drop down and select "Initialization Script". You import any NPM module using the CommonJS require function. To import the Firebase Admin NPM module and initialize Firebase, you'll use a Firebase Service Account Key for your project. To retrieve a Service Account Key, go to your Firebase project, and then go to the "Service Account" tab. From here, you can click on "Generate Private Key" to retrieve the serviceAccount object.

Screenshot: Generate private Firebase service account key

Now, go back to the ChatKitty dashboard and add the following into the Initialization Script:

const admin = require('firebase-admin');

const serviceAccount = {
// ADD YOUR SERVICE ACCOUNT KEY HERE
};

admin.initializeApp({
credential: admin.credential.cert(serviceAccount)
});

Screenshot: ChatKitty runtime initialization script Replacing serviceAccount with the value from your Firebase Service Account Key snippet. Also, remember to click the "Save" icon to confirm your changes.

Now we're ready to define a chat function to check if a user's email and password exist and match what we expect from Firebase, whenever a user tries to begin a new chat session.

Checking user credentials using a chat function

Before we let users begin a chat session and access sensitive data like messages, we need to be sure their credentials match what's stored in Firebase using a chat function.

From your ChatKitty application dashboard, go to the "Functions" page, and select "User Attempted Start Session" event chat function:

Screenshot: ChatKitty chat functions

This chat function runs whenever a user attempts to start a chat session. Edit the chat function to delegate user authentication to Firebase.

const firebase = require('firebase-admin');

async function handleEvent(event: UserAttemptedStartSessionEvent, context: Context) {
const username = event.username;

const idToken = event.authParams.idToken;

const { uid, name } = await firebase.auth().verifyIdToken(idToken);

if (username !== uid) {
throw new Error('This token was not issued for this user');
}

const userApi = context.ChatKitty.Users;

await userApi.checkUserExists(username).catch(async () => {
await userApi.createUser({
name: username,
displayName: name || 'anon',
isGuest: false
});
});
}

Screenshot: ChatKitty chat function user attempted start session Remember to click the "Save" icon to confirm your chat function changes.

Now, whenever a user tries to log in, ChatKitty checks if a user with their login credentials exists from your Firebase backend, and if so, begins a chat session. With that, we're ready to connect your Expo React Native app to the ChatKitty JavaScript Chat SDK.

Connecting to ChatKitty JS SDK

To use the ChatKitty JavaScript Chat SDK, you'll need to add the ChatKitty JavaScript SDK NPM package to your Expo React Native project:

npx expo install @chatkitty/core

Next, you'll need to configure the ChatKitty SDK with your ChatKitty API key. You can find your API key through the ChatKitty dashboard, inside the "Settings" page.

Screenshot: ChatKitty side menu settings

Copy the string value, under "API Key", you'll need it to initialize a ChatKitty client instance:

Screenshot: ChatKitty settings api key

Now, in your Expo React Native project, inside the src/ directory, create a chatkitty/ directory. Inside this src/chatkitty/ directory, create an index.js file to hold your ChatKitty client instance.

The index.js file should contain:

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

export const chatkitty = ChatKitty.getInstance('YOUR CHATKITTY API KEY');

With your API key from the ChatKitty dashboard

Let's now complete the chat login flow with the initialized ChatKitty client instance.

Completing user login and sign up with ChatKitty

You now have everything needed to implement a complete and secure chat app signup and login. Let's fill out the missing pieces left in our authentication provider.

Edit the src/context/authProvider.js file to use ChatKitty to login users and create chat sessions.

The authProvider.js file should now contain:

src/context/authProvider.js
import React, { createContext, useState } from 'react';
import {
createUserWithEmailAndPassword,
updateProfile,
signInWithEmailAndPassword
} from 'firebase/auth/react-native';
import { auth } from '../firebase';
import { chatkitty } from '../chatkitty';

export const AuthContext = createContext({});

export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);

return (
<AuthContext.Provider
value={{
user,
setUser,
loading,
setLoading,
login: async (email, password) => {
setLoading(true);

try {
const userCredential = await signInWithEmailAndPassword(auth,
email, password);

// Signed-in Firebase user
const currentUser = userCredential.user;

const result = await chatkitty.startSession({
username: currentUser.uid,
authParams: {
idToken: await currentUser.getIdToken()
}
});

if (result.failed) {
console.log('could not login');
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
},

register: async (displayName, email, password) => {
setLoading(true);

try {
const userCredential = await createUserWithEmailAndPassword(
auth, email, password);

await updateProfile(auth.currentUser, {
displayName: displayName
});

// Signed-in Firebase user
const currentUser = userCredential.user;

const startSessionResult = await chatkitty.startSession({
username: currentUser.uid,
authParams: {
idToken: await currentUser.getIdToken()
}
});

if (startSessionResult.failed) {
console.log('Could not sign up');
}
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
},
logout: async () => {
try {
await chatkitty.endSession();
} catch (error) {
console.error(error);
}
}
}}
>
{children}
</AuthContext.Provider>
);
};

Now, we can check if a user is logged in, and if so, route them to the home screen.

Edit the src/context/routes.js file to change the current navigation stack depending on if a user is logged in or not.

The routes.js file should now contain:

src/context/routes.js
import { NavigationContainer } from '@react-navigation/native';
import React, { useContext, useState, useEffect } from 'react';

import { chatkitty } from '../chatkitty';
import Loading from '../components/loading';

import AuthStack from './authStack';
import HomeStack from './homeStack';
import { AuthContext } from './authProvider';

export default function Routes() {
const { user, setUser } = useContext(AuthContext);
const [loading, setLoading] = useState(true);
const [initializing, setInitializing] = useState(true);

useEffect(() => {
return chatkitty.onCurrentUserChanged((currentUser) => {
setUser(currentUser);

if (initializing) {
setInitializing(false);
}

setLoading(false);
});
}, [initializing, setUser]);

if (loading) {
return <Loading />;
}

return (
<NavigationContainer>
{user ? <HomeStack /> : <AuthStack />}
</NavigationContainer>
);
}

Now, let's hook the login function to the Login screen.

To do so, edit the src/screens/loginScreen.js file to call the login function.

The loginScreen.js file should now contain:

src/screens/loginScreen.js
import React, { useContext, useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { Title } from 'react-native-paper';

import FormButton from '../components/formButton.js';
import FormInput from '../components/formInput.js';
import Loading from '../components/loading.js';
import { AuthContext } from '../context/authProvider.js';

export default function LoginScreen({ navigation }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');

const { login, loading } = useContext(AuthContext);

if (loading) {
return <Loading />;
}

return (
<View style={styles.container}>
<Title style={styles.titleText}>Welcome!</Title>
<FormInput
labelName='Email'
value={email}
autoCapitalize='none'
onChangeText={(userEmail) => setEmail(userEmail)}
/>
<FormInput
labelName='Password'
value={password}
secureTextEntry={true}
onChangeText={(userPassword) => setPassword(userPassword)}
/>
<FormButton
title='Login'
modeValue='contained'
labelStyle={styles.loginButtonLabel}
onPress={() => {
login(email, password);
}}
/>
<FormButton
title='Sign up here'
modeValue='text'
uppercase={false}
labelStyle={styles.navButtonText}
onPress={() => navigation.navigate('Signup')}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
titleText: {
fontSize: 24,
marginBottom: 10
},
loginButtonLabel: {
fontSize: 22
},
navButtonText: {
fontSize: 16
}
});

Finally, let's hook the logout authentication function to the Home screen and show the user's display name.

Edit the src/screens/homeScreen.js file to call the logout function and greet your user.

The homeScreen.js file should now contain:

src/screens/homeScreen.js
import React, { useContext } from 'react';
import { StyleSheet, View } from 'react-native';
import { Title } from 'react-native-paper';

import FormButton from '../components/formButton';
import { AuthContext } from '../context/authProvider';

export default function HomeScreen() {
const { logout } = useContext(AuthContext);

return (
<View style={styles.container}>
<Title>ChatKitty Example</Title>
<FormButton
modeValue='contained'
title='Logout'
onPress={() => logout()}
/>
</View>
);
}

const styles = StyleSheet.create({
container: {
backgroundColor: '#f5f5f5',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
}
});

If you run the application now and try to login with the test user credentials you created earlier

Screenshot: Login screen filled


You should be redirected to your home screen

Screenshot: Home screen

Conclusion

Awesome! You've completed the first part of this tutorial series and successfully implemented secure user authentication for your Expo React Native chat app, using Firebase and powered by the ChatKitty chat platform. By defining your authentication logic in a chat function while delegating user credentials checking to Firebase, you were able to extend the ChatKitty login flow to handle your specific case, without having to build your own backend. In future parts of this series, we'll explore more features you can implement with chat functions like sending push notifications to a user's device when the user isn't online.

What's next?

In the next post of this series, we'll implement more chat features including creating discussion channels for users to discover, join and chat. Stay tuned for more. 🔥

If you have any questions, comments or need help with any part of this article, join our Discord Server where you can ask questions, discuss what you're working on, and I'll be more than happy to help.

You can find the complete source code for this project inside this GitHub repository.

👉 Checkout the other blog posts in this series:


This article contains materials adapted from "Chat app with React Native" by Aman Mittal, originally published at Heartbeat.Fritz.Ai.

This article features an image by Volodymyr Hryshchenko.

· 2 min read

Happy New Year to all of our valued customers and supporters!

As we enter into this new year, I wanted to take a moment to reflect on the past year and share some of our plans for the future.

Looking back, it's been an incredible journey for ChatKitty. When we first launched, we had a vision to make it easier for businesses to integrate messaging and chat functionality into their products and services. And through your support and feedback, we've been able to achieve that and so much more.

In the past year, we've seen tremendous growth in our customer base and are now serving hundreds of businesses across a variety of industries. We've also released a number of new features and updates, including support for multimedia messaging and increased security measures.

As we move into the new year, we have even more exciting plans in store. We're working on some major updates to our API that will make it even more powerful and easy to use, as well as expanding our platform to support additional messaging channels.

But most importantly, we remain committed to providing the best possible service to our customers. We value your feedback and are always working to improve and evolve our platform to meet your needs.

Thank you for your support and for choosing our service. We can't wait to see what the new year brings and to continue serving you in the months and years ahead.

Sincerely,
Aaron

· 2 min read

Are you considering creating a Slack-like chat experience on your platform? Yes, ChatKitty can help with that. With the recent pricing changes, Slack may not be the best place to grow your user base or create your community.

Instead of using Slack or another similar service, more and more companies decided to develop their own collaboration chat experience on their platform.

With ChatKitty, you can develop and release a premium chat experience in less than a day. It’s completely white label, fully customizable, and 100% concurrency guaranteed.

Starting with the features, here are the top 10 popular Slack features you can develop via our chat API on day one.

  • Authentication
  • Creating channels
  • One-to-one chat
  • Group chat
  • Threads reply
  • Emoji reaction
  • Multimedia attachment
  • In-App Notification
  • Typing indicator
  • Presence indicator

Authentication

A user can log into your platform and start using chat without being authenticated again. ChatKitty simplifies authentication using Chat Functions. An end-developer can write custom code and hook it to any user authentication provider (eg Firebase, Auth0, etc.) into their application. Here is how you can start developing authentication.

Creating channels

A user can create channels and invite others to join the channels. Users can also join and leave channels. Here is how you can start building channels.

One-to-one chat

A user can chat with another user directly and privately in a one-to-one setting.

Group chat

A user can chat with other users in channels that include as many users as you have.

Thread replies

A user can reply directly to a specific message and create a separate chat within the one-to-one or group chats.

Emoji reactions

A user can send emojis in chat or react to messages with emojis.

Multimedia attachments

A user can send files, images, and voice memos or share them to other channels.

In-app Notifications

A user can receive chat notifications directly from your app.

Typing indicator

When the other user is typing, an indicator will show up next to the chat window and indicates that the other user is responding.

Presence indicator

When a user is online, an indicator will show up next to the profile and indicates that the user is available to chat.

This guide covers how you can enhance your chat experience with features like replies, reactions, notifications and more.

· 3 min read

When designing our pricing plans, our goal is to provide our customers modern chat features their customers expect no matter what plan they choose.

We believe in letting customers pick and choose their features without limits on their user experience and creativity.

To help you better understand all the chat features we offer, we created the following table to list our features and descriptions. Our Modern Chat Essentials include the majority of features you see on all the popular social or chat apps on the market.

Modern Chat Essentials
FeatureDescription
Private chatPrivate communication between users
Direct chatOne-to-one user chat
Group chatMulti-user chat
Broadcast chatMake announcements to your group
Push NotificationsSend notification to your users
Unread message countShow you the number of messages that haven’t been read
Text messagesDisplay text messages
Image messagesUpload and display images in any sizes
Audio messagesDisplay audio recording like a walkie talkie
File messagesUpload and display files in any sizes
Message Thread replyYou can reply to a specific message directly
Edit messageMake changes to messages that have been sent
Delete messageDelete messages that have been sent
ReactionsReact to a message using emoji
Message reportingReport messages to the admin
User reportingReport users to the admin
Typing indicatorAn indicator shows when someone is typing
Present indicatorAn indicator shows when someone is online
Auto-moderationHide content and send alert to admin when identify inappropriate content
Read Receipt indicatorAn indicator shows when a message is read
Delivery Receipt indicatorAn indicator shows when a message is received
User BlockingBlock a user
Custom User PermissionsCustomize permissions on each of the users
Event WebhooksAllow you to connect your chat to other services
Chat FunctionsChat Functions allows you to integrate with third party software and solutions
Audio socialAn audio chat room to have an informal discussion
LivestreamBroadcast events to other users
ScreensharingAllow you share your screen with other users
SMS supportSend message, notification, invite and more via SMS
Email supportSend message, notification, invite and more via email
Multi Language SupportSend and display message in various languages

Don’t see what you are looking for? We only listed some of the popular features. We still have lots of bells and whistles. Contact us at hello@chatkitty.com and we will show you how we can help you.

· 3 min read

Man searching the internet

No matter, if you are not happy with your chat API provider or your chat API provider is shutting down, we understand finding a new chat API provider is a frustrating and stressful experience. Especially, if you want to find a chat API company that handles the technology and data with care. However, your search is over now because ChatKitty is here to help!

How can ChatKitty help right away?

  • It’s free until your go-live – We will work closely with your team to ensure the chat is implemented successfully before having you commit to a payment plan.
  • Migration support – Our team helps you to make sure the migration goes smoothly and handles your data with due diligence and extreme care.
  • Price matching – We know prices are different from company to company, but we will make sure you will pay similar to what you did before.
  • Ongoing technology support – Unlike our competitors, we offer ongoing technology review and support to ensure you always get the best support from us possible.
  • Amazing customer service – Many companies don’t pay attention to their customer service experience, but we do. See our amazing customer service by joining our discord server.

What are ChatKitty's features?

ChatKitty provides a wide range of products: text chat, UI library, and all modern chat features – and many more. We are not going to show off here, so you can find all our features in our comprehensive documentation.

Here are some of the popular features our customers are using

User side features:

User side features

Admin side features:

Admin side features

Why you should try out ChatKitty

There is no commitment to trying out ChatKitty. Our team will help you to build a demo without you committing to any payment plans. As you are trying out our chat API, you will experience the following six benefits from us right away:

  • Quick Turnaround: We provide extensive documentation, detailed tutorials, and functional code samples to ensure you have chat working in minutes.

  • Customizable: Our powerful webhooks and serverless functions provide modular design and allow you to use ChatKitty like LEGO blocks.

  • Plug and Play: Advanced messaging features like group chat, multimedia, auto-moderation, etc. are ready to deploy at your fingertip.

  • Scalability: ChatKitty is an auto-scalable solution powered by a highly optimized, elastic cloud infrastructure.

  • Encryption: We designed our chat API solutions with the toughest data regulation (such as GDPR and HIPAA) in mind.

  • Full concurrency: ChatKitty allows all your users to be online at the same time without limits unlike other chat API providers.

Want to talk to someone before starting?

You can reach out to us anytime by sending us a message or start asking questions on our Discord Server. Our team is happy to help you get started.