10

How to Build a Private Messaging Chat App with React-Native (Signal Clone)

 2 years ago
source link: https://dev.to/daltonic/how-to-build-a-private-messaging-chat-app-with-react-native-signal-clone-44pn
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

What you’ll be building. Git Repo Here.

Introduction

Mobile communication applications are here to stay, and they are changing our world, the way we communicate and interact with one another in public, private, and at work. The advancement of modern technologies enables developers all over the world to incorporate these communication features into applications, thereby resolving issues for their various clients and business associates. React Native is a solid JavaScript cross-platform application development framework. It is, without a doubt, one of the most advantageous technologies for developing a chat application. In this tutorial, you will learn how to create a cross-platform private messaging chat app using the power of React Native, Firebase, and Expo.

Prerequisites

To benefit from this tutorial, you must first understand how to use React Native and Firebase. This will allow you to follow along with the rest of the tutorial. This tutorial utilizes the following technologies.

Installing The Project Dependencies

First, you need to have NodeJs installed on your machine; visit their website to do that.

Second, 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 modeExit fullscreen mode

Next, on the terminal create a new expo project with the name signal-clone and the blank template when prompted.

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

#Start the newly created expo project
expo start
Enter fullscreen modeExit 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.

Now, install these essential dependencies for our project using the instruction below. The default package manager for the expo is yarn.

# 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
Enter fullscreen modeExit fullscreen mode

Fantastic, now let’s go ahead to setting up Firebase for the project.

Setting Up Firebase

First, run the command below on your expo project.

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

Next, signup for a firebase account if you don’t have one already. Afterward, head to Firebase and create a new project named signal-clone, activate the email and password authentication service, details are spelled out below.

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 Options

Next, click the edit icon on the email/password provider and enable it.

Firebase Enabling Authentication

Now, 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.

Project Page

Once you’re done registering the application, you’ll be presented with a screen containing your application credentials. Take note of the second script tag as we’ll be using it shortly in our application. Congratulations, now that you're done with the installations, let's do some configurations.

On the Firebase console click on the project settings, you will be navigated to the project settings page.

Project Settings Page

Scroll down below, and you will see the project SDK setup and configuration which we will be very instrumental in the course of our project.

Firebase SDK setup and Configuration

Copy the firebaseConfig object, create a separate file called firebase.js in the root of your project. Paste the above firebase config object codes in the file and save. This configuration object is to be kept secret in a production app, however, since this is a demo app, let’s just don’t bother with keeping it secret.

The firebase.js file should carry the code snippet below.

import { initializeApp, getApps } from 'firebase/app' import { getFirestore, collection, addDoc, onSnapshot, serverTimestamp, query, orderBy, } from 'firebase/firestore' import { getAuth, onAuthStateChanged, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile, signOut, } from 'firebase/auth' // import { getDoc } from 'firebase/firestore'

const firebaseConfig = { apiKey: 'api-key', authDomain: 'project-id.firebaseapp.com', databaseURL: 'https://project-id.firebaseio.com', projectId: 'project-id', storageBucket: 'project-id.appspot.com', messagingSenderId: 'sender-id', appId: 'app-id', measurementId: 'G-measurement-id', }

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

export { getAuth, onAuthStateChanged, createUserWithEmailAndPassword, signInWithEmailAndPassword, updateProfile, signOut, collection, addDoc, getFirestore, onSnapshot, serverTimestamp, query, orderBy, }

Note to replace the firebaseConfig object with your firebase config setting. Great, let's take a look at our project structure.

Project Structure

Your project structure should be looking like this.

Project Structure

You should keep this as a reference guide as you code along. Jump in along with me and let's start creating our project directories and files one after the other.

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. Now, in this components folder create a file called the CustomListItem.js. This component will later be used to control how we render chats on the HomeScreen.

CustomListItem.js Component

This CustomListitem component includes parameters such as the chatAvatar, chatName, and a chatLastMessage. Below is the code snippet responsible for this behavior.

import React, { useEffect, useState } from 'react' import { StyleSheet } from 'react-native' import { ListItem, Avatar } from 'react-native-elements' import { collection, getFirestore, onSnapshot, query, orderBy, } from '../firebase'

const CustomListItem = ({ id, chatName, enterChat }) => { const [chatMessages, setChatMessages] = useState([]) const db = getFirestore()

useEffect(() => onSnapshot( query( collection(db, `chats/${id}`, 'messages'), orderBy('timestamp', 'desc') ), (snapshot) => { setChatMessages( snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })) ) } ) )

