Create a Shared Digital Albums Hybrid Application with Monaca, Firebase, Framework7 and React (part 2)
This is the second part of a 2-parts tutorial for the development of a Shared Digital Albums Hybrid application. You can find the first part here.
In this second part of the tutorial we will code the logic behind the Login, SignUp, Groups, Account and Newsfeed screens of the application.
Let’s begin!
Setting up User Authentication
Regarding authentication, we will provide the ability to register as a new user by signing up, login as an existing one, change the password and logout. Also, we will make the application remember which user logged in previously by using the onAuthStateChanged observer provided by firebase.
We first check whether the operating system of the phone is Android or iOS and according to that we initialize firebase authentication. After that we have the fb_login and fb_signup functions where they call the respective functions from firebase.
Each time a user will login or sign up, the onAuthStateChanged observer will be triggered, so we have to find out each time which action between the two happened. If the user logs in, then we have to populate the local store with the user information we fetch from the firestore database. If the user signs up, then we have to create the user and send the data to the database. After the successful completion of any of the actions, we navigate to the TabsView mentioned earlier.
I will explain each called function in getLoggedInUser later in the tutorial.
When the user signs out from the application, we first navigate back to the login screen, clear the previous navigation history and then clear all the values from the local store. We will talk about line 81 (of file auth.js) a little bit later in the tutorial.
The users can also change their passwords, and to ensure safety, we first prompt them for their old password and then the new password twice. We will check if the provided password matches the one saved in their account data with the reauthenticateWithCredential function and if it does, then we will proceed with the update.
After we finish with this, we will go to the app.jsx file in the /src/components folder and set the Login page as the starting point (or main view) of the application. The file should look like this:
Creating the pages for the Tabbed Navigation
1. Account
On their account page, besides being able to change their password and sign out, users will also be able to change their username and their profile picture.
Here is the code to update the username both in the document of the user and each document in the newsfeed that will appear for the day and is created by the user.
Update firestore.js to include these:
To change their profile pictures, users first have to pick an image from their gallery. We will use the Cordova camera plugin for this. After they choose the image, we will update the local store and the user document in the firestore db, we will upload the new image to the cloud storage and at the end, we will update again each newsfeed post related to the user.
Update firestore.js and include these two functions:
And finally, create the cloud_storage.js file like this:
2. Viewing Groups
On this tab, the users will be able to view the groups they are a member of, view their invitations to groups created by other users by opening a popup, create a new group, and open any of their groups to view or edit the images included in them.
The groups the user is a member of are already loaded with the login process, so the list in the local store is already populated, together with the profile pictures of them. All we have to do is generate the list, and update it each time the user creates a new group, or accepts an invitation to a group.
Include this function in the firestore.js file:
In the getLoggedInUser we have the two following lines:
const imagesPromises = groupsSnap.docs.map(doc => downloadImage(doc.get("group_picture")));
const imagesData = await Promise.all(imagesPromises);
Where groupsSnap is a snapshot of the user’s groups fetched from the database. What happens here is that we first get the path of the profile picture of every group and then download the image. With the await Promise.all we ensure that we wait for all the images to download first before continuing to the next part of the code.
Managing Invitations
In the getLoggedInUser function we have the call to the function setInvitationsListener where we set a listener for the pending_invitations sub collection of the current user. This is to be able to get live updates on the new invitations the user gets and display them immediately to the popup without the need to refresh the app.
We should declare invitationsListener as a global variable at the start of the firestore.js file.
A user can either accept or reject an invitation to join a group. In both cases the invitation should be removed from the invitations saved in the local store for the the user, as well as from the invitations saved on the database.
When the user accepts the invitation, we also have to include the related group to their list of groups, both locally and online, as well as make a newsfeed post about the new member of the group.
The createNewsfeedPost function will be used when a user adds a new picture to a group too, so in order to distinguish between the two actions, we will pass as a first argument to the function the type of the action/ post, which in this case is “new_user”.
Creating Groups
In the process of creating a group, the user will have the ability to select a profile picture representative of the group, input a name and a description and invite any of the current users of the app to join their group. Only the name of the group is necessary for the completion of the process.
Before doing anything, we first have to fetch all the current users registered to the application. This is done this way:
For the selection of the image for the profile of the group we will again use the function select_image from the image_utils.js file again but this time we will pass as the typeOfImg the string “group_profile”.
select_image("#beforeImg", "#afterImg", setImg, "group_profile");
After the user clicks on the “Create Group” button, we will confirm the creation of the group and then we will collect all the given data (name, description, image and invited people) and proceed with it. The popup will then be cleared from all input and be closed.
The creation of a group process has three sub-functions. First of all, we will have to add the new group to the list of groups of the user in the database and upload the image to the cloud. After that, we will have to update the local array with the groups as well, in order for the user to be able to view the change. And last, we have to add the new group as invitation to the invited people (if any).
Opening and Managing a Group
The moment users click on a group to open it, we will have to load the relative group data to the openedGroup variable in the app’s state. To load all the pictures uploaded in the specific group, we will call the function listGroupImages which first, lists all the images under the group’s id and then, downloads them and saves them in a local store’s array.
/*Included in the groups_list.jsx file*/
const setOpenedGroup = function (group, index) {
listGroupImages(group.groupId).then((imagesData) => {
store.dispatch("setOpenedGroup", {
groupId: group.groupId,
name: group.name,
pictures: imagesData
});
f7.views.current.router.navigate("/groups/" + index + "/");
}).catch(error => console.log("Error: " + error.code + " " + error.message));
}
Once all of this is done, we will navigate the users to the group they clicked on.
That screen will include a scrollable list of all the pictures available in the group, a button to add new pictures and the ability to delete pictures. To add multiple images at a time we will use the plugin we installed previously, ImagePicker. We will allow the users to pick 10 images at a time and after that, for every selected image we will create a respective document in the “pictures” collection in our database.
Add the following to the image_utils.js file:
And the following in the firestore.js file:
After we create the document, we will upload the new pictures to the cloud storage, under the current group’s id folder and when this task finishes, we will create a newsfeed post with the function createNewsfeedPost (with the first parameter being “new_images”) about every newly uploaded picture.
We also want to give the ability to the users to delete images, so when an image is clicked, the option to delete it will appear. If the users select the option, we will ask them if they are sure about their selection and if they proceed, we will delete the picture from the cloud storage, from the firestore database, from the newsfeed (as a post) and from the local store.
/*Included in the group_main.jsx file*/
let imageForDeletion;
const deleteConfirmation = () => {
f7.dialog.confirm("Are you sure you want to delete this picture?", async function () {
//Delete the image from the cloud storage
deleteImage(imageForDeletion.url);
//Delete the image from the firestore db
deleteGroupPicture(imageForDeletion.imageId);
//Delete the image post from the newsfeed
deleteNewsfeedPost(imageForDeletion.imageId);
//Delete the image from the local store
const newListOfImg = groupData.pictures.filter((picture) => picture.imageId !== imageForDeletion.imageId);
store.dispatch("setOpenedGroup", {
...store.getters.openedGroup.value,
pictures: newListOfImg
});
f7.popover.close(".popover-delete", true);
})
}
Here is the code to delete it from the cloud:
And here is how to delete it from the db as a picture document and as a newsfeed document:
The way we delete the newsfeed post is by querying on the picture id that is saved both in the newsfeed document as well as the picture document.
3. Newsfeed
When the getLoggedInUser function is called, the last thing that happens in the function is the call of the loadPosts function. In this function basically we will load all the newsfeed posts related to the groups the user is a member of. But due to the fact that a user can join a group through invitations or create a new group, the number of groups the user is a member of can increase while the application is running. So, we will have to keep listening for changes on the groups sub-collection of the current user. We do that by setting a listener with the onSnapshot method.
Besides that, we also want to have live updates for the posts, imagine something like Instagram where each time someone you follow uploads a picture, it is shown *almost* immediately on your newsfeed. To achieve that we will also set a listener on the newsfeed collection. But we want the listener to trigger only when the newsfeed document that is added (or deleted or updated) is related to the user’s groups and was created in the last 24 hours. What we will do, is build a query to pick the documents that have these exact characteristics.
We should declare the listeners in the firestore.js file like this:
let groupsListener, newsfeedListeners = [];
And add the following function:
You can see in the code above that we split the array of group IDs into 10 elements each time and then execute the query. This is because firestore doesn’t allow comparisons with the in operator, with arrays with more than 10 elements. This is a compromise we have to do in case a user is a member in more than 10 groups. It results in having more than one listeners for the newsfeed collection and this is the reason for line 28 too. Each time the above listeners are triggered, the newsfeed that is stored locally gets updated too.
Previously, when we discussed the signOut function, there was this following line:
unsubscribeListeners();
Which corresponds to this function:
Basically, what we do is detach the listeners so that our event callbacks stop getting called and the client stops using bandwidth to receive updates.
Building the Project
Before building the project, we can preview the application by running it on the browser. Run “monaca preview” in the project’s folder. The corresponding url should be http://localhost:8080. This way we can see the general image of the application, test the functionalities and find errors. (In the browser, selecting images from the gallery is not supported).
If you have been developing locally, you should first upload the project to Monaca to be able to build it. To do so, run the command “monaca upload” in the project’s folder.
Build for Android
Login in with you Monaca account and navigate to your dashboard. Find your project and click on it. Choose Remote Build → Android → Debug Build. Wait for the build to finish, download the app to your phone and install it. If you also want to test your app, instead of a Debug Build, do a Custom Build Debugger.
Build for iOS
Building for iOS requires the same steps, except that you have to choose iOS on the Remote Build step. The quirk about iOS building is that Apple requires a certificate. Follow this guide to learn how to build your app.
Conclusion
In this tutorial we learned how to create an application to share digital albums with a group of users we choose, with the help of Monaca, Firebase, Framework7 and React. We can easily see how quick it is to develop a full-working application nowadays that IAAS, BAAS and SAAS are becoming more and more popular.
The full project can be found here. Please take a look as it includes files and code that haven’t been mentioned in this tutorial.
I hope this tutorial was easy to understand and enjoyable to read.
Have fun coding! 😊