Build a Video Conferencing App Using Dolby Voxeet and Stream Chat

With the recent launch of Stream Chat, the team here has been working with vendors to add real-time chat and messaging to various platforms. We’ve had several requests for voice and video chat, and after looking around at the market, it was a clear choice to partner up with Voxeet (recently acquired by Dolby). The proof is in the pudding, so we built out a fully functional proof of concept for a live video-conferencing application using Stream Chat and Voxeet

Voxeet

With the recent launch of Stream Chat, the team here has been working with vendors to add real-time chat and messaging to various platforms. We’ve had several requests for voice and video chat, and after looking around at the market, it was a clear choice to partner up with Voxeet (recently acquired by Dolby). The proof is in the pudding, so we built out a fully functional proof of concept for a live video-conferencing application using Stream Chat and Voxeet as a web application with React.

The reason for our decision to use Voxeet over other competitors such as Twilio’s Programmable Video platform was primarily due to the following:

  • Voxeet is an industry leader in the video space and provides SDKs for popular languages – in either your choice of JavaScript, iOS (Swift), or Android (Java)
  • Predictable and generous pricing model that flat out makes sense – it’s based on the number of voice and video minutes
  • Their documentation is written for developers with an emphasis on how to integrate – including nice example tutorials in the beginning
  • They provide a React integration that works flawlessly with the Stream Chat React components making it easy for developers to access the underlying Voxeet API and connect it with Stream Chat

Furthermore, our initial tests showed that Voxeet had substantially less latency and offered far more clarity when it came to real-time video (and their video conferencing API) compared to Twilio. With better documentation, we knew ahead of time that we would be able to execute without running into any hiccups.

For the rest of this post, we will outline how we went about building a competitor similar to Zoom (the video communication tool) in less than a week. We'll be using Voxeet for live video (via WebRTC) as well as Stream Chat for real-time chat/messaging capabilities.

Note: You can find the demo located here. The full code for the frontend UI is available on GitHub. If you would like to run the demo using a backend API, please see this GitHub repo.

Prerequisites

To fully follow along with this tutorial, you’ll want to make sure that you have a decent understanding of the following:

  • JavaScript / Node.js
  • React
  • Redux / Redux Saga

You will also want to ensure that you have the following installed on your machine:

And, you’ll need to have created free accounts with the following services:

  • Heroku (for hosting the API – free tier)
  • Stream (for real-time chat functionality – free 14-day trial)
  • Voxeet (for video conferencing – free tier)

Note: We’ll be using the latest version of Chrome on macOS to test throughout this tutorial.

Getting Started

The Build

The build-out of this application is rather straightforward. It requires two primary elements – the frontend UI and the backend API. If you want to take it a step further, you can host the frontend CRA application on Netlify and the API on Heroku in just a few minutes.

Step 1: Frontend UI

First, make sure you have an account set up with both Stream and Voxeet, and you have all the keys and tokens for each in hand, you will need the following:

  • Stream Key
  • Stream Secret
  • Voxeet Key
  • Voxeet Secret

Next, run npx create-react-app stream-voxeet in your terminal. Once complete, add the Stream API key and the Voxeet keys from the associated dashboards to a .env file within your project's root directory for safe-keeping.

Note: _With create-react-app, you must prepend all environment variables with REACTAPP.

You will need the following environment variables in your .env file:

  • REACT_APP_STREAM_KEY
  • REACT_APP_VOX_KEY
  • REACT_APP_VOX_SECRET

Head back to your terminal and install the dependencies we need for the project:

https://gist.github.com/nparsons08/e4ec99bab16cd5bca596500db9c457f4

Note: In our initial testing we realized that the Voxeet components currently throw an error with the latest version of react-redux. Make sure you enter react-redux@5.1.1 when you install the dependencies above.

Finally, create a jsconfig.json file in the root of your application. Copy and paste the following:

https://gist.github.com/nparsons08/a26454567780ec0d4b405ffd694f862a

Note: The jsconfig.json file will allow aliased imports based on your directory structure without having to use absolute paths to your modules and components. E.g. import Button from components/Button.

Now that our project is all set up let’s jump into our code editor and start building.

Step 2: Setup

First up, we’ll get our Redux store, middleware, and styled-components theme set up, as well as removing the defaults provided by the CRA boilerplate.

Next, navigate to the src directory in your project and remove the index.css, app.css, and logo.svg files.

