DEV Community

Cover image for How to Build a Chat App using ReactNative and Firebase (LinkedIn Clone)
Gospel Darlington
Gospel Darlington

Posted on • Edited on

4

How to Build a Chat App using ReactNative and Firebase (LinkedIn Clone)

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

LinkedIn Clone Posts
LinkedIn Clone Chat

Introduction

Do you want to advance in your app development career? Then it's time to complete this LinkedIn Clone Project. We will create a LinkedIn Clone using ReactNative, which will vastly improve your understanding of this cross-platform framework. This tutorial was put together with the help of ReactNative, Firebase, and CometChat. We will be creating project with your JavaScript skills, as shown in the images above.

If you want to add this project to your portfolio, then jump on the keyboard with me and we'll figure it out.

Prerequisite

To digest this tutorial, you should already have ground knowledge of ReactNative, the rest of the stacks can easily be understood with no hassle. Listed below are the packages used for developing this application.

Installing The Project Dependencies

First, Download and Install NodeJs on your machine, visit their website and complete the installation if you haven’t already. Next, you need to have the Expo-CLI installed on your computer using the command below. You can visit their doc page using this link.

# Install Expo-CLI
npm install --global expo-cli
Enter fullscreen mode Exit fullscreen mode

Afterward, open the terminal and create a new expo project with the name linkedin-clone and choose the blank template when prompted. Use the example below to do this.

#Create a new expo project and navigate to the directory
expo init linkedin-clone
cd linkedin-clone

#Start the newly created expo project
expo start
Enter fullscreen mode Exit fullscreen mode

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.

Awesome, now let's install these essential dependencies for our project using the instruction below. The default package manager for the expo is yarn, 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

# Install Yup and Formik for validating our forms
yarn add formik yup
Enter fullscreen mode Exit fullscreen mode

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

Setting Up Firebase

First, run the command below on your expo project terminal. Use the code below to properly install it.

#Install firebase with the command
expo install firebase
Enter fullscreen mode Exit fullscreen mode

Good, let's set up the firebase console for this project including the services we will be using.

We will proceed by signing up for a firebase account if you don’t have one already. Afterward, head to Firebase and create a new project named linkedin-clone, activate the email and password authentication service, details are spelled out below.

Firebase Projects page

Step 1
Step 2
Step 3

Firebase provides support for authentication using different providers. For example, Social Auth, phone numbers, as well as the standard email and password method. Since we’ll be using the email and password authentication method in this tutorial, we need to enable this method for the project we created in Firebase, as it is by default disabled. Under the authentication tab for your project, click the sign-in method and you should see a list of providers currently supported by Firebase.

Firebase Authentication Service

Step 1
Step 2

Epic, let's activate the Firestore service which we will be used for storing all the posts coming from our linkedin-clone application.

To activate the Firestore service, navigate to the Firestore tab on the sidebar as seen in the images below and click on “create database”.

Firestore Service

Next, go to the Firestore rules and make the changes as seen in the images below.

Initial Rule
Updated Rule

Next, we want to use the timestamp as an index for ordering our posts, to do so, we have to create an index for it. Follow the process as seen in the images below.

Cloud Firestore Index

Click on the single field tab and add an exception as seen in the image below.

Firestore Exception

Enter posts as the collection ID and timestamp as the field. Click next, and enable the scopes as seen in the images below.

Step 1
Step 2

If you have done the above steps correctly, you should have the same result as seen in the image below.

Exception Creation in Progress, takes up to 5min

Once everything is completed, the loading indicators on the exception blocks should disappear and should now look like this.

After The Creation

Nice, you have set up all that we will need for the Firestore services, now lets generate the Firebase SDK configuration keys.

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

Project Overview Page

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

Registering a Firebase SDK

Once you finish up the SDK config registration, navigate back to the project overview page as seen in the image below.

Project overview page

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

Project Setups

The config keys seen in the image above must be copied to a separate file which we will later use in the course of this project.

Create a file in the root of this project called firebase.js and paste the following codes and save.

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,
}
view raw firebase.js hosted with ❤ by GitHub

If you followed all that correctly, you are awesome. Now we will do a similar thing 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.

CometChat Dashboard

Click on the Add New App button to create a new app, you will be presented with a modal where you can enter the app details to be created. An example is seen in the image below.

Add New App Modal

After the app creation, you will be navigated to your app dashboard which should look like this.

App Dashboard

You will also need to copy those keys to a separate file in the manner below. Simply create a file called CONSTANTS.js in the root of the project and paste the code below. Now list this file in the gitIgnore file which is also at the root of this project, this will make sure it won’t be published online.

export const CONSTANTS = {
  APP_ID: 'xxx-xxx-xxx',
  REGION: 'us',
  Auth_Key: 'xxx-xxx-xxx-xxx-xxx-xxx-xxx-xxx',
}
Enter fullscreen mode Exit fullscreen mode

Fantastic, that will be enough for the setups, let’s start integrating them all into our application.

The Components Directory

We have several directories in this project, let's start with the components folder. Create a folder called components within the root of this project. Let's start with the Header component.

Header Component

Header Component

Using the power of react-native-media-query, you will be able to craft out the header component as seen in the image above.

import React from 'react'
import { View, TouchableOpacity } from 'react-native'
import StyleSheet from 'react-native-media-query'
import { Avatar, Input } from 'react-native-elements'
import Icon from 'react-native-vector-icons/FontAwesome'
import { getAuth, signOut } from '../firebase'
import { CometChat } from '@cometchat-pro/react-native-chat'
const Header = ({ navigation }) => {
const auth = getAuth()
const PLACEHOLDER_AVATAR =
'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png'
const signOutUser = async () => {
try {
await signOut(auth).then(() => {
CometChat.logout().then(
() => {
console.log('Logout completed successfully')
},
(error) => {
console.log('Logout failed with exception:', { error })
}
)
})
} catch (error) {
console.log(error)
}
}
return (
<View style={styles.headerWrapper} dataSet={{ media: ids.headerWrapper }}>
<View style={styles.container} dataSet={{ media: ids.container }}>
<View style={styles.headerLeft} dataSet={{ media: ids.headerLeft }}>
<TouchableOpacity onPress={signOutUser} activeOpacity={0.5}>
<Avatar
rounded
source={{
uri: auth?.currentUser?.photoURL || PLACEHOLDER_AVATAR,
}}
/>
</TouchableOpacity>
</View>
<View style={styles.headerCenter} dataSet={{ media: ids.headerCenter }}>
<Input
placeholder="Search"
leftIcon={<Icon name="search" size={24} color="gray" />}
rightIcon={<Icon name="qrcode" size={24} color="gray" />}
containerStyle={{ borderRadius: 7, backgroundColor: '#eff2f7' }}
inputContainerStyle={{ borderBottomWidth: 0 }}
errorStyle={{ margin: 0 }}
/>
</View>
<View style={styles.headerRight} dataSet={{ media: ids.headerRight }}>
<TouchableOpacity
onPress={() => navigation.navigate('ChatsListScreen')}
activeOpacity={0.5}
>
<Icon name="commenting" size={24} color="gray" />
</TouchableOpacity>
</View>
</View>
</View>
)
}
export default Header
const { ids, styles } = StyleSheet.create({
headerWrapper: {
shadowColor: '#171717',
shadowOffsetWidth: 0,
shadowOffsetHeight: 2,
shadowOpacity: 0.2,
shadowRadius: 3,
paddingVertical: 5,
backgroundColor: 'white',
},
container: {
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
'@media (min-width: 990px)': {
width: '50%',
marginVertical: 0,
marginHorizontal: 'auto',
},
'@media (min-width: 690px) and (max-width: 989px)': {
width: '75%',
marginVertical: 0,
marginHorizontal: 'auto',
},
},
headerLeft: {
marginRight: 20,
},
headerCenter: {
flex: 1,
},
headerRight: {
marginLeft: 20,
},
})
view raw Header.js hosted with ❤ by GitHub

