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.

Demo of app built in the course of the 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:

npx create-react-app self-destructing-voice-notes

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:

  • @cometchat-pro/chat: This package will allow you use CometChat JavaScript SDK to send and receive voice messages in real-time.
  • 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:

# install dependencies using npm
npm install @cometchat-pro/chat react-router-dom axios react-icons

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:

   <link 
      rel="stylesheet" 
      href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" 
      integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" 
      crossorigin="anonymous" 
    /> 

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:

REACT_APP_COMETCHAT_API_KEY=YOUR_COMETCHAT_API_KEY
REACT_APP_COMETCHAT_APP_ID=YOUR_COMETCHAT_APP_ID

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:

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { CometChat } from '@cometchat-pro/chat';

CometChat.init(process.env.REACT_APP_COMETCHAT_APP_ID)
  .then(() => {
    console.log('Initialized CometChat');
  })
  .catch(() => {
    console.log('Failed to Initialize CometChat');
  });
ReactDOM.render(<App />, document.getElementById('root'));

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:

// src/App.js

import React from 'react'
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom'
import Home from './components/home'
import Login from './components/login'
import Chat from './components/chat'
function App() {
    return (
        <Router>
            <Switch>
                <Route exact path='/' component={Home} />
                <Route exact path='/login' component={Login} />
                <Route exact path='/chat/:uid' component={Chat} />
            </Switch>
        </Router>
    )
}
export default App

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:

// src/components/login.js

import React, { useState } from 'react';
import { Redirect } from 'react-router-dom';
import { CometChat } from '@cometchat-pro/chat';
function Login() {
  const [username, setUsername] = useState('');
  const [error, setError] = useState(null);
  const [isRedirected, setIsRedirected] = useState(false);
  const [isLoggingIn, setIsLoggingIn] = useState(false);
  // other functions
}
export default Login;

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:

// src/components/login.js

function Login() {
  // ...state variables

  const handleUsernameChange = e => {
    setUsername(e.target.value);
  };
  const handleLogin = e => {
    e.preventDefault();
    const _username = username;
    setIsLoggingIn(true);
    setUsername('');
    CometChat.login(_username, process.env.REACT_APP_COMETCHAT_API_KEY).then(
      user => {
        localStorage.setItem('cometchat:token', user.authToken);
        setIsLoggingIn(false);
        setIsRedirected(true);
      },
      error => {
        setIsLoggingIn(false);
        setError(error.message);
      }
    );
  };

}

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:

// src/components/login.js

if (isRedirected) return <Redirect to='/' />;
return (
  <div className='container'>
    <div className='row' style={{ marginTop: '30vh' }}>
      <div className='col-xs-12 col-sm-12 col-md-8 col-lg-6 mx-auto'>
        <h1>Login</h1>
        {error !== null && <div className='alert alert-danger'>{error}</div>}
        <div className='card card-body'>
          <form onSubmit={e => handleLogin(e)}>
            <div className='form-group'>
              <label htmlFor='username'>Username</label>
              <input
                type='text'
                id='username'
                value={username}
                onChange={handleUsernameChange}
                required
                className='form-control'
                placeholder='Username'
              />
            </div>
            <input
              disabled={isLoggingIn ? 'disabled' : ''}
              type='submit'
              value={isLoggingIn ? 'Please wait...' : 'Login'}
              className='btn btn-primary'
            />
          </form>
        </div>
      </div>
    </div>
  </div>
);

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:

// src/components/home.js

import React, { useEffect, useState } from 'react';
import { Redirect } from 'react-router-dom';
import { CometChat } from '@cometchat-pro/chat';
import { FaSignOutAlt } from 'react-icons/fa';

function Home({ history }) {
  const authToken = localStorage.getItem('cometchat:token');
  const [users, setUsers] = useState([]);
  const [isRedirected, setIsRedirected] = useState(false);

  // other functions
}
export default Home;

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:

// src/component/home.js

// ... state variables
  useEffect(() => {
    if (authToken !== null) {
      CometChat.login(authToken).then(
        user => {
          const limit = 4;
          const usersRequest = new CometChat.UsersRequestBuilder()
            .setLimit(limit)
            .build();
          usersRequest.fetchNext().then(
            userList => {
              setUsers(userList);
            },
            error => {
              console.log('error:', error);
            }
          );
        },
        err => {
          console.log({ err });
        }
      );
    }
  }, [authToken]);

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:

// src/components/home.js