You’ll want to remove the imports for these assets in the App.js and the index.js files, as well as strip all boilerplate markup out of App.js – leaving only the parent div.

Note: You can also remove the className prop as we will replace all of the CSS with styled-components. Doing so will provide a theme file where you can easily tweak aspects of the design of the app once it is fully built.

Then, create the following directories inside your /src directory:

  • components
  • containers
  • data
  • styles
  • screens

Styles

We’ll start by creating a couple of files inside of the styles directory. First, create styles/colors.js and styles/breakpoints.js respectively, and paste the following:

Colors:

https://gist.github.com/nparsons08/51da31206d95631308735dcc712f3f55

Breakpoints:

https://gist.github.com/nparsons08/d79b0f1940391023739291db0cad542e

Next, we will import these variables in a moment to help populate our styled-components theme. Then, we’ll create a colorUtils.js file and paste the following code inside:

https://gist.github.com/nparsons08/e2b65df7ee344a7b0034cfce95c9b7d0

Note: This file is adapted from v0.x of Material UI and offers handy utilities for handling colors – such as converting hex to RGB/RGBA and quickly altering the opacity, hue, and saturation of the color just by passing in a hex value. We often add this to our theme definition for easy access inside styled-components.

Now, we can bring this all together in styles/theme.js – this theme will be available inside all of our styled-components and accessible through the withTheme HOC.

https://gist.github.com/nparsons08/8beeadf066444ab3c76061ca789f9974

Last, we’ll create styles/global.js. This file is effectively a drop-in replacement for index.css that we removed earlier. Please note that we’ll also import two additional files at a later time to override styles for both stream-chat-react and voxeet.

https://gist.github.com/nparsons08/a84d7085a899c1d8268201a869abf8ef

We’ll get this all tied together in our App.js shortly, but first, we’ll set up our Redux store.

Redux

Inside of your data directory, add a file called createReducer.js with the following code inside of it:

https://gist.github.com/nparsons08/e3cc8d3cad163762b6189b7aea85c3fe

The following code will hook the Voxeet reducer up to our Redux store and enable the Conference components to work correctly. We will also create a rootSaga.js file within the data directory; however, for now, we will leave it blank and come back to it later.

https://gist.github.com/nparsons08/5d40f4cd65253b6264996db14a40bd57

Next, let's create the Redux store itself in data/createStore.js to tie it all together – we will add the thunk middleware which is required under the hood in the Voxeet components, as well as the redux-saga middleware which we will use a bit later on when we add simple authentication to the app as well as some other fun stuff.

https://gist.github.com/nparsons08/2690da6cb7b6e2dbe0eb36702a7e5908

Now that our theme and our Redux store are ready to go, we’ll wrap our app in ThemeProvider, react-redux’s Provider and include our global styles and initial route definitions. To do this, open App.js and make sure it looks like the following snippet:

https://gist.github.com/nparsons08/b75ac9a5d84c0d5b87d1502a6c457c12

We’re now ready to start including Voxeet’s components and later, Stream Chat too. However, you should now have two missing imports from the file above. One is the Conference screen which we will create in just a second. The other is utils/history.js.

By importing history in this way, we can pass it to the Router in the file above and it will operate identically to react-router v4’s BrowserRouter. We can also import it in our sagas (and anywhere in our code for that matter) to navigate programmatically without using <Link /> or in areas where the history prop is inaccessible.

Here is the code for the history util:

https://gist.github.com/nparsons08/e834d53db9852ea7d476bdfacbbfc801

Note: We will still want to use the component in our JSX code – but this allows us, for example, to redirect a user when they log in or log out, from within the authentication saga itself.

Step 3: Initializing Voxeet

We’ll start by creating the Conference screen where the video calls themselves will take place. Create Conference/Conference.js inside of your screens directory, open it in your editor and paste in the following:

https://gist.github.com/nparsons08/cffc1cd348a32dc1a8346c7b2c7b2e70

You should now be able to go to http://localhost:3000/test to see a video call screen that is fully operational. The Voxeet components are fantastic. Out of the box, Voxeet provides full functionality to all of the features available by their SDK and APIs.

We are using the URL param match.params.conferenceAlias as defined in App.js to scope each conference to the URL – which also enables users to share the URL to invite other participants, much like Google Hangouts or Zoom. At this point, if you were to push your site up to Netlify, you can share the URL with a friend, and you will be able to conduct a full-featured video call! Note that you can use any random string after the / in the URL to set the conference id – but more on that later.

Right now we only have control over the styling of the Voxeet UI through overriding the default CSS and users will have randomly generated usernames and ids. So, let’s fix that.

We can achieve much higher customization of the UI by passing in some additional props to the ConferenceRoom component, namely attendeesChat and actionsButtons. Both of these props should be passed either a function that returns a component, or the component itself. We can start by passing NOOP functions to these props as follows:

https://gist.github.com/nparsons08/db35e90c6c1e40130e7b92a81ecc0714

The components passed to these props will replace the chat drawer and the bottom actions bar, respectively. The ConferenceRoom will pass down the necessary state from Redux as props, allowing us to use custom components that will alter the state of the call. Furthermore, it will aid us in providing a custom chat UI, which we'll use Stream Chat for later in the tutorial!

With the above changes, you should now see the main wrapper of the Conference screen will be full-height and full-width in the browser, and the actions bar along the bottom will be gone. We can now start to build our custom call UI.

Inside of the screens/Conference directory, create a components directory and inside create a new file called ActionsButtons.js.

For now, we can leave this file empty as we will need some other components first that we can then utilize within the ActionButtons bar itself.

Back in src/components let’s create a new directory called Icons and an Icon.js file inside with the following code:

https://gist.github.com/nparsons08/68bd0bd39fc8e299636dbf96eaf4cc02

This file creates a re-usable Icon component that we can use to generate a set of consistent SVG icons. We can then use these icons just the same as any other react component. The Icon component also offers additional functionality – such as the ability to control the icon height/width via the size prop (while maintaining consistency if we set the viewBox correctly), as well as the ability to change the color with the color prop.

Note: Thanks to the withTheme HOC, we can pass color names that map directly to our styled-components theme – i.e., purple, red, error, placeholder, text, etc. meaning we can add more color options by adding values to styles/colors.js.

For the sake of speed and ease, we used Material Icons – and converting them for use inside of our new Icon component is a breeze. To prevent repeating ourselves over and over, here is a link to a ZIP file containing the complete Icons folder that you can drop straight into you components directory.

Now that we have icons to use, we can create our ActionButton component that we will use inside of the ActionsButtons.js file we created earlier. Create a new ActionButton.js file inside of screens/Conference/components and paste in the following code:

https://gist.github.com/nparsons08/0b0d612a56b323180d14f308b5b8448a

And finally, we’re ready to create our custom actions bar!

Back in screens/Conference/components/ActionsButtons.js we can now add the following:

https://gist.github.com/nparsons08/b5f97aa055b25372bead23a4a0224cb0

Currently, we are setting the unreadCount prop to fall back to 0 as we are not defining it anywhere just yet. We will pull this data off of our Redux store later once we have Stream Chat set up.

All other props are provided by the Voxeet ConferenceRoom component and are used internally by the default ActionsButtons UI to change the state of buttons conditionally – so we will do the same. This state depends on parameters such as whether or not the user's mic or camera are disabled, if the chat drawer is open, etc.

Note that we have left some of the default Voxeet views in the UI such as settings (for controlling the input/output devices and the AttendeesList for viewing all the active participants).

These views are great out of the box, and small amounts of CSS can get them looking as we would want without building them out by hand. Currently, there is not a prop available to pass a custom settings view; however, should you wish to build the attendees list by hand, as we have with the ActionsButtons, you can pass a component in the same way to the attendeesList prop of the ConferenceRoom.

Now all that’s left to do is import our shiny new ActionsButtons into the ConferenceRoom as below:

https://gist.github.com/nparsons08/9771e90febfb4ab64302b39d8375e49a

Now in your browser, you should see our custom call UI along the bottom of the screen! You can hang up the call, disable your camera or microphone, share your screen with the other participants, change your I/O settings, view the list of participants and, last but not least, toggle the chat drawer. This will be empty for now, but we’ll come back to this shortly.

We are also going to want to override some of Voxeets CSS to have the Conference screen match the rest of our application. For the sake of brevity, below is the complete CSS file from our finished version - feel free to tweak any of the values you see fit.

Go to your src/styles folder and create a new directory called css – then create a new file inside called voxeet.js with the following styled-components CSS code:

https://gist.github.com/nparsons08/b6b1424e44397b1493f6efd1847ff4df

Note: By creating our CSS overrides in this way, we can write our CSS as we usually would in a standard .css file, but with access to our theme.

All that’s left to do now is to jump into styles/global.js and add the following two lines to import our overrides into the global CSS file:

https://gist.github.com/nparsons08/1897763255d8c003fe38afdcaccd2402

Step 4: Setting up the Backend API & Authentication

Prior to setting up our Chat components, we will make things much easier for ourselves by first setting up the backend API and authentication for the app.

The backend for this project is simple, and Voxeet’s react components handle all of the Voxeet related backend functionality for you. All that we will need to do is pass in user data, which means we only need to generate a token for Stream Chat.

You can download the boilerplate here and follow along with the following section to get the backend and authentication flow working in your app. Alternatively, you can hit the Heroku button to immediately deploy the complete API code to Heroku or click here to view the finished API repo.

Note: You can click the Heroku Deploy button below to launch a Stream Chat trial using our prebuilt boilerplate API. Please take caution when using this, as the API does not enforce auth when hitting the /v1/token endpoint. If you would like to lock this down, we secure adding additional security measures to prevent authorized access.


Deploy Stream API

NOTE: You can safely skip this next part and jump to building the authentication flow if you go the auto-deploy with Heroku route.

Let’s get to coding the backend!

Fortunately, a lot of the work has already been completed through the provided boilerplate API. We need to make a few small changes to get it working how we want.

Outside of your frontend repository, run the following command in your terminal from whichever directory you want to store the API code in.

https://gist.github.com/nparsons08/23ebfc5df9ff74afdda6068609dc688d

Then, run cd stream-voxeet-api && yarn and open the stream-chat-voxeet directory in your editor.

We’ll start by renaming env.example to .env and inserting our Stream key and secret that we saved earlier into the correct variables. Once you're done, it should look something like this:

https://gist.github.com/nparsons08/aebcb0edb135cfb7f4f5541da01c7773

Now, let’s hop back into the terminal and run yarn dev to spin up the development server. The repo uses nodemon to automatically refresh when you make a change whilst keeping the server alive.

Finally, we need to open src/controllers/v1/token/token.action.js and do a quick “find all” so that we can change all references to data.email to data.username. Your token.action.js file should now look like the following snippet:

https://gist.github.com/nparsons08/2a8f3f5d847de604e78da31dc44e300a

Note: By changing data.email to data.username we are changing the default behavior of authenticating via an email address, to using a simple username that we will build a login form for in just a second.

Optionally, you will see on line 28 the repo uses robohash to generate an avatar for the user. In our final version, we used ui-avatars.com as the images it generates are better suited to our design, but you can drop in any avatar generation URL here that you see fit.

And that’s it for the backend!

You can test that this is all working properly with Postman (or your REST client of choice is) by leaving the server running, and firing a POST request to http://localhost:8080/v1/token where the body is the following object:

https://gist.github.com/nparsons08/72a2e1cb51e954fadedb7e0ce3c2e418

If successful, you will see the following in the response body:

https://gist.github.com/nparsons08/64ee1d7b31dfd8d9565870e6b331e4cb

Awesome! We can now start to build our login form and authenticate users inside our app.

Leave your API server running for now, and head back to your frontend code in your editor.

First up, let’s add a new env variable (in .env) named REACT_APP_API_ENDPOINT and set it’s value to http://localhost:8080/v1 and then make sure we have rebooted the frontend by closing the dev server and running yarn start once again.

We’ll also need this small wrapper utility around Axios to make a request to our backend, save it in your project file in src/utils/fetch.js

https://gist.github.com/nparsons08/9f35c054523fe3503a0eb2f544ac047f

Building the Authentication Flow

Now that we’re all set up to generate tokens for our users, we’ll kick off the auth flow by creating a new directory inside of our screens directory called Login with a Login.js file inside of that with the following code:

https://gist.github.com/nparsons08/2d289e3a355383bd6f37e1efad46b5d7

In the Login.js file, we are importing a few components that we don’t have in our project just yet. The biggest of which is the LoginForm component which we will come back to in a second but first, here is the code for the Text, Input and Button` components we will need to build the form UI.

Input:

https://gist.github.com/nparsons08/09f6bbde2efaeae61fb25eb677302a24

Button:

https://gist.github.com/nparsons08/470b2b9b7e22a680cb33d7c02076607c

Text:

https://gist.github.com/nparsons08/45162770dc5cd5e392af3625358b8a67

Now we have all the pieces we need to build our LoginForm component using Formik and Yup which we installed at the start of the tutorial.

Formik is an awesome library that allows us to easily build complex forms with React, and Yup provides a nice and powerful way to validate our input values. We won’t be utilizing the full potential of Formik in this tutorial, but should you decide to build upon this project after your done with the tutorial, this will set you up with a strong foundation for building any kind of form quickly and painlessly.

First, we’ll create a new directory inside of src called forms and inside of there create a file at this path forms/LoginForm/LoginForm.js.

https://gist.github.com/nparsons08/1f03a8fe57d18468599efd0cf61e8149

We start off by importing Formik and Field from formik. The <Formik /> component will be the root element in our render function and we pass in our onSubmit handler from props as well as some initial values as defined in the getter function on line 24 and finally, our this.renderForm function as the child of the component.

Note: The initial values ensure that our inputs are always ‘controlled’. If we don’t do this, the value will be undefined and react will throw an error to say we have switched the input from a non-controlled to a controlled state.

The <Field /> component provided by Formik encapsulates all the functionality needed to pass the onChange handler down to our custom input component, along with error messages (if there are any). It also ensures that the value is updated in the correct key of the forms value store using the name prop, which we will set to name=”username” to match the key in the initial values.

We are also importing validationSchema.js – this is where Yup comes in. The syntax is pretty easy to grasp and not only are the docs easy to follow but Yup also provides useful helpers for validating emails, phone numbers, etc as well as the option to pass custom regex validators and easily set the error messages that will be passed into the input component if the conditions are not met.

Create the validation file in your LoginForm directory, next to the form component and paste in the following code:

https://gist.github.com/nparsons08/cd64d26c2f4e0485b7fa96d6128c747d

Now, back inside of LoginForm.js you’ll see that we are passing our Yup schema into Formik as the validationSchema prop. At this point, our form will be fully operational, but isn’t yet rendered to the DOM anywhere and doesn’t send it’s value to the backend just yet either.

Let’s quickly jump into App.js and update the file to render our login route.

https://gist.github.com/nparsons08/06c83025cf7822fe1ae7dad5cd8b84da

Now, when you navigate to http://localhost:3000/ you should see our login page rendered to the screen, complete with the login form. You can also fill out the form, click the button below and open your console to see the output from the form.

We now need a way to send our form data to the backend, retrieve our token, and authenticate the user before sending them to the Conference Screen. Time for some Redux magic! 🧙‍♂️

We’ll start out by creating our action creators and action types. You can find the relative snippets below, which will save inside of data/auth in our project.

data/auth/types.js:

https://gist.github.com/nparsons08/2dc8370e1bc625b4d0070565af6c902d

data/auth/actions.js:

https://gist.github.com/nparsons08/3c5b0fed12db259a544b8ec309d00249

Note: You’ll notice we are passing conferenceAlias as the second argument to the loginRequest action. This will be used later on when a user receives a link to join a call so that we can route them to the correct place once they have logged in.

Now, inside of auth/sagas/index.js add the following code:

https://gist.github.com/nparsons08/b1ac9acc6bb9ee923b6d62d3b6ba075f

Generators, used heavily in redux-saga, are a new feature of JS as of ES6 and work very similarly to async/await. They are initialized using the function * () syntax and have a special keyword yield that works very much like await by “pausing” the function and waiting for the expression to resolve before continuing.

redux-saga also provides some helpers like takeEvery (used in the above snippet) which will take an action type as the first argument and a saga as the second. Every time the action is fired, the saga will run.

So the next logical step is to build our loginRequest saga. Inside of auth/sagas create a new file, loginRequest.js and paste in the following code.

https://gist.github.com/nparsons08/ebd60d75e1eecc0e007b933c0e229ebd

Let’s quickly break down what’s going on here.

  • We import the all, call and put helpers from redux-saga as well as shortid for generating the conference alias if we aren’t provided one by our login action.
  • We then import history and fetch from our utils directory.
  • We also import our LOGIN action type so we can fire a success action or error action later in the saga.
  • Next, we define our saga and destructure its first and only argument, which will always be the action creator we fired (loginRequest), meaning we can pull the values off of the action and use them in our saga, in this case, the optional conference alias and the form data.
  • We then open a try/catch block and do some simple formatting on the username, to make sure it is all lowercase and contains no spaces (we’re replacing them with ‘_’ using regex) before running our fetch call.
  • We use the call helper and pass in fetch as the first argument. All following arguments will be passed through to fetch itself.
  • Axios (and therefore our fetch util) returns the response body from the server as the data key, so we also destructure that to pull out the token and user object from the response.
  • Next, we store these values in localStorage so we can persist the user across sessions (but more on this later when we get to our reducer) and then check if a conferenceAlias was present in the action data. If it wasn’t, we generate one using shortid.
  • And finally, we use the all helper to run two saga helpers in parallel. The first is put, which will fire our success action for the reducer to consume and the second is call in which we pass history.push to navigate the user to the conference screen.
  • We also use put in the catch block to fire the LOGIN.ERROR action and pass the message to the reducer.

Now that our login saga is ready to rock, we need to make sure we are actually running the saga so that it can listen for the LOGIN.REQUEST action. We will do this by using another saga helper (fork) in the root saga that we created when we initialized our Redux store.

Open data/rootSaga.js in your editor and amend it to look like the following snippet:

https://gist.github.com/nparsons08/670fbde9c11697b076ac2461b7b166b4

Next, create a reducer.js file inside of data/auth and add the following code to it:

https://gist.github.com/nparsons08/7def99e3e5a8e6271ce362bd3f1b62f3

The reducer will handle the LOGIN.SUCCESS action and save the users token and profile data to the Redux store. We’re passing in some initial state with the defaults set to pull the values from localStorage – if they aren’t present they will just return null and therefore leave the app in an unauthenticated state ready for login.

We can now use this data in our components to create a wrapper around the Conference screen route to protect it from unauthenticated users. In your src/containers directory, create a new file called AuthedRoute.js and past in the following snippet:

https://gist.github.com/nparsons08/10912f5a8a135097df81c65125d7a16f

The above component handles protecting our route from unauthenticated users as well as showing a loading screen if the auth request is in progress. It will also pass the conferenceAlias parameter to location.state so that if a user tries to access a conference without logging in, it will store the id so we can redirect them by checking for the value in the location state and passing it through to the action creator that we set up previously.

For the LoadingScreen add the following file to screens/LoadingScreen.js:

https://gist.github.com/nparsons08/4fe40110b8b6cca685f227c43464abc9

We are also using reselect here to pull values out of the Redux store. reselect memoizes the values from our store to reduce accidental or unnecessary re-renders of our components if the values haven’t changed. But, we haven’t defined these selectors yet. Back in data/auth create a new file called selectors.js and add the following:

https://gist.github.com/nparsons08/5aa572f7d41cdf3831af89bb23cc0b0c

Now to implement our AuthedRoute, jump back into your App.js file, import it and update the route definition for the conference screen to use AuthedRoute instead of React Routers <Route />.

https://gist.github.com/nparsons08/e9012b22678f17447b911cc4e6d292aa

Finally, let’s go back into our screens/Login/Login.js file and make the following changes:

https://gist.github.com/nparsons08/c1fc1a8ff7e3cf91b3cbdd69af09e87c

We added some getter functions for formatting the user's name and retrieving the conferenceAlias from the location state. We also connected our component to the Redux store so that we can access our store as well as mapping our loginRequest function to props so we can fire it in the handleLogin method.

Lastly, we added a new method to our class, renderWelcome which renders a slightly different UI that shows the uses avatar and a “Start Video Call” button to launch a call. We conditionally call either that or renderLogin in our render function depending on whether the user is authenticated or not.

Now that our backend is in place and our Login screen is fully operational, we can finish up our app by building our chat implementation with Stream! 🎉

Step 5: Stream Chat

If you have clicked the chat button in our call UI we built earlier, you will have no doubt noticed that opening the drawer will move the videos over to the left, but the drawer itself will not be visible. So now is as good a time as any to start integrating Stream Chat and building the custom drawer that we can pass into our ConferenceRoom.

First up, you will need to add the following Portal util, built using Reacts internal Portal implementation. If you haven’t used Portals before, they essentially allow you to define a component anywhere in the tree but have it rendered to the DOM anywhere outside of its current parent hierarchy. We will use the Portal to render a chat drawer inside of the body – above the rest of the app.

https://gist.github.com/nparsons08/e5e91b2c4fe296370c7e13210f1286b7

We will also need a couple more action creators to handle toggling our drawer from outside of our ActionsButtons component and setting the unread count in Redux. Create a new directory called chat inside of your src/data directory and place the following three files inside that contain our Action Types, Action Creators and Selectors for pulling the values out of Redux.

https://gist.github.com/nparsons08/fbb117bf97fada09a09748302e53252c

https://gist.github.com/nparsons08/1594e6d30fa833d9e27452414980c22f

https://gist.github.com/nparsons08/6528df370e482909281a099ac4e8ee4c

Last, we will need to create our chat reducer and import it into our root reducer also. Below is the code for the reducer, followed by a snippet showing the root reducer.

https://gist.github.com/nparsons08/68efe716d4fd8c2c4447ad597420bc22

https://gist.github.com/nparsons08/71c61be6eb939ca7f555fcdfab2a59ec

Now, on to our chat drawer!

In our screens/Conference directory, let's make a new directory called containers and create a new directory inside of that called AttendeesChat. Inside, we’ll create AttendeesChat.js and place the following code inside:

https://gist.github.com/nparsons08/d1f8cc286bd011e6af74481be88547ec

There is quite a lot going on here – so here is a breakdown.

First, in our constructor, we define our default state and also set this.chatClient to contain our StreamChat instance and pass it the .env variables we set at the beginning of the tutorial. Then, in componentDidMount we call this.chatClient.setUser and pass in an object representing our user which we are pulling into props using our Redux selectors.

We then initialize the chat channel using this.chatClient.channel() and pass in the “messaging” config parameter, along with the conferenceAlias from our URL parameter so that we can scope the chat channel to the current conference and finally, call this.setState to store our channel in our components state for later.

https://gist.github.com/nparsons08/28e70b2ac2a1a9a90110dfe2e2f6e394

Next, in componentDidUpdate we run a comparison check that will run when our channel is first initialized, by comparing the previous state value to the current one and checking our channel exists. If the condition is truthy, we run this.init() and in turn, calls await this.state.channel.watch() which subscribes the channel to future updates.

https://gist.github.com/nparsons08/ed40bc0a88158066a8a685396a75919e

We also initialize a listener here, that fires on the message.new event. Every time a new message is sent in the channel, the callback will run and updates our unread count using our setUnreadCount action creator.

Next, in our render function, we render the stream-chat-react components that will power our UI and handle a lot of the state management for us.

https://gist.github.com/nparsons08/2aa4bfc38c56395a9d3364867624b360

We wrap everything with the Chat component and pass in this.chatClient to the client prop. Inside of this, we render the Channel component to which we pass our channel from this.state, followed by Window which will wrap the visual aspects of our Chat UI. Then, Inside of our Window we render the ChatHeader, MessageList, MessageInput.

Back inside of screens/Conference/components create ChatHeader.js and add the following snippet:

https://gist.github.com/nparsons08/3dce61ce81c0396766b1fc1481ff1626

And that’s everything we need for a fully operational chat UI!

But we aren’t finished just yet...

We need to include the CSS file from stream-chat-react. The default file exported from the library is, of course, responsive. However, this presents some issues due to the fact we are rendering the Chat components inside of a 375px wide wrapper div.

Because CSS media queries react to the size of the window and not the size of the parent div, properties such as padding and max-width will be incorrect – using the values it would for the full viewport regardless of the container size. To fix this, we’ll strip out the media queries so that we only use the mobile-sized definitions.

https://gist.github.com/nparsons08/df20fae80b04874003507c91368cdb38

Place the above file in your public directory, then in public/index.html you can import the file like so:

https://gist.github.com/nparsons08/8ce033d421f5ceb543a9dcc34b775437

We also need to jump back into our ActionsButtons.js component we made for our custom call UI along the bottom of the screen and connect it to our redux store so we can retrieve the unread count and show the notification badge accordingly:

https://gist.github.com/nparsons08/fd964689f0845444cf687c554e3397e1

Final Thoughts

In this tutorial, you’ve successfully built a fully functioning video-conferencing application. For more information on Voxeet, head over to their website at https://voxeet.com.

If you’re interested in adding chat to your application, Stream Chat has you covered – with React Components (that we used in this tutorial), as well as SDKs and libraries for JS, iOS, Android, and most other popular programming languages.

Happy coding! ✌️

Tutorials

Chat