How to Use React Native to Build a Private Messaging App

Written by daltonic | Published 2022/01/24
Tech Story Tags: react-native | firebase | cometchat | crossplatform-application | how-to-build-a-chat-app | chat | javascript | mobile-app-development

TLDRIn this tutorial, you will learn how to use ReactNative, [CometChat], and Firebase to build a chat app with a stunning UI. The packages used to create this application are listed below. To spin up the development server on IOS or Android you will need a simulator for that, use an IOS and Android simulator, otherwise, use the web interface and follow up the tutorial. The **Expo-CLI** must then be installed on your computer using the command below.via the TL;DR App

What you’ll be building. See live demo and Git Repo Here.

Introduction

Let’s be real, if you haven’t built a chat app yet, you are still a little bit behind the cutting edge of software development. You need to upskill your app development to the next level. Luckily, cross-platform frameworks such as React Native can get you building a modern chat app in no time like the one seen above.

In this tutorial, you will learn how to use; React Native, CometChat, and Firebase to build a one-on-one chat app with a stunning UI.

If you are ready, let’s get started…

Prerequisite

To understand this tutorial, you should already be familiar with React Native. The rest of the stack is simple to grasp. The packages used to create this application are listed below.

Installing The Project Dependencies

First, download and install NodeJs on your machine. If you don’t have it installed already, go to their website and follow the installation process. The Expo-CLI must then be installed on your computer using the command below. You can get to their doc page by clicking on this LINK.

# Install Expo-CLI
npm install --global expo-cli

After that, open the terminal and create a new expo project called privex, selecting the blank template when prompted. Use the example below to demonstrate this.

#Create a new expo project and navigate to the directory
expo init privex
cd privex

#Start the newly created expo project
expo start

Running the above commands on the terminal will create a new react-native project and start it up on the browser. Now you will have the option of launching the IOS, Android, or the Web interface by simply selecting the one that you want. To spin up the development server on IOS or Android you will need a simulator for that, use the instruction found here to use an IOS or Android simulator, otherwise, use the web interface and follow up the tutorial.

Great, now follow the instructions below to install these critical dependencies for our project. Yarn is the expo's default package manager; see the codes below.

# Install the native react navigation libraries
yarn add @react-navigation/native
yarn add @react-navigation/native-stack

#Installing dependencies into an Expo managed project
expo install react-native-screens react-native-safe-area-context react-native-gesture-handler

# Install an Icon pack and a state manager
yarn add react-native-vector-icons react-hooks-global-state

Nice, now let’s set up Firebase for this project.

Setting Up Firebase

Run the command below to properly install firebase in the project.

#Install firebase with the command
expo install firebase

Let's get started by configuring the Firebase console for this project, including the services we'll be using.

If you do not already have a Firebase account, create one for yourself. After that, go to Firebase and create a new project called privex, then activate the Google authentication service, as detailed below.

Firebase supports authentication via a variety of providers. For example, social authentication, phone numbers, and the traditional email and password method. Because we'll be using the Google authentication in this tutorial, we'll need to enable it for the project we created in Firebase, as it's disabled by default. Click the sign-in method under the authentication tab for your project, and you should see a list of providers currently supported by Firebase.

Super, that will be all for the firebase authentication, let's generate the Firebase SDK configuration keys.

You need to go and register your application under your Firebase project.

On the project’s overview page, select the add app option and pick web as the platform.

Return to the project overview page after completing the SDK config registration, as shown in the image below.

Now you click on the project settings to copy your SDK configuration setups.

The configuration keys shown in the image above must be copied to a separate file that will be used later in this project.

Create a file called firebase.js in the root of this project and paste the following codes into it before saving.

import { initializeApp, getApps } from 'firebase/app'
import {
  getAuth,
  onAuthStateChanged,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  updateProfile,
  signOut,
} from 'firebase/auth'
import {
  getFirestore,
  collection,
  addDoc,
  setDoc,
  getDoc,
  getDocs,
  doc,
  onSnapshot,
  serverTimestamp,
  query,
  orderBy,
  collectionGroup,
  arrayUnion,
  arrayRemove,
  updateDoc,
} from 'firebase/firestore'

const firebaseConfig = {
  apiKey: 'xxx-xxx-xxx-xxx-xxx',
  authDomain: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx',
  projectId: 'xxx-xxx-xxx-xxx-xxx',
  storageBucket: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx',
  messagingSenderId: 'xxx-xxx-xxx',
  appId: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
}