return ( <ListItem onPress={() => enterChat(id, chatName)} key={id} bottomDivider> <Avatar rounded source={{ uri: chatMessages?.[0]?.photoURL || 'https://upload.wikimedia.org/wikipedia/commons/7/7c/Profile_avatar_placeholder_large.png', }} /> <ListItem.Content> <ListItem.Title style={{ fontWeight: 800 }}>{chatName}</ListItem.Title> <ListItem.Subtitle numberOfLines={1} ellipsizeMode="tail"> {chatMessages?.[0]?.displayName}: {chatMessages?.[0]?.message} </ListItem.Subtitle> </ListItem.Content> </ListItem> ) }

export default CustomListItem

const styles = StyleSheet.create({})

This intelligent component does more than just render a list of chats. It also watches and renders the last message discussed on that chat group and also the avatar of the last person to say anything in that chat group.
Now, let's make sure our app is secured by authenticating all users before letting them in.

The Login Screen

The Login Screen

This screen is responsible for authenticating our already existing users before allowing them to chat with our app. The user must provide an email and password used in registering on our platform. To proceed with this process, create a folder within the root of the project called screens. Next, create a new file called LoginScreen.js in the screens directory, paste, and save the code below inside it. The code snippet below best describes this behavior.

Note: Download a PNG logo of the image above, rename it to “logo.png” and move it to the assets folder at the root of our application.

import React, { useEffect, useState } from 'react' import { StyleSheet, View, KeyboardAvoidingView } from 'react-native' import { Button, Input, Image } from 'react-native-elements' import { StatusBar } from 'expo-status-bar' import { getAuth, onAuthStateChanged, signInWithEmailAndPassword, } from '../firebase'

const logo = require('../assets/logo.png')

const LoginScreen = ({ navigation }) => { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const auth = getAuth()

useEffect( () => onAuthStateChanged(auth, (user) => { if (user) navigation.replace('Home') }), [] )

const signIn = () => { signInWithEmailAndPassword(auth, email, password).catch((error) => console.log(error.message) ) }

return ( <KeyboardAvoidingView behavior="padding" style={styles.container}> <StatusBar style="light" /> <Image source={logo} style={styles.ImageDimension} /> <View style={styles.inputContainer}> <Input placeholder="Email" autoFocus type="email" value={email} onChangeText={(text) => setEmail(text)} /> <Input placeholder="Password" secureTextEntry type="password" value={password} onChangeText={(text) => setPassword(text)} onSubmitEditing={signIn} /> </View>

<Button containerStyle={styles.button} onPress={signIn} title="Login" /> <Button containerStyle={styles.button} onPress={() => navigation.navigate('Register')} type="outline" title="Registers" /> <View style={{ height: 100 }} /> </KeyboardAvoidingView> ) }

export default LoginScreen

const styles = StyleSheet.create({ ImageDimension: { width: 100, height: 100, }, inputContainer: { width: 300, marginVertical: 10, }, button: { width: 200, marginTop: 10, }, container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 10, backgroundColor: 'white', }, })

Fantastic, with that setup, we are good to proceed with the registration screen.

The Registration Screen

The Registration Screen

Almost like the login screen, the RegistrationScreen is responsible for signing up new users into our chat app and authorizing their credentials for subsequent authentications using the firebase authentication service. The screen collects user information such as the name, email, password, and image URL. The code snippet below showcases how to implement the registration screen using the new firebase version 9 syntax.

import React, { useLayoutEffect, useState } from 'react' import { StyleSheet, View, KeyboardAvoidingView } from 'react-native' import { Button, Input, Text } from 'react-native-elements' import { StatusBar } from 'expo-status-bar' import { getAuth, createUserWithEmailAndPassword, updateProfile, } from '../firebase'

