Skip to main content

· 3 min read
Raymond Chen

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, video/voice call, 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, video call, 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.

· 3 min read
Raymond Chen

EU Flag

As ChatKitty grows, more and more customers who have operations in Europe have reached out for service and support. So naturally, their most asked questions are about GDPR. And we always tell them; ChatKitty was developed with GDPR in mind. We are a GDPR compliant processor.


What is GDPR?

The General Data Protection Regulation (GDPR) is a privacy and security law drafted and passed by the European Union (EU). It imposes obligations onto organizations anywhere, so long as they target or collect data related to people in the EU. This regulation was put into effect on May 25, 2018.

What data does ChatKitty store?

We store chat data on AWS encrypted servers as part of the chat solution we provide to our customers.

Who owns the data?

Our customers have complete control over their data. They can access, modify, delete, and transit their data any time upon user request.

How has ChatKitty approached GDPR?

ChatKitty was developed with GDPR in mind, so we had our focus on the following five components.

  1. Access control – All access is restricted to the only designated system administrator that maintains those systems. No third party has access to the data ChatKitty stores as part of our operations.

  2. Historical data – Our API currently allows customers to read all data they collected, and our customers can modify, delete, migrate the data upon user request.

  3. Encryption – All API communication with ChatKitty is encrypted. We also allow our customers to use their encryption method to enhance protection.

  4. Store and process – ChatKitty only stores chat data that our customers permitted it. ChatKitty does not process data for any purpose except when fulfilling data requests from our customers.

  5. Audit and logging – All access to stored data is logged.

What additional steps has ChatKitty taken to comply with GDPR?

  • Appointed a Data Protection Officer (DPO) to oversee our compliance program.

  • Host ongoing discussions and training to educate the team to take data security as a priority.

  • Conduct periodic reviews on our data protection strategy to ensure changes to our service, such as making changes or developing features that will not jeopardize our store's user data.

  • Maintain formal processes around data subject rights to ensure we can help customers fulfill requests they receive.

  • Pay close attention to regulatory guidance around GDPR compliance and making changes to our product features and contracts when they’re needed.

Conclusion

We understand you have concerns about GDPR. We have made GDPR our priority while developing ChatKitty. We will continue to work hard to ensure we are compliant and transparent throughout the process. If you have any other questions about GDPR or other certifications and compliance, feel free to reach out at support@chatkitty.com.


This article features the image "Flag of the European Union in front of the EU-Parliament in Brussels, Belgium" by Christian Lue licensed under the Unsplash License

· 22 min read
Aaron Nwabuoku

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. To make sure you have the latest version of ChatKitty, run the yarn upgrade command:

# upgrade ChatKitty SDK to the latest version
yarn upgrade chatkitty

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) => {
kitty
.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 { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';
import { AuthContext } from '/blog/navigation/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 = kitty.startChatSession({
channel: channel,
onReceivedMessage: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
});