if (!getApps().length) initializeApp(firebaseConfig)

export {
  getAuth,
  onAuthStateChanged,
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  updateProfile,
  signOut,
  collection,
  collectionGroup,
  addDoc,
  getFirestore,
  onSnapshot,
  serverTimestamp,
  query,
  orderBy,
  getDoc,
  getDocs,
  setDoc,
  doc,
  arrayUnion,
  arrayRemove,
  updateDoc,
}

You are awesome if you followed everything correctly. We'll do something similar for CometChat next.

Setting CometChat

Head to CometChat and signup if you don’t have an account with them. Next, log in and you will be presented with the screen below.

To create a new app, click the Add New App button. You will be presented with a modal where you can enter the app details. The image below shows an example.

Following the creation of your app, you will be directed to your dashboard, which should look something like this.

You must also copy those keys to a separate file in the manner described below. Simply create a file called CONSTANTS.js in the project's root directory and paste the code below into it. Now include this file in the .gitIgnore file, which is also located at the root of this project; this will ensure that it is not published online.

export const CONSTANTS = {
  APP_ID: 'xxx-xxx-xxx',
  REGION: 'us',
  Auth_Key: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
}

Finally, delete the preloaded users, and groups as shown in the images below.

Awesome, that will be enough for the setups, let’s start integrating them all into our application, we will start with the components.

The Components Directory

This project contains several directories; let's begin with the components folder. Within the root of this project, create a folder called components. Let's begin with the Header component.

The Header Component

This is a styled component supporting the beauty of our app. Within it is the avatar element which displays the current user's profile picture. Also with this avatar, you can log out from the application. Create this file by going into the components directory and creating a file named HomeHeader.js, afterward, paste the code below into it.

import { StyleSheet, TouchableOpacity, View } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { Avatar, Text } from 'react-native-elements'
import { getAuth, signOut } from '../firebase'
import { CometChat } from '@cometchat-pro/react-native-chat'
import { setGlobalState } from '../store'

const HomeHeader = () => {
  const PLACEHOLDER_AVATAR =
    'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'

  const auth = getAuth()

  const signOutUser = async () => {
    try {
      await signOut(auth).then(() => {
        CometChat.logout()
          .then(() => {
            console.log('Logout completed successfully')
            setGlobalState('currentUser', null)
            setGlobalState('isLoggedIn', false)
          })
          .catch((error) =>
            console.log('Logout failed with exception:', { error })
          )
      })
    } catch (error) {
      console.log(error)
    }
  }

  return (
    <View style={{ paddingVertical: 15, paddingHorizontal: 30 }}>
      <View style={styles.container}>
        <TouchableOpacity onPress={signOutUser} activeOpacity={0.5}>
          <Avatar
            rounded
            source={{
              uri: auth?.currentUser?.photoURL || PLACEHOLDER_AVATAR,
            }}
          />
        </TouchableOpacity>

        <View
          style={{
            flexDirection: 'row',
            justifyContent: 'center',
            alignItems: 'center',
          }}
        >
          <TouchableOpacity style={{ marginRight: 15 }} activeOpacity={0.5}>
            <Icon name="search" size={18} color="white" />
          </TouchableOpacity>
        </View>
      </View>

      <Text h4 style={{ color: 'white', marginTop: 15 }}>
        Messages
      </Text>
    </View>
  )
}

export default HomeHeader

const styles = StyleSheet.create({
  container: {
    justifyContent: 'space-between',
    alignItems: 'center',
    flexDirection: 'row',
  },
})

The ChatContainer Component

This component is responsible for showcasing the recent conversations of a user. It does more than that, it can also display users' stories and a floating button that launches a modal for showing all the users registered in our app. We will look at each of them separately, shown below is the code for it.

import { CometChat } from '@cometchat-pro/react-native-chat'
import { useEffect, useState } from 'react'
import {
  ScrollView,
  StyleSheet,
  useWindowDimensions,
  View,
  TouchableOpacity,
} from 'react-native'
import { Avatar, Text } from 'react-native-elements'
import { useGlobalState } from '../store'
import { getAuth } from '../firebase'

const auth = getAuth()