const RegisterScreen = ({ navigation }) => { const [fullname, setFullname] = useState('') const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [imgurl, setImgurl] = useState('')

useLayoutEffect(() => { navigation.setOptions({ headerBackTitle: 'Back To Login', }) }, [navigation])

const register = () => { const auth = getAuth() createUserWithEmailAndPassword(auth, email, password) .then((authUser) => { const user = authUser.user updateProfile(user, { displayName: fullname, photoURL: imgurl, }) .then(() => console.log('Profile Updated!')) .catch((error) => console.log(error.message)) }) .catch((error) => alert(error.message)) }

return ( <KeyboardAvoidingView behavior="padding" style={styles.container}> <StatusBar style="light" />

<Text h3 style={{ marginBottom: 50 }}> Create a signal account </Text>

<View style={styles.inputContainer}> <Input placeholder="Fullname" autoFocus type="text" value={fullname} onChangeText={(text) => setFullname(text)} /> <Input placeholder="Email" type="email" value={email} onChangeText={(text) => setEmail(text)} /> <Input placeholder="Password" secureTextEntry type="password" value={password} onChangeText={(text) => setPassword(text)} /> <Input placeholder="Profile ImageURL (Optional)" type="text" value={imgurl} onChangeText={(text) => setImgurl(text)} onSubmitEditing={register} /> </View>

<Button raised containerStyle={styles.button} onPress={register} title="Register" /> <View style={{ height: 100 }} /> </KeyboardAvoidingView> ) }

export default RegisterScreen

const styles = StyleSheet.create({ ImageDimension: { width: 100, height: 100, }, inputContainer: { width: 300, marginVertical: 10, }, button: { width: 200, marginTop: 10, }, container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 10, backgroundColor: 'white', }, })

Congrats on that screen, now let’s implement the Home Screen.

The Home Screen

The Home Screen

The Home Screen is responsible for outputting all the group chats on our platform. It uses the CustomListItem component to render each of the chat groups. At the header of the screen are two groups of elements, the left contains the avatar of the currently signed-in user and “signal”, which is the name of our app. By the right is another group containing three icons, a camera, pencil, and logout icons. When pressed, the pencil icon leads the user to an AddChatScreen, whereas, the logout icon signs out a user and navigates them to the login screen. Enough talk, here is the codes responsible for these actions.

import { StatusBar } from 'expo-status-bar' import { SimpleLineIcons } from '@expo/vector-icons' import React, { useEffect, useLayoutEffect, useState } from 'react' import { StyleSheet, Text, View, SafeAreaView, ScrollView, TouchableOpacity, } from 'react-native' import { Avatar } from 'react-native-elements' import CustomListItem from '../components/CustomListItem' import { getAuth, signOut, collection, getFirestore, onSnapshot, } from '../firebase'

const HomeScreen = ({ navigation }) => { const [chats, setChats] = useState([]) const auth = getAuth() const db = getFirestore() const signOutUser = () => { signOut(auth).then(() => navigation.replace('Login')) }

useEffect( () => onSnapshot(collection(db, 'chats'), (snapshot) => { setChats(snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() }))) }), [] )

useLayoutEffect(() => { navigation.setOptions({ title: 'Signal', headerStyle: { backgroundColor: 'white' }, headerTitleStyle: { color: 'black' }, headerTintColor: 'black', headerLeft: () => ( <View style={{ marginLeft: 20 }}> <TouchableOpacity activeOpacity={0.5}> <Avatar rounded source={{ uri: auth?.currentUser?.photoURL }} /> </TouchableOpacity> </View> ), headerRight: () => ( <View style={{ marginRight: 20, width: 120, flexDirection: 'row', justifyContent: 'space-between', backgroundColor: 'white' }} > <TouchableOpacity activeOpacity={0.5}> <SimpleLineIcons name="camera" size={18} color="black" /> </TouchableOpacity>

<TouchableOpacity activeOpacity={0.5} onPress={() => navigation.navigate('AddChat')} > <SimpleLineIcons name="pencil" size={18} color="black" /> </TouchableOpacity>

<TouchableOpacity activeOpacity={0.5} onPress={signOutUser}> <SimpleLineIcons name="logout" size={18} color="black" /> </TouchableOpacity> </View> ), }) }, [navigation])

const enterChat = (id, chatName) => { navigation.navigate('Chat', { id, chatName, }) }

return ( <SafeAreaView> <StatusBar style="light" /> <ScrollView style={styles.container}> {chats.map(({ id, chatName }) => ( <CustomListItem key={id} id={id} chatName={chatName} enterChat={enterChat} /> ))} </ScrollView> </SafeAreaView> ) }

export default HomeScreen

const styles = StyleSheet.create({ container: { height: '100%', }, })

Now that we are done with the Home screen, lets proceed to the AddChatScreen.

The Add Chat Screen

The Add Chat Screen

This screen is saddled with the responsibility of adding a new chat group to our chat app. Employing the services of firestore, this component collects a chat name from the UI to create/add a new chat to our chat list. Here is the code snippet catering for this behavior.