Cool, lets add the next component to the components directory.

The BottomTabs Component

BottomTabs

This sticky component is seen at the bottom of the HomeScreen. When you click on the post button, you will be navigated to the AddPostScreen. Create a component called BottomTabs.js and paste the codes below into it. See the code snippet below.

import React, { useState } from 'react'
import { Text, TouchableOpacity, View, Platform } from 'react-native'
import Icon from 'react-native-vector-icons/FontAwesome'
import StyleSheet from 'react-native-media-query'
const BottomTabs = ({ navigation }) => {
const [activeTab, setActiveTab] = useState('Home')
return (
<View style={styles.wrapper} dataSet={{ media: ids.wrapper }}>
<View style={styles.container} dataSet={{ media: ids.container }}>
<TouchableOpacity
dataSet={{ media: ids.iconContainer }}
style={[
styles.iconContainer,
activeTab === 'Home' ? styles.active : styles.inactive,
]}
>
<Icon name="home" size={24} color="gray" />
<Text style={{ color: 'gray' }}>Home</Text>
</TouchableOpacity>
<TouchableOpacity
dataSet={{ media: ids.iconContainer }}
style={[
styles.iconContainer,
activeTab === 'Post' ? styles.active : styles.inactive,
]}
onPress={() => navigation.push('AddPostScreen')}
>
<Icon name="plus-square" size={24} color="gray" />
<Text style={{ color: 'gray' }}>Post</Text>
</TouchableOpacity>
<TouchableOpacity
dataSet={{ media: ids.iconContainer }}
style={[
styles.iconContainer,
activeTab === 'Jobs' ? styles.active : styles.inactive,
]}
>
<Icon name="briefcase" size={24} color="gray" />
<Text style={{ color: 'gray' }}>Jobs</Text>
</TouchableOpacity>
</View>
</View>
)
}
export default BottomTabs
const { ids, styles } = StyleSheet.create({
wrapper: {
shadowColor: '#171717',
shadowOffsetWidth: 0,
shadowOffsetHeight: 2,
shadowOpacity: 0.2,
shadowRadius: 3,
position: 'absolute',
bottom: 0,
backgroundColor: 'white',
...Platform.select({
default: {
position: 'sticky',
},
}),
},
container: {
flexDirection: 'row',
justifyContent: 'space-between',
height: 50,
'@media (min-width: 990px)': {
width: '50%',
marginVertical: 0,
marginHorizontal: 'auto',
},
'@media (min-width: 690px) and (max-width: 989px)': {
width: '75%',
marginVertical: 0,
marginHorizontal: 'auto',
},
},
iconContainer: {
alignItems: 'center',
borderTopWidth: 2,
borderTopColor: 'transparent',
paddingHorizontal: 20,
paddingVertical: 5,
},
active: {
borderTopColor: 'black',
},
inactive: {
borderTopColor: 'transparent',
},
})
view raw BottomTabs.js hosted with ❤ by GitHub

Lastly, lets include the card component for this project.

The Card Component

The Card Component

This is a well-crafted card with many parts to it, it's better to see the code yourself. Create a component called Card.js in the components directory and paste the codes below into it.