useEffect(() => {
  const listenerID = 'online_listener';
  CometChat.addUserListener(
    listenerID,
    new CometChat.UserListener({
      onUserOnline: onlineUser => {
        const otherUsers = users.filter(u => u.uid !== onlineUser.uid);
        setUsers([onlineUser, ...otherUsers]);
      },
      onUserOffline: offlineUser => {
        const targetUser = users.find(u => u.uid === offlineUser.uid);
        if (targetUser && targetUser.uid === offlineUser.uid) {
          const otherUsers = users.filter(u => u.uid !== offlineUser.uid);
          setUsers([...otherUsers, offlineUser]);
        }
      }
    })
  );
  return () => CometChat.removeUserListener(listenerID);
}, [users]);

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:

// src/components/home.js

useEffect(() => {
  const listenerID = 'home_component';
  CometChat.addMessageListener(
    listenerID,
    new CometChat.MessageListener({
      onMediaMessageReceived: mediaMessage => {
        const _users = [...users];
        const selectedUser = _users.find(
          u => u.uid === mediaMessage.sender.uid
        );
        selectedUser.messageCount = selectedUser.messageCount
          ? selectedUser.messageCount + 1
          : 1;
        const filtered = [..._users].filter(u => u.uid !== selectedUser.uid);
        setUsers([selectedUser, ...filtered]);
      },
      onMessageRead: messageReceipt => {
        CometChat.deleteMessage(messageReceipt.messageId).then(
          msg => {},
          err => {
            console.log({ err });
          }
        );
      }
    })
  );

  return () => CometChat.removeMessageListener(listenerID);
}, [users]);

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:

// src/components/home.js

  if (authToken === null || isRedirected) return <Redirect to='/login' />; 

  return (
    <div className='container'>
      <h2 className='text-center mt-2'>Ephemeral Voice Messaging</h2>
      <div className='d-flex justify-content-between align-items-end'>
        {users.length > 0 && (
          <p className='lead mt-5'>Users ({users.length})</p>
        )}
        <button
          className='btn btn-light mb-2'
          onClick={() => {
            localStorage.clear();
            setIsRedirected(true);
          }}
        >
          <FaSignOutAlt /> Logout
        </button>
      </div>
      <ul className='list-group-item'>
        {users.length > 0 ? (
          users.map(user => (
            <li
              className='list-group-item d-flex justify-content-between align-items-center'
              key={user.uid}
              onClick={() => history.push(`/chat/${user.uid}`)}
              style={{ cursor: 'pointer' }}
            >
              <div className='left d-flex'>
                <img
                  style={{ borderRadius: '50%' }}
                  src={user.avatar}
                  height={50}
                  width={50}
                  alt={user.name}
                />
                <div className='ml-3'>
                  <span className='d-block'>{user.name}</span>
                  <small
                    className={
                      user.status === 'online'
                        ? 'd-block text-success'
                        : 'd-block text-danger'
                    }
                  >
                    {user.status}
                  </small>
                </div>
              </div>
              <span className='badge badge-primary'>
                {user.messageCount !== undefined && user.messageCount}
              </span>
            </li>
          ))
        ) : (
          <p className='text-center'>Fetching Users...</p>
        )}
      </ul>
    </div>
  );

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:

// src/scripts.js

export const audioRecorder = stream =>
  new Promise(async resolve => {
    const mediaRecorder = await new MediaRecorder(stream);
    const audioChunks = [];
    mediaRecorder.addEventListener('dataavailable', event => {
      audioChunks.push(event.data);
    });
    const record = () => mediaRecorder.start();
    const stop = () =>
      new Promise(resolve => {
        mediaRecorder.addEventListener('stop', () => {
          const audioBlob = new Blob(audioChunks);
          const audioFile = new File([audioBlob], 'voice note', {
            type: 'audio/wav'
          });
          resolve({ audioFile });
        });
        mediaRecorder.stop();
      });
    resolve({ record, stop });
  });

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:

// src/components/audio-visualizer.js

import React from 'react';