import React, { useLayoutEffect, useState } from 'react' import { StyleSheet, KeyboardAvoidingView } from 'react-native' import { Button, Input } from 'react-native-elements' import Icon from 'react-native-vector-icons/FontAwesome' import { collection, addDoc, getFirestore } from '../firebase'

const AddChatScreen = ({ navigation }) => { const [chat, setChat] = useState('')

const createChat = async () => { const db = getFirestore() await addDoc(collection(db, 'chats'), { chatName: chat, }) .then(() => navigation.goBack()) .catch((error) => alert(error.message)) }

useLayoutEffect(() => { navigation.setOptions({ title: 'Add a new Chat', headerBackTitle: 'Chats', }) }, [navigation])

return ( <KeyboardAvoidingView behavior="padding" style={styles.container}> <Input placeholder="Enter a chat name" value={chat} onChangeText={(text) => setChat(text)} onSubmitEditing={createChat} leftIcon={ <Icon name="wechat" size={18} type="antdesign" color="black" /> } /> <Button disabled={!chat} onPress={createChat} title="Create New Chat" /> </KeyboardAvoidingView> ) }

export default AddChatScreen

const styles = StyleSheet.create({ container: { backgroundColor: 'white', padding: 30, height: '100%', }, })

Nice Job, you are just a few steps from completing this signal-clone app.

The Chat Screen

The Chat Screen

This is the chat screen, where all the magic happens. This screen is responsible for the core purpose of this application. It engages users on a one to many chats. Below is the code responsible for its behavior.

import React, { useEffect, useLayoutEffect, useState } from 'react' import { KeyboardAvoidingView, Platform, SafeAreaView, ScrollView, StyleSheet, Text, TextInput, TouchableOpacity, View, Keyboard, TouchableWithoutFeedback, } from 'react-native' import { Avatar } from 'react-native-elements' import { FontAwesome, Ionicons } from '@expo/vector-icons' import { StatusBar } from 'expo-status-bar' import { getAuth, collection, addDoc, getFirestore, serverTimestamp, onSnapshot, query, orderBy, } from '../firebase'

const ChatScreen = ({ navigation, route }) => { const [msgInput, setMsgInput] = useState('') const [messages, setMessages] = useState([]) const auth = getAuth() const db = getFirestore()

const sendMsg = async () => { Keyboard.dismiss()

await addDoc(collection(db, `chats/${route.params.id}`, 'messages'), { timestamp: serverTimestamp(), message: msgInput, displayName: auth.currentUser.displayName, email: auth.currentUser.email, photoURL: auth.currentUser.photoURL, }) .then(() => setMsgInput('')) .catch((error) => alert(error.message)) }

useEffect( () => onSnapshot( query( collection(db, `chats/${route.params.id}`, 'messages'), orderBy('timestamp', 'desc') ), (snapshot) => { setMessages( snapshot.docs.map((doc) => ({ id: doc.id, ...doc.data() })) ) } ), [route] )

useLayoutEffect(() => { navigation.setOptions({ title: 'Chat', headerBackTitleVisible: false, headerTitleAlign: 'left', headerTitle: () => ( <View style={{ flexDirection: 'row', alignItems: 'center' }}> <Avatar rounded source={{ uri: messages[0]?.photoURL, }} /> <Text style={{ color: 'white', marginLeft: 10, fontWeight: 700 }}> {route.params.chatName} </Text> </View> ), headerRight: () => ( <View style={{ flexDirection: 'row', justifyContent: 'space-between', width: 80, marginRight: 20, }} > <TouchableOpacity> <FontAwesome name="video-camera" size="24" color="white" /> </TouchableOpacity>

<TouchableOpacity> <Ionicons name="call" size="24" color="white" /> </TouchableOpacity> </View> ), }) }, [navigation, messages])

return ( <SafeAreaView style={{ flex: 1, backgroundColor: 'white', }} > <StatusBar style="light" /> <KeyboardAvoidingView behavior={Platform.OS === 'ios' ? 'padding' : 'height'} style={styles.container} keyboardVerticalOffset={90} > <TouchableWithoutFeedback onPress={Keyboard.dismiss}> <> <ScrollView contentContainerStyle={{ paddingTop: 15 }}> {messages.map((message) => message.email === auth.currentUser.email ? ( <View key={message.id} style={{ alignItems: 'flex-end' }}> <View style={styles.receiver}> <Avatar rounded source={{ uri: message.photoURL }} size={30} position="absolute" bottom={-15} right={-5} containerStyle={{ position: 'absolute', bottom: -15, right: -5, }} /> <Text style={styles.receiverText}>{message.message}</Text> </View> </View> ) : ( <View key={message.id} style={{ alignItems: 'flex-start' }}> <View style={styles.sender}> <Avatar rounded source={{ uri: message.photoURL }} size={30} position="absolute" bottom={-15} right={-5} containerStyle={{ position: 'absolute', bottom: -15, right: -5, }} /> <Text style={styles.senderText}>{message.message}</Text> <Text style={styles.senderName}> {message.displayName} </Text> </View> </View> ) )} </ScrollView> <View style={styles.footer}> <TextInput placeholder="Signal Message..." style={styles.textInput} value={msgInput} onChangeText={(text) => setMsgInput(text)} onSubmitEditing={sendMsg} /> <TouchableOpacity onPress={sendMsg} activeOpacity={0.5}> <Ionicons name="send" size="24" color="#2b68e6" /> </TouchableOpacity> </View> </> </TouchableWithoutFeedback> </KeyboardAvoidingView> </SafeAreaView> ) }

