WhatsApp Web Clone Part 2: Let’s Add Video Calling

10 min read

Let’s add video calling to our WhatsApp web clone.

Jeroen L.
Jeroen L.
Published September 27, 2023

Let's Add Video Calling! 📞

After a recent update to WhatsApp, users can now call each other directly from the web and desktop clients. In the previous part of this article series, we implemented a chat feature resembling WhatsApp chat using Stream’s Chat SDK, we used Supabase to quickly get user authentication going and focussed most of our effort on making sure our implementation resembles WhatsApp.

In this part of the series, we want to take our project to the next level by adding user-to-user calling using the Stream Video Calling API. When using WhatsApp it is straightforward to step into a video call right from the chat you are having. Using Stream’s Video SDK and UI components, we can get a similar user experience set up fast. Without a ready-to-use video calling API, we would have to create and configure our backend infrastructure, which can prove challenging when reliability, performance, and scale are all required.

Our code here is mostly based on the existing React Chat Integration Guide but we will be making a few customizations along the way to make things look more like WhatsApp.

Let’s start where we left off in Part 1 of this series.

First, we need to add Stream Video as a dependency:

shell
npm install @stream-io/video-react-sdk stream-chat-react stream-chat

We will also need to install sass, this will allow NextJS to compile and use some SASS-based templates taken directly from Stream’s React Chat Integration Guide.

shell
npm install sass

This will be a lot of code, but I will explain things as we go.

We first need to create a number of components and assets. These components are what tie our Chat and Video services together.

Create a file in the directory app/hooks named useDragable.ts

The contents should be:

jsx
import { useEffect, useRef } from "react";