function AudioVisualizer({ audio }) {
  const canvasRef = React.useRef();
  const animationFrameRef = React.useRef();
  const audioSrcRef = React.useRef();
  const analyserRef = React.useRef();
  React.useEffect(() => {
    window.AudioContext =
      window.AudioContext ||
      window.webkitAudioContext ||
      window.mozAudioContext;
    const audioContext = new AudioContext();
    analyserRef.current = audioContext.createAnalyser();
    audioSrcRef.current = audioContext.createMediaElementSource(audio);
    audioSrcRef.current.connect(analyserRef.current);
    analyserRef.current.connect(audioContext.destination);
    const cwidth = canvasRef.current.width;
    const cheight = canvasRef.current.height - 2;
    const meterWidth = 10;
    const capHeight = 2;
    const capStyle = '#fff';
    const meterNum = 800 / (10 + 2);
    const capYPositionArray = [];
    const ctx = canvasRef.current.getContext('2d');
    const gradient = ctx.createLinearGradient(0, 0, 0, 300);
    gradient.addColorStop(1, '#0f0');
    gradient.addColorStop(0.5, '#ff0');
    gradient.addColorStop(0, '#f00');
    function renderFrame() {
      const array = new Uint8Array(analyserRef.current.frequencyBinCount);
      analyserRef.current.getByteFrequencyData(array);
      const step = Math.round(array.length / meterNum);
      ctx.clearRect(0, 0, cwidth, cheight);
      for (let i = 0; i < meterNum; i++) {
        const value = array[i * step];
        if (capYPositionArray.length < Math.round(meterNum)) {
          capYPositionArray.push(value);
        }
        ctx.fillStyle = capStyle;
        if (value < capYPositionArray[i]) {
          ctx.fillRect(
            i * 12,
            cheight - --capYPositionArray[i],
            meterWidth,
            capHeight
          );
        } else {
          ctx.fillRect(i * 12, cheight - value, meterWidth, capHeight);
          capYPositionArray[i] = value;
        }
        ctx.fillStyle = gradient;
        ctx.fillRect(i * 12, cheight - value + capHeight, meterWidth, cheight);
      }
      animationFrameRef.current = requestAnimationFrame(renderFrame);
    }
    renderFrame();
    return () => {
      cancelAnimationFrame(animationFrameRef.current);
      audioSrcRef.current.disconnect();
      analyserRef.current.disconnect();
    };
  }, [audio]);
  return <canvas ref={canvasRef} style={{ height: '60px', width: '50%' }} />;
}

export default AudioVisualizer;

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:

// src/components/chat.js

import React, { useState, useEffect, useRef } from 'react';
import { CometChat } from '@cometchat-pro/chat';
import {
  FaMicrophone,
  FaPlay,
  FaPause,
  FaChevronLeft,
  FaSignOutAlt
} from 'react-icons/fa';
import { audioRecorder } from '../scripts';
import { Redirect } from 'react-router-dom';
import axios from 'axios';
import AudioVisualizer from './audio-visualizer';

function Chat({ match, history }) {
  const [UID] = useState(match.params.uid);
  const [messages, setMessages] = useState([]);
  const [currentUser, setCurrentUser] = useState(null);
  const [currentMessage, setCurrentMessage] = useState(null);
  const [isPlaying, setIsPlaying] = useState(false);
  const [isRedirected, setIsRedirected] = useState(false);
  const [isVisible, setIsVisible] = useState(true);
  const recordButtonRef = useRef();
  const recorderRef = useRef();
  const audioPlayerRef = useRef();
  const streamRef = useRef();
  const URLRef = useRef();

  // other functions

}
export default Chat;

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:

// get audio permission
streamRef.current = navigator.mediaDevices.getUserMedia({ audio: true })

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:

// src/components/chat.js

useEffect(() => {
  // get user via uid
  CometChat.getUser(UID).then(
    user => {
      setCurrentUser(user);
    },
    error => {
      console.log('User details fetching failed with error:', error);
    }
  );

  // listen for messages in real-time
  const messagesRequest = new CometChat.MessagesRequestBuilder()
    .setUID(UID)
    .setLimit(100)
    .build();
  messagesRequest.fetchPrevious().then(
    messages => {
      const filtered = messages.filter(m => m.file !== undefined);
      setMessages(prevMessages => [...prevMessages, ...filtered]);
    },
    error => {
      console.log('Message fetching failed with error:', error);
    }
  );
}, [UID]);

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:

// src/components/chat.js

useEffect(() => {

  // receive messages
  const listenerID = UID;
  CometChat.addMessageListener(
    listenerID,
    new CometChat.MessageListener({
      onMediaMessageReceived: mediaMessage => {
        setMessages(prevMessages => [...prevMessages, mediaMessage]);
      },
      onMessageDeleted: deletedMessage => {
        const filtered = messages.filter(m => m.id !== deletedMessage.id);
        setMessages([...filtered]);
      },
      onMessageRead: messageReceipt => {
        CometChat.deleteMessage(messageReceipt.messageId).then(
          msg => {
            const filtered = messages.filter(
              m => m.id !== messageReceipt.messageId
            );
            setMessages([...filtered]);
          },
          err => {
            console.log({ err });
          }
        );
      }
    })
  );
  return () => CometChat.removeMessageListener(listenerID);
}, [UID, messages]);

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:

// src/components/chat.js

const handleMouseDown = async () => {
    recordButtonRef.current.classList.replace('btn-secondary', 'btn-danger');
    streamRef.current
        .then(async stream => {
            recorderRef.current = await audioRecorder(stream);
            recorderRef.current.record();
        })
        .catch(err => console.log({ err }));
};

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:

// src/components/chat.js
const handleMouseUp = async () => {
  recordButtonRef.current.classList.replace('btn-danger', 'btn-secondary');
  const audio = await recorderRef.current.stop();
  sendAudioFile(audio.audioFile);
};