import React from 'react'
import { Image, TouchableOpacity, View } from 'react-native'
import { Avatar, Button, Divider, Text } from 'react-native-elements'
import StyleSheet from 'react-native-media-query'
import Icon from 'react-native-vector-icons/FontAwesome5'
import {
getAuth,
getFirestore,
doc,
updateDoc,
arrayUnion,
arrayRemove,
} from '../firebase'
const Card = ({ post }) => {
const auth = getAuth()
const db = getFirestore()
const handleLike = async (post) => {
const currentLikeStatus = !post.liked.includes(auth.currentUser.email)
const likesRef = doc(db, `users/${post.email}/posts`, post.id)
try {
await updateDoc(likesRef, {
liked: currentLikeStatus
? arrayUnion(auth.currentUser.email)
: arrayRemove(auth.currentUser.email),
})
console.log('Document successfully updated!')
} catch (error) {
console.log('Error updating document: ', error)
}
}
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'
}
return (
<View style={[styles.card]} dataSet={{ media: ids.card }}>
<View style={[styles.shadowProp]} dataSet={{ media: ids.container }}>
<View style={styles.container}>
<TouchableOpacity>
<Avatar rounded source={{ uri: post.pic }} />
</TouchableOpacity>
<View style={styles.headerCenter}>
<Text style={{ fontWeight: 600 }}>{post.fullname}</Text>
<Text style={styles.grayText} dataSet={{ media: ids.grayText }}>
{post.profession}
</Text>
<Text style={styles.grayText} dataSet={{ media: ids.grayText }}>
{timeAgo(post.timestamp.toDate())} .{' '}
<Icon name="globe" size={14} color="gray" />{' '}
</Text>
</View>
<TouchableOpacity>
<Icon name="ellipsis-v" size={24} color="gray" />
</TouchableOpacity>
</View>
<View style={styles.container}>
<Text>
{post.description.length > 300
? post.description.slice(0, 300) + '...'
: post.description}
</Text>
</View>
<Image
source={{ uri: post.imgURL }}
style={{ width: '100%', height: 400, resizeMode: 'cover' }}
/>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingVertical: 5,
paddingHorizontal: 10,
}}
>
<View style={styles.iconContainer}>
<View style={[styles.iconWrapper, { backgroundColor: '#378fe9' }]}>
<Icon name="thumbs-up" size={12} color="white" />
</View>
<View style={[styles.iconWrapper, { backgroundColor: '#5e9e43' }]}>
<Icon name="heart" size={12} color="white" />
</View>
<View style={[styles.iconWrapper, { backgroundColor: '#ffcf40' }]}>
<Icon name="lightbulb" size={12} color="white" />
</View>
<Text style={{ marginHorizontal: 5 }}>{post.liked.length}</Text>
</View>
<View>
<Text style={{ color: 'gray' }}>
{post.comments.length}
{post.comments.length == 1 ? ' comment' : ' comments'}
</Text>
</View>
</View>
<View style={{ marginTop: 5, marginBottom: 10, alignItems: 'center' }}>
<Divider style={{ width: '95%' }} />
</View>
<View
style={{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
}}
>
<Button
type="clear"
iconPosition="top"
icon={
<Icon
name="thumbs-up"
size={15}
color={
post.liked.includes(auth.currentUser.email)
? '#378fe9'
: 'gray'
}
/>
}
titleStyle={{
color: post.liked.includes(auth.currentUser.email)
? '#378fe9'
: 'gray',
}}
containerStyle={{ flex: 1 }}
title="Like"
onPress={() => handleLike(post)}
/>
<Button
type="clear"
iconPosition="top"
icon={<Icon name="comment-dots" size={15} color="gray" />}
titleStyle={{ color: 'gray' }}
containerStyle={{ flex: 1 }}
title="Comment"
/>
<Button
type="clear"
iconPosition="top"
icon={<Icon name="share" size={15} color="gray" />}
titleStyle={{ color: 'gray' }}
containerStyle={{ flex: 1 }}
title="Share"
/>
<Button
type="clear"
iconPosition="top"
icon={<Icon name="paper-plane" size={15} color="gray" />}
titleStyle={{ color: 'gray' }}
containerStyle={{ flex: 1 }}
title="Send"
/>
</View>
</View>
</View>
)
}
export default Card
const { ids, styles } = StyleSheet.create({
card: {
paddingBottom: 20,
paddingHorizontal: 25,
width: '100%',
marginVertical: 10,
},
shadowProp: {
shadowColor: '#171717',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.15,
shadowRadius: 5,
borderRadius: 8,
backgroundColor: 'white',
},
container: {
flexDirection: 'row',
padding: 5,
alignItems: 'center',
paddingVertical: 10,
paddingHorizontal: 20,
},
headerCenter: {
flex: 1,
marginRight: 20,
marginLeft: 10,
},
grayText: {
color: 'gray',
},
iconContainer: {
flexDirection: 'row',
alignItems: 'center',
},
iconWrapper: {
padding: 5,
borderRadius: 50,
backgroundColor: 'gray',
width: 23,
height: 23,
textAlign: 'center',
marginVertical: 5,
marginHorizontal: 2.5,
},
})
view raw Card.js hosted with ❤ by GitHub

Awesome, we are just done with the components directory, its time we create the screens.

The Screens Directory

The screens can be likened to pages on a website, each screen represents a page and you can navigate from screen to screen using the ReactNative navigator package. Let's proceed with the SignupScreen.

The Signup Screen

The Signup Screen

This screen is responsible for creating new users. It interfaces between Firebase authentication and CometChat’s. With this screen, you will be able to signup new users to Firebase, and also to CometChat all at once. See the codes below.