const throttle = <T,>(fn: (arg: T) => void, delay: number) => {
  let time = Date.now();

  return (arg: T) => {
    if (time + delay - Date.now() <= 0) {
      fn(arg);
      time = Date.now();
    }
  };
};
export const useDraggable = (element?: HTMLElement | null) => {
  const dragStart = useRef<{ x: number; y: number } | null>(null);

The aboce listing creates a helper object allowing an on screen element to be dragged and move within the browser window. We will use this component later in our project.

Next, we need to expose this file’s interface. We do this by creating a file next to it called index.ts.

export * from './useDraggable';

To build our intended user interface, we need to create customized versions of 5 UI components provided by the Stream Video SDK.

All these components are related. The Video component contains the Channel component, the Channel contains the ChannelHeader component, and the ChannelHeader contains the CreateCallButton. It’s pretty straightforward when you see the result.

Let’s begin with the CreateCallButton. This component will display a button in the UI that will initiate a video call. This component also contains a lot of the logic related to enabling Stream’s Video SDK. Create a file named CreateCallButton.ts in the directory components/CreateCallButton.

The code should looks similar to this:

jsx
import { useCallback } from "react";
import {
  MemberRequest,
  useStreamVideoClient,
} from "@stream-io/video-react-sdk";
import { useChannelStateContext } from "stream-chat-react";
import { customAlphabet } from "nanoid";

const nanoid = customAlphabet(
  "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
  10,
);

import type { LiteralStringForUnion, UR } from "stream-chat";
import { User } from "@stream-io/video-react-sdk";

We need to create an index.ts file next to the file we created as well.
Make it look as follows:

jsx
export * from "./CreateCallButton";

Let’s move on to the ChannelHeader. The ‘ChannelHeader’ is adjusted to act as a container for the CreateCallButton and display metadata like an avatar image and user name. Create a file ChannelHeader.ts in the directory components/ChannelHeader.

This one should look something like this:

jsx
import React from "react";
import {
  Avatar,
  useChannelPreviewInfo,
  useChannelStateContext,
  useChatContext,
  useTranslationContext,
} from "stream-chat-react";

import type { StreamChatType } from "../../types/chat";
import { CreateCallButton } from "../CreateCallButton";

export type ChannelHeaderProps = {
  /** Manually set the image to render, defaults to the Channel image */
  image?: string;

And again an index.ts file is required.

jsx
export * from "./ChannelHeader";

Now, let’s focus our attention on creating a channel view.

Create the file Channel.tsx in the directory components/Channel.

This file should look as follows:

jsx
import {
  Channel as StreamChatChannel,
  MessageInput,
  MessageList,
  Thread,
  Window,
} from "stream-chat-react";
import { CustomEventComponent } from "../CustomEventComponent";
import { ChannelHeader } from "../ChannelHeader";

export const Channel = () => (
  <StreamChatChannel MessageSystem={CustomEventComponent}>
    <Window>
      <ChannelHeader />
      <MessageList />
      <MessageInput focus />
    </Window>
    <Thread />
  </StreamChatChannel>
);

Not too bad, right? We already did most of the work in the code we wrote earlier.

Make sure to add this implementation as an export as well. Create the file index.ts next to the Channel file.

jsx
export * from "./Channel";

And finally, we need to add the Video component. Create a file called Video.tsx in Components/Video The Video component will contain the actual video call being performed by the Stream SDK. So, there is some logic involved with call state management.

Building your own app? Get early access to our Livestream or Video Calling API and launch in days!
jsx
import { StreamCall, useCalls } from "@stream-io/video-react-sdk";

import {
  CallingState,
  CancelCallButton,
  PaginatedGridLayout,
  RingingCall,
  ScreenShareButton,
  SpeakingWhileMutedNotification,
  ToggleAudioPublishingButton,
  ToggleVideoPublishingButton,
  useCall,
  useCallStateHooks,
} from "@stream-io/video-react-sdk";
import { useChatContext } from "stream-chat-react";

And again, we need an index.ts, with a single export.

typescript
export * from "./Video";

That was a lot of code, but we are nearly there. Before we move towards integrating our Video component, let’s ensure it’ll all look somewhat decent.

Create a directory called styles in the components directory. In that directory, we need to add five files.

_layout.scss

scss
body {
  margin: 0;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
  'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

code {
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}

html,
body {
  margin: unset;

CallPanel.scss

scss
.rmc__call-panel-wrapper {
  position: absolute;
  top: 60px;
  right: 20px;
}

.str-video__call-panel {
  height: 350px;

  .rmc__active-call-controls {
    bottom: 10px;
    display: flex;
    justify-content: center;
    gap: 10px;
  }

ChannelPreview.scss

scss
.rmc__channel-preview__call-controls {
    display: flex;
    gap: 0.5rem;

  .str-video__call-controls__button {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 20px;
    height: 20px;
    padding: 0;

    .str-video__icon--call-accept {
      width: 10px;
      height: 10px;

SideBar.scss

scss
#sidebar {
  display: flex;
  flex-direction: column;
  border-inline-end: var(--str-chat__channel-list-border-inline-end);

  .str-chat__channel-list {
    border: none;
  }

  .rmc__button--logout {
    svg {
      rotate: 180deg;
    }
  }
}

And finally, index.scss

scss
@layer default-chat-sdk {
  @import 'stream-chat-react/dist/scss/v2/index.scss';
}

@import '@stream-io/video-react-sdk/dist/css/styles.css'
  layer(default-video-sdk);

@import 'layout';
@import './CallPanel.scss';
@import './ChannelPreview.scss';
@import './Sidebar.scss';

The final thing we need to do is integrate the Video components into the WhatsAppComponent we created earlier.

So open up the file components/WhatsAppChat.tsx.

Start with modifying the imports at the top of the file.

Replace all existing imports with the following.

jsx
import { useEffect, useState } from "react";

import { StreamChat, ChannelSort, ChannelFilters } from "stream-chat";
import { ChannelList, Chat } from "stream-chat-react";

import { Channel } from "./Channel";
import {
  StreamTheme,
  StreamVideo,
  StreamVideoClient,
} from "@stream-io/video-react-sdk";

import { Video } from "./Video";
import "@stream-io/video-react-sdk/dist/css/styles.css";
import "./layout.css";
import "./styles/index.scss";

import { User } from "@supabase/supabase-js";
import ChannelListHeader from "./ChannelListHeader";

Right below the declaration of the const chatClient = … add a line declaring a useState variable.

jsx
const [videoClient, setVideoClient] = useState<StreamVideoClient>();

Right after calling connectUser on the chatClient (should be somewhere around line 52), add the creation of a StreamVideoClient.

jsx
await chatClient.connectUser({ id: userId }, response.userToken);

// Insert after this line
const _videoClient = new StreamVideoClient({
  apiKey,
  user: chatUser,
  token: response.userToken,
});

await _videoClient.connectUser({ id: userId }, response.userToken);

setVideoClient(_videoClient);

// And make sure the following line is after the setting of the video client.
setIsLoading(false);

And finally, we replace this bit of code:

jsx
{
  !isLoading && (
    <div id="root">
      <Chat client={chatClient}>
        <div className="channel-list-container">
          <ChannelListHeader user={chatUser} />
          <ChannelList sort={sort} filters={filters} showChannelSearch />
        </div>
        <Channel>
          <Window>
            <ChannelHeader />
            <MessageList />
            <MessageInput />
          </Window>
          <Thread />
        </Channel>
      </Chat>
    </div>
  );
}

Replace it with this:

jsx
{
  !isLoading && videoClient && (
    <div id="root">
      <Chat client={chatClient}>
        <StreamVideo client={videoClient}>
          <StreamTheme as="main" className="main-container">
            <div className="channel-list-container">
              <ChannelListHeader user={chatUser} />
              <ChannelList sort={sort} filters={filters} showChannelSearch />
            </div>
            <Channel />
            <Video />
          </StreamTheme>
        </StreamVideo>
      </Chat>
    </div>
  );
}

Moving Onward to Deployment

In part two of this article series we covered how to add video calling to the chat application we built in part 1. You can see how powerful Stream’s UI components are, and how straightforward they are to integrate into a project.

In part 1 you learned how easy it is to add chat to a product, and how quickly we could implement an authentication layer using Supabase’s authentication feature.

If you enjoyed this article, please drop a ⭐️ star ⭐️on the sample GitHub repository.

To learn more about Stream’s Video product, click here.

And remember, you will need a trial account sign-up to get started using Stream. If you followed along with this article series, you already have created an account with Stream. But, in case you haven’t, Stream provides powerful UI components and infrastructure to build any chat and video use case you can dream of. You can add reliable and scalable chat and/or video to your project in about a week instead of months. Most importantly, by using Stream’s Chat and Video, you are relying on our global edge network, providing a reliable, scalable infrastructure suitable for any scale.

We also have a Maker account available for small projects and non-commercial projects.

We have created a nice codebase, we can run this locally. But what about showing it to the world? Let’s look into deployment next. In part 3 of this article series we will look at deploying our project to Vercel. Vercel is the creator and maintainer of the NextJS framework, and their infrastructure is perfectly suitable to deploying a cloud API based project like our WhatsApp clone.

So let’s look at deployment in part 3.

decorative lines
Integrating Video With Your App?
We've built an audio and video solution just for you. Launch in days with our new APIs & SDKs!
Check out the BETA!