const timeAgo = (date) => {
  let seconds = Math.floor((new Date() - date) / 1000)
  let interval = seconds / 31536000

  if (interval > 1) {
    return Math.floor(interval) + 'yr'
  }
  interval = seconds / 2592000
  if (interval > 1) {
    return Math.floor(interval) + 'mo'
  }
  interval = seconds / 86400
  if (interval > 1) {
    return Math.floor(interval) + 'd'
  }
  interval = seconds / 3600
  if (interval > 1) {
    return Math.floor(interval) + 'h'
  }
  interval = seconds / 60
  if (interval > 1) {
    return Math.floor(interval) + 'm'
  }
  return Math.floor(seconds) + 's'
}

const ChatContainer = ({ navigation }) => {
  return (
    <View style={styles.container}>
      <Stories />
      <ChatList navigation={navigation} />
    </View>
  )
}

const Stories = () => {
  const [stories] = useGlobalState('stories')

  return (
    <ScrollView
      style={[
        styles.shadow,
        {
          flexGrow: 0,
          position: 'absolute',
          left: 0,
          right: 0,
          top: 0,
          paddingVertical: 15,
        },
      ]}
      horizontal={true}
      showsHorizontalScrollIndicator={false}
    >
      {stories.map((story) => (
        <View
          key={story.id}
          style={{ alignItems: 'center', marginHorizontal: 5 }}
        >
          <Avatar size={60} rounded source={{ uri: story.avatar }} />
          <Text style={{ fontWeight: 600 }}>{story.fullname}</Text>
        </View>
      ))}
    </ScrollView>
  )
}

const ChatList = ({ navigation }) => {
  const viewport = useWindowDimensions()
  const [conversations, setConversations] = useState([])

  const getConversations = () => {
    let limit = 30
    let conversationsRequest = new CometChat.ConversationsRequestBuilder()
      .setLimit(limit)
      .build()

    conversationsRequest
      .fetchNext()
      .then((conversationList) => setConversations(conversationList))
      .catch((error) => {
        console.log('Conversations list fetching failed with error:', error)
      })
  }

  useEffect(() => {
    getConversations()
  }, [navigation])

  return (
    <ScrollView
      style={{
        minHeight: viewport.height.toFixed(0) - 189,
        marginTop: 50,
        paddingTop: 15,
      }}
      showsVerticalScrollIndicator={false}
    >
      {conversations.map((conversation, index) => (
        <Conversation
          key={index}
          currentUser={auth.currentUser.uid.toLowerCase()}
          owner={conversation.lastMessage.receiverId.toLowerCase()}
          conversation={conversation.lastMessage}
          navigation={navigation}
        />
      ))}
    </ScrollView>
  )
}

const Conversation = ({ conversation, currentUser, owner, navigation }) => {
  const possessor = (key) => {
    return currentUser == owner
      ? conversation.sender[key]
      : conversation.receiver[key]
  }

  const handleNavigation = () => {
    navigation.navigate('ChatScreen', {
      id: possessor('uid'),
      name: possessor('name'),
      avatar: possessor('avatar'),
    })
  }

  return (
    <TouchableOpacity
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginTop: 20,
      }}
      onPress={handleNavigation}
    >
      <Avatar size={50} rounded source={{ uri: possessor('avatar') }} />

      <View
        style={{
          flex: 1,
          marginLeft: 15,
          flexDirection: 'row',
          justifyContent: 'space-between',
        }}
      >
        <View>
          <Text h5 style={{ fontWeight: 700 }}>
            {possessor('name')}
          </Text>
          <Text style={{ color: 'gray' }}>
            {conversation.text.slice(0, 30) + '...'}
          </Text>
        </View>

        <Text style={{ color: 'gray' }}>
          {timeAgo(new Date(Number(conversation.sentAt) * 1000).getTime())}
        </Text>
      </View>
    </TouchableOpacity>
  )
}

export default ChatContainer

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'white',
    borderTopLeftRadius: 40,
    borderTopRightRadius: 40,
    paddingTop: 30,
    overflow: 'hidden',
    paddingHorizontal: 15,
  },
  shadow: {
    shadowColor: '#171717',
    shadowOffsetWidth: 0,
    shadowOffsetHeight: 2,
    shadowOpacity: 0.2,
    shadowRadius: 3,
    backgroundColor: 'white',
    zIndex: 9999,
  },
})

The FloatingButton Component

This component is responsible for launching the UserList modal which allows us to chat with a new user on our platform. Here is the code snippet for it.

import { View, StyleSheet, TouchableOpacity } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { setGlobalState } from '../store'

const FloatingButton = () => {
  return (
    <View style={styles.container}>
      <TouchableOpacity
        onPress={() => setGlobalState('showUsers', true)}
        style={styles.button}
        activeOpacity={0.5}
      >
        <Icon name="plus" size={24} color="white" />
      </TouchableOpacity>
    </View>
  )
}

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    bottom: 60,
    right: 30,
  },
  button: {
    shadowColor: '#171717',
    shadowOffsetWidth: 0,
    shadowOffsetHeight: 2,
    shadowOpacity: 0.2,
    shadowRadius: 3,
    paddingVertical: 7,
    paddingHorizontal: 9,
    borderRadius: 50,
    backgroundColor: '#122643',
  },
})

export default FloatingButton

The UserList Component

This component is launched by the FloatingButton. It displays all the users that are on our platform and enables you to have a first-time conversation with them. After then, they can appear in your conversation list. Below is the code responsible for its implementation.

import { CometChat } from '@cometchat-pro/react-native-chat'
import { useEffect, useState } from 'react'
import {
  View,
  Text,
  StyleSheet,
  TouchableOpacity,
  ScrollView,
  useWindowDimensions,
} from 'react-native'
import { Avatar, Button, Overlay } from 'react-native-elements'
import { setGlobalState, useGlobalState } from '../store'

const timeAgo = (date) => {
  let seconds = Math.floor((new Date() - date) / 1000)
  let interval = seconds / 31536000

  if (interval > 1) {
    return Math.floor(interval) + 'yr'
  }
  interval = seconds / 2592000
  if (interval > 1) {
    return Math.floor(interval) + 'mo'
  }
  interval = seconds / 86400
  if (interval > 1) {
    return Math.floor(interval) + 'd'
  }
  interval = seconds / 3600
  if (interval > 1) {
    return Math.floor(interval) + 'h'
  }
  interval = seconds / 60
  if (interval > 1) {
    return Math.floor(interval) + 'm'
  }
  return Math.floor(seconds) + 's'
}

const UserList = ({ navigation }) => {
  const viewport = useWindowDimensions()
  const [showUsers] = useGlobalState('showUsers')
  const [users, setUsers] = useState([])

  const toggleOverlay = () => setGlobalState('showUsers', !showUsers)

  const getUsers = () => {
    const limit = 30
    const usersRequest = new CometChat.UsersRequestBuilder()
      .setLimit(limit)
      .build()

    usersRequest
      .fetchNext()
      .then((userList) => setUsers(userList))
      .catch((error) => {
        console.log('User list fetching failed with error:', error)
      })
  }

  useEffect(() => {
    getUsers()
  }, [])

  return (
    <Overlay
      isVisible={showUsers}
      onBackdropPress={toggleOverlay}
      overlayStyle={{
        backgroundColor: 'white',
        justifyContent: 'center',
        alignItems: 'center',
        minWidth: viewport.width.toFixed(0) - 200,
        maxWidth: viewport.width.toFixed(0) - 194,
      }}
    >
      <ScrollView
        style={{
          maxHeight: viewport.height.toFixed(0) - 196,
          padding: 20,
          width: '100%',
        }}
        showsVerticalScrollIndicator={false}
      >
        {users.map((user, index) => (
          <User user={user} key={index} navigation={navigation} />
        ))}
      </ScrollView>
    </Overlay>
  )
}

const User = ({ navigation, user }) => {
  const handleNavigation = () => {
    navigation.navigate('ChatScreen', {
      id: user.uid,
      name: user.name,
      avatar: user.avatar,
    })
    setGlobalState('showUsers', false)
  }

  return (
    <TouchableOpacity
      style={{
        flexDirection: 'row',
        justifyContent: 'space-between',
        alignItems: 'center',
        marginTop: 20,
      }}
      onPress={handleNavigation}
    >
      <View>
        <Avatar
          size={50}
          rounded
          source={{ uri: user.avatar }}
          placeholderStyle={{ opacity: 0 }}
        />
      </View>

      <View
        style={{
          flex: 1,
          marginLeft: 15,
          flexDirection: 'row',
          justifyContent: 'space-between',
        }}
      >
        <View>
          <Text h5 style={{ fontWeight: 700 }}>
            {user.name}
          </Text>
          <Text style={{ color: 'gray' }}>{user.status}</Text>
        </View>

        <Text style={{ color: 'gray' }}>
          {timeAgo(new Date(Number(user.lastActiveAt) * 1000).getTime())}
        </Text>
      </View>
    </TouchableOpacity>
  )
}

const styles = StyleSheet.create({})

export default UserList

Fantastic, we have just finished building the dedicated components, let’s proceed to craft out the screens now.

The Screens Directory

The screens are similar to website pages; each screen represents a page, and you can navigate from one to the next using the React

Native navigator package. Let's proceed with the LoginScreen.

The LoginScreen

This well-crafted screen does a lot of things behind the scene. It uses Firebase Google authentication to sign you into the system. Once you are signed in, Firebase authStateChange function will take note of you as a logged-in user. This is carried out in the AuthNavigation file. But before you are let into the system, your authenticated details will be retrieved and sent to CometChat either for signing up or signing in. Once CometChat is done, you are then let into the system. See the code below for a full breakdown.

import {
  Image,
  ImageBackground,
  Pressable,
  StyleSheet,
  Text,
  View,
} from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'
import { provider, signInWithPopup, getAuth } from '../firebase'
import { CONSTANTS } from '../CONSTANTS'
import { CometChat } from '@cometchat-pro/react-native-chat'
import { setGlobalState } from '../store'

const LoginScreen = () => {
  const signInPrompt = () => {
    const auth = getAuth()
    signInWithPopup(auth, provider)
      .then((result) => {
        const user = result.user
        console.log(user)
      })
      .catch((error) => console.log(error))
  }

  return (
    <SafeAreaView style={styles.container}>
      <ImageBackground
        style={styles.bgfy}
        source={{
          uri: 'https://images.pexels.com/photos/3137056/pexels-photo-3137056.jpeg?auto=compress&cs=tinysrgb&dpr=3&h=750&w=1260',
        }}
      >
        <View style={styles.wrapper}>
          <Image style={styles.logo} source={require('../assets/privex.png')} />
          <Text style={{ color: '#fff' }}>
            Your fully functional secured chat app solution.
          </Text>

          <Pressable titleSize={20} style={styles.button}>
            <Text style={styles.buttonText} onPress={signInPrompt}>
              Log In with Google
            </Text>
          </Pressable>

          <Pressable style={{ marginTop: 20 }}>
            <Text style={{ color: 'white' }}>New User? Sign up</Text>
          </Pressable>
        </View>
      </ImageBackground>
    </SafeAreaView>
  )
}

export default LoginScreen

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  bgfy: {
    width: '100%',
    height: '100%',
    resizeMode: 'contain',
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  wrapper: {
    backgroundColor: 'rgba(18, 38, 67, 0.96);',
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    width: '100%',
    height: '100%',
  },
  logo: {
    width: 200,
    height: 50,
    resizeMode: 'contain',
  },
  button: {
    backgroundColor: '#0caa92',
    alignItems: 'center',
    justifyContent: 'center',
    minHeight: 42,
    borderRadius: 20,
    paddingHorizontal: 40,
    paddingVertical: 10,
    marginTop: 30,
  },
  buttonText: {
    color: 'white',
    fontWeight: 600,
    fontSize: 20,
  },
})

The HomeScreen

This well-crafted screen brings together all the dedicated components in the components directory to one space. Each of the components knows how to perform its duties. Not many words here, let the code below do all the explaining.

import { SafeAreaView, StyleSheet } from 'react-native'
import ChatContainer from '../components/ChatContainer'
import FloatingButton from '../components/FloatingButton'
import HomeHeader from '../components/HomeHeader'
import UserList from '../components/UserList'
const HomeScreen = ({ navigation }) => {
  return (
    <SafeAreaView style={styles.container}>
      <HomeHeader />
      <ChatContainer navigation={navigation} />
      <FloatingButton />
      <UserList navigation={navigation} />
    </SafeAreaView>
  )
}
export default HomeScreen
const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#122643',
  },
})

The ChatScreen

Lastly for the screens, we have the chat screen that lets us perform one-on-one conversations with another user. It heavily uses the CometChat SDK for its operations, let's take a look at the function of the code.

import { CometChat } from '@cometchat-pro/react-native-chat'
import { useEffect, useRef, useState } from 'react'
import {
  SafeAreaView,
  ScrollView,
  StyleSheet,
  TouchableOpacity,
  useWindowDimensions,
  View,
} from 'react-native'
import { Avatar, Input, Text } from 'react-native-elements'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { getAuth } from '../firebase'

const ChatScreen = ({ navigation, route }) => {
  return (
    <SafeAreaView style={styles.container}>
      <Header navigation={navigation} route={route} />
      <MessageContainer route={route} />
    </SafeAreaView>
  )
}

const Header = ({ navigation, route }) => (
  <View
    style={[styles.flexify, { paddingHorizontal: 15, paddingVertical: 25 }]}
  >
    <TouchableOpacity onPress={() => navigation.goBack()} activeOpacity={0.5}>
      <Icon name="arrow-left" size={18} color="white" />
    </TouchableOpacity>

    <View style={[styles.flexify, { flex: 1, marginLeft: 15 }]}>
      <View style={styles.flexify}>
        <Avatar
          rounded
          source={{ uri: route.params.avatar }}
          placeholderStyle={{ opacity: 0 }}
        />
        <Text
          style={{
            color: 'white',
            fontWeight: 600,
            marginLeft: 10,
            textTransform: 'capitalize',
          }}
        >
          {route.params.name}
        </Text>
      </View>

      <View style={styles.flexify}>
        <TouchableOpacity activeOpacity={0.5} style={{ marginRight: 25 }}>
          <Icon name="video" size={18} color="white" />
        </TouchableOpacity>

        <TouchableOpacity activeOpacity={0.5}>
          <Icon name="phone-alt" size={18} color="white" />
        </TouchableOpacity>
      </View>
    </View>
  </View>
)

const MessageContainer = ({ route }) => {
  const viewport = useWindowDimensions()
  const [message, setMessage] = useState('')
  const [messages, setMessages] = useState([])
  const scrollViewRef = useRef()
  const auth = getAuth()

  const sendMessage = () => {
    let receiverID = route.params.id
    let messageText = message
    let receiverType = CometChat.RECEIVER_TYPE.USER
    let textMessage = new CometChat.TextMessage(
      receiverID,
      messageText,
      receiverType
    )

    CometChat.sendMessage(textMessage).then(
      (message) => {
        setMessages((prevState) => [...prevState, message])
        setMessage('')
        console.log('Message sent successfully:', message)
      },
      (error) => {
        console.log('Message sending failed with error:', error)
      }
    )
  }

  const getMessages = () => {
    let UID = route.params.id
    let limit = 30
    let messagesRequest = new CometChat.MessagesRequestBuilder()
      .setUID(UID)
      .setLimit(limit)
      .build()

    messagesRequest
      .fetchPrevious()
      .then((messages) => setMessages(messages))
      .catch((error) => {
        console.log('Message fetching failed with error:', error)
      })
  }

  const listenForMessage = () => {
    const listenerID = Math.random().toString(16).slice(2)
    CometChat.addMessageListener(
      listenerID,
      new CometChat.MessageListener({
        onTextMessageReceived: (message) => {
          setMessages((prevState) => [...prevState, message])
        },
      })
    )
  }

  useEffect(() => {
    getMessages()
    listenForMessage()
  }, [route])

  return (
    <>
      <ScrollView
        ref={scrollViewRef}
        onContentSizeChange={(width, height) =>
          scrollViewRef.current.scrollTo({ y: height })
        }
        style={{
          backgroundColor: 'white',
          maxHeight: viewport.height.toFixed(0) - 162,
          padding: 20,
        }}
        showsVerticalScrollIndicator={false}
      >
        <View
          style={{
            flex: 1,
            alignItems: 'center',
            justifyContent: 'center',
            marginVertical: 5,
          }}
        ></View>
        {messages.map((message, index) => (
          <Message
            key={index}
            currentUser={auth.currentUser.uid.toLowerCase()}
            owner={message.receiverId.toLowerCase()}
            message={message}
          />
        ))}
      </ScrollView>

      <View style={[styles.flexify, styles.positAtBottom, styles.shadow]}>
        <TouchableOpacity
          style={{
            paddingVertical: 5,
            paddingHorizontal: 7,
            borderRadius: 50,
            backgroundColor: '#122643',
          }}
          activeOpacity={0.5}
        >
          <Icon name="plus" size={12} color="white" />
        </TouchableOpacity>

        <View style={{ flex: 1 }}>
          <Input
            placeholder="Write a message..."
            inputContainerStyle={{ borderBottomWidth: 0 }}
            onSubmitEditing={() => sendMessage()}
            onChangeText={(text) => setMessage(text)}
            value={message}
            inputStyle={{ fontSize: 12 }}
            autoFocus={true}
          />
        </View>

        <TouchableOpacity
          style={{
            paddingVertical: 5,
            paddingHorizontal: 7,
            borderRadius: 50,
            backgroundColor: '#c5c5c5',
          }}
          activeOpacity={0.5}
          disabled={message.length < 1}
          onPress={() => sendMessage()}
        >
          <Icon name="arrow-right" size={12} color="black" />
        </TouchableOpacity>
      </View>
    </>
  )
}

const Message = ({ message, currentUser, owner }) => {
  const dateToTime = (date) => {
    let hours = date.getHours()
    let minutes = date.getMinutes()
    let ampm = hours >= 12 ? 'pm' : 'am'
    hours = hours % 12
    hours = hours ? hours : 12
    minutes = minutes < 10 ? '0' + minutes : minutes
    let strTime = hours + ':' + minutes + ' ' + ampm
    return strTime
  }

  const isDateToday = (date) => {
    const today = new Date().getDate()
    const day = new Date(date * 1000).getDate()

    return today == day
  }

  return currentUser == owner ? (
    <View style={[styles.flexify, styles.spaceMsg]}>
      <Avatar
        placeholderStyle={{ opacity: 0 }}
        rounded
        source={{ uri: message.sender.avatar }}
      />

      <View style={[styles.msgBg, { marginLeft: 10 }]}>
        <Text
          style={{
            fontWeight: 800,
            fontSize: 13,
            color: '#4c4c4c',
            textTransform: 'capitalize',
          }}
        >
          {message.sender.name}
        </Text>
        <Text style={{ fontWeight: 600, marginVertical: 5 }}>
          {message.text}
        </Text>
        <Text style={{ fontWeight: 600 }}>
          {dateToTime(new Date(message.sentAt * 1000))}
        </Text>
      </View>
    </View>
  ) : (
    <View style={[styles.flexify, styles.spaceMsg]}>
      <View
        style={[styles.msgBg, { backgroundColor: '#c5c5c5', marginRight: 10 }]}
      >
        <Text
          style={{
            fontWeight: 800,
            fontSize: 13,
            color: '#4c4c4c',
            textTransform: 'capitalize',
          }}
        >
          {message.sender.name}
        </Text>
        <Text styles={{ fontWeight: 600, marginVertical: 5 }}>
          {message.text}
        </Text>
        <Text style={{ fontWeight: 600 }}>
          {dateToTime(new Date(message.sentAt * 1000))}
        </Text>
      </View>

      <Avatar
        placeholderStyle={{ opacity: 0 }}
        rounded
        source={{ uri: message.sender.avatar }}
      />
    </View>
  )
}

export default ChatScreen

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#122643',
  },
  flexify: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
  },
  msgBg: {
    flex: 1,
    backgroundColor: '#efefef',
    borderRadius: 20,
    padding: 10,
  },
  spaceMsg: {
    alignItems: 'flex-end',
    marginVertical: 5,
  },
  shadow: {
    shadowColor: '#171717',
    shadowOffsetWidth: 0,
    shadowOffsetHeight: 2,
    shadowOpacity: 0.2,
    shadowRadius: 3,
    backgroundColor: 'white',
  },
  positAtBottom: {
    position: 'absolute',
    left: 0,
    right: 0,
    bottom: 0,
    paddingHorizontal: 15,
    paddingVertical: 15,
  },
})

There are three functions you should take note of, they are the meat of this screen. The getMessages, sendMessage, and listenForMessage functions. They utilize the CometChat SDK and each one of them performs their operations according to their names.

That’s the last screen for the application, let’s seal it up with the App component and the router set up…

Setting Up The Router

Now that we've finished coding the project, let's set up the navigation routers and guards. To do so, create and paste the following codes as directed below.

The Navigation file This categorizes the screens into two groups: those that require authentication and those that do not. Make a new file called "navigation.js" in the project's root and paste the code below into it.

import React from 'react'
import { createStackNavigator } from '@react-navigation/stack'
import { NavigationContainer } from '@react-navigation/native'
import HomeScreen from './screens/HomeScreen'
import LoginScreen from './screens/LoginScreen'
import ChatScreen from './screens/ChatScreen'

const Stack = createStackNavigator()
const screenOption = {
  headerShown: false,
}

export const SignedInStack = () => (
  <NavigationContainer>
    <Stack.Navigator initialRouteName="HomeScreen" screenOptions={screenOption}>
      <Stack.Screen name="HomeScreen" component={HomeScreen} />
      <Stack.Screen name="ChatScreen" component={ChatScreen} />
    </Stack.Navigator>
  </NavigationContainer>
)

export const SignedOutStack = () => (
  <NavigationContainer>
    <Stack.Navigator
      initialRouteName="LoginScreen"
      screenOptions={screenOption}
    >
      <Stack.Screen name="LoginScreen" component={LoginScreen} />
    </Stack.Navigator>
  </NavigationContainer>
)

The AuthNavigation file This file displays screens logically to you based on the authState of the firebase authentication service. It is also responsible for signing a user to CometChat depending on whether they are registering or logging in into the system. To proceed, create a new file in the project's root called AuthNavigation.js and paste the code below into it.

import React, { useEffect, useState } from 'react'
import { SignedInStack, SignedOutStack } from './navigation'
import { onAuthStateChanged, getAuth } from './firebase'
import { CometChat } from '@cometchat-pro/react-native-chat'
import { CONSTANTS } from './CONSTANTS'

const AuthNavigation = () => {
  const [currentUser, setCurrentUser] = useState(null)
  const auth = getAuth()

  const userHandler = (user) =>
    user ? setCurrentUser(user) : setCurrentUser(null)

  const signUpWithCometChat = (data) => {
    const authKey = CONSTANTS.Auth_Key
    const user = new CometChat.User(data.uid)
    user.setName(data.displayName)
    user.setAvatar(data.photoURL)
    CometChat.createUser(user, authKey)
      .then((res) => {
        console.log('User signed up...', res)
        CometChat.login(data.uid, authKey)
          .then((u) => {
            console.log(u)
            userHandler(data)
          })
          .catch((error) => console.log(error))
      })
      .catch((error) => {
        console.log(error)
        alert(error.message)
      })
  }

  const loginWithCometChat = (data) => {
    const authKey = CONSTANTS.Auth_Key
    CometChat.login(data.uid, authKey)
      .then((u) => {
        console.log('User Logged in...', u)
        userHandler(data)
      })
      .catch((error) => {
        if (error.code === 'ERR_UID_NOT_FOUND') {
          signUpWithCometChat(data)
        } else {
          console.log(error)
        }
      })
  }

  useEffect(
    () =>
      onAuthStateChanged(auth, (user) => {
        if (currentUser == null && user) {
          loginWithCometChat(user)
        } else {
          userHandler(null)
        }
      }),
    []
  )

  return <>{currentUser ? <SignedInStack /> : <SignedOutStack />}</>
}

export default AuthNavigation

Finally, the App component…

The App Component This component puts together every part of this project. Please replace the content of this file with the code below.

import { CometChat } from '@cometchat-pro/react-native-chat'
import { useEffect } from 'react'
import AuthNavigation from './AuthNavigation'
import { CONSTANTS } from './CONSTANTS'
export default function App() {
  const initCometChat = () => {
    let appID = CONSTANTS.APP_ID
    let region = CONSTANTS.REGION
    let appSetting = new CometChat.AppSettingsBuilder()
      .subscribePresenceForAllUsers()
      .setRegion(region)
      .build()
    CometChat.init(appID, appSetting)
      .then(() => console.log('Initialization completed successfully'))
      .catch((error) => console.log('Initialization failed with error:', error))
  }
  useEffect(() => initCometChat(), [])
  return <AuthNavigation />
}

Cheers, you just smashed this app now it’s time you do more than this with this new trick you’ve learned.

You can spin up your server using the code below on your terminal if you have not done that already.

# Start your ReactNative local server on the web view
yarn web

The App should function like the one in the image below.

Conclusion

We've reached the end of this tutorial; hopefully, you learned something new. Becoming a modern-day developer can be difficult, but it is not impossible; you can accomplish more than you think; all you need is a little guidance. And now you know how to use React Native, Firebase, and CometChat to create a fantastic chat app with a beautiful interface. I have more of these tutorials that will show you how to make a private or public group chat. I'm excited to see your magnificent creations.

All the best!

About the Author

Gospel Darlington kick-started his journey as a software engineer in 2016. Over the years, he has grown full-blown skills in JavaScript stacks such as React, React Native, VueJs, and more.

He is currently freelancing, building apps for clients, writing technical tutorials teaching others how to do what he does.

Gospel Darlington is open and available to hear from you. You can reach him on LinkedIn, Facebook, Github, or on his website.

Previously published on Medium’s subdomain.


Written by daltonic | Youtuber | Blockchain Developer | Writer | Instructor
Published by HackerNoon on 2022/01/24