King Somto
21 Oct 2021
•
7 min read
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.
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.
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'
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.
Create a listOfUsers
page by going to the routes folder create a file called listOfUsers.jsx
.
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>
}
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'),
};
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',
};
}
}
)
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
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.
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)
})
}, []);
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
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.
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.
The image above is a visual explanation of how our database would look like.
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'),
};
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
}
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}`
}
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
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.
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.
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.
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.
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)
}
}, []);
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.
So that works
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.
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!