Building a Clubhouse clone with Svelte and 100ms
Tags: javascript, svelte, clubhouse, programming
Published: 2022-04-02 14:43:31

In this article, we will go through the process of building a Clubhouse clone with 100ms and Svelte. Clubhouse is a popular app that enables people to speak together in audio rooms over the internet.

**This content was originally published - [HERE](https://www.100ms.live/blog/clubhouse-clone-with-svelte)**

## What is Svelte?

Svelte is a new framework that institutes a whole new way for frameworks to interact with the DOM. It doesn't use a VDOM but surgically updates the DOM based on what you want to display. 

**We also have step-by-step guides to build Clubhouse like app with different technologies**
- [Clubhouse clone with 100ms iOS SDK](https://www.100ms.live/blog/building-an-audio-app-like-clubhouse-for-ios)
- [Clubhouse clone with 100ms React SDK](https://www.100ms.live/blog/clubhouse-clone-in-react)
- [Clubhouse clone with 100ms Flutter SDK](https://www.100ms.live/blog/building-clubhouse-clone-using-100ms-in-flutter)
- [Clubhouse clone with 100ms Javascript SDK](https://www.100ms.live/blog/clubhouse-clone-with-javascript)

It’s also lightweight and, thus, faster because it doesn't ship svelte to the frontend. Instead, it ships code that performs the update.

## What is 100ms?

100ms provides video conferencing infrastructure designed to help businesses build powerful video applications in hours. Its SDK is tailored to suit numerous use cases like game streaming, virtual events, audio rooms, classrooms, and much more. It abstracts the complexities involved in building these apps and reduces development time drastically. 

To top it all off, 100ms has been created and is managed by the team who created live infrastructure at Disney and Facebook (Meta).

## Prerequisites

- A fair understanding of Javascript
- A 100ms account; [if you don't have one, you can create your account here](https://dashboard.100ms.live/register)

## Explanation of 100ms terms

There are a couple of terms used by 100ms that we need to get familiar with to understand this article:


- **Room:** A room is the basic object that 100ms SDKs return on a successful connection. This contains references to peers, tracks and everything you need to render a live audio/visual app.
- **Peer:** A peer is the object returned by 100ms SDKs that contain all information about a user: name, role, video track, etc.
- **Track:** A track represents either the audio or video that a peer is publishing.
- **Role:** A role defines who a peer can see/hear, the quality at which they publish their video, whether they have permissions to publish video/screen share, mute someone, and/or change someone's role.

[You can read about the other terms here](https://www.100ms.live/docs/javascript/v2/foundation/basics)

## Steps around 100ms

- Setting up 100ms
- Creating an account
- Creating Roles & Rooms

## Steps around setting up the Frontend

- Add 100ms to the Svelte app
- Join a Room
- Leave a Room
- Mute/Unmute a Peer
- Chat in a Room

## Setting up 100ms

-Log in to your 100ms account. On the dashboard, click the **'Add a New App'** Button. 
 
![1.png](https://storage.googleapis.com/100ms-cms-dev/cms/1_2522574aa2/1_2522574aa2.png)

- Choose a subdomain for your app.

![2.png](https://storage.googleapis.com/100ms-cms-dev/cms/2_d58d4e2863/2_d58d4e2863.png)

- Choose the template for the app. Since we're building a Clubhouse Clone, 'Audio Rooms' will give us the right setup out-of-the-box. Select that option.

![3.png](https://storage.googleapis.com/100ms-cms-dev/cms/3_1943c9c41b/3_1943c9c41b.png)

- You should then see this appear on your screen. 

![4.png](https://storage.googleapis.com/100ms-cms-dev/cms/4_324e21b67f/4_324e21b67f.png)

Click on any of the **Join as** buttons to test out the platform.

To save time, head to the Developer tab of the dashboard, copy the token endpoint and store it in a safe place. Additionally, head to the Rooms tab, and store the Room ID of the room we just created. 

## Setting up the Frontend

To get started, clone this [starter pack](https://github.com/dro-1/clubhouse-clone). It contains the main setup needed for the app, like SCSS and page routing as well as its components. After cloning, run


```bash
yarn
```

to install all dependencies of the starter pack. 

Run


```bash
yarn dev
```

to start the project. You should see the following:

![5.png](https://storage.googleapis.com/100ms-cms-dev/cms/5_f78efa352a/5_f78efa352a.png)

Under `src/services/hms.js`, we have set up the basic 100ms functions. These functions enable us to connect our components to 100ms.

Head into the `App.svelte` file in `src` and replace its content with:


```js
<script>
  import router from "page";
  import Home from "./pages/home.svelte";
  import Room from "./pages/room.svelte";
//NEW LINE HERE
  import { onMount } from "svelte";
  import { hmsStore } from "./services/hms";
  import { selectIsConnectedToRoom } from "@100mslive/hms-video-store";
//NEW LINE ENDS

  let page;

  router("/", () => (page = Home));

  router("/room", () => (page = Room));

  router.start();
//NEW LINE HERE
  const onRoomStateChange = (connected) => {
    if (connected) router.redirect("/room");
    else router.redirect("/");
  };

  onMount(async () => {
    hmsStore.subscribe(onRoomStateChange, selectIsConnectedToRoom);
  });
//NEW LINE ENDS
</script>

<svelte:component this={page} />

```

Going from the top, 3 new variables are imported:

- **onMount:** This is a function by Svelte that runs after a component mounts (like `componentDidMount` in React). You mainly use it to subscribe to listeners or make requests to API endpoints.
- **hmsStore:** This contains the complete state of the room at any given time. It includes participant details, messages, and track states.
- **selectIsConnectedToRoom:** The 100ms package provides a number of selectors that we can use to extract information from the store. In this case, we're extracting a `boolean` value that tells you if you're connected to a room or not. 

[You can read about other selectors here.
](https://www.100ms.live/docs/api-reference/javascript/v2/home/content#functions)

In the **onMount** function, we set a listener that calls **onRoomStateChange** whenever the connection state changes. The **onRoomStateChange** reacts to this by redirecting you to the appropriate page based on its input.

Head to the `home.svelte` file and replace its contents with:


```js
<script>
  import { hmsActions } from "./../services/hms";
  import { getToken } from "./../utils/utils";
  let userName = "";
  let role = "";

  const submitForm = async () => {
    if (!userName || !role) return;
    try {
      const authToken = await getToken(role, userName);
      const config = {
        userName,
        authToken,
        settings: {
          isAudioMuted: true,
          isVideoMuted: false,
        },
        rememberDeviceSelection: true,
      };
      hmsActions.join(config);
    } catch (error) {
      console.log("Token API Error", error);
    }
  };
</script>

<main>
  <form>
    <header>Join Room</header>
    <label for="username">
      Username
      <input
        bind:value={userName}
        id="username"
        type="text"
        placeholder="Username"
      />
    </label>
    <label>
      Role
      <select bind:value={role} name="role">
        <option value="speaker">Speaker</option>
        <option value="listener">Listener</option>
        <option value="moderator">Moderator</option>
      </select>
    </label>
    <button on:click|preventDefault={submitForm}> Join </button>
  </form>
</main>

```

Here we import:

- **hmsActions:** This is used to perform any action such as joining, muting, and sending a message.
- **getToken:** It helps us generate a token that we need to join any 100ms room.

We also have a function, `submitForm`, that couples the `config` variable and adds us to the room using `hmsAction`.  

In the markup, you'll notice we have `bind`: in the input. This is called a directive and Svelte gives us numerous directives to make our lives easier. 

The `bind:value` directive links the value of the input to the specified variable. 

In your case, this variable is the `username` variable. You also use it in the `select` element. The `on:click` directive, on the other hand, attaches the specified function as the handler to the click event on that button. 

Svelte also gives us modifiers like `|preventDefault` that customizes the directive to our taste. In our case, `|preventDefault` calls the `event.preventDefault` function before running the handler.

You'll also notice that we haven't implemented the `getToken` function, so let's get to it. Create a `utils.js` file in the directory `src/utils` and paste the following:


```
const TOKEN_ENDPOINT = process.env.TOKEN_ENDPOINT;
const ROOM_ID = process.env.ROOM_ID;

export const getToken = async (userRole, userName) => {
  const role = userRole.toLowerCase();
  const user_id = userName;
  const room_id = ROOM_ID;
  let payload = {
    user_id,
    role,
    room_id,
  };
  let url = `${TOKEN_ENDPOINT}api/token`;
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });
  let resp = await response.json();
  return resp.token;
};

```

First, you extract the environment variables from `process.env`. Then, make a call to the endpoint provided to you by 100ms. This endpoint responds with the needed token.

But we haven't set up our environmental variables. We can do this easily by installing some packages. Run

```bash
yarn -D dotenv @rollup/plugin-replace
```

to get them installed. Then open the **rollup.config.js** in the root of the folder and paste the following:


```js
//NEW LINE STARTS
import replace from "@rollup/plugin-replace";
import { config } from "dotenv";
//NEW LINE ENDS

const production = !process.env.ROLLUP_WATCH;

//CODE OMITTED FOR BREVITY

export default {
  input: "src/main.js",
  output: {
    sourcemap: true,
    format: "iife",
    name: "app",
    file: "public/build/bundle.js",
  },
  plugins: [
      //NEW LINE STARTS
    replace({
      "process.env.NODE_ENV": JSON.stringify("production"),
      "process.env.TOKEN_ENDPOINT": JSON.stringify(
        config().parsed?.TOKEN_ENDPOINT || process.env.TOKEN_ENDPOINT
      ),
      "process.env.ROOM_ID": JSON.stringify(
        config().parsed?.ROOM_ID || process.env.ROOM_ID
      ),
    }),
      //NEW LINE ENDS
    svelte({
      preprocess: preprocess(),
      compilerOptions: {
        dev: !production,
      },
    }),

```

Our `getToken` function should be up and running now.
Next, replace the code in `room.svelte` with the following:


```js
<script>
  import page from "page";
  import Peer from "./../components/peer.svelte";
  import { hmsActions, hmsStore } from "./../services/hms";
  import { selectPeers,selectLocalPeerRole,
    selectIsLocalAudioEnabled, } from "@100mslive/hms-video-store";
  import { onMount, onDestroy } from "svelte";
  import { PeerStore } from "./../stores";

  let peers = [];
  let localPeerRole = "";
  let audioEnabled = null;

  const handlePeers = (iPeers) => {
    let res = hmsStore.getState(selectLocalPeerRole);
    localPeerRole = res ? res.name : "";
    audioEnabled = hmsStore.getState(selectIsLocalAudioEnabled);
    PeerStore.set(iPeers);
  };

  const handleMute = async () => {
    await hmsActions.setLocalAudioEnabled(!audioEnabled);
    audioEnabled = hmsStore.getState(selectIsLocalAudioEnabled);
  };

  onMount(async () => {
    hmsStore.subscribe(handlePeers, selectPeers);
  });

  const leaveRoom = () => hmsActions.leave();

  onDestroy(leaveRoom);

  $: peers = $PeerStore;
</script>

<main>
  <h1>Welcome To The Room</h1>

  <section class="peers">
    {#each peers as peer (peer.id)}
      <Peer {localPeerRole} {peer} />
    {/each}
  </section>
  <div class="buttons">
    {#if localPeerRole != "listener"}
      <button on:click={handleMute} class="mute"
        >{audioEnabled ? "Mute" : "Unmute"}</button
      >
    {/if}
    <button on:click={leaveRoom} class="leave">Leave Room</button>
  </div>
</main>

```

This page houses the most important features of our app. First, we import the required variables. Some of these are:

- **onDestroy:** This function is similar to `onMount` except it is called immediately before the component is unmounted.
- **PeerStore:** This is a store that would keep track of the current peers in the room.

The `handlePeers` function does three things:

- It stores the role of the local peer in the `localPeerRole` variable.
- It sets the audio state of the local peer in the `audioEnabled` variable.
- It stores the current peers in the room within the `PeerStore` store.

The `handleMute` function simply toggles the audio state of the local peer. A `leaveRoom` is called when the component is to be unmounted or when the `Leave Room` button is clicked.

The `$:` syntax helps us create reactive statements. These statements run immediately before the component updates, whenever the values that they depend on have changed.

We have 2 new syntaxes in our markup:

- `{#each peers as peer (peer.id)}`:This helps us map out each peer in the `peers` array using the peer's ID as a key.
- `{#if localPeerRole != "listener"}`:This renders the component between the `if` block, if the condition is true. Therefore, it renders the `Mute` button if the local peer is not a listener.

On to the last component, **peer.svelte**. For the last time, copy the code below into the file:


```js
<script>
  import {
    selectIsPeerAudioEnabled,
  } from "@100mslive/hms-video-store";
  import { onMount } from "svelte";
  import { hmsActions, hmsStore } from "../services/hms";
  export let peer = null;
  export let localPeerRole = "";

  let isContextOpen = false;
  let firstCharInName = "";
  let isPeerMuted = false;

  const togglePeerAudio = () => {
    hmsActions.setRemoteTrackEnabled(peer.audioTrack, isPeerMuted);
  };

  const changeRole = (role) => {
    hmsActions.changeRole(peer.id, role, true);
  };

  onMount(async () => {
    hmsStore.subscribe((isPeerAudioEnabled) => {
      isPeerMuted = !isPeerAudioEnabled;
    }, selectIsPeerAudioEnabled(peer?.id));
  });

  $: firstCharInName = peer ? peer.name.split(" ")[0][0].toUpperCase() : "";
</script>

<div class="peer">
  <div on:click={() => (isContextOpen = !isContextOpen)} class="content">
    <div class="image">
      <p>{firstCharInName}</p>
    </div>
    <p>{peer ? peer.name : ""}{peer && peer.isLocal ? " (You)" : ""}</p>
  </div>
  {#if localPeerRole == "moderator" && !peer.isLocal}
    <div class="context" class:open={isContextOpen}>
      <button on:click={togglePeerAudio}
        >{isPeerMuted ? "Unmute" : "Mute"}</button
      >
      <button on:click={() => changeRole("speaker")}>Make Speaker</button>
      <button on:click={() => changeRole("listener")}>Make Listener</button>
    </div>
  {/if}
</div>

```

Once again, all needed variables are imported. You are expecting 2 props: **peer** and **localPeerRole**. 

2 functions are declared: **togglePeerAudio** and **changeRole**. They do exactly what their names describe. In the **onMount** function, a handler is added to update the **isPeerMuted **state of a peer.

Each peer component has a context menu that has options for muting the peer or changing their role. But this menu is only available to moderators as only they should have such permissions.

At this point, we are done. 

You can run

```bash
yarn dev
```

in the terminal to see the application. 

![6.gif](https://storage.googleapis.com/100ms-cms-dev/cms/6_f5c3d068fe/6_f5c3d068fe.gif)

- [You can see the completed application here.](https://dro-ch-clone.netlify.app/)
- [You can see the GitHub code for the application here.](https://github.com/dro-1/clubhouse-clone-fin)