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:
Create direct channels for users to chat privately
Integrate Gifted Chat's in-built typing indicator and implement a custom more detailed indicator
Notify chat users when a user enters or leaves the chat from a chat screen
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:
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
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.
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.
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.
Start by importing
StyleSheet
andView
fromreact-native
, andText
fromreact-native-paper
.Create a helper method
renderFooter
inside thechatScreen.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 ornull
otherwise.Lastly, on the
GiftedChat
component, set itsrenderFooter
prop to therenderFooter
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:
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.
Edit the homeScreen.js
you created earlier in src/screens/
with the following steps:
- Import
Button
,Dialog
andPortal
fromreact-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 theonLongPress
flat list item prop to select a channel to leave by settingleaveChannel
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.
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:
Creating an Expo React Native chat app with Firebase Authentication
Building a group chat screen with the Gifted Chat React Native library
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.