We're planting a tree for every job application! Click here to learn more

Firebase and Ionic React Chat (Fire Chat) Part 4: Firebase Functions - Building Private Chats

King Somto

21 Oct 2021

•

7 min read

Firebase and Ionic React Chat (Fire Chat) Part 4: Firebase Functions - Building Private Chats
  • React

Introduction

Building complex apps hasn't ever been easy but firebase as a service has been able to abstract most of the complexity of handling a backend or database away with ready-to-use JavaScript libraries that can be called on the frontend. That's amazing and good enough to build some apps but not all apps, let's face facts - some things are just better left to be done on the backend, my previous article on [Building a React - Ionic application with firebase](firebase-cloud functions https://javascript.works-hub.com/learn/firebase-and-ionic-react-chat-fire-chat-part-3-firebase-functions-d9619) explains this. This article expands on our learnings from the previous chapter to build more complex functions we would be making use of things like authentications and queries.

So far our project has been a group chat that lets users signup, login and posts messages in a group chat, so needless to say messages are not private and can be seen by anyone, we would be moving away from that to building a system where we can see all users available on the app and send them private messages.

Quick changes

Before going further if you have been following up on the series I have some things that should be changed just to make the app better.

The chatbox class in the container.jsx file should be changed to

.chatBox{
 background:# cccfff;
 height: 100vh;
 width: 100vw;
 margin: 0 auto;
 border-radius: 10px;
 position: relative;
 max-width: 500px;
}

We omitted the code that makes our app connect to our local firebase instead of our remote option so let's quickly change that in our config/firebase.js file.

// eslint-disable-next-line no-restricted-globals
if (location.hostname === 'localhost') {
 firebase.firestore().useEmulator('localhost', 8080);
 firebase.functions().useEmulator('localhost', 5001);
 firebase.database().useEmulator('localhost',9000)

 firebase
   .auth()
   .useEmulator('http://localhost:9099/', { disableWarnings: true });
}

Adding firebase.database().useEmulator('localhost',9000) would force our app to use our emulator when trying to connect to firebase database.

Setting up Ionic Library

So this article was initially meant to be for creating A mobile app with react Ionic framework, but we have been able to make it this far without actually using any Ionic library, we can obviously go further without them because Ionic React Libraries are just a collection of Ui components, but they do make life easier.

To install it we type npm I @ionic/react

Add the following to the app.js to import the needed styling. import '@ionic/react/css/core.css'

Let's get to work

Creating our listOfUsers page We need to create a page where our users can see other users signed up on the application, and initialize a chat with them, to do this we need to create a listOfUsers page.

Steps#### Step 1

Create a listOfUsers page by going to the routes folder create a file called listOfUsers.jsx.

Step 2

Paste the following code.

import React from 'react';
 
const Page = style.div`
 
width: -webkit-fill-available;
height: 100vh;
position: absolute;
 
.wrap{
   text-align: center;
   max-width: 90%;
   margin: 15vh auto;
   width: 450px;
   height: 350px;
   padding: 13px 50px;
   text-align: left;
}
 
input{
   width: -webkit-fill-available;
   height: 40px;
   padding: 10px;
   border: 0;
   border-radius: 7px;
   font-size: 20px;
   font-weight: 300;
}
 
button{
   width: -webkit-fill-available;
   margin-top: 35px;
   height: 49px;
   border-radius: 10px;
   border: 0;
   font-size: 20px;
}
 
`;
 
 
export default function ListOfUsers() {
 
   const [contacts, setContacts] = React.useState([]);
   const history = useHistory()

	return 
              <div>
	         Users list 
              </div>
}

Step 3

Add a new firebase cloud function that is responsible for loading all the available signed up users Edit the config/firebase.js to be

export const allFunctions = {
createAnAccount: firebase.functions().httpsCallable('createAnAccount'),
getFriends: firebase.functions().httpsCallable('getFriends'),
};

Step 4

Now we have to actually create the firebase function, so let's add the following function to our firebase cloud functions

export const getFriends  =  functions.https.onCall(
   async ({},context) =>{
 
   let user: any = false;
       try {
           const tokenId = (context.rawRequest.headers['authorization'] || '').split(
               'Bearer ',
           )[1];
           if (!tokenId) {
               throw new Error('Cant find user');
           }
           const usersRef = admin.firestore().collection('users')
 
           user = await auth().verifyIdToken(tokenId);
           ///get this user username
          
           if (!user) {
               throw new Error('Cant find user');
           };
 
           const allUsers = await (await usersRef.where('email', '!=', user.email).get())
 
           const usersList:any = []
 
           allUsers.forEach((doc)=>{
              usersList.push({
                  ...doc.data()
              })
           })
 
          
          return {
               usersList
           }
 
 
       } catch (error) {
           console.log({error})
           return {
               error: true,
               message: 'Failed to get chatId',
           };
       }
   }
)

