Welcome back to my tech blog. This time I will show you my work during the winter break: Messenger Clone
During the winter break, as everybody knows, there were many crashes to big platforms such as Facebook and Messenger. Noticing this, I decided to create a real-time messaging platform for me and my friends (potentially become bigger in the future). I hope you will find this post interesting because it has a lot of cool techniques that I have just learned.
List of important parts in the project:
- Firebase Authentication system
- Friends relationship management (add, remove friends like on Facebook)
- Firebase storage to store and upload images
- Real-time messaging of individuals using Firestore and Redux
- Real-time messaging of groups using Firestore and Redux
Important Note:
One important thing to note here is that I am always using the information and the ids from Firestore not from the auto generated user of Firebase Authentication. This means that anytime the user logs in and inputs any information, it will be uploaded encriptedly to the Firestore and we will draw the information from the database down to our client.
Setting Up Firebase in a React Project
I have written a blog about how to set up firebase from start to finish Here. It is vital that you set up everything correctly in order for Firebase to work. In this project, I will use 3 services Firebase Firestore, Firebase Authentication, and Firebase Storage
1. Authentication
1.1 Login Form
Definitely one of the core parts of any project. In this project, I created a Private Router for authentication which will only allow authenticated users to enter the platform.
We start by creating 2 pages with 2 forms: Login and Signup. We will use useRef to handle the value that the users input in both forms.
1 | <Form onSubmit={handleSubmit}> |
The form is structure quite standardly. I am using all the components in react-bootstrap (Form, Button, Modal). We have 2 form groups correspond to the email and the password. We then save the reference using useRef to store the input from user. Finally, there is a button to handle the submit event of the form.
In the submit event, we will execute the following actions. First we will run the login function with email and password. This action is loaded from a useContext which I will mention later. Then we will store the result of the successful Authentication call and save it to the Database and LocalStorage (this is used to remember the user if you want to have that feature). I am also using useHistory to navigate between the webpages
1 | try { |
This is an application of try catch function. Note that here the login will be imported from our useContext (an Authentication context) and will be described later in section 1.3
1.2 Signup Form
This is very similar to the Login Form. It has 5 form groups firstname, lastname, email, password and confirmpassword (you can add more but the last 3 is a must).
Here we will also use useRef to handle the input value from the users. The Signup Form is below:
1 | <Form onSubmit={handleSubmit}> |
Again, very similar to the Login Form, we will have a submit button to handle the submit event of the form. Here is when it gets a bit different.
We will first check if the password is equal to the confirmpassword. I think Bootstrap Forms requires 6 characters at the minimum but you can freely add more conditions. If they are equal, we would perform the try catch function just as in the Login Form:
1 | try { |
We set the result variable to be the result of the successful call to the signup function of Firebase Authentication. Again, the signup function will be imported from the useContext that we will mention in section 1.3. Then I will update the displayName of the currentUser that is successfully signed up to Firebase (note that the displayName is a provided feature). However, as I have mentioned in my Important Notes I will uploaded the users’ information to Firestore even if we already have the currentUser from Authentication. This will allows us to freely manage the encripted information and apply it to later stage of the project.
1 | await db.collection("users").add({ |
Here is the function I called to add to my Firestore database. It includes some fields such as uid (id given to us from Firebase Authentication), createdAt (exact time that the user signed up), isOnline (manage if the user is currently online or not), profileImage (to store profile image), friendList (to store our current friendlist), and pendingFriends (to store the users sent us a request but we have not respond). After pushing the data to Firestore, we are basically done for signup. Now we should switch to Login page and start building our Dashboard. Note that in my website I also built a Update Profile and a Forgot Password page but that is for later and not the main function of the project
2. Friends relationship management (add, remove friends like on Facebook)
This will be our Dashboard page. It will help you show the personal information, manage your friends, and the list of all users currently using the app so that people can add friend and connect to each other. So the core function of this page is to handle adding and removing friends. Like I mentioned in the Signup section (1.2), we will use 2 arrays to handle friends pendingFriends (users sent us a request but we have not respond) and friendList (the people are in our friends’ list)
This is my dashboard page:
It has a really simple design with a navigating bar (drawer) to the left side. Now, I will show you how to create the relationship between the users.
First, I will use a useEffect to call to the Firestore database and list out all the users that are using the app. In order to do this, we create a User component that has 3 props:
id (the document id of the friend that I sent a friend request to in Firestore), docId (the document id of the current user in Firestore), and user (the data of the current usere in Firestore).
Everytime we want to pull real-time information from the database to reflect the changes, we will write a useEffect hook. Here I have pendingFriends (users sent us a request but we have not respond) and sentFriendRequests (users that we sent a friend request to). Remember this data is taken from the user collection in the database
1 | useEffect(() => { |
Second, we will have 4 operations that we want to execute: sent a friend request to someone, accept a friend request, decline a friend request, and remove someone from our friend list. The way you handle these actions is up to you but here is how I handle it
1. Sent a friend request to someone
When we sent a friend request to someone, we want to add the document id of that user to our sentFriendRequest list and add the current user’s document id to the pendingList of that user.
For example, A sent to B a request -> add B to sentFriendRequest list of A and add A to pendingList of B. Here is how to execute it. In Firebase, whenever we want to change a array type data, we can use the FieldValue.arrayUnion provided by Firebase
1 | const handleAddFriend = async (id) => { |
2. Accept a friend request from someone
When A accepts B friend request, we want to add A to B’s friendList, add B to A’s friendList, remove B from pendingList of A and remove A from sendFriendRequest list of B. Here is the code to perform this action
1 | const handleAcceptFriend = async (id) => { |
3. Decline a friend request of someone
When A declines the friend request from B, we basically just remove B from the pendingList of A and remove A from the sentFriendRequest of B
1 | const handleDeclineFriend = async (id) => { |
4. Remove someone from our friendlist
When A removes B from A’s friendlist, we want to remove A from B’s friendList and remove B from A’s friendList.
1 | const handleRemoveFriend = (e) => { |
Right now, we are done with the friends’ relationship operations. Now we will try to display the operations to the screen in the way we want. For example, our current user is A. Note that the logic will flow like this. First, next to every user (that is not our friend yet) in the List of Users will have a “add friend” button. After sending B a friend request, we will change that button to Pending and disable it. Then in B’s screen, the buttons next to A will be yes/no (represent if B want to add friend A or not). If B choose yes, they will both be added to each other’s friendList and remove from the each other list of users. If B choose no, then A will be removed from B’s users list. Here is the ternary expression I used to display this logic in the Dashboard
1 | { |
The rest of the dashboard is pretty simple. Here is where I list of the current users that are not my friend.
1 | userList && |
And here is where I list my friends in the left side of the dashboard.
1 | { |
3. Firebase storage to store and upload images
One of the most important features in the app is to handle profile images: upload or change them. Here we will use Firebase Storage to upload the image and then extract the link from Storage to store it to corresponding users’ database
1 | <input type="file" onChange={handleImageAsFile} /> |
We will display these 2 basic HTML on the screen. 1 is an input where we set the type to files. We will only accept image format here. I will capture the event handleImageAsFile
1 | const handleImageAsFile = (e) => { |
Here is an example of the files in storage. Remember the path that leads to the images is:
“/images/{name of image}”
We save our image to a state imageAsFile. This will be a Image File so we cannot access this yet. From here we have to extract the link to this file from Storage. We will first upload the image we selected to Firebase Storage and access the path that leads to the image using reference and the method getDownloadURL of Firebase.
1 | const handleFirebaseUpload = (e) => { |
Here we will get our downloadURL of the image to be firebaseURL. After receiving this, we will save this to the corresponding user database of that user. Now all we have to do is take the URL and display it on the screen
4. Real-time messaging of individuals using Firestore and Redux
It has been quite a long set up right … We are finally here, the most important and also the core feature of my project the real-time chat between individuals and groups. In this section 4, I will show you how to use Redux to do real-time messaging.
First we need to set up Redux and Redux Thunk (this is critical in order to perform asynchronous actions in Redux)
First in the main index.js outside, we must set up as the following:
1 | ReactDOM.render( |
Basically, the store variable will be from Redux and it will contain all of our states. We just need to wrap our App with the Provider and we are good to go.
Now we will set up the reducer where will we perform the messaging logic. Below is my reducer folder structure. We will not need the authReducer but just an index.js file and the userReducer
The user.reducer.js will contain all the definitions for the actions (name, type and corresponding payload) that we will perform
1 | import { userConstants } from "../actions/constants"; |
The most important actions here are GET_REALTIME_MESSAGES and GET_REALTIME_MESSAGES_GROUP. In these actions, we will update the state conversations (for individuals chat) and conversationsGroup (for group chat) with the corresponding newState we got after the user texts something.
Next we will set up the most important file of the webchat, user.action.js. This is where all the magic happens !
The first asynchronous action we will perform is createMessage. This is fired when the user sends something to another person.
1 | export const createMessage = (messageObject) => { |
Remember using Redux Thunk will help us perform async functions like above. We have a parameter messageObject. This parameter will be taken from the client side after the user sent something. REMEMBER THIS PARAMETER.
After firing createMessage, we will instantly fire the next function below:
1 | export const getRealTimeConversations = (user) => { |
All the encrypted messages will be stored in the conversations collection in Firestore. Here everytime a person sends a message to another person, we will retake all the data from that conversation in Firestore and add the new message. This might seem to be time consuming but the next filtering step will improve our performance by a lot.
To query and filter in the Firebase, I use the where method. The user.uid1 and user.uid_2 are the ids (generated by the Firebase Authentication) of the current user and the person you are messaging to. Whenever the current user sends a message, it will reload the _conversation in the database and it will look for all conversation with the user_uid_2 that is contained in the array [user.uid_1 and user.uid_2]. This will ensure that if A sends a message to B, the first filter will get all the conversations that B is either the sender or the receiver.
Then we sort the order by ascending because we want the lastest messages to be seen last. Finally, this condition will check if the conversation we are taking from the database is exactly the conversation between A and B
1 | if ( |
After that we will dispatch the action with the payload of conversations so that our reducer file will receive the action. So we are finished with the user.action.js. We have finished setting up for the real-time chat. Now let’s move to the HomePage Screen (which is the Chat screen)
const initChat = (user) => {
setCurrentChatId(user.id)
setChatStarted(true)
setChatGroup(false)
setChatUser(${user.data.firstName} ${user.data.lastName}
)
dispatch(getRealTimeConversations({ uid_1: docId, uid_2: user.id }))
// setUserUid(user.data.uid)
// console.log(user)
}
First, we have to set up the conversations column, which lists all of our friends and groups (just like in Messenger). Here is the set up for the column
1 | <div> |
We will map out all of the current user’s friends, each in a component called User.
When we click into each user, we will have to initiate the conversations between 2 people (which means we need to use the actions we just wrote in Redux).
1 | const initChat = (user) => { |
We get the id of the user that we just clicked into as id. Then we set the ChatUser as the name of the user we are chatting with. Finally, we dispatch the action getRealTimeConversations to load all the conversations with that person.
Next, when we send a message to a user, we need to initiate the the sendMessage function to execute the action we did in user.action.js
1 | const sendMessage = async (e) => { |
Remember the messageObject that I told you to remember. Inside the messageObject will be all the information about the 2 users and that message: message (the text that the user sent), haveReply (Is the message replied or not), replyMessage (the replyMessage if this message has 1). Finally, we will dispatch the createMessage action from the user.action.js
Here is the most sophisticated part of the project, where I set up and display all the messages:
1 | (chatStarted && !chatGroup) ? |
The above code is the display of the message from the sender. The message from the receiver will be the same but just A message can be splited into 4 parts:
- Checking what user this is the sender or the receiver
1 | conver.conver.user_uid_1 == docId ? |
This line of code helps us know that this is the sender of the message.
- Emoji container
Do you know the emojis we typically use in Messenger. Here we use a emoji unicode library to display the emojis
- Showing the Reacted Emoji
Here is where we handle the event of a user reacts to a message. We need a container to display the emoji. For individuals chatting, we only need to add an emojiSelected field in each message document.
1 | const handleReaction = async (e, id) => { |
- Displaying the Reply portion
Of course, we need a reply function for each message. Here we basically add another field replyMessage to each message document in the database.
1 | const handleReplyMess = (e, id) => { |
And thats it, we can now chat individually to each other. For the groups’s function is really similar to the individual chat so I will let the Github Link to Project so that you can understand it better. Here is the link to the Website MessengerClone. I hope you can experience the website and give me some feedback.
I hope you like this post. It is really long but it contains a lot of useful information about web technologies. I will see you guys in my next post.
If you like this blog or find it useful for you, you are welcome to leave a comment. You are also welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them. Thank you so much!