Building a Chat Application with React, Node/Express Js. [Progress Update]

in #hive-1693212 years ago

Grey Minimalist Hello Facebook Cover (1).png

Yarn means to talk, in Nigeria Pidgin English

I should have written about this project earlier but I guess the idiom; ' better late than never ' still works. I have been building projects since some months ago. As an entry level full stack developer without experience, this is a way for me to learn new concepts. This chat application contains the basic features now but I am about to implement websocket. Before that, I want to talk through what I have done so far, and how I have implemented solutions to the challenges I encountered.

FRONT END

I didn't actually build out the whole of the front end first because there were some things I wasn't sure of. I was sure of the sign up and login page though and what user information would be required to effectively use the chat. I started developing the front end with the help of Tailwind Css.

How Tailwind works

I have used the plain old CSS, bootstrap, and, material UI. I think Tailwind does it the fastest. Every single CSS keyword being shortened and passed directly to classNames without the need of creating classes and styling separately. Right there in your markup, you can just define how you want the styling to go. Although, the downside being that you would have to restyle every page. This is my second project with Tailwind and I am loving it. In my next project, I might use something different

Sweet Alert 2 Js

I think this is better than React Toastify for me -Highly subjective. I used React Toastify in my previous project and it was actually okay. It pops out from the side and does it's thing. I think this one is more clean although can be overwhelming for the user. It just feels good trying something new I guess.

  • The Sign Up and Login

IMG_20221007_094030.jpg

All that is required for the user to Sign up is the Username, Email and Password. I have come to understand that the end user is lazy and may back out of your website if you have a lot of unnecessary fields in your sign up. I am still yet to build something that would not necessarily have you sign up but I think my next project would be that, I have gotten just the right idea.

  • Multi Avatar API
    Link to the API
    I just decided to go with Anonymity. May decide otherwise later. I am calling 6 random Avatars from this API for you to choose from. Actually, this API allows a maximum of 10 calls per min so refreshing the page immediately will cause an error. If there is a better suggestion , I am open to it.
  • The Homepage

IMG_20221007_093954.jpg

Honestly, this is still undergoing revamp. Contains the search bar and user messages. It still contains the users but I'm thinking of reducing the information on the homepage. I'd instead create a message button that will take you a page containing the list of registered users.

  • Your Profile

IMG_20221007_093914.jpg

I kind of changed the concept here a bit. Each input sends an API post request to the server to update the specific field.

  • Chat Page

IMG_20221007_094005.jpg

I still have a problem with this page. It doesn't fix to the bottom of the page when the chat opens. if any one could help with that. I used React Emoji Picker to handle Emojis. It is a little bit slow but does the job, if there is a better option, I'm also open to it.

const addEmoji = ( emoji)=> {
       
        let msg = message
        msg+=emoji.emoji
        setMessage(msg)
    }

<emojiPicker && <Picker onEmojiClick={addEmoji}/>

  • Redux Toolkit
export const authSlice = createSlice({
    name: "auth",
    initialState,
    reducers: {

        reset: (state)=> {
            state.isError= false
            state.isSuccess= false
            state.isLoading= false
            state.message= ''
        }
    },

    extraReducers: (builder)=> {
        builder.addCase(register.pending, (state)=> {
            state.isLoading=true
        })
        .addCase(register.fulfilled, (state, action)=> {
            state.isLoading = false
            state.isSuccess = true
            state.user = action.payload
        })
        .addCase(register.rejected,(state, action)=> {
            state.isLoading = false
            state.isError = true
            state.message = action.payload
        })
        .addCase(login.pending, (state)=> {
            state.isLoading=true
        })
        .addCase(login.fulfilled, (state, action)=> {
            state.isLoading = false
            state.isSuccess = true
            state.user = action.payload
        })
        .addCase(login.rejected,(state, action)=> {
            state.isLoading = false
            state.isError = true
            state.message = action.payload
        })

        
        .addCase(logout.pending, (state)=> {
            state.isLoading=true
        })
        .addCase(logout.fulfilled, (state, action)=> {
            state.isLoading = false
            state.isSuccess = true
            state.user = null
        })
        .addCase(logout.rejected,(state, action)=> {
            state.isLoading = false
            state.isError = true
            state.message = action.payload
        })

        .addCase(setAvatar.pending, (state)=> {
            state.isLoading= true

        })
        .addCase(setAvatar.fulfilled, (state, action)=> {
            state.isLoading= false
            state.isSuccess = true
            state.message = action.payload
        })
        .addCase(setAvatar.rejected, (state, action)=> {
            state.isLoading = false
            state.isError = true
            state.message= action.payload
        })
        .addCase(getAvatars.pending, (state)=> {
            state.isLoading=true
            
        })
        .addCase(getAvatars.fulfilled, (state, action)=> {
            state.isLoading = false
            
            state.avatars = action.payload

        })
        .addCase(getAvatars.rejected, (state, action)=> {
            state.isLoading = false
            state.isError = true
            state.message= action.payload

        })
    }
})

A Snippet of my state management

I'm getting more comfortable with Redux Toolkit. I have three reducers (Auth, Users, Messages). The first one manages the states related to authentication (Login, Logout, Userprofile), The second manages the states related to fetching registered a single/all users. The third one manages the states related to user messages and chats.

THE BACKEND

This is implemented with MongoDb and Node/Express Js. I have just two Models; One for the User and one for Messages. Those two have been doing the job so far. I was tempted to create another Model called 'Chat' due to an issue I faced but I eventually fixed it. If I am going to implement a group chat though, I will have to do that.

