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
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
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
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
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
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
})
}
})
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
Follow for More♥️