kitty
.getMessages({
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 kitty.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={(clickedUser) => {
kitty
.createChannel({
type: 'DIRECT',
members: [{ id: clickedUser._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.

Screenshot: Direct chat

ChatKitty automatically creates a unique channel name for direct channels, so we see a UUID in the app title bar. Let's use a more appropriate name for the chat screen title.

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

export function getChannelDisplayName(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';

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

export function getChannelDisplayName(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/navigation to use the getChannelDisplayName method:

import { getChannelDisplayName, kitty } from '/blog/chatkitty';

// Unchanged

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

Also update BrowseChannelsScreen.js and HomeScreen.js in src/screens to use the helper method:

import { getChannelDisplayName, kitty } from '/blog/chatkitty';

// Unchanged

<List.Item
title={getChannelDisplayName(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 sending typing keystrokes to ChatKitty, so it can know when a user is typing:

 function handleInputTextChanged(text) {
kitty.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 = kitty.startChatSession({
channel: channel,
onReceivedMessage: (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 { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';
import { AuthContext } from '/blog/navigation/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 = kitty.startChatSession({
channel: channel,
onReceivedMessage: (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);
}
},
});

kitty
.getMessages({
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 kitty.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) {
kitty.sendKeystrokes({
channel: channel,
keys: text,
});
}

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

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(clickedUser) => {
kitty
.createChannel({
type: 'DIRECT',
members: [{ id: clickedUser._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 { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';
import { AuthContext } from '/blog/navigation/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 = kitty.startChatSession({
channel: channel,
onReceivedMessage: (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);
}
},
});

kitty
.getMessages({
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 kitty.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) {
kitty.sendKeystrokes({
channel: channel,
keys: text,
});
}

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

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(clickedUser) => {
kitty
.createChannel({
type: 'DIRECT',
members: [{ id: clickedUser._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 the ChatKitty fork of the react-native-in-app-notification React Native library to provide in-app notifications for your chat app. Let's use this library in your chat sessions' onParticipantEnteredChat and onParticipantLeftChat handler methods to notify users when users enter or leave a chat.

Screenshot: Entered chat partial

Update HomeStack.js in the src/navigation directory to make the ChatScreen capable of showing in-app notifications by using the withInAppNotification Higher-Order Component (HOC).

      <ChatStack.Screen
name="Chat"
component={withInAppNotification(ChatScreen)} /* Add this */
options={({ route }) => ({
title: getChannelDisplayName(route.params.channel),
})}
/>

After this, HomeStack.js should look like

import { withInAppNotification } from '@chatkitty/react-native-in-app-notification';
import { createStackNavigator } from '@react-navigation/stack';
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import React, { useEffect } from 'react';
import { Platform } from 'react-native';
import { IconButton } from 'react-native-paper';

import { getChannelDisplayName, kitty } from '/blog/chatkitty';
import BrowseChannelsScreen from '/blog/screens/BrowseChannelsScreen';
import ChatScreen from '/blog/screens/ChatScreen';
import CreateChannelScreen from '/blog/screens/CreateChannelScreen';
import HomeScreen from '/blog/screens/HomeScreen';

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

export default function HomeStack() {
useEffect(() => {
registerForPushNotificationsAsync().then((token) => {
kitty.updateCurrentUser((user) => {
user.properties = {
...user.properties,
'expo-push-token': token,
};

return user;
});
});
}, []);

return (
<ModalStack.Navigator mode="modal" headerMode="none">
<ModalStack.Screen
name="ChatApp"
component={withInAppNotification(ChatComponent)}
/>
<ModalStack.Screen name="CreateChannel" component={CreateChannelScreen} />
</ModalStack.Navigator>
);
}

function ChatComponent({ navigation, showNotification }) {
useEffect(() => {
return kitty.onNotificationReceived((notification) => {
showNotification({
title: notification.title,
message: notification.body,
onPress: () => {
switch (notification.data.type) {
case 'USER:SENT:MESSAGE':
case 'SYSTEM:SENT:MESSAGE':
kitty.getChannel(notification.data.channelId).then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
break;
}
},
});
});
}, [navigation, showNotification]);

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

async function registerForPushNotificationsAsync() {
let token;

if (Constants.isDevice && Platform.OS !== 'web') {
const {
status: existingStatus,
} = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('Failed to get push token for push notification!');
return;
}

token = (await Notifications.getExpoPushTokenAsync()).data;
} else {
console.log('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',
});
}

return token;
}

We can now access the showNotification prop in the ChatScreen component inside ChatScreen.js:

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

// Unchanged
}

Lastly, register chat session handler methods to show a notification when a user enters or leaves the chat.

useEffect(() => {
const startChatSessionResult = kitty.startChatSession({
channel: channel,
onReceivedMessage: (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) => { /* Add this */
showNotification({
title: `${participant.displayName} entered the chat`,
});
},
onParticipantLeftChat: (participant) => { /* Add this */
showNotification({
title: `${participant.displayName} left the chat`,
});
},
});

// Unchanged

return startChatSessionResult.session.end;
}, [user, channel, showNotification /* Add this */]);

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 { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';
import { AuthContext } from '/blog/navigation/AuthProvider';

export default function ChatScreen({ route, navigation, showNotification }) {
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 = kitty.startChatSession({
channel: channel,
onReceivedMessage: (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) => {
/* Add this */
showNotification({
title: `${participant.displayName} entered the chat`,
});
},
onParticipantLeftChat: (participant) => {
/* Add this */
showNotification({
title: `${participant.displayName} left the chat`,
});
},
});

kitty
.getMessages({
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, showNotification]);

async function handleSend(pendingMessages) {
await kitty.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) {
kitty.sendKeystrokes({
channel: channel,
keys: text,
});
}

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

function renderAvatar(props) {
return (
<Avatar
{...props}
onPressAvatar={(clickedUser) => {
kitty
.createChannel({
type: 'DIRECT',
members: [{ id: clickedUser._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:

Screenshot: Entered chat

You should also see a notification when the user leaves the chat:

Screenshot: Left 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() {
kitty.leaveChannel({ channel: leaveChannel }).then(() => {
setLeaveChannel(null);

kitty.getChannels({ 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={getChannelDisplayName(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.

· 6 min read
Raymond Chen

Keep Things Simple

Using a chat SDK not only can shorten your development time, but also help you build a stable product and scale quickly. Before you instruct your team to start investing time adding all available features from ChatKitty, have you identified your product's purpose, and its primary use case?


Are you and your team considering adding chat to your product roadmap? Congratulations! You have made the right choice. As a product owner myself, I believe all excellent products eventually need to connect all users via a good chat experience. At this point, you probably have spoken to your developers about the options to make the chat happen. If you are looking at the ChatKitty website, you are probably thinking about using a chat SDK.

Using a chat SDK not only can shorten your development time, but also help you build a stable product and scale quickly. Before you instruct your team to start investing time adding all available features from ChatKitty, have you identified your product's purpose, and its primary use case?

You may be wondering why I am asking these questions. Many founders will tell you one of the hardest things before getting the blade and glory of developing a product is to plan out the features that need to be included in the road map.

Your developers and your users may have different expectations of what your product will accomplish. Before digging into it, simplify what the chat needs to accomplish and who the users are.

Here is a breakdown of all the features ChatKitty offers. You may be surprised by the variety of features right away, but we trimmed our offerings to provide our partners' essential features compared to our competitions on the market.

User features:

User features

Admin features:

Admin features

Rather than going through them one by one. I want to use a more product-driven way to help you discover what chat experience you need. To help you better plan out your features with ChatKitty, I have broken down various standard chat features on the market.

Relationship apps like WhatsApp, Facebook Messenger, and Tinder

You are not a stranger to the apps I mentioned above. Suppose you are building something to connect people in the same communities or who already know each other; private chat and push notification are going to be your bread & butter. Besides, you can add Reactions and GIF to the conversation. For a dating app like Tinder, small things like GIF will get the conversation going. For the rest of the features, it will be up to the user survey. Whatever helps the users to make the conversation interesting, you add it.

Must: Private Chat and Push Notification Nice to have: Reactions, GIF and Delivery and read receipts

Collaboration apps like Slack, Microsoft Teams and Google Hangout

You need to make this category as productivity-driven as possible. Group chat and message threads are a must in this case. Especially message threads, this should come as a standard for collaboration tools. The users can follow up conversation back to the original post. Since now, we are all working from home, Typing and Presence Indicators will be nice to have. It gives the insurance that you are there and receives the message.

Must: Group Chat and Threads Nice to have: Typing and Presence Indicators

Community apps like Twitch, Reddit, and YouTube

If you are working on a product in this category, you bring the entire world together. Rather than having public chat rooms, message threads and reactions, you need to have control. ChatKitty admin tool will help you to keep heated conversations under control and restrict troublemakers on your platform. Speaking from experience, one bad character in the group will scare off other users. You need admin control as early as in the development.

Must: Public Chat Room and Admin Control Nice to have: Reactions and Threads

Marketplace app apps like eBay, Airbnb and Upwork

This category is similar to the relationship app, but your users are not here to build an ongoing relationship. Users are here to find a perfect match of things. Once it's done, the conversation can end there. In this case, delivery and message receipts are something I considered essential. You want the buyers, and the sellers are aware that the transaction is happening. A stable push notification will also be a plus, but when a seller misses a text, this may lead to a potential loss. Lastly, a presence indicator will be something nice to have, but you need to test it with your users.

Must: Private Chat and Push Notification Nice to have: Delivery and message receipts

One and done apps like Uber, Instacart, and DoorDash

Finally, we have to talk about this category. It's a category that has changed our lives forever, but we don't care too much about the message feature within them. When you think back, users only use it for a few phrases: "Where are you?" "Almost there." and "Food is here!". For this category, a private one to one chat, stable push notification and location indicator will do the job.

Must: Private Chat and Push Notification Nice to have: Location Indicators

Aside from these common chat apps we mentioned above, many other types of apps on the market have excellent chat features Hinges, Poshmark, Instagram DM, and many more. When you dig into those products' history, you can find a common theme, which is to find the bread and butter feature you need in the chat. When you don't know where to start with your feature planning, you can't go wrong, starting with a one-to-one chat and stable push notifications.

Once these features are running smoothly, your team can start to validate your idea right away without spending extra time and money on development. My team and I have learned from experience.

My co-founder Aaron elaborates in detail on our chat features in this series of articles. Stay tuned for more.


This article features the image "CEO planning and prioritizing the quarter with the team." by airfocus.

· 12 min read
Aaron Nwabuoku

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 in-app and push notifications to inform users when new messages are received or relevant actions happen inside a channel and across your app.

After reading this article, you will be able to:

  1. Implement in-app 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. To make sure you have the latest version of ChatKitty, run the yarn upgrade command:

# upgrade ChatKitty SDK to the latest version
yarn upgrade chatkitty

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

What are in-app notifications?

In-app 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 the react-native-in-app-notification library to provide a customizable in-app React Native component. Add the ChatKitty fork of react-native-in-app-notification that fixes this issue to your project:

# install react-native-in-app-notification
yarn add @chatkitty/react-native-in-app-notification

You'll also be using Expo push notifications, so install the Expo notifications, permissions, and other dependency modules you'll need to get expo push tokens needed to register user devices for push notifications:

# install the Expo push notifications modules
yarn add expo-constants expo-notifications expo-permissions

Handling user in-app notifications

To use react-native-in-app-notification within your app, you'll need to wrap your routes component with its provider component.

Edit the index.js file you created earlier in src/navigation/ with a InAppNotificationProvider component wrapping the existing Routes component.

The index.js file should contain:

import { InAppNotificationProvider } from '@chatkitty/react-native-in-app-notification';
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>
<InAppNotificationProvider>
<Routes />
</InAppNotificationProvider>
</AuthProvider>
</PaperProvider>
);
}

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

Next, you'll need to update the HomeStack.js file in the src/navigation/ directory to:

  • Wrap the chat component, which will be displaying the in-app notification, with the withInAppNotification higher-order component.

  • Register a ChatKitty onNotificationReceived event listener using a useEffect React hook to show received notifications.

The HomeStack.js file should contain:

import { withInAppNotification } from '@chatkitty/react-native-in-app-notification';
import { createStackNavigator } from '@react-navigation/stack';
import React, { useEffect } from 'react';
import { IconButton } from 'react-native-paper';

import { kitty } from '/blog/chatkitty';
import BrowseChannelsScreen from '/blog/screens/BrowseChannelsScreen';
import ChatScreen from '/blog/screens/ChatScreen';
import CreateChannelScreen from '/blog/screens/CreateChannelScreen';
import HomeScreen from '/blog/screens/HomeScreen';

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

export default function HomeStack() {
return (
<ModalStack.Navigator mode="modal" headerMode="none">
<ModalStack.Screen
name="ChatApp"
component={withInAppNotification(ChatComponent)}
/>
<ModalStack.Screen name="CreateChannel" component={CreateChannelScreen} />
</ModalStack.Navigator>
);
}

function ChatComponent({ navigation, showNotification }) {
useEffect(() => {
return kitty.onNotificationReceived((notification) => {
showNotification({
title: notification.title,
message: notification.body,
onPress: () => {
switch (notification.data.type) {
case 'USER:SENT:MESSAGE':
case 'SYSTEM:SENT:MESSAGE':
kitty.getChannel(notification.data.channelId).then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
break;
}
},
});
});
}, [navigation, showNotification]);

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

If you run the app now, go to the home screen, and send a message from another device as another user, you should see an in-app notification:

Screenshot: In-app notification

Nice. Now your users can stay informed on the latest events happening across your app screens.

Let's now handle what happens when your users have left your app.

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.

Update the HomeStack.js file to register the user's device for push notifications and then store the user's expo push token as a user property.

The HomeStack.js file should contain:

import { withInAppNotification } from '@chatkitty/react-native-in-app-notification';
import { createStackNavigator } from '@react-navigation/stack';
import Constants from 'expo-constants';
import * as Notifications from 'expo-notifications';
import * as Permissions from 'expo-permissions';
import React, { useEffect } from 'react';
import { IconButton } from 'react-native-paper';

import { kitty } from '/blog/chatkitty';
import BrowseChannelsScreen from '/blog/screens/BrowseChannelsScreen';
import ChatScreen from '/blog/screens/ChatScreen';
import CreateChannelScreen from '/blog/screens/CreateChannelScreen';
import HomeScreen from '/blog/screens/HomeScreen';

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

export default function HomeStack() {
useEffect(() => {
registerForPushNotificationsAsync().then((token) => {
kitty.updateCurrentUser((user) => {
user.properties = {
...user.properties,
'expo-push-token': token,
};

return user;
});
});
}, []);

return (
<ModalStack.Navigator mode="modal" headerMode="none">
<ModalStack.Screen
name="ChatApp"
component={withInAppNotification(ChatComponent)}
/>
<ModalStack.Screen name="CreateChannel" component={CreateChannelScreen} />
</ModalStack.Navigator>
);
}

function ChatComponent({ navigation, showNotification }) {
useEffect(() => {
return kitty.onNotificationReceived((notification) => {
showNotification({
title: notification.title,
message: notification.body,
onPress: () => {
switch (notification.data.type) {
case 'USER:SENT:MESSAGE':
case 'SYSTEM:SENT:MESSAGE':
kitty.getChannel(notification.data.channelId).then((result) => {
navigation.navigate('Chat', { channel: result.channel });
});
break;
}
},
});
});
}, [navigation, showNotification]);

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

async function registerForPushNotificationsAsync() {
let token;

if (Constants.isDevice && Platform.OS !== 'web') {
const {
status: existingStatus,
} = await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== 'granted') {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== 'granted') {
console.log('Failed to get push token for push notification!');
return;
}

token = (await Notifications.getExpoPushTokenAsync()).data;
} else {
console.log('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',
});
}

return token;
}

With that, you should have the user's expo push token as the expo-push-token user property.

Setting up Expo push notification credentials

For iOS, the managed Expo workflow handles push notification credentials automatically when you run expo build:ios. 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.

After creating your Expo account, login into Expo by running

# enter your Expo credentials when prompted
expo login

From the "Project settings" section of your Firebase project, go to the "Cloud Messaging" tab, copy the "Server key" value and upload it to Expo:

# replace <your-token-here> with your server key
expo push:android:upload --api-key <your-token-here>

Cool, 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.6.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

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 in-app 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
Aaron Nwabuoku

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 checkout 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. To make sure you have the latest version of ChatKitty, run the yarn upgrade command:

# upgrade ChatKitty SDK to the latest version
yarn upgrade chatkitty

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.

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:

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 { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';

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

const isFocused = useIsFocused();

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

kitty.getChannels({ 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 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/navigation/ directory to apply header bar options across screens and define a stack navigator for app modal screens.

The HomeStack.js file should contain:

import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';

import HomeScreen from '/blog/screens/HomeScreen';

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

export default function HomeStack() {
return (
<ModalStack.Navigator mode="modal" headerMode="none">
<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:

import React, { useState } from 'react';
import { StyleSheet, View } from 'react-native';
import { IconButton, Title } from 'react-native-paper';

import { kitty } from '/blog/chatkitty';
import FormButton from '/blog/components/FormButton';
import FormInput from '/blog/components/FormInput';

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

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

return (
<View style={styles.rootContainer}>
<View style={styles.closeButtonContainer}>
<IconButton
icon="close-circle"
size={36}
color="#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:

import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { IconButton } from 'react-native-paper';

import CreateChannelScreen from '/blog/screens/CreateChannelScreen';
import HomeScreen from '/blog/screens/HomeScreen';

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

export default function HomeStack() {
return (
<ModalStack.Navigator mode="modal" headerMode="none">
<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}
color="#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:

# install Gifted Chat
yarn add 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 onReceivedMessage 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 getMessages 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:

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

import { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';
import { AuthContext } from '/blog/navigation/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 = kitty.startChatSession({
channel: channel,
onReceivedMessage: (message) => {
setMessages((currentMessages) =>
GiftedChat.append(currentMessages, [mapMessage(message)])
);
},
});

kitty
.getMessages({
channel: channel,
})
.then((result) => {
setMessages(result.paginator.items.map(mapMessage));

setLoading(false);
});

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

async function handleSend(pendingMessages) {
await kitty.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/navigation/ with a new screen entry.

The HomeStack.js file should contain:

import { createStackNavigator } from '@react-navigation/stack';
import React from 'react';
import { IconButton } from 'react-native-paper';

import ChatScreen from '/blog/screens/ChatScreen';
import CreateChannelScreen from '/blog/screens/CreateChannelScreen';
import HomeScreen from '/blog/screens/HomeScreen';

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

export default function HomeStack() {
return (
<ModalStack.Navigator mode="modal" headerMode="none">
<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}
color="#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:

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 { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';

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

const isFocused = useIsFocused();

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

kitty.getChannels({ 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 getMessages result to fetch more pages and prepend the messages fetched into our messages collection.

// Fetching a next page:

const result = kitty.getMessages({ 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:

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

import { kitty } from '/blog/chatkitty';
import Loading from '/blog/components/Loading';
import { AuthContext } from '/blog/navigation/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]