let token

    if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')){
        try {
            // Get token
            token = req.headers.authorization.split(' ')[1]
            
            const decoded = jwt.verify(token, `${process.env.JWT_KEY}`)
            //Get user from token
            req.user = await userModel.findById(decoded.id).select('-password')
            
             next()
        }   
        
        catch (err) { 
           
          return  res.status(401).send({message: "Not Authorized"})
        }
       
    }  


The Login and Sign up routes are protected by Jwt Authentication Middleware. It generates session tokens for successful logged in users and each route in the chat requires the presence of such token in the Header. I am currently exploring other means of protection.

  • The Logic to the Chat
message: {type: String, required: true},
users: {type: Array, required:true},

sender: {type: Schema.Types.ObjectId, ref: "Users"},
to: {type: Schema.Types.ObjectId, ref: "Users"},

Each message contains three important fields, the users involved in the chat(This is used for chat grouping), and both the sender, and receiver. When you open a chat, of a user Id, it gets all the messages from the currentUserId to the SelectedUserId like so;

dispatch(getChat({from:user._id, to: selectedUser._id}))

The 'From' messages are styled to the left while the sender messages are styled to the right. That's all there is to the messages.

Chat Grouping

This is where I nearly twisted my brain nerves. I think I spent three days thinking ways to group a chat. I kept experimenting with different methods and I spent time reading a lot up on MongoDb Group aggregation and Query population and it did the trick for me.

const {currentUserId} = req.body
        if (currentUserId) {
         let  currentUserMessages= await messageModel.aggregate([
                //Check for any chat with the current User Id
                 {$match: {users: currentUserId}},

                 //Select the fields we want to retain
                 {
                     "$project": {
                         to: 1,
                         sender:1,
                         message:1,
                         createdAt: 1,
                         users: 1
 
                     }
                 },
                 //Destructure the users array and sort it so a/b or b/a returns a single array
                 {
 
                     $unwind: "$users"
                 },
                 {
                     $sort: {"users": 1}
                 }, 
                 {
                     $group: {
                         _id: "$_id",
                         "users": {
                             $push: "$users"
                         },
                         "sender": {
                             "$first": "$sender"
                         },
                         "to": {
                             "$first": "$to"
                         },
                         "message": {
                             "$first" : "$message"
                         },
                         "timeStamp": {
                             "$first" : "$createdAt"
                         }
                     }
                 },
                 {
                     "$sort": {
                         "timeStamp": -1
                     }
                 },
                 //Group by the sorted array
                 {
                     "$group": {
                         "_id": "$users",
                         "sender": {
                             "$first": "$sender"
                         },
                         
                         "to": {
                             "$first": "$to"
                         },
                         "message": {
                             "$first": "$message"
                         },
                         "timeStamp": {
                             "$first": "$timeStamp"
                         }
 
                     }
                 }
                 
 
                 
             ])
             currentUserMessages = await messageModel.populate(currentUserMessages, {path: "sender", select: 'nickname avatarImage'})
                 currentUserMessages = await messageModel.populate(currentUserMessages, {path: "to", select: 'nickname avatarImage'})
            res.status(200).json(currentUserMessages)
          }

I enjoyed the query population the more, it makes you populate a field with information from another model you had earlier referenced when setting the field. In this case, In my 'to' and 'sender' field, I specifically made them Mongoose object Id and referenced the Users Model

sender: {type: Schema.Types.ObjectId, ref: "Users"},
to: {type: Schema.Types.ObjectId, ref: "Users"},

So when I called the population query, it filled this field with data that matched the userID on this field from the Users Model. It also allows you to select some specific fields instead of just populating with irrelevant info. I selected the nickname and the avatar Image. Which I displayed on Chat.

{chat.to._id=== user._id? chat.sender.avatarImage : chat.to.avatarImage}

{chat.to._id=== user._id? chat.sender.nickname : chat.to.nickname}


I have to make sure I'm always displaying the image and the name of the other party on the home page.

What's Next now?

I want to implement the socket as soon as possible for real time messaging. What's a chat without it anyways? I also heard that it can implement voice and video calls, bit of a long stretch? If it does ill certainly explore it. I want to deploy it as soon as possible so I'll be fast tracking the development. I'd be providing more updates as time goes on.

**Current Issues
  • Doesn't focus to the end of the chat on page load
  • Multi Avatar 10 API calls/Min
  • React Emoji Picker slow Loading

Picsart_22-10-07_10-14-38-243.png

Follow for More♥️

Sort:  


The rewards earned on this comment will go directly to the people sharing the post on Twitter as long as they are registered with @poshtoken. Sign up at https://hiveposh.com.

Good code structure thanks!
!1UP

PIZZA!

PIZZA Holders sent $PIZZA tips in this post's comments:
@curation-cartel(18/20) tipped @marvel1206 (x1)

You can now send $PIZZA tips in Discord via tip.cc!

Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!

Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).

You may also include @stemsocial as a beneficiary of the rewards of this post to get a stronger support. 
 

1UP-PIZZA.png

You have received a 1UP from @gwajnberg!

The @oneup-cartel will soon upvote you with:
@stem-curator, @vyb-curator, @pob-curator
And they will bring !PIZZA 🍕.

Learn more about our delegation service to earn daily rewards. Join the Cartel on Discord.

Thank you for sharing this amazing post on HIVE!

Your content got selected by our fellow curator tibfox & you just received a little thank you upvote from our non-profit curation initiative!

You will be featured in one of our recurring curation compilations which is aiming to offer you a stage to widen your audience within the DIY scene of Hive.

Make sure to always post / cross-post your creations within the DIYHub community on HIVE so we never miss your content. We also have a discord server where you can connect with us and other DIYers. If you want to support our goal to motivate other DIY/art/music/gardening/... creators just delegate to us and earn 100% of your curation rewards!

Stay creative & hive on!