Breaking down our codebase Part 1

   const tokenId = (context.rawRequest.headers['authorization'] || '').split(
               'Bearer ',
           )[1];
           if (!tokenId) {
               throw new Error('Cant find user');
           }
   const usersRef = admin.firestore().collection('users')
 
           user = await auth().verifyIdToken(tokenId);
           ///get this user username
          
           if (!user) {
               throw new Error('Cant find user');
           };

This part of the code gets the user authorization header add validates if token exists for a userId if not throws an error

Part 2

const allUsers = await (await usersRef.where('email', '!=', user.email).get())
 
           const usersList:any = []
 
           allUsers.forEach((doc)=>{
              usersList.push({
                  ...doc.data()
              })
           })
 
          
          return {
               usersList
           }

The code snippet above is responsible for getting all the available users and returning a list of them.

Step 5

Now we have to call our function to load the data on page load, for this part we would make use of useEffect.

  React.useEffect(() => {
 
	///calls the  get friend function
 
   allFunctions.getFriends({}).then(( {data} )=>{
       setContacts(data.usersList)
 
   }).catch((error)=>{
           console.log(error)
   })
 
   }, []);

Step 6

Updating our Ui component, we replace our div component with

<Page id='conttacts' >
           <IonList>
               <IonListHeader>
                   Recent Conversations
               </IonListHeader>
 
               {contacts.map(({userName}) => {
                   return <IonItem onClick={e=>{
                       history.push('app/'+userName)
                   }}  >
                       <IonAvatar slot="start">
                           <img src="./avatar-finn.png"></img>
                       </IonAvatar>
                       <IonLabel>
                           <h2>{userName}</h2>
                           {/* <h3>T</h3> */}
                           <p>Last Message...</p>
                       </IonLabel>
                   </IonItem>
               })}
 
 
           </IonList>
 
 </Page>

Our final output

Screen Shot 2021-09-28 at 10.40.08 PM.png

Here we can see that we have a list of every user of the application(except you of course), and we can actually click on them to start a chat.

Making changes to the way chats are stored.

Building private chats into our already current project requires a bit of re-thinking on how we store messages. Previously we stored all our messages under one collection (our chat collection) which is cool but we now need to identify which messages are meant for a certain pair of people, to do that we need to give every single message sent an ID.

Previously our data object for a message included the senderID and the message, now we need to add the chatID object to it, but since we are using firebase we can take advantage of the collection structure firebase gives us and store message data between individuals inside a collection and use the chat ID as the collection ID/name.

Let's look at a representation of that. Screen Shot 2021-10-06 at 1.47.10 AM.png

The image above is a visual explanation of how our database would look like.

Making more edits to our codebase

Step 1

We need to change the way messages are sent to firebase, to do that we add a new firebase cloud function to the application. Edit our config/firebase.js

export const allFunctions = {
 createAnAccount: firebase.functions().httpsCallable('createAnAccount'),
 sendUserMessage: firebase.functions().httpsCallable('sendUserMessage'),
 getFriends: firebase.functions().httpsCallable('getFriends'),
};

Step 2

Create a system for generating a constant chatID between two individuals (users). Creating a unique chat ID was a bit tricky to build, but we would need a function that takes in 2 user ID and returns the same uniqueId regardless of which user is the sender or receiver.

generateUiniqueId(receiver,sender) 
///uniqueId1234	

generateUiniqueId(sender,receiver) 
///uniqueId1234	
const combineTwoIds = ( id1 : string,id2:string ) =>{
   const [first,second] = [id1,id2].sort()
   ///same length for both
   let result = ''
   for (let i = 0; i < first.length; i++) {
       const letter1 = first[i];
       const letter2 = second[i];
       result = `${result}${letter1}${letter2}`
    }
   return result
}

Breaking the code

Here we sort both user IDs by name, so they are always in the same order

 const [first,second] = [id1,id2].sort()

Next, we just combine both strings together with a loop

for (let i = 0; i < first.length; i++) {
      const letter1 = first[i];
      const letter2 = second[i];
      result = `${result}${letter1}${letter2}`
  }

Step 3

Create the cloud function for sending messages