export default ChatScreen

const styles = StyleSheet.create({ container: { flex: 1, }, footer: { flexDirection: 'row', alignItems: 'center', width: '100%', padding: 15, }, textInput: { bottom: 0, height: 40, flex: 1, marginRight: 15, backgroundColor: '#ececec', padding: 10, color: 'gray', borderRadius: 30, }, receiverText: { color: 'black', fontWeight: 500, marginLeft: 10, }, senderText: { color: 'white', fontWeight: 500, marginLeft: 10, marginBottom: 15, }, receiver: { padding: 15, backgroundColor: '#ececec', alignItems: 'flex-end', borderRadius: 20, marginRight: 15, marginBottom: 20, maxWidth: '80%', position: 'relative', }, sender: { padding: 15, backgroundColor: '#2b68e6', alignItems: 'flex-start', borderRadius: 20, marginLeft: 15, marginBottom: 20, maxWidth: '80%', position: 'relative', }, senderName: { left: 10, paddingRight: 10, fontSize: 10, color: 'white', }, })

Terrific job, you have just added all the screens we will need for this application, lets proceed to write the code for App.js.

The App.js

This is the wrapper file for our application. All the screens in our applications are boarded on this entry file. You can apply global styles in your application in this App.js file. Take a look at the piece of code carrying out these responsibilities.

import React from 'react' import { StyleSheet } from 'react-native' import { NavigationContainer } from '@react-navigation/native' import { createNativeStackNavigator } from '@react-navigation/native-stack' import LoginScreen from './screens/LoginScreen' import RegisterScreen from './screens/RegisterScreen' import HomeScreen from './screens/HomeScreen' import AddChatScreen from './screens/AddChatScreen' import ChatScreen from './screens/ChatScreen'

const Stack = createNativeStackNavigator() const globalScreenOptions = { headerStyle: { backgroundColor: '#2c68ed' }, headerTitleStyle: { color: 'white' }, headerTintColor: 'white', }

export default function App() { return ( <NavigationContainer> <Stack.Navigator screenOptions={globalScreenOptions}> <Stack.Screen name="Login" component={LoginScreen} /> <Stack.Screen name="Register" component={RegisterScreen} /> <Stack.Screen name="Home" component={HomeScreen} /> <Stack.Screen name="AddChat" component={AddChatScreen} /> <Stack.Screen name="Chat" component={ChatScreen} /> </Stack.Navigator> </NavigationContainer> ) }

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

Epic Job, you have just completed writing this application.

Conclusion

Finally, creating a chat app has become much simpler, and React Native makes it feel like a cool breeze in the middle of a hot summer. Private messaging apps will always be in demand, and understanding how to build one is almost a requirement for all developers. This tutorial walked you through the process of creating a private messaging chat application with React Native. You should use this newfound trick to crush other chat apps.

About Author

Gospel Darlington is a remote Fullstack developer, prolific with technologies such as VueJs, Angular, ReactJs, React Native, and API development.

He takes a huge interest in the development of high-grade and responsive web applications.
Gospel Darlington currently works as a freelancer developing apps and writing tutorials that teach other developers how to integrate software products into their projects.

He spends his free time coaching young people on how to be successful in life. His hobbies include inventing new recipes, book writing, songwriting, and singing. You can reach me on Website, LinkedIn, Twitter, Facebook, or GitHub for any discussion.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK