The issue of privacy in messaging apps cannot be overemphasized. As a result, messaging apps find it handy to implement short-lasting messages to maintain the secrecy of conversations. Recently, we built a solution that gives a five-second window before read messages are deleted.
In this tutorial, you will take a step further by sending voice notes instead. You will also add some extra spice such as audio visualization, notifications, online presence and support for private messaging. If this sounds like something you’d be interested in then stick with me till the end of this tutorial or find the entire code on this repository.
Here’s a GIF of what your app should like by the end of this tutorial.
Creating a CometChat app
In this tutorial, you’ll use CometChat as a service provider. CometChat enables you to add voice, video, text chat to your apps easily. To begin, head on over to CometChat to create an account. Next, head to your dashboard and create a new app called self-destructing-voice-notes. At this point, you should be redirected to a page with your newly created app. Click on the explore button then go to the API Keys tab, copy your APP ID and API Key from the list with fullAccess scope and save that for when you scaffold your new project.
Hold your horses ⚠️🐴!
To follow this tutorial or run the example source code you'll need to create a V1 application.
v2 will be out of beta soon at which point we will update this tutorial.
Scaffolding a React project
You will make use of the popular create-react-app package to scaffold a new react project. Open a terminal window, move into a directory of your choice and run this command:
This step installs all the necessary dependencies needed to start this project.
Installation of dependencies
The next step is to install some project-specific dependencies. Here are the dependencies you will make use of:
react-router-dom: Since this is a single page application, you need this package for client-side routing.
axios: This package will be used for making HTTP requests in the application.
react-icons: You will use font-awesome icons present in this package to make the design pleasing.
To install the above dependencies, move into the project directory and run this command:
In this tutorial, you will use Bootstrap for styling. You need to include a link to the CDN in your public/index.html, under the <head> tag like so:
Initializing CometChat Pro SDK
Now that you have installed your dependencies, go ahead and open your project in any IDE of your choice. When you open your project, create a .env file at the root of the project folder and add this code to it:
Replace the placeholders with the actual credentials from your CometChat dashboard. Also, note that you should not commit this file to version control.
After replacing your keys, open the index.js file located in the src folder and replace it with the following snippet:
In this snippet, you are initializing CometChat with the keys provided in the .env file.
Setting up application routes
As mentioned earlier, you need the react-router-dom package for navigation. Here is a list of routes you need for this application:
/: This is the home page of your application where you will have a list of friends you can chat with.
/login: This route directs to the login page of your app.
/chat/:uid: This is the route that renders a component for private conversations. The :uid is a placeholder for data about the user you are chatting with.
Now that you know the routes you need, go ahead and add them in the App.js file. Replace the file with this snippet:
In this file, you’ve declared the routes to be used in the app. You imported components that don’t exist yet. But you’ll build them out in the coming sections.
Creating the Login Component
The first component you will build is the Login component. Create a file called login.js in the src/components directory and add this snippet:
In this snippet, you imported the modules you will make use of. You also defined four state variables. Here is what each variable does:
username - This is used to store the username of the user currently logged in.
error - This is used to keep error messages if any exist.
isRedirected - To know whether the user has been redirected or not.
isLoggingIn - This will serve as an indicator to know when a login request has been made. If set to true for example, the login button will be disabled until the request is resolved or rejected. And a loading spinner will be shown in the process.
Next, you will create two functions. The first one will update the username as the user types and the second to handle login. Add these functions in the Login() of your login.js file:
From the snippet above, the handleUsernameChange function is used to update the username variable defined earlier when the user types while the handleLogin function uses that username entered to request the CometChat API to log in the user.
Notice how the setIsLogginIn state updater is being called multiple times. This is meant to update the login button to essentially enable or disable it depending on whether a request is being made or not.
If everything goes successfully, a user token is returned and stored in local storage for later use. Otherwise, an error is returned and displayed to the user. Finally, for this component, you will render a UI. Add this snippet after the handleLogin function:
Before the return statement, you have the isRedirected variable to redirect the user to the Home component after a successful login if its value is true. Remember that this variable is set to true when login is successful with this: setIsRedirected(true);.
With the return statement, you have a form rendered on the page and those functions declared earlier are now hooked up to the HTML.
Creating the Home Component
The next component you will build is the Home component. Create a file called home.js in the src/components directory. Add this snippet to your file:
Here, you imported modules you need and you also declared some state variables. There are three state variables:
authToken to get the authentication token,
users to keep track of the friends of the logged-in user and
isRedirected to check if the user has been redirected or not.
After that, you will do the following:
Fetch the friends list of the logged-in user.
Listen for the online or offline status of each friend, and
Show notification for new messages.
To start, add this useEffect hook inside your Home function just below the state variables like so:
In the snippet above, there is a check to ensure that the token saved in local storage is present so that it can be used to re-authenticate returning users without the need to login manually.
Once that condition has been satisfied, the next thing you do is to use CometChat’s API to fetch users list and update the users state variable. In this tutorial, you will stick with the default users created by CometChat.
As a second argument to the useEffect function, the authToken variable is passed in the dependency array to enable this component to be aware of any changes in the token and re-render as needed.
After that, the next thing you will do is listen for the online/offline status of the friends stored in the users variable. You will use yet another useEffect hook to achieve this. Add this snippet in your home.js file just below the previous useEffect hook:
In this useEffect hook, an event listener is setup with the users state variable as a dependency. When the Home component is first mounted, here is what you listened for:
onUserOnline: This function returns an object containing the data of any user that comes online. In effect, the users state variable gets updated with the online user. Later in the UI, you'll use this to distinguish online users with a green online status badge. The user listener needs a unique id to listen on and that is what you used the listenerID variable for. It is recommended to store it in a variable so that you can reference it in the cleanup function when the event listener is removed.
onUserOffline: This function is similar to the onUserOnline function. In this case, the data of any user that goes offline is returned. With that data, the users state variable gets updated again to reflect changes in the UI.
The last part of this useEffect function is the cleanup function where the user listener is removed. This part is called when the component un-mounts. You can think of it like componentWillUnmount in class components. The user event listener is removed when the component is unmounted to prevent a memory leak.
You will now see how you can add realtime notifications when a new message comes in. CometChat provides a message listener function that gets called every time a message is sent. Still, in your home.js file, add this snippet under the previous useEffect to listen for new messages:
In this useEffect hook, a message event listener is setup with the users state variable as a dependency. When a new message is received, it is passed down to the onMediaMessageReceived function. In this function, there is a comparison between each user’s UID and the sender UID to determine who the message is meant for.
After the correct user is found, the user object is updated with a key and value containing the message count for that user and then displayed on the UI. This is useful for displaying a badge with the new message count for that user. Finally, as usual, the message listener is removed when the component is unmounted in the cleanup function.
The final step to complete in this component is to return some HTML. Add this snippet after the previous useEffect hook:
In this snippet, the users list is mapped and displayed in the DOM. Each user has a name, avatar, online status, message count (maybe), and an onClick event listener. When you select any user, you will be taken to another screen to chat with that particular user. Here, you also have a logout button. When clicked, the local storage will be cleared and you will be redirected to the Login component.
Setting up helper files
As mentioned earlier, when you select a user on the Home component, you will be redirected to the Chat component. Before creating that component, you will create some helper files that it requires. You will start with the audio recorder script.
In the src directory, create a file named scripts.js and paste this snippet:
In this snippet, you have a function that receives a media stream as a parameter. This media stream will be passed down from the Chat component when the user grants permission to the devices’ audio. Once the stream data is available, an instance of the media recorder is created with the stream.
Also, the record and stop functions are returned to allow control of when to start and stop the recording. In the stop function, a promise is returned with the actual audio file that will be sent to the user.
Another feature you would add in this chat app is waveforms. This is so that you can visualize the current voice note being played. So, you will create a file that will perform this action. Create a new file audio-visualizer.js in the src/components directory and paste this snippet:
In this snippet, you passed the audio element from the Home component. This component is going to be responsible for using the Web Audio API to analyze the audio stream display it in the UI with a canvas.
I highly recommend that you read through the MDN article on visualizations with the Web Audio API to find out what else you could achieve with this.
This snippet was majorly extracted from this GitHub repository.
Creating the Chat Component
Now that you have finished writing the helper files, you will stitch them together in the Chat component. This is the component that is rendered when a user in the Home component is selected. In essence, this is where private voice messages will be sent, received, deleted, and visualized.
Create a chat.js file in src/components directory and add this snippet to it:
In this snippet, you have imported the audio recorder function, audio visualizer component, and other core modules. You have also declared the initial state of the component.
Just below the variable declarations, add this snippet to get permission from the user to use their audio device as soon as this component is mounted:
If this was an app where you wanted to send video recordings as well, you’d need to pass
video:true in the getUserMedia function but the audio is all you should care about right now.
The next thing you will do is get the details of the user selected. Add this useEffect hook just below the audio permission snippet:
In this snippet, you used the UID stored earlier in the state to get information about the current user and store in the state as well. You also fetched previous messages exchanged with the current user and updated the messages in state.
Now, you will add a message event listener with a useEffect hook to have full control over what is done with the messages. Here are the triggers you will pay attention to:
onMediaMessageReceived This function will be called whenever a new media message is received.
onMessageDeleted: This function will be called after sending a request to delete a message.
onMessageRead: This function is called when the receiver has read the message. This is where you will perform the voice message deletion.
Paste this snippet under the last useEffect hook in your chat.js file:
From this snippet, you are performing different tasks in the message listeners.
First, the onMediaMessageReceived function is called when there is a new message. The new message returned from the function is then used to update the state of the previous messages by calling the setMessages function. In the onMessageRead function, you delete the message whose id is returned. Thereafter, you update the messages state variable accordingly. Finally, in the onMessageDeleted function, you filter out the deleted message from the DOM.
After that, you will now add the ability to recording media messages to the component. You had created a script (scripts.js) to ease this earlier. Now, paste the following snippet below your last useEffect function:
This function will be called when the record button is held down. The recordButtonRef is attached to the record button in the DOM so that it can be styled accordingly when pressed or released. The streamRef variable is returned when the user grants permission to their audio device which is in turn passed to the audioRecorder function.
Once the stream is available in the recorder, the recording of voice notes begins until the user releases the record button. The process is sending the voice is automatically triggered when the record button is released. Add these functions in your chat.js file under the handleMouseDown function:
Here, you defined two functions - handleMouseUp and sendAudioFile. The first is called immediately the user releases the record button. In this function, the recording is stopped and the second function is called. The second function takes the audio file and sends it using CometChat's API. After sending, the messages in the state are updated.
Now that you know how to send voice messages, you will now handle audio playback. Add this snippet just below the last function:
This function takes the current media message selected by the user. It uses the URL present in the message to fetch the actual blob that will be played back to the user. This is the case because the original URL provided when the message was delivered throws a CORS error when trying to extract data needed for audio visualization.
When the GET request is made, it returns a blob which is then converted to an object URL to be used as the source for the audio. If no error is thrown, the audio should be playable.
Next up is deleting the message after the audio had played to completion. To do that, you need to set up an event listener that listens for when the audio playback is complete. At that point, you know the user has listened to the message and you can, therefore, delete it. To do this, create another useEffect function that handles this next to the last useEffect function and paste this snippet:
In this snippet, you set up an event listener to listen for changes in the audio playback, specifically the current time and duration. If the current time is exactly equal to the duration of the audio, then it means the audio has finished playing. In that case, the markMessageAsRead function is called with the current message as a parameter.
Earlier in the article, you had already set up an event listener to listen for when a message has been read. In that function, CometChat's deleteMessage function is called to delete the message.
At this point in the application, you've covered all the functionality required to self destruct voice notes.
You will now handle the UI of this component by returning some JSX. Paste this snippet below the last useEffect hook:
First, there is a check to know if the user should be redirected to the Login component. This is when the value of isRedirected in the state is true and this value can only be true when the user clicks the logout button.
Still, in this snippet, the audio element is conditionally rendered only when there's a current audio playing and that is made possible by setting the value of isVisible to true. Next, the messages in the state are mapped over and displayed as a list item, each containing information about the sender, a play button and a canvas that's only visible if audio is being played.
Finally, there's a record button rendered at the footer of the page that's responsible for recording and stopping the audio.
That concludes this tutorial! Whoop!
You can test this app by opening this project in your terminal and running this command:
Your app will be hosted at http://localhost:3000
In this tutorial, you have learned how to create a self-destructing voice notes application with audio visualization using React and CometChat. You learned some advanced CometChat features and how you can use it to achieve real-life use cases like what we have here. There is still so much more you can achieve with CometChat. Feel free to dive into the GitHub repo and built on top of what is in there already.