const sendAudioFile = audioFile => {
  const receiverID = currentUser.uid;
  const messageType = CometChat.MESSAGE_TYPE.AUDIO;
  const receiverType = CometChat.RECEIVER_TYPE.USER;
  const mediaMessage = new CometChat.MediaMessage(
    receiverID,
    audioFile,
    messageType,
    receiverType
  );
  CometChat.sendMediaMessage(mediaMessage).then(
    message => {
      setMessages([...messages, message]);
    },
    error => {
      console.log('Media message sending failed with error', error);
    }
  );
};

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:

const playbackAudio = message => {
  setIsVisible(true);
  axios(message.url, {
    method: 'get',
    responseType: 'blob'
  })
    .then(res => {
      const audioUrl = URL.createObjectURL(new Blob([res.data]));
      audioPlayerRef.current.src = audioUrl;
      setCurrentMessage(message);
      URLRef.current = audioUrl;
      audioPlayerRef.current.play();
      setIsPlaying(true);
    })
    .catch(err => {});
};

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:

useEffect(() => {
  const ref = audioPlayerRef;
  const handleTimeUpdate = e => {
    if (e.target.duration === e.target.currentTime) {
      setIsPlaying(false);
      setIsVisible(false);
      CometChat.getMessageReceipts(currentMessage.id).then(
        receipts => {
          receipts.forEach(receipt => {
            if (
              receipt.sender.uid === currentUser.uid &&
              receipt.readAt === undefined
            ) {
              return;
            }
            CometChat.markMessageAsRead(currentMessage);
          });
        },
        error => {
          console.log('Error in getting messag details ', error);
        }
      );
    }
  };
  ref.current.addEventListener('timeupdate', handleTimeUpdate);
  return () => {
    if (ref.current) {
      ref.current.removeEventListener('timeupdate', handleTimeUpdate);
    }
  };
}, [isPlaying, currentUser, currentMessage]);

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:

// src/components/chat.js

// .. other functions

if (isRedirected) return <Redirect to='/' />;
return (
  <div
    className='chat-page container'
    style={{
      display: 'flex',
      flexDirection: 'column',
      justifyContent: 'space-between',
      height: '100vh'
    }}
  >
    {isVisible && <audio ref={audioPlayerRef} style={{ display: 'none' }} />}
    <div className='mt-5' style={{ height: '60px' }}>
      {currentUser !== null && (
        <div className='d-flex justify-content-between align-items-end'>
          <div className='d-flex justify-content-start'>
            <button
              onClick={() => {
                history.goBack();
              }}
              className='btn mr-3'
              style={{ fontWeight: '500' }}
            >
              <FaChevronLeft /> Chats
            </button>
            <div className='d-flex align-items-center'>
              <span className='d-block'>{currentUser.name}</span>
            </div>
          </div>
          <button
            className='btn btn-light'
            onClick={() => {
              setIsRedirected(true);
              localStorage.clear();
            }}
          >
            <FaSignOutAlt /> Logout
          </button>
        </div>
      )}
    </div>
    <ul className='list-group-item' style={{ flex: 1 }}>
      {messages.length > 0
        ? messages.map((message, i) => (
            <li
              className='list-group-item d-flex align-items-center justify-content-between'
              key={i}
            >
              <div className='d-flex align-items-center'>
                <button
                  disabled={
                    currentMessage &&
                    currentMessage.url !== message.url &&
                    isPlaying
                      ? 'disabled'
                      : ''
                  }
                  onClick={e => playbackAudio(message, message.id)}
                  style={{
                    width: '50px',
                    height: '50px',
                    borderRadius: '50%'
                  }}
                  className='btn btn-secondary'
                >
                  {currentMessage && currentMessage.url === message.url ? (
                    isPlaying ? (
                      <FaPause />
                    ) : (
                      <FaPlay />
                    )
                  ) : (
                    <FaPlay />
                  )}
                </button>
                <p className='pl-3'>{message.sender.uid}</p>
              </div>
              {currentMessage &&
                currentMessage.url === message.url &&
                isPlaying && <AudioVisualizer audio={audioPlayerRef.current} />}
            </li>
          ))
        : null}
    </ul>
    <footer
      className='text-center d-flex align-items-center'
      style={{ height: '80px' }}
    >
      <button
        disabled={currentUser === null ? 'disabled' : ''}
        ref={recordButtonRef}
        className='btn btn-secondary mx-auto'
        style={{ height: '60px', width: '60px', borderRadius: '50%' }}
        onMouseDown={handleMouseDown}
        onMouseUp={handleMouseUp}
      >
        <FaMicrophone />
      </button>
    </footer>
  </div>
);

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:

npm start

Your app will be hosted at http://localhost:3000

Conclusion

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.