import React from 'react'
import StyleSheet from 'react-native-media-query'
import {
SafeAreaView,
View,
Image,
ScrollView,
Platform,
Alert,
} from 'react-native'
import { Button, Divider, Input, Text } from 'react-native-elements'
import Icon from 'react-native-vector-icons/FontAwesome'
import { Formik } from 'formik'
import * as Yup from 'yup'
import {
getAuth,
createUserWithEmailAndPassword,
getFirestore,
setDoc,
doc,
updateProfile,
} from '../firebase'
import { CometChat } from '@cometchat-pro/react-native-chat'
import { CONSTANTS } from '../CONSTANTS'
const signupFormSchema = Yup.object().shape({
email: Yup.string().email().required('A email is required.'),
fullname: Yup.string()
.required('A fullname is required.')
.min(3, 'fullname needs to be at least 3 characters long.'),
profession: Yup.string()
.required('A profession is required.')
.min(5, 'Profession needs to be at least 5 characters long.')
.max(20, 'Profession exceeded 20 characters.'),
imgURL: Yup.string()
.matches(
/(http(s?):\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.gif|.jpeg)(\?[^\s[",><]*)?/g,
'Enter a correct image URL'
)
.required('An image URL is required'),
password: Yup.string()
.required('A password is required.')
.min(6, 'Password needs to be at least 6 characters long.'),
})
const SignupScreen = ({ navigation }) => {
const auth = getAuth()
const db = getFirestore()
const onSignup = async (email, password, fullname, profession, imgURL) => {
try {
const authed = await createUserWithEmailAndPassword(auth, email, password)
const userDocRef = doc(db, 'users', authed.user.email)
await setDoc(userDocRef, {
fullname,
email,
profession,
pic: imgURL,
uid: authed.user.uid,
}).then(() => {
signInWithCometChat(authed.user.uid, fullname, imgURL)
updateUserProfile(authed.user, fullname, imgURL)
console.log('Firebase Signed Up Successful...')
})
} catch (error) {
Platform.OS != 'web' ? Alert.alert(error.message) : alert(error.message)
}
}
const updateUserProfile = (user, displayName, photoURL) => {
updateProfile(user, {
displayName,
photoURL,
})
.then(() => console.log('Profile Updated!'))
.catch((error) => console.log(error.message))
}
const signInWithCometChat = (UID, name, avatar) => {
let authKey = CONSTANTS.Auth_Key
let user = new CometChat.User(UID)
user.setName(name)
user.setAvatar(avatar)
CometChat.createUser(user, authKey).then(
(user) => {
console.log('user created', user)
loginWithCometChat(UID)
},
(error) => {
console.log('error', error)
}
)
}
const loginWithCometChat = (UID) => {
const authKey = CONSTANTS.Auth_Key
CometChat.login(UID, authKey).then(
(user) => {
console.log('Login Successful:', { user })
},
(error) => {
console.log('Login failed with exception:', { error })
}
)
}
return (
<SafeAreaView style={styles.container} dataSet={{ media: ids.container }}>
<View style={styles.header} dataSet={{ media: ids.header }}>
<Image
style={styles.logo}
dataSet={{ media: ids.logo }}
source={require('../assets/logo.png')}
/>
<Button
onPress={() => navigation.navigate('LoginScreen')}
title="Login"
color="#016bb4"
type="clear"
/>
</View>
<ScrollView>
<Formik
initialValues={{
email: '',
password: '',
fullname: '',
profession: '',
imgURL: '',
}}
onSubmit={(values) =>
onSignup(
values.email,
values.password,
values.fullname,
values.profession,
values.imgURL
)
}
validationSchema={signupFormSchema}
validateOnMount={true}
>
{({
handleBlur,
handleChange,
handleSubmit,
values,
errors,
isValid,
}) => (
<View
style={styles.formContainer}
dataSet={{ media: ids.formContainer }}
>
<Text
h2
style={{ fontWeight: 500, marginTop: 40, marginBottom: 10 }}
>
Sign up
</Text>
<View>
<Input
style={{ marginBottom: 15 }}
containerStyle={{
paddingHorizontal: 0,
}}
errorStyle={{ color: 'red' }}
errorMessage={values.email.length >= 4 ? errors.email : ''}
placeholder="Email or Phone"
autoCapitalize="none"
keyboardType="email-address"
textContentType="emailAddress"
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
value={values.email}
/>
<Input
style={{ marginBottom: 15 }}
containerStyle={{
paddingHorizontal: 0,
}}
errorStyle={{ color: 'red' }}
errorMessage={
values.fullname.length >= 2 ? errors.fullname : ''
}
placeholder="fullname"
autoCapitalize="none"
textContentType="fullname"
onChangeText={handleChange('fullname')}
onBlur={handleBlur('fullname')}
value={values.fullname}
/>
<Input
style={{ marginBottom: 15 }}
containerStyle={{
paddingHorizontal: 0,
}}
errorStyle={{ color: 'red' }}
errorMessage={
values.profession.length >= 3 ? errors.profession : ''
}
placeholder="Profession"
autoCapitalize="none"
textContentType="profession"
onChangeText={handleChange('profession')}
onBlur={handleBlur('profession')}
value={values.profession}
/>
<Input
style={{ marginBottom: 15 }}
containerStyle={{
paddingHorizontal: 0,
}}
errorStyle={{ color: 'red' }}
errorMessage={values.imgURL.length >= 1 ? errors.imgURL : ''}
placeholder="Image URL"
autoCapitalize="none"
textContentType="imgURL"
onChangeText={handleChange('imgURL')}
onBlur={handleBlur('imgURL')}
value={values.imgURL}
/>
<Input
style={{ marginBottom: 15 }}
containerStyle={{
paddingHorizontal: 0,
}}
errorStyle={{ color: 'red' }}
errorMessage={
values.password.length >= 3 ? errors.password : ''
}
rightIcon={<Icon name="eye" size={15} color="gray" />}
placeholder="Password"
secureTextEntry={true}
autoCapitalize="none"
textContentType="password"
autoCorrect={false}
onChangeText={handleChange('password')}
onBlur={handleBlur('password')}
value={values.password}
/>
<Button
title="Sign Up"
color="white"
containerStyle={{ borderRadius: 20, marginTop: 10 }}
buttonStyle={{ backgroundColor: '#016bb4' }}
onPress={handleSubmit}
disabled={!isValid}
/>
</View>
<Divider
width={1}
orientation="horizontal"
style={{
marginVertical: 30,
borderColor: '#e7e7e9',
position: 'relative',
}}
/>
<Button
icon={
<Icon
name="google"
size={15}
color="black"
style={{ marginRight: 10 }}
/>
}
title="Sign up with Google"
iconLeft
type="outline"
buttonStyle={{ borderColor: 'gray', borderRadius: 20 }}
style={{ marginBottom: 10, color: 'gray' }}
titleStyle={{ color: 'gray' }}
/>
<Button
icon={
<Icon
name="apple"
size={15}
color="black"
style={{ marginRight: 10 }}
/>
}
title="Sign up with Apple"
iconLeft
type="outline"
buttonStyle={{ borderColor: 'gray', borderRadius: 20 }}
style={{ marginBottom: 20, color: 'gray' }}
titleStyle={{ color: 'gray' }}
/>
</View>
)}
</Formik>
</ScrollView>
</SafeAreaView>
)
}
export default SignupScreen
const { ids, styles } = StyleSheet.create({
container: {
paddingHorizontal: 20,
paddingTop: 30,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
},
logo: {
width: 100,
resizeMode: 'contain',
},
formContainer: {
justifyContent: 'center',
'@media (min-width: 990px)': {
width: '50%',
marginVertical: 0,
marginHorizontal: 'auto',
},
'@media (min-width: 690px) and (max-width: 989px)': {
width: '75%',
marginVertical: 0,
marginHorizontal: 'auto',
},
},
})
view raw SignupScreen.js hosted with ❤ by GitHub

The Login Screen

The Login Screen

This screen authenticates already existing users in our platform. When you log in with your correct details, you will be authenticated both with Firebase and CometChat. If you have supplied the correct data you will be let into the system, otherwise, you will be kicked out. Check out the codes below.

import React from 'react'
import StyleSheet from 'react-native-media-query'
import { SafeAreaView, View, Image, Platform, Alert } from 'react-native'
import { Button, CheckBox, Divider, Input, Text } from 'react-native-elements'
import Icon from 'react-native-vector-icons/FontAwesome'
import { Formik } from 'formik'
import * as Yup from 'yup'
import { getAuth, signInWithEmailAndPassword } from '../firebase'
import { CometChat } from '@cometchat-pro/react-native-chat'
import { CONSTANTS } from '../CONSTANTS'
const loginFormSchema = Yup.object().shape({
email: Yup.string().email().required('A email is required'),
password: Yup.string()
.required('A password is required')
.min(6, 'Password needs to be at least 6 characters long'),
})
const LoginScreen = ({ navigation }) => {
const auth = getAuth()
const onLogin = async (email, password) => {
try {
const authed = await signInWithEmailAndPassword(auth, email, password)
loginWithCometChat(authed.user.uid)
console.log('Firebase Login Successful')
} catch (error) {
Platform.OS != 'web' ? Alert.alert(error.message) : alert(error.message)
}
}
const loginWithCometChat = (UID) => {
const authKey = CONSTANTS.Auth_Key
CometChat.login(UID, authKey).then(
(user) => {
console.log('Login Successful:', { user })
},
(error) => {
console.log('Login failed with exception:', { error })
}
)
}
return (
<SafeAreaView style={styles.container} dataSet={{ media: ids.container }}>
<View style={styles.header} dataSet={{ media: ids.header }}>
<Image
style={styles.logo}
dataSet={{ media: ids.logo }}
source={require('../assets/logo.png')}
/>
<Button
onPress={() => navigation.navigate('SignupScreen')}
title="Join now"
color="#016bb4"
type="clear"
/>
</View>
<Formik
initialValues={{ email: '', password: '' }}
onSubmit={(values) => onLogin(values.email, values.password)}
validationSchema={loginFormSchema}
validateOnMount={true}
>
{({
handleBlur,
handleChange,
handleSubmit,
values,
errors,
isValid,
}) => (
<View
style={styles.formContainer}
dataSet={{ media: ids.formContainer }}
>
<Text
h2
style={{ fontWeight: 500, marginTop: 40, marginBottom: 10 }}
>
Sign in
</Text>
<View>
<Input
style={{ marginBottom: 15 }}
containerStyle={{
paddingHorizontal: 0,
}}
errorStyle={{ color: 'red' }}
errorMessage={values.email.length > 4 ? errors.email : ''}
placeholder="Email or Phone"
autoCapitalize="none"
keyboardType="email-address"
textContentType="emailAddress"
onChangeText={handleChange('email')}
onBlur={handleBlur('email')}
value={values.email}
/>
<Input
style={{ marginBottom: 15 }}
containerStyle={{
paddingHorizontal: 0,
}}
errorStyle={{ color: 'red' }}
errorMessage={
values.password.length >= 3 ? errors.password : ''
}
rightIcon={<Icon name="eye" size={15} color="gray" />}
placeholder="Password"
secureTextEntry={true}
autoCapitalize="none"
textContentType="password"
autoCorrect={false}
onChangeText={handleChange('password')}
onBlur={handleBlur('password')}
value={values.password}
/>
<CheckBox
title="Remember me"
checked={true}
checkedColor="#008400"
containerStyle={{
backgroundColor: 'transparent',
borderColor: 'transparent',
marginHorizontal: 0,
padding: 0,
}}
style={{ marginVertical: 15 }}
/>
<Button
title="Forget Password?"
color="#016bb4"
type="clear"
containerStyle={{ alignItems: 'flex-start' }}
style={{ marginVertical: 10 }}
/>
<Button
title="Sign In"
color="white"
containerStyle={{ borderRadius: 20 }}
buttonStyle={{ backgroundColor: '#016bb4' }}
onPress={handleSubmit}
disabled={!isValid}
/>
</View>
<Divider
width={1}
orientation="horizontal"
style={{
marginVertical: 30,
borderColor: '#e7e7e9',
position: 'relative',
}}
/>
<Button
icon={
<Icon
name="google"
size={15}
color="black"
style={{ marginRight: 10 }}
/>
}
title="Sign in with Google"
iconLeft
type="outline"
buttonStyle={{ borderColor: 'gray', borderRadius: 20 }}
style={{ marginBottom: 10, color: 'gray' }}
titleStyle={{ color: 'gray' }}
/>
<Button
icon={
<Icon
name="apple"
size={15}
color="black"
style={{ marginRight: 10 }}
/>
}
title="Sign in with Apple"
iconLeft
type="outline"
buttonStyle={{ borderColor: 'gray', borderRadius: 20 }}
style={{ marginBottom: 10, color: 'gray' }}
titleStyle={{ color: 'gray' }}
/>
</View>
)}
</Formik>
</SafeAreaView>
)
}
export default LoginScreen
const { ids, styles } = StyleSheet.create({
container: {
paddingHorizontal: 20,
paddingTop: 30,
},
header: {
flexDirection: 'row',
justifyContent: 'space-between',
},
logo: {
width: 100,
resizeMode: 'contain',
},
formContainer: {
justifyContent: 'center',
'@media (min-width: 990px)': {
width: '50%',
marginVertical: 0,
marginHorizontal: 'auto',
},
'@media (min-width: 690px) and (max-width: 989px)': {
width: '75%',
marginVertical: 0,
marginHorizontal: 'auto',
},
},
})
view raw LoginScreen.js hosted with ❤ by GitHub

The HomeScreen

The Home Screen

The Home Screen comprises three components. The Header, Card, and BottomTabs components. This screen dynamically renders the Card component in synchronization with the posts in Firestore. See the code snippet below.

import React, { useEffect, useState } from 'react'
import { StatusBar } from 'expo-status-bar'
import { SafeAreaView, ScrollView } from 'react-native'
import StyleSheet from 'react-native-media-query'
import Header from '../components/Header'
import Card from '../components/Card'
import BottomTabs from '../components/BottomTabs'
import {
collectionGroup,
getFirestore,
getDocs,
query,
orderBy,
} from '../firebase'
const HomeScreen = ({ navigation }) => {
const [posts, setPosts] = useState([])
const db = getFirestore()
const getPosts = async () => {
const posts = query(
collectionGroup(db, 'posts'),
orderBy('timestamp', 'desc')
)
const snapshot = await getDocs(posts)
setPosts(snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })))
}
useEffect(() => getPosts(), [navigation])
return (
<SafeAreaView style={{ height: '100%', backgroundColor: '#f3f2ef' }}>
<StatusBar style="light" />
<Header navigation={navigation} />
<ScrollView
style={[styles.container, { paddingTop: 30 }]}
dataSet={{ media: ids.container }}
>
{posts.map((post, index) => (
<Card post={post} key={index} />
))}
</ScrollView>
<BottomTabs navigation={navigation} />
</SafeAreaView>
)
}
export default HomeScreen
const { ids, styles } = StyleSheet.create({
container: {
'@media (min-width: 990px)': {
width: '50%',
marginVertical: 0,
marginHorizontal: 'auto',
paddingVertical: 10,
},
'@media (min-width: 690px) and (max-width: 989px)': {
width: '75%',
marginVertical: 0,
marginHorizontal: 'auto',
},
},
})
view raw HomeScreen.js hosted with ❤ by GitHub

