I was researching chat systems for awhile now but I couldn't find any that fit my particular use case. Then I saw several articles from DeadSimpleChat. From there I was reviewing howtos on using:
-NodeJS + WebSockets + Redis ❌ Too many components!
-NodeJS + WebSockets ❌ Still didn't want to use Node!
-Firebase Chat ❌ Getting better but the UI isn't workable!
My Requirements
- No backend
- Off-the-shelf BaaS
- No-Code / Lo-Code front-end
- Simple general purpose UI
Today, we will be using Firebase Chat as inspiration to building our own chat but using a few other tools. I really liked the flexibility of No-Code/Lo-Code tools these days and when I came across Thorium Builder, I wanted to see if I can make it work.
This article won't cover how to use this No-Code/Lo-Code tool as it's beyond the scope. But, it's a simple way to wire a UI quickly and with not too much effort. Bonus points, Thorium Builder uses the up-and-coming Framework7 HTML Framework for building IOS/Android and Desktop apps with native look and feel. Since Framework7 itself is a full-featured framework we won't use anything more like React or Vue just plain old JavaScript.
Pre-requisities
- Firebase account ✅ A spark plan can be setup for free.
- Thorium Builder ✅ A Free version can be downloaded to your desktop.
- Framework7 ✅ A multi-purpose UI that is open source and free.
Concept
In meeting the requirements, I'm using the Firebase Platform as our BaaS. It's also free for small projects and will fit this proof-of-concept nicely. For our POC we will be drawing these screens below in Thorium Builder.
Login Screen
To-Do
Main Screen
To-Do
Wire up the logout button
Wire up the chat room route
Chat Screen
To-Do
Add additional classes and widget
Wire up the send message event
Wire up the subscribe listener
Add some helper functions
Configure Firebase Connection
Since we are using Firebase Auth and Firestore real-time database we'll need the connection properties. Firebase connection properties are configured in Thorium Builder. When it is applied, the login-screen widget is added upon initiating the build process. To find your Firebase connection navigate to your Firebase console and select your project.
Wire Up The Logout Button
Find the logout button id in the index.html
. In my case it was button-1073
. Then, in the custom.js
, create a click listener for that button. The code should look like below.
// Handle logging out the user
$(document).on('click', '#button-1073', (e) => {
e.preventDefault();
thoriumapi.firebase.logout(); // Logout the user
});
Wire Up The Chat Room Route
In the custom.js
create a page init listener. This will add a route to the chat room when we navigate to it. The code should look like below.
// Handle getting all messages in default room
$(document).on('page:init', '.page[data-name="chat"]', (e) => {
const page = e.detail;
shownDates = []; // Reset shown dates
shownMessages = []; // Reset shown messages
lastUserId = null; // Reset last user ID
lastRoom = defaultRoom; // Change to default room
chatRoom = subscribe(lastRoom); // Subscribe to messages
});
Add Additional Classes And Widget
In Thorium Builder add an HTML Snippet widget. This will add message toolbar and button to send messages. Add the following code to the widget.
<!--— Message Toolbar —-->
<div id="obj-1234" class="toolbar messagebar">
<div id="obj-5678" class="toolbar-inner">
<div class="messagebar-area">
<textarea id="obj-99" class="resizable" placeholder="Message">
</textarea>
</div>
<a class="link icon-only demo-send-message-link" id="obj-33">
<i class="icon f7-icons if-not-md">arrow_up_circle_fill</i>
<i class="icon material-icons md-only">send</i>
</a>
</div>
</div>
Widget Placement
Classes Placement
Wire Up The Send Message Event
When you click on build project in Thorium builder all the HTML for the project will have been created for you. Find the Message Toolbar in chat.html
and locate the send message button. Add the event code in custom.js
with obj-33
. The code should look like below.
// Handle sending the message
$(document).on('click', '#obj-33', (e) => {
e.preventDefault();
const msgInput = '#obj-99';
const msgVal = $(msgInput).val().trim();
if (msgVal !== '') {
$(msgInput).val(null); // Clear the input field
// Add message to Firestore
(async () => {
try {
// Set the last chat ID
lastChatId = !lastChatId ? 'chat_' + Math.round(+new Date() / 1000) : lastChatId;
await msgRef.add({
room_id: lastRoom,
chat_id: lastChatId,
msg_text: msgVal,
user_id: thoriumapi.firebase.getUser().uid,
user_name: thoriumapi.firebase.getUser().displayName,
time_stamp: Math.round(+new Date() / 1000),
});
}
catch (err) {
console.error('Error adding message: ', err);
}
})();
}
});
Wire Up The Subscribe Listener
Since Firestore needs a way to respond to chat messages when they are sent, we will need to add a function to subscribe to document changes in the chat room. Add the subscribe listener in custom.js
. The code should look like below.
// Handle new messages in Firestore
const subscribe = (room) => {
const ref = firestoredb.collection(room).orderBy('time_stamp').onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
const msgData = change.doc.data();
const date = new Date(msgData.time_stamp * 1000);
// Check if the user has already been displayed
checkDisplay(msgData.user_id, msgData.user_name);
// Check if it's a sent message or received message
const message = checkMessage(msgData);
// Check if the date has already been displayed
checkDate(date.toLocaleString('en-US', dateOptions).replaceAll(',', ''));
// Append the message to the container
pushDisplay({ time_stamp: msgData.time_stamp, message: message });
// Update message time BUG query selector can't select starting digit
const time = date.toLocaleTimeString('en-US', timeOptions);
$('#' + msgData.chat_id).text(lastUserName + ' ' + time);
// Play the audio received notification
notifyRecipients(msgData.user_id);
// Set the last chat ID
lastChatId = thoriumapi.firebase.getUser().uid === msgData.user_id ? msgData.chat_id : null;
// Scroll to the bottom of the page
$('.page-content').scrollTop(10000, 400);
});
});
return ref;
}
// Handle switching to the main page
$(document).on('click', '#o-1088', function (e) {
e.preventDefault();
chatRoom(); // Unsubscribe from messages
});
Add Some Helper Functions
We're not out of the woods yet. See all those stubs in the previous section's code? This is where we will add all those helper functions to tie it all in and clean up the code abit. While in custom.js
add the helper functions. The code should look like below.
// Chat POC
const defaultRoom = 'room.1'; // Room ID
const msgRef = firestoredb.collection(defaultRoom); // Messages collection
const messages = '#messages-container'; // Message container
let shownMessages = []; // Track messages already displayed to prevent duplicates
let shownDates = []; // Track dates already displayed to prevent duplicates
let showReceiver = false; // Show receiver name
let lastUserId = null; // Last receiver user ID
let lastChatId = null; // Chat ID
let lastUserName = null; // Last user name
const audio = $('#pop')[0]; // Get the audio element
let chatRoom = null; // Chatroom subscription
let lastRoom = null; // Last room
// Date options
const dateOptions = {
weekday: 'short',
year: 'numeric',
month: 'short',
day: 'numeric',
};
// Time options
const timeOptions = {
hour: '2-digit',
minute: '2-digit'
}
// Check app initialized state
if (app.initialized === true) {
app.emit('onAuthStateChanged', thoriumapi.firebase.getUser()); // Emit auth state
}
// Handle auth state changes
app.on('onAuthStateChanged', (user) => {
const auth = user;
if (auth) {
$('#title-o-1008').text(`Hi, ${auth.displayName}`);
} // Check if user is authenticated
});
// Function to notify recipients of new messages
function notifyRecipients(id) {
if (id !== thoriumapi.firebase.getUser().uid) {
audio.play();
}
}
// Function to push the message to the display
function pushDisplay(data) {
if (!shownMessages.includes(data.time_stamp)) {
shownMessages.push(data.time_stamp);
$(messages).append(data.message);
}
}
// Check if the date has already been displayed
function checkDate(date) {
if (!shownDates.includes(date)) {
shownDates.push(date);
$(messages).append(messageTitle(date));
}
}
// Check if the user has already been displayed
function checkDisplay(id, name) {
if (id === thoriumapi.firebase.getUser().uid) {
showReceiver = false;
lastUserId = null;
lastUserName = null;
}
else {
showReceiver = lastUserId !== id;
lastUserId = id;
lastUserName = name;
}
}
// Check if it's a sent message or received message
const checkMessage = (data) => {
return data.user_id === thoriumapi.firebase.getUser().uid ?
itemize(null, showReceiver, data.msg_text, data.chat_id) :
itemize(data.user_name, showReceiver, data.msg_text, data.chat_id);
}
// Function to format message title
const messageTitle = (title) => {
const msgTpl = `
<div class="messages-title">${title}</div>
`;
return msgTpl;
};
// Function to format messages
const itemize = (name, show, msg, chatId) => {
let msgTpl;
if (name) {
const names = show ? `<div id="${chatId}" class="messages-name">${name}</div>` : '';
msgTpl = `
<div class="message message-received">
<div class="message-content">
${names}
<div class="message-bubble">
<div class="message-text">${msg}</div>
</div>
</div>
</div>
`;
}
else {
msgTpl = `
<div class="message message-sent">
<div class="message-content">
<div class="message-bubble">
<div class="message-text">${msg}</div>
</div>
</div>
</div>
`;
}
return msgTpl;
}
The Finished Product
Go ahead and test it! Create some test accounts first on Firebase and then login. Send a message. Then login with the other account. You should see the message and can reply back. On the first account you should be receiving the messages. You just built your chat app! How cool is that!?
Improvements
This is a very basic chat app so improvements are not only required but also expected but beyond the scope here. Some notable areas are listed below.
- caching messages
- multi-room chats
Top comments (0)