export const sendUserMessage = functions.https.onCall(
   async ({userName,message},context) =>{
       let user: any = false;
       try {
           const tokenId = (context.rawRequest.headers['authorization'] || '').split(
               'Bearer ',
           )[1];
           console.log({ tokenId });
 
           if (!tokenId) {
               throw new Error('Cant find user');
           }
           const usersRef = admin.firestore().collection('users')
 
           user = await auth().verifyIdToken(tokenId);
           ///get this user username
          
           if (!user) {
               throw new Error('Cant find user');
           }
 
 
           const getUserName = await (await usersRef.where('userName', '==', userName).get())
            
          const chatId =  combineTwoIds(getUserName.docs[0].id,user.user_id)
          
           admin.database().ref('chats').child(chatId).push({
           message,   
           sender: user.user_id,
          })
 
          return {
              message:'sent'
          }
 
 
 
       } catch (error) {
           console.log(error)
           return {
               error: true,
               message: 'Failed to send message',
           };
       }
   }
)

Let's break this down into parts

Part 1

 const tokenId = (context.rawRequest.headers['authorization'] || '').split(
               'Bearer ',
           )[1];
           console.log({ tokenId });
 
           if (!tokenId) {
               throw new Error('Cant find user');
           }
           const usersRef = admin.firestore().collection('users')
 
           user = await auth().verifyIdToken(tokenId);
           ///get this user username
          
           if (!user) {
               throw new Error('Cant find user');
           }

The above snippet helps us confirm if the user is valid.

Part 2

           const getUserName = await (await usersRef.where('userName', '==', userName).get())

We now need to search for the user,this is needed to get the UserId saved in the user ref collection.

Part 3


    const chatId =  combineTwoIds(getUserName.docs[0].id,user.user_id)

Next, combine the send Id and the receiver Id to generate the chatId, this ID is constant regardless of whoever is sender or receiver among two users.

Part 4

           admin.database().ref('chats').child(chatId).push({
           message,   
           sender: user.user_id,
          })

Finally, we save the message using the chat Id as the collection name.

Step 6

Time to edit our messages.jsx component and change the useEffect to load the chat data.

 React.useEffect(() => {
   const receiver = window.location.href.split('/')[4]
 
   try {
     allFunctions.getChatID({
       userName:receiver
     }).then(({data})=>{
       const {chatId=''} =  data
         db.ref('chats').child(chatId).on('value', (snapShot) => {
       let chats = [];
       snapShot.forEach((snap) => {
         chats.push(snap.val());
       });
       setMessages(chats);
       var element = document.getElementById('messages');
       element.scrollTop = element.scrollHeight - element.clientHeight;
     });
     }).catch((error)=>{
       console.log(error)
     })
    
   } catch (error) {
     console.log(error)
   }
 }, []);

Step 7

We then edit our container.jsx file so our button component becomes

<button
           onClick={async (e) => {
 
             if (input.current.length === 0) {
               return
             }
 
             document.getElementById('input').value = '';
            
             await allFunctions.sendUserMessage({
               userName: receiver,
               message: input.current
             })
 
             var element = document.getElementById('messages');
             element.scrollTop = element.scrollHeight - element.clientHeight;
 
             input.current = ''
           }}
         >
           Send
         </button>

Let's test that out.

![Screen Shot 2021-09-29 at 3.36.51 PM.png](https://functionalworks\-backend\-\-prod.s3.amazonaws.com/logos/47011baea7d92b48dca52f9a46d35772)
![Screen Shot 2021-09-29 at 3.36.57 PM.png](https://functionalworks\-backend\-\-prod.s3.amazonaws.com/logos/ddd24617aacc6c7e6e6483c5e78c8564)

So that works

Conclusions

We were able to create a friends list page where users can see other users registered on the application and create personal chats between individual users, to perform we had to figure a way of creating a unique chat ID between users regardless of who is the sender or receiver, abstracting most of our processes to our cloud function made things simpler in our frontend code.

Did you like this article?

King Somto

Dev

See other articles by King

Related jobs

See all

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Title

The company

  • Remote

Related articles

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

•

12 Sep 2021

JavaScript Functional Style Made Simple

JavaScript Functional Style Made Simple

Daniel Boros

•

12 Sep 2021

WorksHub

CareersCompaniesSitemapFunctional WorksBlockchain WorksJavaScript WorksAI WorksGolang WorksJava WorksPython WorksRemote Works
hello@works-hub.com

Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ

108 E 16th Street, New York, NY 10003

Subscribe to our newsletter

Join over 111,000 others and get access to exclusive content, job opportunities and more!

© 2024 WorksHub

Privacy PolicyDeveloped by WorksHub