The AddPostScreen

The AddPost Screen

This screen is responsible for creating new posts. Using Formik and Yup, we collect data from a form and save it to the Firestore database. Here are the codes exhibiting this behavior.

import React, { useEffect, useState } from 'react'
import { View, TouchableOpacity, SafeAreaView } from 'react-native'
import { Text, Button, Avatar, Input } from 'react-native-elements'
import StyleSheet from 'react-native-media-query'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { Formik } from 'formik'
import * as Yup from 'yup'
import {
getAuth,
collection,
addDoc,
getDoc,
doc,
getFirestore,
serverTimestamp,
} from '../firebase'
const addPostFormSchema = Yup.object().shape({
title: Yup.string()
.required('A post title is required')
.min(6, 'Post title needs to be at least 6 characters long'),
imgURL: Yup.string()
.matches(
/(http(s?):\/\/)([^\s(["<,>/]*)(\/)[^\s[",><]*(.png|.jpg|.gif)(\?[^\s[",><]*)?/g,
'Enter a correct image URL'
)
.required('An image URL is required'),
description: Yup.string()
.required('A post description is required')
.min(10, 'Post description needs to be at least 10 characters long'),
})
const AddPostScreen = ({ navigation }) => {
const [profile, setProfile] = useState(null)
const auth = getAuth()
const db = getFirestore()
useEffect(() => getProfile(), [])
const getProfile = async () => {
const userDocRef = doc(db, `users/${auth.currentUser.email}`)
const docSnap = await getDoc(userDocRef)
const data = docSnap.data()
setProfile({
fullname: data.fullname,
pic: data.pic,
uid: data.uid,
email: data.email,
})
}
const addPost = async (title, imgURL, description) => {
try {
await addDoc(collection(db, `users/${profile.email}`, 'posts'), {
timestamp: serverTimestamp(),
fullname: profile.fullname,
pic: profile.pic,
uid: profile.uid,
email: profile.email,
title,
description,
imgURL,
liked: [],
comments: [],
})
navigation.goBack()
} catch (error) {
console.log(error.message)
}
}
return (
<SafeAreaView>
<Header navigation={navigation} />
<View style={styles.container} dataSet={{ media: ids.container }}>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginVertical: 20,
}}
>
<Avatar rounded source={{ uri: profile?.pic }} />
<View style={{ marginLeft: 10 }}>
<Text style={{ fontWeight: 500 }}>{profile?.fullname}</Text>
<Button
icon={
<Icon
name="caret-down"
size={24}
color="gray"
style={{ marginLeft: 5 }}
/>
}
iconRight
title="Anyone"
type="outline"
buttonStyle={{
borderColor: 'gray',
borderRadius: 20,
padding: 0,
}}
titleStyle={{ color: 'black', fontWeight: 700, fontSize: 14 }}
/>
</View>
</View>
<Formik
initialValues={{
title: '',
imgURL: '',
description: '',
}}
onSubmit={(values) => {
addPost(values.title, values.imgURL, values.description)
}}
validationSchema={addPostFormSchema}
validateOnMount={true}
>
{({
handleBlur,
handleChange,
handleSubmit,
values,
errors,
isValid,
}) => (
<View>
<Input
placeholder="Post Title*"
errorStyle={{ color: 'red' }}
errorMessage={values.title.length >= 4 ? errors.title : ''}
autoCapitalize="none"
keyboardType="title"
textContentType="titlePost"
onChangeText={handleChange('title')}
onBlur={handleBlur('title')}
value={values.title}
/>
<Input
placeholder="Post Image URL*"
errorStyle={{ color: 'red' }}
errorMessage={values.imgURL.length >= 4 ? errors.imgURL : ''}
autoCapitalize="none"
keyboardType="imgURL"
textContentType="imgURLPost"
onChangeText={handleChange('imgURL')}
onBlur={handleBlur('imgURL')}
value={values.imgURL}
/>
<Input
placeholder="Share an article, photo, video, or an idea*"
multiline={true}
numberOfLines={4}
errorStyle={{ color: 'red' }}
errorMessage={
values.description.length >= 4 ? errors.description : ''
}
autoCapitalize="none"
keyboardType="description"
textContentType="descriptionPost"
onChangeText={handleChange('description')}
onBlur={handleBlur('description')}
value={values.description}
inputContainerStyle={{ borderBottomWidth: 0 }}
/>
<Button
title="Post"
color="white"
containerStyle={{ borderRadius: 20, marginTop: 10 }}
buttonStyle={{ backgroundColor: '#016bb4' }}
onPress={handleSubmit}
disabled={!isValid}
/>
</View>
)}
</Formik>
<View style={{ height: '25%' }}></View>
<Footer />
</View>
</SafeAreaView>
)
}
const Header = ({ navigation }) => (
<View style={styles.headerWrapper} dataSet={{ media: ids.headerWrapper }}>
<View
style={
(styles.container,
{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
paddingHorizontal: 20,
})
}
dataSet={{ media: ids.container }}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Icon name="times" size={24} color="gray" />
</TouchableOpacity>
<Text style={{ fontWeight: 500, marginLeft: 20, fontSize: 16 }}>
Share post
</Text>
</View>
<TouchableOpacity>
<Button title="Post" type="clear" titleStyle={{ color: 'gray' }} />
</TouchableOpacity>
</View>
</View>
)
const Footer = () => (
<>
<Text style={{ color: '#016bb4', fontWeight: 600 }}>Add hashtag</Text>
<View style={[styles.flexify, { marginTop: 10 }]}>
<View style={styles.flexify}>
<Icon
style={{ marginRight: 20 }}
name="camera"
size={24}
color="gray"
/>
<Icon style={{ marginRight: 20 }} name="video" size={24} color="gray" />
<Icon style={{ marginRight: 20 }} name="image" size={24} color="gray" />
<Icon
style={{ marginRight: 20 }}
name="ellipsis-h"
size={24}
color="gray"
/>
</View>
<View style={styles.flexify}>
<Icon
style={{ marginRight: 10 }}
name="comment"
size={24}
color="gray"
/>
<Text>Anyone</Text>
</View>
</View>
</>
)
export default AddPostScreen
const { ids, styles } = StyleSheet.create({
headerWrapper: {
shadowColor: '#171717',
shadowOffsetWidth: 0,
shadowOffsetHeight: 2,
shadowOpacity: 0.2,
shadowRadius: 3,
paddingVertical: 5,
backgroundColor: 'white',
},
container: {
paddingHorizontal: 20,
height: '100%',
'@media (min-width: 990px)': {
width: '50%',
marginVertical: 0,
marginHorizontal: 'auto',
},
'@media (min-width: 690px) and (max-width: 989px)': {
width: '75%',
marginVertical: 0,
marginHorizontal: 'auto',
},
},
flexify: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
})

The ChatsListScreen

The Chats List Screen

The ChatList Screen is responsible for listing all users registered on our platform. Using the CometChat SDK we will retrieve the list of users registered with us. This is how the code looks like.

import React, { useState, useEffect } from 'react'
import StyleSheet from 'react-native-media-query'
import {
Text,
SafeAreaView,
View,
TouchableOpacity,
ScrollView,
} from 'react-native'
import { Avatar, Input, Overlay } from 'react-native-elements'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { CometChat } from '@cometchat-pro/react-native-chat'
const ChatsListScreen = ({ navigation }) => {
const [users, setUsers] = useState([])
useEffect(() => getChatList(), [])
const getChatList = () => {
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)
})
}
return (
<SafeAreaView>
<Header navigation={navigation} />
<ScrollView style={styles.container} dataSet={{ media: ids.container }}>
<Search />
{users.map((user, index) => (
<ListItem key={index} user={user} navigation={navigation} />
))}
</ScrollView>
</SafeAreaView>
)
}
const Header = ({ navigation }) => {
const [visible, setVisible] = useState(false)
const [email, setEmail] = useState('')
const toggleOverlay = () => {
setVisible(!visible)
}
const addFriend = (email) => {
console.log(email)
}
return (
<View style={styles.headerWrapper} dataSet={{ media: ids.headerWrapper }}>
<View
style={[
styles.container,
{
flexDirection: 'row',
justifyContent: 'space-between',
alignItems: 'center',
},
]}
dataSet={{ media: ids.container }}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Icon name="long-arrow-alt-left" size={24} color="gray" />
</TouchableOpacity>
<Text style={{ fontWeight: 500, marginLeft: 20 }} h4>
Messaging
</Text>
</View>
<View style={styles.flexify}>
<TouchableOpacity style={{ marginRight: 40 }}>
<Icon name="ellipsis-v" size={24} color="gray" />
</TouchableOpacity>
<TouchableOpacity onPress={toggleOverlay}>
<Icon name="edit" size={24} color="gray" />
</TouchableOpacity>
</View>
</View>
<Overlay isVisible={visible} onBackdropPress={toggleOverlay}>
<View
style={[styles.flexify, { marginHorizontal: 5, marginTop: 10 }]}
dataSet={{ media: ids.flexify }}
>
<Input
placeholder="Add user by email..."
leftIcon={<Icon name="user" size={18} color="gray" />}
autoCapitalize="none"
keyboardType="email-address"
textContentType="emailAddress"
onChangeText={(text) => setEmail(text)}
onSubmitEditing={() => addFriend(email)}
value={email}
/>
</View>
</Overlay>
</View>
)
}
const Search = () => (
<View
style={[styles.flexify, { marginHorizontal: 5, marginTop: 10 }]}
dataSet={{ media: ids.flexify }}
>
<Input
placeholder="Search messages"
leftIcon={<Icon name="search" size={24} color="gray" />}
inputContainerStyle={{ borderBottomWidth: 0 }}
/>
<Icon name="sort-alpha-down" size={24} color="gray" />
</View>
)
const ListItem = ({ navigation, user }) => (
<TouchableOpacity
style={[styles.flexify, styles.bordered]}
dataSet={{ media: ids.flexify }}
onPress={() =>
navigation.navigate('ChatScreen', {
id: user.uid,
name: user.name,
avatar: user.avatar,
})
}
>
<View style={styles.flexify} dataSet={{ media: ids.flexify }}>
<Avatar rounded source={{ uri: user.avatar }} />
<View style={{ marginLeft: 10 }}>
<Text h4 style={{ fontWeight: 600 }}>
{user.name}
</Text>
{/* <Text>Nice to meet you too!</Text> */}
</View>
</View>
{/* <Text>Nov 12</Text> */}
</TouchableOpacity>
)
export default ChatsListScreen
const { ids, styles } = StyleSheet.create({
headerWrapper: {
shadowColor: '#171717',
shadowOffsetWidth: 0,
shadowOffsetHeight: 2,
shadowOpacity: 0.2,
shadowRadius: 3,
paddingVertical: 15,
backgroundColor: 'white',
},
container: {
paddingHorizontal: 20,
height: '100%',
'@media (min-width: 990px)': {
width: '50%',
marginVertical: 0,
marginHorizontal: 'auto',
},
'@media (min-width: 690px) and (max-width: 989px)': {
width: '75%',
marginVertical: 0,
marginHorizontal: 'auto',
},
},
flexify: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
bordered: {
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: '#f5f5f5',
paddingVertical: 10,
},
})

The ChatScreen

The Chat Screen

Lastly, we have the chat screen where users engage in a one-on-one chat. Below is the code for it.

import { CometChat } from '@cometchat-pro/react-native-chat'
import React, { useEffect, useRef, useState } from 'react'
import {
SafeAreaView,
ScrollView,
Text,
TouchableOpacity,
View,
} from 'react-native'
import { Avatar, Input } from 'react-native-elements'
import StyleSheet from 'react-native-media-query'
import Icon from 'react-native-vector-icons/FontAwesome5'
import { getAuth } from '../firebase'
import InvertibleScrollView from 'react-native-invertible-scroll-view'
import { vh } from 'react-native-expo-viewport-units'
const auth = getAuth()
const ChatScreen = ({ navigation, route }) => {
const [messages, setMessages] = useState([])
const [message, setMessage] = useState('')
const scrollViewRef = useRef()
useEffect(() => {
getMessages()
listenForMessage()
}, [route])
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),
(error) => {
console.log('Message fetching failed with error:', error)
}
)
}
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 listenForMessage = () => {
const listenerID = Math.random().toString(16).slice(2)
CometChat.addMessageListener(
listenerID,
new CometChat.MessageListener({
onTextMessageReceived: (message) => {
setMessages((prevState) => [...prevState, message])
},
})
)
}
return (
<SafeAreaView style={{ position: 'relative' }}>
<Header navigation={navigation} route={route} />
<InvertibleScrollView
ref={scrollViewRef}
onContentSizeChange={(width, height) =>
scrollViewRef.current.scrollTo({ y: height })
}
style={([styles.container], { paddingVertical: 20, height: vh(90) })}
dataSet={{ media: ids.container }}
>
{messages.map((message, index) => (
<Message
key={index}
message={message}
iAmSender={auth.currentUser.uid == message.receiverID}
isLastMessage={messages.at(-1) == message}
/>
))}
</InvertibleScrollView>
{/* Bottom Input */}
<View
style={[
styles.headerWrapper,
{ position: 'absolute', bottom: 0, left: 0, right: 0 },
]}
dataSet={{ media: ids.headerWrapper }}
>
<View style={styles.container} dataSet={{ media: ids.container }}>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
<TouchableOpacity
style={[styles.shadow, { padding: 10, borderRadius: 50 }]}
>
<Icon name="plus" size={24} color="blue" />
</TouchableOpacity>
<Input
placeholder="Write a message..."
inputContainerStyle={{ borderBottomWidth: 0 }}
containerStyle={{
backgroundColor: '#dedede',
marginHorizontal: 15,
}}
onSubmitEditing={() => sendMessage()}
onChangeText={(text) => setMessage(text)}
value={message}
/>
<TouchableOpacity style={{ padding: 10, borderRadius: 50 }}>
<Icon name="microphone" size={24} color="gray" />
</TouchableOpacity>
</View>
</View>
</View>
</SafeAreaView>
)
}
const Header = ({ navigation, route }) => {
return (
<View style={styles.headerWrapper} dataSet={{ media: ids.headerWrapper }}>
<View
style={[styles.container, styles.flexify, { paddingHorizontal: 0 }]}
dataSet={{ media: ids.container }}
>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'center',
}}
>
<TouchableOpacity onPress={() => navigation.goBack()}>
<Icon name="long-arrow-alt-left" size={24} color="gray" />
</TouchableOpacity>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
marginLeft: 20,
}}
>
<Avatar rounded source={{ uri: route.params.avatar }} />
<Text style={{ fontWeight: 500, marginLeft: 5 }} h4>
{route.params.name}
</Text>
</View>
</View>
<View style={styles.flexify}>
<TouchableOpacity style={{ marginRight: 40 }}>
<Icon name="ellipsis-v" size={24} color="gray" />
</TouchableOpacity>
<TouchableOpacity>
<Icon name="video" size={24} color="gray" />
</TouchableOpacity>
</View>
</View>
</View>
)
}
const Message = ({ message, iAmSender, isLastMessage }) => {
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
}
return (
<View
style={[
styles.flexify,
{ justifyContent: 'flex-start' },
isLastMessage ? { marginBottom: 70 } : { marginBottom: 10 },
]}
>
<Avatar
rounded
source={{
uri: iAmSender ? message.receiver?.avatar : message.sender?.avatar,
}}
/>
<View style={{ marginLeft: 10 }}>
<View style={{ flexDirection: 'row', alignItems: 'center' }}>
<Text h4 style={{ fontWeight: 500 }}>
{iAmSender ? message.receiver?.name : message.sender?.name}
</Text>
<Text style={{ marginLeft: 5, fontSize: 10, color: 'gray' }}>
{dateToTime(new Date(message.sentAt * 1000))}
</Text>
</View>
<Text>{message.text}</Text>
</View>
</View>
)
}
export default ChatScreen
const { ids, styles } = StyleSheet.create({
headerWrapper: {
shadowColor: '#171717',
shadowOffsetWidth: 0,
shadowOffsetHeight: 2,
shadowOpacity: 0.2,
shadowRadius: 3,
paddingVertical: 15,
backgroundColor: 'white',
},
container: {
paddingHorizontal: 20,
height: '100%',
'@media (min-width: 990px)': {
width: '50%',
marginVertical: 0,
marginHorizontal: 'auto',
},
'@media (min-width: 690px) and (max-width: 989px)': {
width: '75%',
marginVertical: 0,
marginHorizontal: 'auto',
},
},
flexify: {
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
},
bordered: {
borderTopWidth: 1,
borderBottomWidth: 1,
borderColor: '#f5f5f5',
paddingVertical: 10,
},
shadow: {
shadowColor: '#171717',
shadowOffsetWidth: 0,
shadowOffsetHeight: 2,
shadowOpacity: 0.2,
shadowRadius: 3,
backgroundColor: 'white',
paddingVertical: 5,
paddingHorizontal: 7,
borderRadius: 50,
},
})
view raw ChatScreen.js hosted with ❤ by GitHub

Setting Up The Router

Now that we have the project all coded, let's set up the navigation routers and guards, create and paste the following codes as directed below.

The Navigation file
This categorizes the screens into two groups, the ones requiring authentication and the others not requiring authentication.
Create a new file in the root of the project name “navigation.js” and paste the codes below inside of it.

import React from 'react'
import { createStackNavigator } from '@react-navigation/stack'
import { NavigationContainer } from '@react-navigation/native'
import HomeScreen from './screens/HomeScreen'
import AddPostScreen from './screens/AddPostScreen'
import LoginScreen from './screens/LoginScreen'
import SignupScreen from './screens/SignupScreen'
import ChatScreen from './screens/ChatScreen'
import ChatsListScreen from './screens/ChatsListScreen'
const Stack = createStackNavigator()
const screenOption = {
headerShown: false,
cardStyle: { backgroundColor: '#fff' },
}
export const SignedInStack = () => (
<NavigationContainer>
<Stack.Navigator initialRouteName="HomeScreen" screenOptions={screenOption}>
<Stack.Screen name="HomeScreen" component={HomeScreen} />
<Stack.Screen name="AddPostScreen" component={AddPostScreen} />
<Stack.Screen name="ChatScreen" component={ChatScreen} />
<Stack.Screen name="ChatsListScreen" component={ChatsListScreen} />
</Stack.Navigator>
</NavigationContainer>
)
export const SignedOutStack = () => (
<NavigationContainer>
<Stack.Navigator
initialRouteName="LoginScreen"
screenOptions={screenOption}
>
<Stack.Screen name="LoginScreen" component={LoginScreen} />
<Stack.Screen name="SignupScreen" component={SignupScreen} />
</Stack.Navigator>
</NavigationContainer>
)
view raw navigation.js hosted with ❤ by GitHub

The AuthNavigation file
This file logically presents screens to you based on the authState of the firebase authentication service. To proceed, create another file in the root of the project name “AuthNavigation.js” and paste the codes below inside of it.

import React, { useEffect, useState } from 'react'
import { SignedInStack, SignedOutStack } from './navigation'
import { onAuthStateChanged, getAuth } from './firebase'
const AuthNavigation = () => {
const [currentUser, setCurrentUser] = useState(null)
const auth = getAuth()
const userHandler = (user) =>
user ? setCurrentUser(user) : setCurrentUser(null)
useEffect(() => onAuthStateChanged(auth, (user) => userHandler(user)), [])
return <>{currentUser ? <SignedInStack /> : <SignedOutStack />}</>
}
export default AuthNavigation

The App file
Lastly, replace the codes in the App.js file with the following codes.

import React, { useEffect } from 'react'
import { CometChat } from '@cometchat-pro/react-native-chat'
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')
},
(error) => {
console.log('Initialization failed with error:', error)
}
)
}
useEffect(() => initCometChat(), [])
return <AuthNavigation />
}
view raw App.js hosted with ❤ by GitHub

Congratulations, you just crushed this app, you just need to bring in some of the static images.

Download the following images and put them in your assets directory.

https://raw.githubusercontent.com/Daltonic/linkedIn-clone/main/assets/logo.png

https://raw.githubusercontent.com/Daltonic/linkedIn-clone/main/assets/avatar.jpg

https://raw.githubusercontent.com/Daltonic/linkedIn-clone/main/assets/default-avatar.jpg

Awesome, 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
Enter fullscreen mode Exit fullscreen mode




Conclusion

Nothing is impossible here; you can crush a chat app with ReactNative, Firebase, and CometChat. You've seen how to implement it step by step; now it's time to crush other chat apps on your own. I also have other tutorials that will show you how to create a private or public group chat. I'm looking forward to seeing your stunning creations.

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, ReactNative, VueJs, and more.

He is currently freelancing, building apps for clients, and 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.

Sentry image

See why 4M developers consider Sentry, “not bad.”

Fixing code doesn’t have to be the worst part of your day. Learn how Sentry can help.

Learn more

Top comments (1)

Collapse
 
sebduta profile image
Sebastian Duta • Edited

Great article, thank you for working on it! This react native chat app is a nice Messenger clone with a ton of advanced features