Full Code of muxinc/meet for AI

main 51eb0fbf9f79 cached
82 files
155.8 KB
41.0k tokens
108 symbols
1 requests
Download .txt
Repository: muxinc/meet
Branch: main
Commit: 51eb0fbf9f79
Files: 82
Total size: 155.8 KB

Directory structure:
gitextract_6okmy78h/

├── .eslintrc.json
├── .gitignore
├── .husky/
│   └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── client/
│   └── token.ts
├── components/
│   ├── Controls.tsx
│   ├── Gallery.tsx
│   ├── GalleryLayout.tsx
│   ├── Header.tsx
│   ├── Meeting.tsx
│   ├── MuteIndicator.tsx
│   ├── Notifications.tsx
│   ├── Participant.tsx
│   ├── ParticipantAudio.tsx
│   ├── ParticipantInfoBar.tsx
│   ├── ParticipantName.tsx
│   ├── Pin.tsx
│   ├── Sounds.tsx
│   ├── Stage.tsx
│   ├── Timer.tsx
│   ├── Toasts.tsx
│   ├── UserInteractionPrompt.tsx
│   ├── controls/
│   │   ├── ControlsCenter.tsx
│   │   ├── ControlsLeft.tsx
│   │   ├── ControlsRight.tsx
│   │   └── buttons/
│   │       ├── CameraButton.tsx
│   │       ├── ChatButton.tsx
│   │       ├── MicrophoneButton.tsx
│   │       ├── ScreenShareButton.tsx
│   │       ├── SendButton.tsx
│   │       └── SettingsButton.tsx
│   ├── icons/
│   │   ├── ChatIcon.tsx
│   │   ├── ChevronIcon.tsx
│   │   ├── ChevronLeftIcon.tsx
│   │   ├── ChevronRightIcon.tsx
│   │   ├── LeaveIcon.tsx
│   │   ├── MuteCameraIcon.tsx
│   │   ├── MuteMicrophoneIcon.tsx
│   │   ├── ScreenShareIcon.tsx
│   │   ├── SendIcon.tsx
│   │   ├── SettingsIcon.tsx
│   │   ├── UnmuteCameraIcon.tsx
│   │   └── UnmuteMicrophoneIcon.tsx
│   ├── modals/
│   │   ├── ACRScoreDialog.tsx
│   │   ├── ErrorModal.tsx
│   │   └── RenameParticipantModal.tsx
│   └── renderers/
│       ├── AudioRenderer.tsx
│       ├── ChatRenderer.tsx
│       ├── ScreenShareRenderer.tsx
│       └── VideoRenderer.tsx
├── context/
│   ├── Chat.tsx
│   ├── Space.tsx
│   ├── User.tsx
│   └── UserMedia.tsx
├── hooks/
│   ├── useLocalStorage.tsx
│   ├── useParticipant.ts
│   ├── useSpace.ts
│   ├── useSpaceEvent.tsx
│   ├── useUserMedia.ts
│   └── useWindowDimension.ts
├── lib/
│   ├── constants.ts
│   ├── gallery.ts
│   ├── theme.ts
│   └── utils.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages/
│   ├── _app.tsx
│   ├── _document.tsx
│   ├── api/
│   │   ├── spaces/
│   │   │   └── [id].ts
│   │   ├── spaces.ts
│   │   ├── token.ts
│   │   └── webhooks.ts
│   ├── index.tsx
│   └── space/
│       └── [id].tsx
├── server-lib/
│   └── services.ts
├── shared/
│   ├── defaults.tsx
│   └── toastConfigs.tsx
└── tsconfig.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc.json
================================================
{
  "extends": ["next/core-web-vitals", "prettier"]
}


================================================
FILE: .gitignore
================================================
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local

# vercel
.vercel

# typescript
*.tsbuildinfo

# VScode
.vscode


================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

npx lint-staged


================================================
FILE: .prettierignore
================================================
# dependencies
/node_modules
/.pnp
.pnp.js

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# typescript
*.tsbuildinfo


================================================
FILE: .prettierrc.json
================================================
{}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2023 Mux

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Mux Meet

> [!WARNING]
> Mux Real-Time Video has been sunset and is unavailable for new usage. Existing access will end on December 31, 2023. [We recommend migrating your application to our partner, LiveKit](https://livekit.io/mux-livekit). Please contact [Mux Support](https://mux.com/support) if you need more help or details. Mux Meet is no-longer maintained and the repository will soon be archived.

Mux Meet is a video conferencing app powered by [Mux Real-Time Video](https://mux.com/real-time-video), written in React, using the [Next.js](https://nextjs.org/) framework.

![Four users in a Mux Meet call](https://user-images.githubusercontent.com/1211390/216212346-b319d137-0d2e-405a-bbab-703cc32763b3.jpg)

# Getting Started

In order for Meet to connect to Mux's APIs, an access token and signing key must be provided. These are generated in the Mux Dashboard and should be set as environment variables.

The easiest way to use Mux Meet is to deploy it to Vercel.

[![Deploy with Vercel](https://vercel.com/button)](<https://vercel.com/new/clone?demo-title=Mux%20Meet&demo-description=Real-time%20conferencing%20(meeting)%20SaaS%20app%2C%20built%20with%20Next.js%2C%20Mux%2C%20and%20Vercel&demo-url=https%3A%2F%2Fmuxmeet.vercel.app%2F&demo-image=%2F%2Fimages.ctfassets.net%2Fe5382hct74si%2F7ISNSvmomH7w7KUPCy8gn0%2F97ab315dcd21aa1d4b23e54dc123b562%2Fmux-meet.png&project-name=Mux%20Meet&repository-name=mux-meet&repository-url=https%3A%2F%2Fgithub.com%2Fmuxinc%2Fmeet&from=templates&skippable-integrations=1&env=MUX_TOKEN_ID%2CMUX_TOKEN_SECRET%2CMUX_SIGNING_KEY%2CMUX_PRIVATE_KEY&envDescription=How%20to%20get%20these%20env%20variables%3A&envLink=https%3A%2F%2Fgithub.com%2Fmuxinc%2Fmeet%23getting-started>)

After creating your project, you will be prompted to configure it.

![Configure Vercel Environment Variables](https://user-images.githubusercontent.com/1211390/216212169-251d87ef-83ae-4b9b-82e8-ae42cb430b02.jpg)

In a separate window, open https://dashboard.mux.com and sign in. You will need to create an account, if you don't already have one.

From the [Mux Dashboard](https://dashboard.mux.com), open Settings from the bottom left and click Access Tokens. Then click "Generate new token" and select Mux Video from the list of permissions. Optionally, give the access token a name like Mux Meet.

![Create new Mux Access Token](https://user-images.githubusercontent.com/1211390/216212226-d98b377b-7105-4db7-89f7-8b3f6aadd805.jpg)

Once your token is generated, copy and paste the ID and Secret as the values for `MUX_TOKEN_ID` and `MUX_TOKEN_SECRET` in Vercel.

Now go back to https://dashboard.mux.com to generate the Signing Key.

From the [Mux Dashboard](https://dashboard.mux.com), open Settings from the bottom left and click Signing Keys. Then click "Generate new key" and make sure you use the same environment as you did for the Access Token. The Product selection should default to Video.

![Create new Mux Signing Key](https://user-images.githubusercontent.com/1211390/216212263-f8fe2d0a-e8f4-4ba6-8197-bbbe745c9cb1.jpg)

Once your key is generated, copy and paste the ID and Secret as the values for `MUX_SIGNING_KEY` and `MUX_PRIVATE_KEY` in Vercel.

_Both the Access Token and Signing Key are sensitive. DO NOT MAKE THEM PUBLIC. It is safe to store them in Vercel as Environment Variables or locally during development._

Once all 4 environment variables are filled in. Click Deploy for Vercel to build and start your app.

## Cleanup Spaces after Meeting

Joining a new Space creates a Space in Mux, but in order to clean up Spaces after a meeting is over, set up a simple [webhook from Mux](https://docs.mux.com/guides/video/listen-for-webhooks).

From the [Mux Dashboard](https://dashboard.mux.com), open Settings from the bottom left and click Webhooks. Then click "Generate new webhook" and make sure you use the same environment as you did for the Access Token. The URL to notify will be your app's URL + `/api/webhooks`.

![Create new Mux webhook](https://user-images.githubusercontent.com/1211390/216212296-93dad4a3-1b91-4402-8eed-c0325dea0d69.jpg)

Now generate the Webhook and copy the Signing Secret by clicking Show Signing Secret.

Configure your deployed app with a new environment variable named `WEBHOOK_SECRET` with the value of the Webhook Signing Secret.

_Make sure you redeploy for your new environment variable to take affect._

## Limit Access

To limit the number of active Spaces, set an integer value for an environment variable `ACTIVE_SPACE_LIMIT`.

To limit the amount of time participants are allowed to spend in a temporary Space, set an integer value in seconds for an environment variable `SPACE_DURATION_SECONDS`.

# Develop locally

Create an .env.local file at the root of the repo with the following secrets:

```bash
MUX_TOKEN_ID=
MUX_TOKEN_SECRET=
MUX_SIGNING_KEY=
MUX_PRIVATE_KEY=
```

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.

# Learn More

## Mux

- [Mux Real-Time Video](https://mux.com/real-time-video)
- [Real-Time Video in React](https://docs.mux.com/guides/video/send-and-receive-real-time-video-from-a-react-application)
- [Real-Time Video on Android](https://docs.mux.com/guides/video/send-and-receive-real-time-video-from-an-android-application)
- [Real-Time Video on iOS](https://docs.mux.com/guides/video/send-and-receive-real-time-video-from-an-ios-application)

## Next.js

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.


================================================
FILE: client/token.ts
================================================
import axios from "axios";

interface TokenParams {
  spaceId: string;
  participantId: string;
}

interface TokenResponse {
  spaceJWT: string;
}

export const tokenPOST = async (
  params: TokenParams
): Promise<TokenResponse> => {
  return axios.post(`/api/token`, params).then(({ data }) => data);
};


================================================
FILE: components/Controls.tsx
================================================
import React, { useCallback } from "react";
import { useRouter } from "next/router";
import { Flex, useDisclosure } from "@chakra-ui/react";

import { useSpace } from "hooks/useSpace";

import ControlsRight from "./controls/ControlsRight";
import ControlsLeft from "./controls/ControlsLeft";
import ControlsCenter from "./controls/ControlsCenter";
import ACRScoreDialog from "./modals/ACRScoreDialog";

export default function Controls(): JSX.Element {
  const router = useRouter();
  const { leaveSpace } = useSpace();
  const { isOpen: isACRScoreDialogOpen, onOpen: onACRScoreDialogOpen } =
    useDisclosure();

  const leaveSpacePage = useCallback(() => {
    router.push("/");
  }, [router]);

  const promptForACR = useCallback(() => {
    try {
      leaveSpace();
      onACRScoreDialogOpen();
    } catch (error) {
      console.error(`Unable to properly leave space: ${error}`);
      leaveSpacePage();
    }
  }, [leaveSpace, leaveSpacePage, onACRScoreDialogOpen]);

  return (
    <>
      <ACRScoreDialog isOpen={isACRScoreDialogOpen} onClose={leaveSpacePage} />
      <Flex
        alignItems="center"
        backgroundColor="#0a0a0b"
        bottom="0px"
        flexDirection="row"
        height={{ base: "60px", sm: "80px" }}
        justifyContent="space-between"
        left="0px"
        padding="10px 40px"
        position="fixed"
        width="100%"
        zIndex={1000}
      >
        <ControlsLeft />
        {!isACRScoreDialogOpen && (
          <>
            <ControlsCenter onLeave={promptForACR} />
            <ControlsRight onLeave={promptForACR} />
          </>
        )}
      </Flex>
    </>
  );
}


================================================
FILE: components/Gallery.tsx
================================================
import React, { useCallback, useMemo, useState } from "react";
import { IconButton, Center, Flex } from "@chakra-ui/react";

import UserContext from "context/User";

import { useSpace } from "hooks/useSpace";

import Participant from "./Participant";
import GalleryLayout from "./GalleryLayout";
import ParticipantAudio from "./ParticipantAudio";
import ChevronLeftIcon from "components/icons/ChevronLeftIcon";
import ChevronRightIcon from "components/icons/ChevronRightIcon";

function pushToFront<T>(array: T[], element: T) {
  const index = array.findIndex((el) => el === element);
  if (index) {
    array.splice(index, 1);
    array.unshift(element);
  }
}

interface Props {
  gap: number;
  width: number;
  height: number;
  participantsPerPage: number;
}

export default function Gallery({
  gap,
  width,
  height,
  participantsPerPage,
}: Props): JSX.Element {
  const [currentPage, setCurrentPage] = useState(1);
  const {
    connectionIds,
    participantCount,
    localParticipantConnectionId,
    screenShareParticipantConnectionId,
  } = useSpace();
  const { pinnedConnectionId } = React.useContext(UserContext);

  const orderedConnectionIds = useMemo(() => {
    const ids = [...connectionIds];
    [
      screenShareParticipantConnectionId,
      pinnedConnectionId,
      localParticipantConnectionId,
    ].forEach((id) => {
      if (id) {
        pushToFront(ids, id);
      }
    });

    return ids;
  }, [
    connectionIds,
    localParticipantConnectionId,
    pinnedConnectionId,
    screenShareParticipantConnectionId,
  ]);

  const numberPages = useMemo(() => {
    if (participantCount >= participantsPerPage) {
      return Math.ceil(participantCount / participantsPerPage);
    } else {
      return 1;
    }
  }, [participantCount, participantsPerPage]);

  const goToPreviousPage = useCallback(() => {
    if (currentPage > 1) {
      setCurrentPage((page) => page - 1);
    }
  }, [currentPage]);

  const paginatedConnectionIds = useMemo(() => {
    const startIndex = currentPage * participantsPerPage - participantsPerPage;
    const endIndex = startIndex + participantsPerPage;
    const pageParticipants = orderedConnectionIds.slice(startIndex, endIndex);
    // if there are no participants, then only the local view will show up on the page
    // we need to go back to the previous page.
    if (pageParticipants.length === 0) {
      goToPreviousPage();
    }
    return pageParticipants;
  }, [
    orderedConnectionIds,
    currentPage,
    participantsPerPage,
    goToPreviousPage,
  ]);

  const hidePaginateCtrlRight = currentPage === numberPages;

  const hidePaginateCtrlLeft = currentPage === 1;

  const goToNextPage = () => {
    if (currentPage < numberPages) {
      setCurrentPage((page) => page + 1);
    }
  };

  const widthBetweenPagination = numberPages === 1 ? width : width - 80;

  return (
    <Flex height="100%" justifyContent="space-between">
      <Center w="40px" marginLeft="12px">
        <IconButton
          aria-label="Paginate left"
          icon={<ChevronLeftIcon />}
          isRound={true}
          onClick={goToPreviousPage}
          opacity={numberPages === 1 ? 0 : 1}
          hidden={hidePaginateCtrlLeft}
          variant="outline"
          border="1px"
          borderColor="#666666"
          backgroundColor="#383838"
          _hover={{
            border: "1px solid #CCCCCC",
            backgroundColor: "#383838",
          }}
          _active={{
            border: "1px solid #CCCCCC",
            backgroundColor: "#444444",
          }}
        />
      </Center>
      <Center width={width} zIndex={100}>
        {connectionIds.map((connectionId) => (
          <ParticipantAudio key={connectionId} connectionId={connectionId} />
        ))}
        <GalleryLayout
          width={widthBetweenPagination}
          height={height}
          gap={gap - 6}
        >
          {paginatedConnectionIds.map((connectionId) => {
            return (
              <Participant key={connectionId} connectionId={connectionId} />
            );
          })}
        </GalleryLayout>
      </Center>
      <Center w="40px" marginRight="12px">
        <IconButton
          aria-label="Paginate right"
          icon={<ChevronRightIcon />}
          isRound={true}
          opacity={numberPages === 1 ? 0 : 1}
          hidden={hidePaginateCtrlRight}
          variant="outline"
          border="1px"
          borderColor="#666666"
          onClick={goToNextPage}
          backgroundColor="#383838"
          _hover={{
            border: "1px solid #CCCCCC",
            backgroundColor: "#383838",
          }}
          _active={{
            border: "1px solid #CCCCCC",
            backgroundColor: "#444444",
          }}
          zIndex={2}
        />
      </Center>
    </Flex>
  );
}


================================================
FILE: components/GalleryLayout.tsx
================================================
import React, {
  Children,
  cloneElement,
  isValidElement,
  ReactNode,
  useMemo,
} from "react";
import { Flex } from "@chakra-ui/react";

import { calcOptimalBoxes } from "lib/gallery";

interface Props {
  gap?: number;
  width: number;
  height: number;
  children: ReactNode;
}

const GalleryLayout = ({ children, width, height, gap = 10 }: Props) => {
  const bestFit = useMemo(() => {
    if (children) {
      return calcOptimalBoxes(
        width,
        height,
        Children.count(children),
        16 / 9,
        gap
      );
    }
  }, [children, width, height, gap]);

  return (
    <Flex
      wrap="wrap"
      width="100%"
      height="100%"
      gap={`${gap}px`}
      overflow="hidden"
      alignItems="center"
      alignContent="center"
      justifyContent="center"
    >
      {Children.map(children, (child) => {
        if (
          isValidElement(child) &&
          bestFit &&
          bestFit.width &&
          bestFit.height
        ) {
          return cloneElement(child as React.ReactElement<any>, {
            width: bestFit.width,
            height: bestFit.height,
          });
        } else {
          return child;
        }
      })}
    </Flex>
  );
};

export default GalleryLayout;


================================================
FILE: components/Header.tsx
================================================
import React from "react";
import Image from "next/image";
import { AiFillGithub } from "react-icons/ai";
import { Flex, Spacer } from "@chakra-ui/react";

import muxLogo from "../public/mux-logo.svg";

export default function Header(): JSX.Element {
  return (
    <Flex
      zIndex={2}
      padding="2"
      alignItems="center"
      justifyContent="space-between"
      backgroundColor="#292929"
      borderBottom="1px solid #e8e8e8"
    >
      <Flex alignItems="center" padding="10px" width="290px">
        <a
          href="https://www.mux.com/real-time-video"
          target="_blank"
          rel="noreferrer"
        >
          <Image
            priority
            alt="logo"
            width={150}
            src={muxLogo}
            style={{ height: "auto" }}
          />
        </a>
      </Flex>
      <Spacer />
      <Flex alignItems="center" padding="10px">
        <a
          href="https://github.com/muxinc/meet"
          target="_blank"
          rel="noreferrer"
        >
          <AiFillGithub color="white" size="40px" />
        </a>
      </Flex>
    </Flex>
  );
}


================================================
FILE: components/Meeting.tsx
================================================
import React, { useContext } from "react";
import { Center, Flex } from "@chakra-ui/react";

import { useSpace } from "hooks/useSpace";
import useWindowDimensions from "hooks/useWindowDimension";

import Gallery from "./Gallery";
import Timer from "./Timer";
import ScreenShareRenderer from "./renderers/ScreenShareRenderer";
import ChatRenderer from "./renderers/ChatRenderer";
import ChatContext from "context/Chat";

const headerHeight = 80;
const chatWidth = 300;

export default function Meeting(): JSX.Element {
  let gap = 10;
  const {
    participantCount,
    attachScreenShare,
    isScreenShareActive,
    spaceEndsAt,
  } = useSpace();
  const { isChatOpen } = useContext(ChatContext);
  const { width = 0, height = 0 } = useWindowDimensions();

  const availableWidth = width - (isChatOpen && width > 800 ? chatWidth : 0);

  const paddingY = height < 600 ? 10 : 40;
  const paddingX = availableWidth < 800 ? 40 : 60;

  let galleryWidth = availableWidth - paddingX * 2;
  if (isScreenShareActive) {
    if (participantCount < 6) {
      galleryWidth = availableWidth * 0.25 - paddingX;
    } else {
      galleryWidth = availableWidth * 0.33 - paddingX / 2;
    }
    galleryWidth = Math.max(galleryWidth, 160);
  }
  let galleryHeight = height - headerHeight - paddingY * 2;

  let screenShareWidth = isScreenShareActive
    ? availableWidth - galleryWidth
    : 0;

  let direction: "row" | "column" = "row";
  if (width < height) {
    gap = 8;
    galleryWidth = availableWidth - paddingX * 2;
    if (isScreenShareActive) {
      direction = "column";
      screenShareWidth = availableWidth;
      galleryHeight = height - headerHeight - (availableWidth / 4) * 3;
    }
  }

  let scaleFactor = 2.25;
  const rows = Math.max(Math.ceil(galleryHeight / (90 * scaleFactor)), 1);
  const columns = Math.max(Math.ceil(galleryWidth / (160 * scaleFactor)), 1);

  const participantsPerPage = Math.round(rows * columns);

  return (
    <Flex width="100%" height="100%" direction="row" position="relative">
      {spaceEndsAt && <Timer />}
      <Flex
        maxWidth={availableWidth}
        height="100%"
        alignItems="center"
        justifyContent="center"
        direction={direction}
        flexGrow={1}
      >
        <Center width={`${screenShareWidth}px`} maxHeight="100%">
          <ScreenShareRenderer attach={attachScreenShare} />
        </Center>
        <Gallery
          gap={gap}
          width={galleryWidth}
          height={galleryHeight}
          participantsPerPage={participantsPerPage}
        />
      </Flex>
      <ChatRenderer show={width > 800 && isChatOpen} />
    </Flex>
  );
}


================================================
FILE: components/MuteIndicator.tsx
================================================
import React from "react";
import { Box, Flex, Icon } from "@chakra-ui/react";
import { IoMicOffOutline } from "react-icons/io5";

interface Props {
  isMuted: boolean;
  parentHeight: number;
}

export default function MuteIndicator({
  isMuted,
  parentHeight,
}: Props): JSX.Element {
  let left = "2px";
  let bottom = "15%";
  let marginLeft = "3";
  let iconWidth = "5";
  let iconHeight = "5";
  let borderRadius = "10px";
  let paddingX = "2";
  let paddingY = "1";
  if (parentHeight <= 250) {
    left = "0";
    bottom = "5%";
    marginLeft = "1";
  }
  if (parentHeight <= 200) {
    paddingX = "1";
    borderRadius = "0 10px 0 0";
    bottom = "0";
    marginLeft = "0";
    iconWidth = "12px";
    iconHeight = "12px";
  }
  if (parentHeight <= 90) {
    paddingX = "0";
    paddingY = "1.5px";
    borderRadius = "0";
  }

  return (
    <Box
      _groupHover={{
        opacity: 1,
        backgroundColor: "transparent",
      }}
      background="rgba(68, 68, 68, 0.75)"
      borderRadius={borderRadius}
      color="white"
      marginLeft={marginLeft}
      marginY="0"
      py={paddingY}
      zIndex={10}
      position="absolute"
      left={left}
      bottom={bottom}
    >
      {isMuted && (
        <Flex alignItems="center" color="#FFFFFF" px={paddingX}>
          <Icon w={iconWidth} h={iconHeight} as={IoMicOffOutline} />
        </Flex>
      )}
    </Box>
  );
}


================================================
FILE: components/Notifications.tsx
================================================
import { useState, useEffect } from "react";
import { useDisclosure } from "@chakra-ui/react";

import { useSpace } from "hooks/useSpace";
import { useUserMedia } from "hooks/useUserMedia";

import Sounds from "./Sounds";
import Toasts from "./Toasts";
import ErrorModal from "./modals/ErrorModal";

export default function Notifications(): JSX.Element {
  const { joinError, screenShareError } = useSpace();
  const { userMediaError } = useUserMedia();
  const [errorModalTitle, setErrorModalTitle] = useState("");
  const [errorModalMessage, setErrorModalMessage] = useState("");
  const {
    isOpen: isErrorModalOpen,
    onOpen: onErrorModalOpen,
    onClose: onErrorModalClose,
  } = useDisclosure();

  useEffect(() => {
    if (joinError) {
      setErrorModalTitle("Error joining space");
      setErrorModalMessage(joinError);
      onErrorModalOpen();
    }
  }, [joinError, onErrorModalOpen]);

  useEffect(() => {
    if (screenShareError === "Permission denied by system") {
      setErrorModalTitle("Can't share your screen");
      setErrorModalMessage(
        "Please check your browser has screen capture permissions and try restarting your browser if you continue to have issues."
      );
      onErrorModalOpen();
    }
  }, [screenShareError, onErrorModalOpen]);

  useEffect(() => {
    if (userMediaError === "NotAllowedError") {
      setErrorModalTitle("Can't show your media");
      setErrorModalMessage(
        "Please check your browser has media capture (webcam and microphone) permissions and try restarting your browser if you continue to have issues."
      );
      onErrorModalOpen();
    }
    if (userMediaError === "OverconstrainedError") {
    }

    return () => {
      onErrorModalClose();
    };
  }, [userMediaError, onErrorModalOpen, onErrorModalClose]);
  return (
    <>
      <Sounds />
      <Toasts />
      <ErrorModal
        title={errorModalTitle}
        message={errorModalMessage}
        isOpen={isErrorModalOpen}
        onClose={onErrorModalClose}
      />
    </>
  );
}


================================================
FILE: components/Participant.tsx
================================================
import React, { useMemo } from "react";
import { Box, Center, Flex } from "@chakra-ui/react";

import UserContext from "context/User";
import { useParticipant } from "hooks/useParticipant";

import Pin from "./Pin";
import VideoRenderer from "./renderers/VideoRenderer";
import ParticipantInfoBar from "./ParticipantInfoBar";
import ParticipantName from "./ParticipantName";

interface Props {
  width?: number;
  height?: number;
  connectionId: string;
}

export default function Participant({
  width,
  height,
  connectionId,
}: Props): JSX.Element {
  const {
    id,
    isLocal,
    isSpeaking,
    hasMicTrack,
    isMicTrackMuted,
    isCameraOff,
    cameraWidth,
    cameraHeight,
    displayName,
    attachVideoElement,
  } = useParticipant(connectionId);

  const outlineWidth = 3;

  return (
    <Box
      width={`${width! - outlineWidth * 2}px`}
      height={`${height! - outlineWidth * 2}px`}
      minWidth="160px"
      minHeight="90px"
      background="black"
      boxShadow={`0 0 0 ${
        !isMicTrackMuted && isSpeaking ? outlineWidth : 1
      }px ${!isMicTrackMuted && isSpeaking ? "#FA50B5" : "black"}`}
      borderRadius="5px"
      margin={`${outlineWidth}px`}
      overflow="hidden"
      position="relative"
      role="group"
    >
      <VideoRenderer
        local={isLocal}
        width={cameraWidth}
        height={cameraHeight}
        attachFunc={attachVideoElement}
        connectionId={connectionId}
      />

      <ParticipantInfoBar
        name={displayName || id}
        isMuted={!hasMicTrack || isMicTrackMuted}
        parentHeight={height!}
      />

      {isCameraOff && (
        <ParticipantName isSmall={width! <= 400}>
          {displayName || id}
        </ParticipantName>
      )}

      {!isLocal && <Pin connectionId={connectionId} />}
    </Box>
  );
}


================================================
FILE: components/ParticipantAudio.tsx
================================================
import React from "react";

import { useParticipant } from "hooks/useParticipant";

import AudioRenderer from "./renderers/AudioRenderer";

interface Props {
  connectionId: string;
}

const ParticipantAudio = ({ connectionId }: Props) => {
  const { isLocal, attachAudioElement } = useParticipant(connectionId);

  return !isLocal ? <AudioRenderer attachFunc={attachAudioElement} /> : null;
};

export default ParticipantAudio;


================================================
FILE: components/ParticipantInfoBar.tsx
================================================
import React from "react";
import { Flex, Text } from "@chakra-ui/react";

import MuteIndicator from "./MuteIndicator";

interface Props {
  name: string;
  isMuted: boolean;
  parentHeight: number;
}

export default function ParticipantInfoBar({
  name,
  isMuted,
  parentHeight,
}: Props): JSX.Element {
  let height = "40px";
  let fontSize = "14px";
  if (parentHeight <= 250) {
    height = "30px";
  }
  if (parentHeight <= 200) {
    height = "20px";
    fontSize = "10px";
  }
  if (parentHeight <= 90) {
    height = "15px";
    fontSize = "10px";
  }

  return (
    <Flex
      position="absolute"
      bottom="0"
      left="0"
      width="100%"
      height={height}
      color="transparent"
      backgroundColor="transparent"
      _groupHover={{
        color: "white",
        background: "rgba(50, 50, 50, 0.5)",
      }}
      justifyContent="center"
      alignItems="center"
    >
      <MuteIndicator parentHeight={parentHeight} isMuted={isMuted} />
      <Text fontSize={fontSize} fontWeight="700">
        {name}
      </Text>
    </Flex>
  );
}


================================================
FILE: components/ParticipantName.tsx
================================================
import { memo } from "react";
import { Box, Center, Flex } from "@chakra-ui/react";

interface Props {
  isSmall: boolean;
  children: string;
}

function ParticipantName({ isSmall, children }: Props): JSX.Element {
  return (
    <Center
      background="black"
      color="white"
      fontSize={isSmall ? "20px" : "45px"}
      h="100%"
      position="absolute"
      top="0"
      w="100%"
    >
      <Flex width="100%" direction="column" textAlign="center">
        <Box overflowWrap="anywhere">{children}</Box>
      </Flex>
    </Center>
  );
}

const MemoizedName = memo(ParticipantName);
export default MemoizedName;


================================================
FILE: components/Pin.tsx
================================================
import React from "react";
import { IconButton } from "@chakra-ui/react";
import { MdPushPin, MdOutlinePushPin } from "react-icons/md";

import UserContext from "context/User";

interface Props {
  connectionId: string;
}

export default function Pin({ connectionId }: Props): JSX.Element {
  const { pinnedConnectionId, setPinnedConnectionId } =
    React.useContext(UserContext);

  return (
    <IconButton
      size="sm"
      opacity={pinnedConnectionId === connectionId ? 1 : 0}
      _groupHover={{
        opacity: 1,
      }}
      aria-label="pin"
      onClick={() => {
        if (setPinnedConnectionId) {
          if (pinnedConnectionId === connectionId) {
            setPinnedConnectionId("");
          } else {
            setPinnedConnectionId(connectionId);
          }
        }
      }}
      variant="ghost"
      position="absolute"
      right={0}
      top={0}
      icon={
        pinnedConnectionId === connectionId ? (
          <MdPushPin />
        ) : (
          <MdOutlinePushPin />
        )
      }
    />
  );
}


================================================
FILE: components/Sounds.tsx
================================================
import useSound from "use-sound";
import { SpaceEvent } from "@mux/spaces-web";

import { useSpaceEvent } from "hooks/useSpaceEvent";
import { useCallback } from "react";
import { useSpace } from "hooks/useSpace";

const participantSoundCutoff = 5;

export default function Sounds(): JSX.Element {
  const { isJoined } = useSpace();

  return <>{isJoined && <JoinedSounds />}</>;
}

function JoinedSounds(): JSX.Element {
  const { participantCount } = useSpace();
  const [playJoinSound] = useSound("/sounds/meet-join.mp3");
  const [playLeaveSound] = useSound("/sounds/meet-leave.mp3");

  useSpaceEvent(
    SpaceEvent.ParticipantJoined,
    useCallback(() => {
      if (document["hidden"] && participantCount < participantSoundCutoff) {
        playJoinSound();
      }
    }, [playJoinSound, participantCount])
  );

  useSpaceEvent(
    SpaceEvent.ParticipantLeft,
    useCallback(() => {
      if (document["hidden"] && participantCount < participantSoundCutoff) {
        playLeaveSound();
      }
    }, [playLeaveSound, participantCount])
  );

  return <></>;
}


================================================
FILE: components/Stage.tsx
================================================
import React from "react";
import { Center, Heading, Spinner } from "@chakra-ui/react";

import { useSpace } from "hooks/useSpace";

import Controls from "./Controls";
import Meeting from "./Meeting";
import Notifications from "./Notifications";
import { ChatProvider } from "context/Chat";

const LoadingSpinner = () => {
  return (
    <>
      <Spinner color="#FA50B5" size="xl" />
      <Heading color="white" ml={5}>
        Joining Space...
      </Heading>
    </>
  );
};

export default function Stage(): JSX.Element {
  const { isJoined } = useSpace();

  return (
    <>
      <Notifications />
      <ChatProvider>
        <Center height="calc(100% - 80px)" zIndex={1}>
          {!isJoined ? <LoadingSpinner /> : <Meeting />}
        </Center>
        <Controls />
      </ChatProvider>
    </>
  );
}


================================================
FILE: components/Timer.tsx
================================================
import { useEffect, useState } from "react";
import { useRouter } from "next/router";
import { Tooltip } from "@chakra-ui/react";
import styled from "@emotion/styled";
import moment from "moment";

import { useSpace } from "hooks/useSpace";
import { transientOptions } from "lib/utils";

const Display = styled("div", transientOptions)<{ $isUrgent: boolean }>`
  position: absolute;
  display: flex;
  flex-direction: column;
  align-items: start;
  background-color: #383838;
  padding: 0.5em;
  top: 12px;
  left: 12px;
  font-size: 1.5em;
  font-variant-numeric: tabular-nums;
  color: white;
  user-select: none;
  border-radius: 3px;
  z-index: 200;

  ${(props) =>
    props.$isUrgent &&
    `
  #countdown {
    color: red;
  }
  `}
`;

const getClock = (diff: number) => {
  const minutes = Math.floor((diff / 1000 / 60) % 60);
  const seconds = Math.floor((diff / 1000) % 60);

  const minuteDisplay = 10 > minutes ? "0" + minutes : minutes;
  const secondDisplay = 10 > seconds ? "0" + seconds : seconds;

  return `${minuteDisplay}:${secondDisplay}`;
};

const Timer = (): JSX.Element => {
  const { spaceEndsAt, leaveSpace } = useSpace();
  const diff = moment(spaceEndsAt).diff(moment());
  const [timeDisplay, setTimeDisplay] = useState<string>(
    spaceEndsAt ? getClock(diff) : "00:00"
  );
  const router = useRouter();
  const [isTwoMinutesLeft, setIsTwoMinutesLeft] = useState(
    120 >= Math.floor(diff / 1000)
  );

  useEffect(() => {
    const timer = setInterval(() => {
      const diff = moment(spaceEndsAt).diff(moment());

      if (diff > 0) {
        const clock = getClock(diff);
        setTimeDisplay(getClock(diff));
        setIsTwoMinutesLeft(120 >= Math.floor(diff / 1000));
      } else {
        leaveSpace();
        router.push("/");
      }
    }, 1000);

    return () => clearInterval(timer);
  }, [router, leaveSpace, spaceEndsAt, isTwoMinutesLeft]);

  return (
    <Tooltip label="This demo space will close after the timer expires.">
      <Display $isUrgent={isTwoMinutesLeft}>
        <span>Time Left</span>
        <span id="countdown">{timeDisplay}</span>
      </Display>
    </Tooltip>
  );
};

export default Timer;


================================================
FILE: components/Toasts.tsx
================================================
import styled from "@emotion/styled";
import { useRouter } from "next/router";
import { useCallback, useEffect, useRef } from "react";
import { Box, ToastId, useToast } from "@chakra-ui/react";

import { useSpace } from "hooks/useSpace";

import {
  broadcastingToastConfig,
  participantEventToastConfig,
  sharingScreenToastConfig,
  ToastIds,
  viewingSharedScreenToastConfig,
} from "shared/toastConfigs";

const ToastBox = styled(Box)`
  color: #0a0a0b;
  background: #cff1fc;
  font-size: 14px;
  padding: 15px 50px;
  text-align: center;
`;

export default function Toasts(): JSX.Element {
  const toast = useToast();
  const router = useRouter();
  const { isReady: isRouterReady } = router;
  const {
    isBroadcasting,
    isScreenShareActive,
    stopScreenShare,
    isLocalScreenShare,
    screenShareParticipantName,
  } = useSpace();
  const screenshareToastRef = useRef<ToastId>();
  const broadcastingToastRef = useRef<ToastId>();
  const participantEventToastIdRefs = useRef<Array<ToastId>>([]);

  const showLocalScreenshareToast = useCallback(() => {
    if (!toast.isActive(ToastIds.SHARING_SCREEN_TOAST_ID)) {
      screenshareToastRef.current = toast({
        ...sharingScreenToastConfig,
        render: ({ onClose }) => {
          return (
            <ToastBox>
              You are sharing your screen.
              <Box
                as="button"
                backgroundColor="transparent"
                border="1px solid #0A0A0B"
                height="30px"
                borderRadius="15px"
                paddingLeft="15px"
                paddingRight="15px"
                marginLeft="20px"
                onClick={() => {
                  stopScreenShare();
                  onClose();
                }}
              >
                Stop sharing
              </Box>
            </ToastBox>
          );
        },
      });
    }
  }, [toast, stopScreenShare]);

  const showRemoteScreenshareToast = useCallback(() => {
    if (
      !toast.isActive(ToastIds.VIEWING_SHARED_SCREEN_TOAST_ID) &&
      screenShareParticipantName
    ) {
      screenshareToastRef.current = toast({
        ...viewingSharedScreenToastConfig,
        render: () => (
          <ToastBox>
            {screenShareParticipantName} is sharing their screen.
          </ToastBox>
        ),
      });
    }
  }, [toast, screenShareParticipantName]);

  const hideScreenshareToast = useCallback(() => {
    if (screenshareToastRef.current) {
      toast.close(screenshareToastRef.current);
    }
  }, [toast]);

  const showBroadcastToast = useCallback(() => {
    if (!toast.isActive(ToastIds.BROADCASTING_SCREEN_TOAST_ID)) {
      broadcastingToastRef.current = toast({
        ...broadcastingToastConfig,
        render: () => (
          <ToastBox>{"⦿ Space is currently being broadcast"}</ToastBox>
        ),
      });
    }
  }, [toast]);

  const hideBroadcastToast = useCallback(() => {
    if (broadcastingToastRef.current) {
      toast.close(broadcastingToastRef.current);
    }
  }, [toast]);

  const pruneParticipantEventRefs = useCallback(() => {
    // Don't show more than 10 participant event toasts on screen at a time, close the oldest if necessary
    while (participantEventToastIdRefs.current.length > 9) {
      let oldest = participantEventToastIdRefs.current.shift();
      if (oldest) {
        toast.close(oldest);
      }
    }

    // Prune ids of toasts that have already closed on their own
    participantEventToastIdRefs.current =
      participantEventToastIdRefs.current.filter((ref) => toast.isActive(ref));
  }, [participantEventToastIdRefs, toast]);

  const showParticipantEventToast = useCallback(
    (eventDescription: string) => {
      pruneParticipantEventRefs();
      participantEventToastIdRefs.current.push(
        toast({
          ...participantEventToastConfig,
          render: () => <ToastBox>{eventDescription}</ToastBox>,
        })
      );
    },
    [toast, pruneParticipantEventRefs]
  );

  useEffect(() => {
    if (isScreenShareActive) {
      if (isLocalScreenShare) {
        showLocalScreenshareToast();
      } else {
        showRemoteScreenshareToast();
      }
    } else {
      hideScreenshareToast();
    }
  }, [
    isScreenShareActive,
    isLocalScreenShare,
    showLocalScreenshareToast,
    showRemoteScreenshareToast,
    hideScreenshareToast,
  ]);

  useEffect(() => {
    if (isBroadcasting) {
      showBroadcastToast();
    } else {
      hideBroadcastToast();
    }
  }, [isBroadcasting, showBroadcastToast, hideBroadcastToast]);

  useEffect(() => {
    if (!isRouterReady) return;

    function closeAllToasts() {
      hideBroadcastToast();
      hideScreenshareToast();
      for (let toastRef in participantEventToastIdRefs.current) {
        toast.close(toastRef);
      }
      pruneParticipantEventRefs();
    }

    router.events.on("routeChangeStart", closeAllToasts);
    return () => {
      router.events.off("routeChangeStart", closeAllToasts);
    };
  }, [
    isRouterReady,
    router,
    toast,
    hideBroadcastToast,
    hideScreenshareToast,
    pruneParticipantEventRefs,
  ]);

  return <></>;
}


================================================
FILE: components/UserInteractionPrompt.tsx
================================================
import React, {
  useState,
  useEffect,
  useMemo,
  useRef,
  useContext,
  MutableRefObject,
} from "react";
import Image from "next/image";
import {
  Button,
  Flex,
  FormControl,
  FormHelperText,
  FormLabel,
  HStack,
  Input,
  Stack,
} from "@chakra-ui/react";
import { HiOutlineArrowNarrowRight } from "react-icons/hi";

import UserContext from "context/User";
import { useUserMedia } from "hooks/useUserMedia";

import MicrophoneButton from "components/controls/buttons/MicrophoneButton";
import CameraButton from "components/controls/buttons/CameraButton";

import muxLogo from "../public/mux-logo.svg";

interface Props {
  onInteraction: () => void;
  participantNameRef: MutableRefObject<string>;
}

export default function UserInteractionPrompt({
  onInteraction,
  participantNameRef,
}: Props): JSX.Element {
  const nameInputRef = useRef<HTMLInputElement>(null);
  const didPopulateDevicesRef = useRef(false);
  const user = useContext(UserContext);
  const [participantName, setParticipantName] = useState("");
  const [hasBlurredNameInput, setHasBlurredNameInput] = useState(false);

  const { requestPermissionAndPopulateDevices } = useUserMedia();

  useEffect(() => {
    if (nameInputRef.current) {
      nameInputRef.current.focus();
    }
  }, []);

  useEffect(() => {
    setParticipantName(user.participantName);
  }, [user.participantName]);

  const isNameInputInvalid = useMemo(
    () => !participantName && hasBlurredNameInput,
    [participantName, hasBlurredNameInput]
  );

  const handleParticipantNameChange = (event: {
    target: { value: string };
  }) => {
    setParticipantName(event.target.value);
  };

  useEffect(() => {
    if (didPopulateDevicesRef.current === false) {
      didPopulateDevicesRef.current = true;
      requestPermissionAndPopulateDevices();
    }
  }, [requestPermissionAndPopulateDevices]);

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    event?.preventDefault();
    participantNameRef.current = participantName;
    user.setParticipantName(participantName);
    user.setInteractionRequired(false);
    onInteraction();
  };

  return (
    <Flex
      gap="2rem"
      height="100%"
      overflow="hidden"
      direction="column"
      alignItems="center"
      justifyContent="center"
    >
      <Image
        priority
        alt="logo"
        width={300}
        src={muxLogo}
        style={{ zIndex: 0, height: "auto" }}
      />
      <form onSubmit={handleSubmit}>
        <Stack spacing="4">
          <FormControl
            isInvalid={isNameInputInvalid}
            onBlur={() => setHasBlurredNameInput(true)}
          >
            <FormLabel textAlign="center" color="white">
              Enter your name
            </FormLabel>
            <Input
              placeholder="Your name"
              color="white"
              size="lg"
              maxLength={40}
              id="participant_name"
              value={participantName}
              onChange={handleParticipantNameChange}
              variant="flushed"
              isRequired={true}
              ref={nameInputRef}
            />
            <FormHelperText
              color={!isNameInputInvalid ? "transparent" : "#E22C3E"}
            >
              This cannot be empty.
            </FormHelperText>
          </FormControl>

          <Button
            type="submit"
            isDisabled={!participantName}
            size="lg"
            rightIcon={<HiOutlineArrowNarrowRight />}
            variant="flushed"
            color="white"
          >
            Join Space
          </Button>
        </Stack>
      </form>
      <HStack marginTop="2rem">
        <MicrophoneButton />
        <CameraButton />
      </HStack>
    </Flex>
  );
}


================================================
FILE: components/controls/ControlsCenter.tsx
================================================
import React from "react";
import { HStack } from "@chakra-ui/react";

import MicrophoneButton from "./buttons/MicrophoneButton";
import CameraButton from "./buttons/CameraButton";
import ScreenShareButton from "./buttons/ScreenShareButton";
import SettingsButton from "./buttons/SettingsButton";
import ChatButton from "./buttons/ChatButton";
import useWindowDimensions from "hooks/useWindowDimension";
import { useSpace } from "hooks/useSpace";

interface Props {
  onLeave: () => void;
}

export default function ControlsCenter({ onLeave }: Props): JSX.Element {
  const { width = 0 } = useWindowDimensions();
  const { isLocalScreenShareSupported } = useSpace();

  return (
    <HStack spacing="24px">
      <MicrophoneButton />
      <CameraButton />
      {isLocalScreenShareSupported && <ScreenShareButton />}
      {width > 800 && <ChatButton />}
      <SettingsButton onLeave={onLeave} />
    </HStack>
  );
}


================================================
FILE: components/controls/ControlsLeft.tsx
================================================
import React from "react";
import Image from "next/image";
import { Flex } from "@chakra-ui/react";

import muxLogo from "../../public/mux-logo.svg";

export default function ControlsLeft(): JSX.Element {
  return (
    <Flex
      alignItems="center"
      display={{ base: "none", md: "flex" }}
      width="290px"
    >
      <a
        href="https://www.mux.com/real-time-video"
        target="_blank"
        rel="noreferrer"
      >
        <Image
          priority
          alt="logo"
          width={150}
          src={muxLogo}
          style={{ height: "auto" }}
        />
      </a>
    </Flex>
  );
}


================================================
FILE: components/controls/ControlsRight.tsx
================================================
import React from "react";
import { Button, Flex, Text } from "@chakra-ui/react";
import LeaveIcon from "components/icons/LeaveIcon";
import SpaceContext from "context/Space";
import styled from "@emotion/styled";
import useWindowDimensions from "hooks/useWindowDimension";

interface Props {
  onLeave: () => void;
}

const ParticipantsLabel = styled.span`
  color: #cccccc;
  font-size: 12px;
  line-height: 12px;
  text-transform: uppercase;
  letter-spacing: 0.8px;
`;

const ParticipantCount = styled.span`
  font-size: 21px;
  line-height: 21px;
  letter-spacing: -0.5px;
  margin-top: 5px;
`;

export default function ControlsRight({ onLeave }: Props): JSX.Element {
  const { participantCount } = React.useContext(SpaceContext);

  const { width } = useWindowDimensions();
  const hideParticipantCount = (width && width < 1000) || false;

  return (
    <Flex
      alignItems="center"
      direction="row-reverse"
      width="290px"
      height="46px"
    >
      <Button
        variant="muxDestructive"
        display={{ base: "none", sm: "flex" }}
        flexDirection="row"
        marginLeft="30px"
        padding="10px 20px"
        onClick={onLeave}
      >
        <LeaveIcon />
        <Text paddingLeft="10px">Leave</Text>
      </Button>
      <Flex
        alignItems="center"
        direction="column"
        color="white"
        fontWeight="bold"
        marginTop="2px"
      >
        {!hideParticipantCount && (
          <>
            <ParticipantsLabel>Participants</ParticipantsLabel>
            <ParticipantCount>{participantCount}</ParticipantCount>
          </>
        )}
      </Flex>
    </Flex>
  );
}


================================================
FILE: components/controls/buttons/CameraButton.tsx
================================================
import React, { useCallback } from "react";
import {
  Box,
  ButtonGroup,
  Flex,
  IconButton,
  Menu,
  MenuButton,
  MenuItem,
  MenuList,
  Tooltip,
  Text,
} from "@chakra-ui/react";
import { useHotkeys } from "react-hotkeys-hook";
import { AiOutlineCheck } from "react-icons/ai";

import UserContext from "context/User";
import { useSpace } from "hooks/useSpace";
import { useUserMedia } from "hooks/useUserMedia";

import ChevronIcon from "components/icons/ChevronIcon";
import MuteCameraIcon from "components/icons/MuteCameraIcon";
import UnmuteCameraIcon from "components/icons/UnmuteCameraIcon";

export default function CameraButton(): JSX.Element {
  const { cameraOff, setCameraOff, cameraDeviceId, setCameraDeviceId } =
    React.useContext(UserContext);
  const { isJoined, publishCamera, unPublishDevice } = useSpace();
  const { cameraDevices, stopActiveCamera } = useUserMedia();

  const selectCameraDevice = useCallback(
    async (deviceId: string) => {
      setCameraDeviceId(deviceId);

      if (isJoined && !cameraOff) {
        publishCamera(deviceId);
      }
    },
    [isJoined, setCameraDeviceId, cameraOff, publishCamera]
  );

  const toggleCamera = useCallback(async () => {
    if (cameraOff) {
      setCameraOff(false);
      if (isJoined) {
        publishCamera(cameraDeviceId);
      }
    } else {
      setCameraOff(true);
      stopActiveCamera();
      if (isJoined) {
        unPublishDevice(cameraDeviceId);
      }
    }
  }, [
    isJoined,
    cameraOff,
    setCameraOff,
    cameraDeviceId,
    stopActiveCamera,
    publishCamera,
    unPublishDevice,
  ]);

  const hotkeyText = "⌘ + e"; // adding this here to make it easy to change later. appears in tooltip on button.
  useHotkeys(
    "meta+e",
    () => {
      toggleCamera();
    },
    { preventDefault: true },
    [toggleCamera]
  );

  const ariaLabel = cameraOff ? "Unhide" : "Hide";

  return (
    <ButtonGroup position="relative">
      <Tooltip
        label={
          cameraOff
            ? `Enable Video (${hotkeyText})`
            : `Disable Video (${hotkeyText})`
        }
      >
        <IconButton
          variant="control"
          aria-label={ariaLabel}
          icon={cameraOff ? <UnmuteCameraIcon /> : <MuteCameraIcon />}
          onClick={toggleCamera}
        />
      </Tooltip>
      <Menu placement="top">
        {({ isOpen }) => (
          <>
            <MenuButton
              position="absolute"
              top="0"
              right="0"
              as={IconButton}
              variant="controlMenu"
              aria-label="Options"
              icon={<ChevronIcon />}
              zIndex={100}
              minWidth="20px"
              {...(isOpen && { transform: "rotate(180deg)" })}
            />
            <MenuList
              background="#383838"
              border="1px solid #323232"
              color="#CCCCCC"
              padding="5px 10px"
            >
              <Text userSelect="none" paddingX="12px" paddingY="6px">
                CAMERA
              </Text>
              {cameraDevices.map((device) => {
                return (
                  <MenuItem
                    key={device.deviceId}
                    onClick={() => selectCameraDevice(device.deviceId)}
                  >
                    <Flex alignItems="center">
                      {device.label}
                      {cameraDeviceId === device.deviceId && (
                        <Box marginLeft="10px">
                          <AiOutlineCheck />
                        </Box>
                      )}
                    </Flex>
                  </MenuItem>
                );
              })}
            </MenuList>
          </>
        )}
      </Menu>
    </ButtonGroup>
  );
}


================================================
FILE: components/controls/buttons/ChatButton.tsx
================================================
import React, { useContext } from "react";
import { IconButton, Tooltip } from "@chakra-ui/react";

import ChatIcon from "../../icons/ChatIcon";
import ChatContext from "context/Chat";
import styled from "@emotion/styled";

interface Props {
  isActive: boolean;
  hasNotifications: boolean;
}

const UnreadCircle = styled.div`
  position: absolute;
  width: 10px;
  height: 10px;
  border-radius: 5px;
  background-color: #fa50b5;
  top: calc(50% - 17.5px);
  right: calc(50% - 17.5px);
`;

export default function ChatButton(): JSX.Element {
  const { numUnreadMessages, isChatOpen, openChat, closeChat } =
    useContext(ChatContext);

  return (
    <Tooltip label={isChatOpen ? "Close chat" : "Open chat"}>
      <IconButton
        variant="control"
        aria-label="Toggle chat"
        icon={
          <>
            <ChatIcon />
            {numUnreadMessages > 0 && <UnreadCircle />}
          </>
        }
        {...(isChatOpen && {
          background: "#3E4247",
          border: "1px solid #FFFFFF",
        })}
        onClick={isChatOpen ? closeChat : openChat}
      />
    </Tooltip>
  );
}


================================================
FILE: components/controls/buttons/MicrophoneButton.tsx
================================================
import React, { useCallback, useEffect, useState } from "react";
import {
  Box,
  Flex,
  IconButton,
  Menu,
  MenuButton,
  MenuItem,
  MenuList,
  Tooltip,
  Text,
  ButtonGroup,
} from "@chakra-ui/react";
import { useHotkeys } from "react-hotkeys-hook";
import { AiOutlineCheck } from "react-icons/ai";

import UserContext from "context/User";
import { useSpace } from "hooks/useSpace";
import { useUserMedia } from "hooks/useUserMedia";

import ChevronIcon from "components/icons/ChevronIcon";
import MuteMicrophoneIcon from "components/icons/MuteMicrophoneIcon";
import UnmuteMicrophoneIcon from "components/icons/UnmuteMicrophoneIcon";

export default function MicrophoneButton(): JSX.Element {
  const {
    userWantsMicMuted,
    setUserWantsMicMuted,
    microphoneDeviceId,
    setMicrophoneDeviceId,
  } = React.useContext(UserContext);
  const {
    microphoneDevices,
    muteActiveMicrophone,
    unMuteActiveMicrophone,
    getActiveMicrophoneLevel,
  } = useUserMedia();
  const { isJoined, publishMicrophone } = useSpace();
  const [temporaryUnmute, setTemporaryUnmute] = useState(false);
  const [mouseOver, setMouseOver] = useState(false);
  const [micLevelPercent, setMicLevelPercent] = useState(0);
  const [micDistorted, setMicDistorted] = useState(false);

  const requestRef = React.useRef<number>();

  const selectAudioDevice = useCallback(
    async (deviceId: string) => {
      setMicrophoneDeviceId(deviceId);
      if (isJoined) {
        publishMicrophone(deviceId);
      }
    },
    [isJoined, setMicrophoneDeviceId, publishMicrophone]
  );

  const toggleMicrophone = useCallback(() => {
    if (userWantsMicMuted) {
      setUserWantsMicMuted(false);
      unMuteActiveMicrophone();
    } else {
      setUserWantsMicMuted(true);
      muteActiveMicrophone();
    }
  }, [
    userWantsMicMuted,
    setUserWantsMicMuted,
    muteActiveMicrophone,
    unMuteActiveMicrophone,
  ]);

  const animateMeter = useCallback(() => {
    if (requestRef.current != undefined && getActiveMicrophoneLevel) {
      let levels = getActiveMicrophoneLevel();

      if (levels) {
        // Convert to 0-100 scale
        let scaled = (Math.max(-60, levels.avgDb) + 60) * 1.66;
        setMicLevelPercent(scaled);

        if (levels.peakDb > -0.5) {
          setMicDistorted(true);
        } else {
          setMicDistorted(false);
        }
      } else {
        setMicLevelPercent(0);
        setMicDistorted(false);
      }
    }
    requestRef.current = requestAnimationFrame(animateMeter);
  }, [getActiveMicrophoneLevel]);

  useEffect(() => {
    requestRef.current = requestAnimationFrame(animateMeter);
    return () => {
      if (requestRef.current) {
        cancelAnimationFrame(requestRef.current);
      }
    };
  }, [animateMeter]);

  const hotkeyText = "⌘ + d"; // adding this here to make it easy to change later. appears in tooltip on button.
  useHotkeys(
    "meta+d",
    () => {
      toggleMicrophone();
    },
    { preventDefault: true },
    [toggleMicrophone]
  );

  useHotkeys(
    "space",
    () => {
      if (userWantsMicMuted && !temporaryUnmute) {
        unMuteActiveMicrophone();
        setTemporaryUnmute(true);
      }
    },
    { keydown: true, preventDefault: true },
    [unMuteActiveMicrophone, userWantsMicMuted, temporaryUnmute]
  );
  useHotkeys(
    "space",
    () => {
      if (temporaryUnmute) {
        setTemporaryUnmute(false);
        if (userWantsMicMuted) {
          muteActiveMicrophone();
        }
      }
    },
    { keyup: true, preventDefault: true },
    [muteActiveMicrophone, userWantsMicMuted, temporaryUnmute]
  );

  const ariaLabel = userWantsMicMuted ? "Unmute" : "Mute";

  return (
    <ButtonGroup position="relative">
      <Tooltip
        label={
          userWantsMicMuted ? `Unmute (${hotkeyText})` : `Mute (${hotkeyText})`
        }
      >
        <IconButton
          variant="control"
          aria-label={ariaLabel}
          onMouseEnter={() => setMouseOver(true)}
          onMouseLeave={() => setMouseOver(false)}
          icon={
            userWantsMicMuted && !temporaryUnmute ? (
              <MuteMicrophoneIcon />
            ) : (
              <UnmuteMicrophoneIcon />
            )
          }
          onClick={toggleMicrophone}
          {...(isJoined &&
            (micDistorted
              ? {
                  background: `radial-gradient(50% 50% at 50% 50%, rgba(255, 92, 56, 0.75) 0%, rgba(255, 92, 56, 0) ${micLevelPercent}%)`,
                }
              : {
                  background: `radial-gradient(50% 50% at 50% 50%, rgba(27, 227, 73, 0.75) 0%, rgba(27, 227, 73, 0) ${micLevelPercent}%)`,
                }))}
        />
      </Tooltip>
      <Menu placement="top">
        {({ isOpen }) => (
          <>
            <MenuButton
              position="absolute"
              top="0px"
              right="0px"
              zIndex={100}
              as={IconButton}
              aria-label="Options"
              icon={<ChevronIcon />}
              variant="controlMenu"
              minWidth="20px"
              {...(isOpen && { transform: "rotate(180deg)" })}
            />
            <MenuList
              background="#383838"
              border="1px solid #323232"
              color="#CCCCCC"
              padding="5px 10px"
            >
              <Text userSelect="none" paddingX="12px" paddingY="6px">
                MICROPHONE
              </Text>
              {microphoneDevices.map((device: MediaDeviceInfo) => {
                return (
                  <MenuItem
                    key={device.deviceId}
                    onClick={() => selectAudioDevice(device.deviceId)}
                  >
                    <Flex alignItems="center">
                      {device.label}
                      {microphoneDeviceId == device.deviceId && (
                        <Box marginLeft="10px">
                          <AiOutlineCheck />
                        </Box>
                      )}
                    </Flex>
                  </MenuItem>
                );
              })}
            </MenuList>
          </>
        )}
      </Menu>
      ;
    </ButtonGroup>
  );
}


================================================
FILE: components/controls/buttons/ScreenShareButton.tsx
================================================
import React from "react";
import { IconButton, Tooltip } from "@chakra-ui/react";

import { useSpace } from "hooks/useSpace";

import ScreenShareIcon from "../../icons/ScreenShareIcon";

export default function ScreenShareButton(): JSX.Element {
  const {
    isLocalScreenShare,
    isScreenShareActive,
    startScreenShare,
    stopScreenShare,
  } = useSpace();

  return (
    <Tooltip
      label={
        isScreenShareActive
          ? isLocalScreenShare
            ? "Stop screen-share"
            : "Someone else is currently screen-sharing"
          : "Share Screen"
      }
    >
      <IconButton
        variant="control"
        aria-label="Share Screen"
        isDisabled={isScreenShareActive && !isLocalScreenShare}
        icon={<ScreenShareIcon />}
        {...(isLocalScreenShare && {
          background: "#3E4247",
          border: "1px solid #FFFFFF",
        })}
        onClick={isLocalScreenShare ? stopScreenShare : startScreenShare}
      />
    </Tooltip>
  );
}


================================================
FILE: components/controls/buttons/SendButton.tsx
================================================
import React, { useContext } from "react";
import { IconButton, Tooltip } from "@chakra-ui/react";

import styled from "@emotion/styled";
import SendIcon from "components/icons/SendIcon";
import { transientOptions } from "lib/utils";

interface Props {
  isButtonEnabled: boolean;
  handleOnClick: () => void;
}

const StatefulSendButton = styled(IconButton, transientOptions)<{
  $isButtonEnabled: boolean;
}>`
  opacity: ${(props) => (props.$isButtonEnabled ? "1" : "0.25")};
  user-select: none;
`;

export default function SendButton({
  isButtonEnabled,
  handleOnClick,
}: Props): JSX.Element {
  return (
    <StatefulSendButton
      tabIndex={-1}
      $isButtonEnabled={isButtonEnabled}
      width="18px"
      height="15px"
      variant="link"
      aria-label="Send message"
      icon={<SendIcon />}
      onClick={handleOnClick}
      disabled={!isButtonEnabled}
    />
  );
}


================================================
FILE: components/controls/buttons/SettingsButton.tsx
================================================
import React from "react";
import {
  Box,
  IconButton,
  Menu,
  MenuButton,
  MenuItem,
  MenuList,
  useDisclosure,
  useToast,
} from "@chakra-ui/react";
import { useRouter } from "next/router";

import { copyLinkToastConfig, ToastIds } from "shared/toastConfigs";
import { useSpace } from "hooks/useSpace";
import useWindowDimensions from "hooks/useWindowDimension";

import SettingsIcon from "components/icons/SettingsIcon";
import RenameParticipantModal from "components/modals/RenameParticipantModal";

interface Props {
  onLeave: () => void;
}

export default function SettingsButton({ onLeave }: Props): JSX.Element {
  const toast = useToast();
  const router = useRouter();
  const { width } = useWindowDimensions();
  const { isLocalScreenShare } = useSpace();

  const {
    isOpen: isRenameModalOpen,
    onOpen: onRenameModalOpen,
    onClose: onRenameModalClose,
  } = useDisclosure();

  const smallWindowWidth = (width && width < 480) || false;

  const shareLink = () => {
    navigator.clipboard.writeText(
      `${window.location.protocol}//${window.location.host}/space/${router.query["id"]}`
    );
    if (!toast.isActive(ToastIds.COPY_LINK_TOAST_ID)) {
      toast(copyLinkToastConfig);
    }
  };

  return (
    <>
      <RenameParticipantModal
        isOpen={isRenameModalOpen}
        onClose={onRenameModalClose}
      />
      <Box>
        <Menu placement="top">
          <MenuButton
            as={IconButton}
            variant="control"
            aria-label="Options"
            icon={<SettingsIcon />}
          />
          <MenuList
            background="#383838"
            border="1px solid #323232"
            color="#CCCCCC"
            padding="5px 10px"
            width="200px"
          >
            <MenuItem disabled={isLocalScreenShare} onClick={onRenameModalOpen}>
              Change Name
            </MenuItem>
            <MenuItem onClick={shareLink}>Copy Invite Link</MenuItem>
            {smallWindowWidth && <MenuItem onClick={onLeave}>Leave</MenuItem>}
          </MenuList>
        </Menu>
      </Box>
    </>
  );
}


================================================
FILE: components/icons/ChatIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import chat from "../../public/chat.svg";

export default function ChatIcon(): JSX.Element {
  return (
    <Image
      priority
      alt="toggle chat"
      width={25}
      height={25}
      src={chat}
      style={{ width: "25px", height: "25px" }}
    />
  );
}


================================================
FILE: components/icons/ChevronIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import chevronUp from "../../public/chevronUp.svg";

export default function ChevronIcon(): JSX.Element {
  return (
    <Image priority alt="open menu" width={12} height={8} src={chevronUp} />
  );
}


================================================
FILE: components/icons/ChevronLeftIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import chevronLeft from "../../public/chevronLeft.svg";

export default function ChevronLeftIcon(): JSX.Element {
  return <Image alt="paginate left" width={7} height={12} src={chevronLeft} />;
}


================================================
FILE: components/icons/ChevronRightIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import chevronRight from "../../public/chevronRight.svg";

export default function ChevronRightIcon(): JSX.Element {
  return (
    <Image alt="paginate right" width={7} height={12} src={chevronRight} />
  );
}


================================================
FILE: components/icons/LeaveIcon.tsx
================================================
import React from "react";

export default function LeaveIcon(): JSX.Element {
  return (
    <svg
      width="17"
      height="16"
      viewBox="0 0 17 16"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        fillRule="evenodd"
        clipRule="evenodd"
        d="M0.5 0C0.223858 0 0 0.223858 0 0.5V15.5C0 15.7761 0.223858 16 0.5 16H5.5C5.77614 16 6 15.7761 6 15.5C6 15.2239 5.77614 15 5.5 15H1V1H5.5C5.77614 1 6 0.776142 6 0.5C6 0.223858 5.77614 0 5.5 0H0.5ZM3.5 8C3.22386 8 3 8.22386 3 8.5C3 8.77614 3.22386 9 3.5 9H14.29L10.6336 12.6564C10.4383 12.8517 10.4383 13.1683 10.6336 13.3636C10.8288 13.5588 11.1454 13.5588 11.3407 13.3636L15.8201 8.88415C15.8805 8.83374 15.9289 8.76934 15.9602 8.69592C15.9931 8.62072 16.0067 8.53907 16.0009 8.45855C16.011 8.31876 15.9626 8.17552 15.8557 8.06864L11.3407 3.55355C11.1454 3.35829 10.8288 3.35829 10.6336 3.55355C10.4383 3.74882 10.4383 4.0654 10.6336 4.26066L14.3729 8H3.5Z"
      />
    </svg>
  );
}


================================================
FILE: components/icons/MuteCameraIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import cameraOnIcon from "../../public/cameraOn.svg";

export default function MuteCameraIcon(): JSX.Element {
  return (
    <Image
      alt="mute camera"
      width={25}
      height={25}
      src={cameraOnIcon}
      style={{ width: "25px", height: "25px" }}
    />
  );
}


================================================
FILE: components/icons/MuteMicrophoneIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import microphoneOffIcon from "../../public/microphoneOff.svg";

export default function MuteMicrophoneIcon(): JSX.Element {
  return (
    <Image
      alt="mute mic"
      width={25}
      height={25}
      src={microphoneOffIcon}
      style={{ width: "25px", height: "25px" }}
    />
  );
}


================================================
FILE: components/icons/ScreenShareIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import screenShare from "../../public/screen-share.svg";

export default function ScreenShareIcon(): JSX.Element {
  return (
    <Image
      priority
      alt="Screen share"
      width={25}
      height={25}
      src={screenShare}
      style={{ width: "25px", height: "25px" }}
    />
  );
}


================================================
FILE: components/icons/SendIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import send from "../../public/send.svg";

export default function SendIcon(): JSX.Element {
  return (
    <Image
      priority
      alt="Send message"
      draggable={false}
      width={18}
      height={15}
      src={send}
      style={{ width: "18px", height: "15px" }}
    />
  );
}


================================================
FILE: components/icons/SettingsIcon.tsx
================================================
import Image from "next/image";

import elipsisIcon from "../../public/elipsis.svg";

export default function SettingsIcon() {
  return (
    <Image
      priority
      alt="settings"
      width={25}
      height={25}
      src={elipsisIcon}
      style={{ width: "25px", height: "25px" }}
    />
  );
}


================================================
FILE: components/icons/UnmuteCameraIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import cameraOffIcon from "../../public/cameraOff.svg";

export default function UnmuteCameraIcon(): JSX.Element {
  return (
    <Image
      alt="unmute camera"
      width={25}
      height={25}
      src={cameraOffIcon}
      style={{ width: "25px", height: "25px" }}
    />
  );
}


================================================
FILE: components/icons/UnmuteMicrophoneIcon.tsx
================================================
import React from "react";
import Image from "next/image";

import microphoneOnIcon from "../../public/microphoneOn.svg";

export default function UnmuteMicrophoneIcon(): JSX.Element {
  return (
    <Image
      priority
      alt="unmute mic"
      width={25}
      height={25}
      src={microphoneOnIcon}
      style={{ width: "25px", height: "25px" }}
    />
  );
}


================================================
FILE: components/modals/ACRScoreDialog.tsx
================================================
import React, { useCallback, useRef, useState } from "react";
import {
  useDisclosure,
  Button,
  AlertDialog,
  AlertDialogOverlay,
  AlertDialogContent,
  AlertDialogHeader,
  AlertDialogBody,
  AlertDialogFooter,
  AlertDialogCloseButton,
  Box,
  useRadio,
  useRadioGroup,
  VStack,
  RadioProps,
  Divider,
} from "@chakra-ui/react";
import { AcrScore } from "@mux/spaces-web";

import { useSpace } from "hooks/useSpace";

function RadioCard(props: RadioProps) {
  const { getInputProps, getCheckboxProps } = useRadio(props);

  const input = getInputProps();
  const checkbox = getCheckboxProps();

  return (
    <Box as="label" width="100%">
      <input {...input} />
      <Box
        {...checkbox}
        color="#242628"
        fontSize="14px"
        cursor="pointer"
        borderWidth="1px"
        borderRadius="5px"
        borderColor="#B2BAC2"
        _checked={{
          background: "#3E4247",
          color: "white",
          borderColor: "#3E4247",
        }}
        _hover={
          !props.isChecked
            ? {
                borderColor: "#242628",
              }
            : {}
        }
        _focus={{
          boxShadow: "none",
        }}
        px="20px"
        py="10px"
      >
        {props.children}
      </Box>
    </Box>
  );
}

type Props = Pick<ReturnType<typeof useDisclosure>, "isOpen" | "onClose">;

const options = ["Excellent", "Good", "Fair", "Poor", "Bad"];

export default function ACRScoreDialog({ isOpen, onClose }: Props) {
  const cancelRef = useRef(null);
  const { submitAcrScore } = useSpace();
  const [acrScore, setAcrScore] = useState<string>();
  const [submitting, setSubmitting] = useState(false);
  const [closing, setClosing] = useState(false);

  const { getRootProps, getRadioProps } = useRadioGroup({
    name: "acr",
    onChange: (nextValue) => setAcrScore(nextValue),
  });

  const handleSubmittingAcrScore = useCallback(async () => {
    if (acrScore) {
      setSubmitting(true);
      const numericScore = AcrScore[acrScore as keyof typeof AcrScore];
      try {
        await submitAcrScore(numericScore);
      } catch (e) {
        console.error(e);
      }
      onClose();
    }
  }, [acrScore, submitAcrScore, onClose]);

  const handleClose = useCallback(() => {
    setClosing(true);
    onClose();
  }, [onClose]);

  const group = getRootProps();
  const disableSubmission = closing || !acrScore;

  return (
    <>
      <AlertDialog
        motionPreset="slideInBottom"
        leastDestructiveRef={cancelRef}
        onClose={handleClose}
        isOpen={isOpen}
        isCentered
      >
        <AlertDialogOverlay />

        <AlertDialogContent borderRadius="0px">
          <AlertDialogHeader
            color="#242628"
            fontSize="18px"
            fontWeight="normal"
          >
            How was the call quality?
          </AlertDialogHeader>
          <AlertDialogCloseButton
            color="#666666"
            marginTop="6px"
            marginRight="3px"
          />
          <Divider color="#E8E8E8" opacity={1} />
          <AlertDialogBody my="17px">
            <VStack {...group} spacing="10px">
              {options.map((value) => {
                const radio = getRadioProps({ value });
                return (
                  <RadioCard key={value} {...radio}>
                    {value}
                  </RadioCard>
                );
              })}
            </VStack>
          </AlertDialogBody>
          <AlertDialogFooter>
            <Button ref={cancelRef} variant="muxDefault" onClick={handleClose}>
              Cancel
            </Button>
            <Button
              variant="muxConfirmation"
              isLoading={submitting}
              isDisabled={disableSubmission}
              onClick={handleSubmittingAcrScore}
              marginLeft="10px"
            >
              Submit
            </Button>
          </AlertDialogFooter>
        </AlertDialogContent>
      </AlertDialog>
    </>
  );
}


================================================
FILE: components/modals/ErrorModal.tsx
================================================
import {
  Button,
  Modal,
  ModalBody,
  ModalCloseButton,
  ModalContent,
  ModalFooter,
  ModalHeader,
  ModalOverlay,
} from "@chakra-ui/react";

interface Props {
  title: string;
  message: string;
  isOpen: boolean;
  onClose: () => void;
}

export default function ErrorModal({ title, message, isOpen, onClose }: Props) {
  return (
    <Modal isOpen={isOpen} onClose={onClose}>
      <ModalOverlay />
      <ModalContent>
        <ModalHeader>{title}</ModalHeader>
        <ModalCloseButton />
        <ModalBody>{message}</ModalBody>

        <ModalFooter>
          <Button variant="ghost" colorScheme="blue" mr={3} onClick={onClose}>
            Dismiss
          </Button>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
}


================================================
FILE: components/modals/RenameParticipantModal.tsx
================================================
import React, {
  useCallback,
  useMemo,
  useRef,
  useState,
  useContext,
} from "react";
import {
  Modal,
  ModalOverlay,
  ModalContent,
  ModalHeader,
  ModalFooter,
  ModalBody,
  ModalCloseButton,
  Button,
  useDisclosure,
  FormControl,
  FormHelperText,
  Input,
  Divider,
} from "@chakra-ui/react";

import UserContext from "context/User";
import SpaceContext from "context/Space";

type Props = Pick<ReturnType<typeof useDisclosure>, "isOpen" | "onClose"> & {};

export default function RenameParticipantModal({
  isOpen,
  onClose,
}: Props): JSX.Element {
  const user = React.useContext(UserContext);
  const nameInputRef = useRef<HTMLInputElement>(null);
  const [participantName, setParticipantName] = useState(user.participantName);
  const { setDisplayName } = useContext(SpaceContext);

  const invalidParticipantName = useMemo(
    () => !participantName,
    [participantName]
  );

  const handleDisplayNameChanged = (event: { target: { value: string } }) => {
    setParticipantName(event.target.value);
  };

  const handleClose = useCallback(() => {
    setParticipantName(user.participantName);
    onClose();
  }, [onClose, setParticipantName, user]);

  const submit = useCallback(async () => {
    if (invalidParticipantName) return;
    if (user.participantName !== participantName) {
      user.setParticipantName(participantName);
      await setDisplayName(participantName);
    }
    onClose();
  }, [invalidParticipantName, onClose, participantName, setDisplayName, user]);

  return (
    <Modal onClose={handleClose} isOpen={isOpen} isCentered>
      <ModalOverlay />

      <ModalContent borderRadius="0px">
        <ModalHeader color="#242628" fontSize="18px" fontWeight="normal">
          {"What's your name?"}
        </ModalHeader>
        <ModalCloseButton color="#666666" marginTop="6px" marginRight="3px" />
        <Divider color="#E8E8E8" opacity={1} />
        <ModalBody my="17px">
          <FormControl isInvalid={invalidParticipantName}>
            <Input
              maxLength={64}
              ref={nameInputRef}
              id="participant_id"
              value={participantName}
              onChange={handleDisplayNameChanged}
              onKeyPress={(e) => {
                if (e.key === "Enter") {
                  submit();
                }
              }}
            />
            <FormHelperText hidden={!invalidParticipantName}>
              This cannot be empty.
            </FormHelperText>
          </FormControl>
        </ModalBody>
        <ModalFooter>
          <Button variant="muxDefault" onClick={handleClose}>
            Cancel
          </Button>
          <Button
            variant="muxConfirmation"
            type="submit"
            marginLeft="10px"
            onClick={submit}
            isDisabled={invalidParticipantName}
          >
            Enter
          </Button>
        </ModalFooter>
      </ModalContent>
    </Modal>
  );
}


================================================
FILE: components/renderers/AudioRenderer.tsx
================================================
import { useEffect, useRef } from "react";

interface AudioTrackProps {
  attachFunc: (element: HTMLAudioElement) => void;
}

const AudioRenderer = ({ attachFunc }: AudioTrackProps) => {
  const audioEl = useRef<HTMLAudioElement | null>(null);

  useEffect(() => {
    const el = audioEl.current;
    if (!el) return;

    attachFunc(el);
  }, [attachFunc]);

  return <audio ref={audioEl} autoPlay />;
};

export default AudioRenderer;


================================================
FILE: components/renderers/ChatRenderer.tsx
================================================
import React, {
  FormEvent,
  KeyboardEvent,
  UIEvent,
  useCallback,
  useContext,
  useEffect,
  useRef,
  useState,
} from "react";
import { Box, Textarea } from "@chakra-ui/react";
import styled from "@emotion/styled";
import ChatContext, { ChatMessage } from "context/Chat";
import SendButton from "components/controls/buttons/SendButton";
import { transientOptions } from "lib/utils";

const ChatContainer = styled(Box, transientOptions)<{ $show: boolean }>`
  position: absolute;
  top: 0px;
  right: 0px;
  width: 300px;
  height: 100%;
  background-color: #292929;
  border-left: 1px solid #666666;
  display: flex;
  flex-direction: column;
  transform: translateX(100%);

  ${(props) =>
    props.$show && "transition: transform 100ms; transform: translateX(0%);"}
`;

const ChatMessageListContainer = styled(Box)`
  flex-grow: 1;
  overflow-y: scroll;
  padding-top: 30px;
  padding-bottom: 30px;
`;

const ChatMessageContainer = styled(Box, transientOptions)<{
  $isLastMessage: boolean;
}>`
  position: relative;
  padding-left: 30px;
  padding-right: 30px;
  margin-bottom: ${(props) => (props.$isLastMessage ? "0px" : "10px")};
  color: #f9f9f9;
  font-size: 14px;
  white-space: pre-line;
`;

const ChatInputContainer = styled(Box)`
  position: relative;
  width: 100%;
  height: 70x;
  padding: 5px;
  border-top: 1px solid #666666;
`;

const ChatTextarea = styled(Textarea)`
  height: 60px;
  border: 1px solid #e8e8e8;
  border-radius: 2px;
  background-color: white;
  padding-right: 40px;
  resize: none;
`;

const SendButtonContainer = styled(Box)`
  position: absolute;
  bottom: 10px;
  right: 10px;
  z-index: 100;
`;

const Name = styled.span`
  font-weight: bold;
`;

const Time = styled.span`
  font-size: 12px;
  color: #cccccc;
  margin-left: 6px;
  vertical-align: top;
`;

const Url = styled.a`
  text-decoration: underline;
`;

interface ChatMessageProps {
  isLast: boolean;
  isConsecutive: boolean;
  message: ChatMessage;
}

const ChatMessage = ({ message, isConsecutive, isLast }: ChatMessageProps) => {
  const urlPositions = [];
  const regexp = /\b(https?:\/\/\S*\b)/g;

  let match;
  while ((match = regexp.exec(message.content)) !== null) {
    urlPositions.push({
      start: match.index,
      end: regexp.lastIndex,
    });
  }

  let content;
  if (urlPositions.length > 0) {
    let oldContent = message.content;
    content = [];

    let currentIndex = 0;
    for (const position of urlPositions) {
      const { start, end } = position;
      content.push(
        <span key={`${currentIndex},${position.start}`}>
          {oldContent.substring(currentIndex, position.start)}
        </span>
      );
      content.push(
        <Url
          target="_blank"
          href={oldContent.substring(start, end)}
          key={`${start},${end}`}
        >
          {oldContent.substring(start, end)}
        </Url>
      );
      currentIndex = end;
    }

    content.push(
      <span key={currentIndex}>{oldContent.substring(currentIndex)}</span>
    );
  } else {
    content = message.content;
  }

  return (
    <ChatMessageContainer $isLastMessage={isLast}>
      {!isConsecutive && (
        <>
          <Name>{message.name}</Name>
          <Time>{message.time}</Time>
          <br />
        </>
      )}
      {content}
    </ChatMessageContainer>
  );
};

export default function ChatRenderer({ show }: { show: boolean }): JSX.Element {
  const { canSendMessage, sendChatMessage, chatMessages } =
    useContext(ChatContext);
  const [input, setInput] = useState("");
  const messageListRef = useRef<HTMLDivElement>(null);
  const isScrollNearBottomRef = useRef<boolean>(true);

  const handleOnChange = useCallback((e: FormEvent<HTMLTextAreaElement>) => {
    const { value } = e.currentTarget;
    if (value === "\n" || value.endsWith("\n\n\n") || value.endsWith("\n ")) {
      return;
    }

    setInput(value);
  }, []);

  const handleSubmit = useCallback(async () => {
    const message = input.trim();
    if (canSendMessage && input !== "") {
      setInput("");
      try {
        await sendChatMessage(message);
      } catch (error) {}
    }
  }, [input, canSendMessage, sendChatMessage]);

  const handleOnKeyDown = useCallback(
    (e: KeyboardEvent<HTMLTextAreaElement>) => {
      if (e.code === "Enter" && !e.shiftKey) {
        e.preventDefault();
        handleSubmit();
      }
    },
    [handleSubmit]
  );

  const handleScroll = useCallback((e: UIEvent<HTMLDivElement>) => {
    const el = e.currentTarget;
    const isNearBottom =
      100 > el.scrollHeight - (el.offsetHeight + el.scrollTop);

    isScrollNearBottomRef.current = isNearBottom;
  }, []);

  const renderMessages = useCallback(() => {
    return chatMessages.map((message, index) => (
      <ChatMessage
        key={message.id}
        message={message}
        isConsecutive={
          index !== 0 &&
          message.connectionId === chatMessages[index - 1].connectionId
        }
        isLast={chatMessages.length - 1 === index}
      />
    ));
  }, [chatMessages]);

  useEffect(() => {
    const el = messageListRef.current;
    if (!el || chatMessages.length === 0) return;

    const isLocalMessage = "state" in chatMessages[chatMessages.length - 1];
    if (isScrollNearBottomRef.current || isLocalMessage) {
      el.scrollTop = el.scrollHeight;
    }
  }, [chatMessages]);

  return (
    <ChatContainer $show={show}>
      <ChatMessageListContainer ref={messageListRef} onScroll={handleScroll}>
        {renderMessages()}
      </ChatMessageListContainer>
      <ChatInputContainer>
        <ChatTextarea
          placeholder="Send a message"
          draggable={false}
          onChange={handleOnChange}
          value={input}
          onKeyDown={handleOnKeyDown}
        />
        {show && (
          <SendButtonContainer>
            <SendButton
              handleOnClick={handleSubmit}
              isButtonEnabled={canSendMessage && input !== ""}
            />
          </SendButtonContainer>
        )}
      </ChatInputContainer>
    </ChatContainer>
  );
}


================================================
FILE: components/renderers/ScreenShareRenderer.tsx
================================================
import React, { useEffect, useRef } from "react";
import { Flex } from "@chakra-ui/react";

interface Props {
  attach: (element: HTMLVideoElement) => void;
}

export default function ScreenShareRenderer({ attach }: Props): JSX.Element {
  const videoEl = useRef<HTMLVideoElement | null>(null);

  useEffect(() => {
    const el = videoEl.current;
    if (!el) return;

    attach(el);
  }, [attach]);

  return (
    <Flex
      height="100%"
      justifyContent="center"
      maxHeight="100%"
      maxWidth="100%"
      position="relative"
    >
      <video
        style={{ height: "100%", margin: "0px auto" }}
        ref={videoEl}
        autoPlay
        playsInline
      />
    </Flex>
  );
}


================================================
FILE: components/renderers/VideoRenderer.tsx
================================================
import React, { useEffect, useRef, useState } from "react";

import poster from "../../public/poster.jpg";
import posterFlipped from "../../public/poster-flipped.jpg";

interface Props {
  width: number;
  height: number;
  local: boolean;
  connectionId: string;
  attachFunc: (element: HTMLVideoElement) => void;
}

export default function VideoRenderer({
  width,
  height,
  local,
  connectionId,
  attachFunc,
}: Props): JSX.Element {
  const videoEl = useRef<HTMLVideoElement | null>(null);
  const [disableFlip, setDisableFlip] = useState(false);

  const handleEnterPiP = () => {
    setDisableFlip(true);
  };

  const handleLeavePiP = () => {
    setDisableFlip(false);
  };

  useEffect(() => {
    const el = videoEl.current;
    if (!el) return;

    attachFunc(el);

    el.addEventListener("enterpictureinpicture", handleEnterPiP);
    el.addEventListener("leavepictureinpicture", handleLeavePiP);
    return () => {
      el.removeEventListener("enterpictureinpicture", handleEnterPiP);
      el.removeEventListener("leavepictureinpicture", handleLeavePiP);
    };
  }, [attachFunc]);

  return (
    <video
      id={connectionId}
      poster={!disableFlip && local ? posterFlipped.src : poster.src}
      ref={videoEl}
      autoPlay
      playsInline
      style={{
        width: "100%",
        height: "100%",
        objectFit: height > width ? "contain" : "cover",
        transform: !disableFlip && local ? "scaleX(-1)" : "",
      }}
    />
  );
}


================================================
FILE: context/Chat.tsx
================================================
import { CustomEvent, SpaceEvent } from "@mux/spaces-web";
import { useSpaceEvent } from "hooks/useSpaceEvent";
import React, {
  createContext,
  ReactNode,
  useCallback,
  useContext,
  useEffect,
  useState,
} from "react";
import SpaceContext from "./Space";
import { v4 } from "uuid";
import moment from "moment";

interface ChatMessagePayloadValue {
  connectionId: string;
  name: string;
  id: string;
  content: string;
}

interface RemoteChatMessage extends ChatMessagePayloadValue {
  time: string;
}

interface LocalChatMessage extends RemoteChatMessage {
  state: SendState;
}

enum SendState {
  Pending,
  Succeeded,
  Failed,
}

export type ChatMessage = RemoteChatMessage | LocalChatMessage;

interface IChatContext {
  isChatOpen: boolean;
  openChat: () => void;
  closeChat: () => void;
  chatMessages: ChatMessage[];
  canSendMessage: boolean;
  numUnreadMessages: number;
  sendChatMessage: (content: string) => Promise<void>;
}

export const ChatContext = createContext({} as IChatContext);

export default ChatContext;

type Props = {
  children: ReactNode;
};

export const ChatProvider: React.FC<Props> = ({ children }) => {
  const { publishCustomEvent, localParticipant } = useContext(SpaceContext);
  const [chatMessages, setChatMessages] = useState<ChatMessage[]>([]);
  const [isChatOpen, setIsChatOpen] = useState(false);
  const [numUnreadMessages, setNumUnreadMessages] = useState(0);
  const [canSendMessage, setCanSendMessage] = useState(!!localParticipant);

  const sendChatMessage = useCallback(
    async (content: string) => {
      if (!localParticipant) {
        throw new Error(
          "Cannot send chat message without having joined the space"
        );
      } else if (!canSendMessage) {
        return;
      }

      const id = v4();
      const payload = {
        type: "chat",
        value: {
          connectionId: localParticipant.connectionId,
          name: localParticipant.displayName || localParticipant.id,
          id,
          content,
        },
      };

      const time = moment().format("hh:mm A");
      let state = SendState.Pending;

      const localChatMessage = { ...payload.value, id, state, time };

      setChatMessages([...chatMessages, localChatMessage]);

      try {
        setCanSendMessage(false);
        await publishCustomEvent(JSON.stringify(payload));
        state = SendState.Succeeded;
      } catch (error) {
        state = SendState.Failed;
        throw error;
      } finally {
        const messageIndex = chatMessages.findIndex(
          (message) => message.id === id
        );

        if (messageIndex !== -1) {
          const message = chatMessages[messageIndex];
          if ("state" in message) {
            message.state = state;
          }

          setChatMessages([
            ...chatMessages.slice(0, messageIndex),
            { ...message },
            ...chatMessages.slice(messageIndex + 1),
          ]);
        }
      }
    },
    [localParticipant, chatMessages, publishCustomEvent, canSendMessage]
  );

  useEffect(() => {
    if (localParticipant && !canSendMessage) {
      const timeout = setTimeout(() => {
        setCanSendMessage(true);
      }, 1000);

      return () => {
        clearTimeout(timeout);
      };
    }
  }, [localParticipant, canSendMessage]);

  useSpaceEvent(
    SpaceEvent.ParticipantCustomEventPublished,
    useCallback(
      (participant, customEvent: CustomEvent) => {
        const payload: { type: string; value: ChatMessagePayloadValue } =
          JSON.parse(customEvent.payload);
        if (participant !== localParticipant && payload.type === "chat") {
          setChatMessages([
            ...chatMessages,
            { ...payload.value, time: moment().format("hh:mm A") },
          ]);

          if (!isChatOpen) {
            setNumUnreadMessages(numUnreadMessages + 1);
          }
        }
      },
      [chatMessages, localParticipant, isChatOpen, numUnreadMessages]
    )
  );

  const openChat = useCallback(() => {
    setIsChatOpen(true);
    setNumUnreadMessages(0);
  }, []);

  const closeChat = useCallback(() => {
    setIsChatOpen(false);
  }, []);

  return (
    <ChatContext.Provider
      value={{
        isChatOpen,
        openChat,
        closeChat,
        chatMessages,
        canSendMessage,
        numUnreadMessages,
        sendChatMessage,
      }}
    >
      {children}
    </ChatContext.Provider>
  );
};


================================================
FILE: context/Space.tsx
================================================
import React, {
  createContext,
  ReactNode,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
} from "react";
import { useRouter } from "next/router";
import {
  AcrScore,
  ActiveSpeaker,
  CustomEvent,
  getDisplayMedia,
  LocalParticipant,
  LocalTrack,
  RemoteParticipant,
  Space,
  SpaceEvent,
  SpaceOptionsParams,
  Track,
  TrackSource,
} from "@mux/spaces-web";

import { MAX_PARTICIPANTS_PER_PAGE } from "lib/constants";

import UserContext from "./User";
import UserMediaContext from "./UserMedia";

interface SpaceState {
  space: Space | null;
  localParticipant: LocalParticipant | null;
  remoteParticipants: RemoteParticipant[];

  joinSpace: (
    jwt: string,
    endsAt?: number,
    displayName?: string
  ) => Promise<void>;
  joinError: string | null;
  isJoined: boolean;

  connectionIds: string[];
  isBroadcasting: boolean;
  participantCount: number;
  publishCamera: (deviceId: string) => void;
  publishMicrophone: (deviceId: string) => void;
  unPublishDevice: (deviceId: string) => void;

  isLocalScreenShareSupported: boolean;
  isScreenShareActive: boolean;
  isLocalScreenShare: boolean;
  screenShareError: string | null;
  attachScreenShare: (element: HTMLVideoElement) => void;
  startScreenShare: () => void;
  stopScreenShare: () => void;
  screenShareParticipantConnectionId?: string;
  screenShareParticipantName?: string;

  spaceEndsAt: number | null;
  leaveSpace: () => void;
  submitAcrScore: (score: AcrScore) => Promise<void> | undefined;

  setDisplayName: (name: string) => Promise<LocalParticipant | undefined>;
  publishCustomEvent: (payload: string) => Promise<CustomEvent | undefined>;
}

export const SpaceContext = createContext({} as SpaceState);

export default SpaceContext;

type Props = {
  children: ReactNode;
};

export const SpaceProvider: React.FC<Props> = ({ children }) => {
  const { userWantsMicMuted, microphoneDeviceId, cameraOff, cameraDeviceId } =
    useContext(UserContext);
  const { getMicrophone, getCamera } = useContext(UserMediaContext);

  const [space, setSpace] = useState<Space | null>(null);
  const [spaceEndsAt, setSpaceEndsAt] = useState<number | null>(null);
  const [localParticipant, setLocalParticipant] =
    useState<LocalParticipant | null>(null);
  const [remoteParticipants, setRemoteParticipants] = useState<
    RemoteParticipant[]
  >([]);
  const [isJoined, setIsJoined] = useState(false);
  const [joinError, setJoinError] = useState<string | null>(null);
  const [isBroadcasting, setIsBroadcasting] = useState(false);
  const [isLocalScreenShareSupported, setIsLocalScreenShareSupported] =
    useState(
      typeof window !== "undefined"
        ? !!navigator?.mediaDevices?.getDisplayMedia
        : false
    );
  const [screenShareTrack, setScreenShareTrack] = useState<Track>();
  const [screenShareError, setScreenShareError] = useState<string | null>(null);
  const [participantScreenSharing, setParticipantScreenSharing] = useState<
    LocalParticipant | RemoteParticipant | null
  >(null);

  const screenShareParticipantName = useMemo(() => {
    return participantScreenSharing?.displayName;
  }, [participantScreenSharing]);

  const screenShareParticipantConnectionId = useMemo(() => {
    return participantScreenSharing?.connectionId;
  }, [participantScreenSharing]);

  useEffect(() => {
    setIsLocalScreenShareSupported(!!navigator.mediaDevices.getDisplayMedia);
  }, []);

  const isScreenShareActive = useMemo(() => {
    return !!screenShareTrack;
  }, [screenShareTrack]);

  const isLocalScreenShare = useMemo(() => {
    return participantScreenSharing instanceof LocalParticipant;
  }, [participantScreenSharing]);

  const participantCount = useMemo(() => {
    return (localParticipant ? 1 : 0) + remoteParticipants.length;
  }, [localParticipant, remoteParticipants]);

  const connectionIds = useMemo(() => {
    return (localParticipant ? [localParticipant.connectionId] : []).concat(
      remoteParticipants.map((p) => p.connectionId)
    );
  }, [localParticipant, remoteParticipants]);

  const publishForLocalParticipant = useCallback(
    async (localParticipant: LocalParticipant) => {
      const tracksToPublish = [];
      if (cameraDeviceId && !cameraOff) {
        const cameraTrack = await getCamera(cameraDeviceId);
        if (cameraTrack) {
          tracksToPublish.push(cameraTrack);
        }
      }
      if (microphoneDeviceId) {
        const microphoneTrack = await getMicrophone(microphoneDeviceId);
        if (microphoneTrack) {
          tracksToPublish.push(microphoneTrack);
        }
      }
      if (tracksToPublish.length > 0) {
        const publishedTracks = await localParticipant.publishTracks(
          tracksToPublish
        );
        const publishedMicrophone = publishedTracks.find(
          (track) => track.source === TrackSource.Microphone
        );
        if (publishedMicrophone && userWantsMicMuted) {
          publishedMicrophone.mute();
        }
      }
    },
    [
      cameraOff,
      getMicrophone,
      microphoneDeviceId,
      getCamera,
      cameraDeviceId,
      userWantsMicMuted,
    ]
  );

  const router = useRouter();

  const joinSpace = useCallback(
    async (jwt: string, endsAt?: number, displayName?: string) => {
      let _space: Space;
      try {
        let spaceOpts: SpaceOptionsParams = { displayName };
        if (router.isReady && typeof router.query.auto_sub_limit === "string") {
          spaceOpts.automaticParticipantLimit = parseInt(
            router.query.auto_sub_limit
          );
        }
        _space = new Space(jwt, spaceOpts);
      } catch (e: any) {
        setJoinError(e.message);
        return;
      }

      if (endsAt) {
        setSpaceEndsAt(endsAt);
      }

      const handleBroadcastStateChange = (broadcastState: boolean) => {
        setIsBroadcasting(broadcastState);
      };

      const handleParticipantJoined = (newParticipant: RemoteParticipant) => {
        setRemoteParticipants((oldParticipantArray) => {
          const found = oldParticipantArray.find(
            (p) => p.connectionId === newParticipant.connectionId
          );
          if (!found) {
            return [...oldParticipantArray, newParticipant];
          }
          return oldParticipantArray;
        });
      };

      const handleParticipantLeft = (participantLeaving: RemoteParticipant) => {
        setRemoteParticipants((oldParticipantArray) =>
          oldParticipantArray.filter(
            (p) => p.connectionId !== participantLeaving.connectionId
          )
        );
      };

      const handleActiveSpeakerChanged = (
        activeSpeakerChanges: ActiveSpeaker[]
      ) => {
        setRemoteParticipants((oldParticipantArray) => {
          const updatedParticipants = [...oldParticipantArray];

          activeSpeakerChanges.forEach((activeSpeaker: ActiveSpeaker) => {
            if (activeSpeaker.participant instanceof RemoteParticipant) {
              const participantIndex = updatedParticipants.findIndex(
                (p) => p.connectionId === activeSpeaker.participant.connectionId
              );

              if (participantIndex >= MAX_PARTICIPANTS_PER_PAGE - 1) {
                updatedParticipants.splice(participantIndex, 1);
                updatedParticipants.unshift(activeSpeaker.participant);
              }
            }
          });
          return updatedParticipants;
        });
      };

      const setupScreenShare = (
        participant: LocalParticipant | RemoteParticipant,
        track: Track
      ) => {
        setScreenShareTrack(track);
        setParticipantScreenSharing(participant);
      };

      const tearDownScreenShare = () => {
        setScreenShareTrack(undefined);
        setParticipantScreenSharing(null);
      };

      const handleParticipantTrackPublished = (
        participant: LocalParticipant | RemoteParticipant,
        track: Track
      ) => {
        if (track.source === TrackSource.Screenshare && track.hasMedia()) {
          setupScreenShare(participant, track);
        }
      };

      const handleParticipantTrackSubscribed = (
        participant: LocalParticipant | RemoteParticipant,
        track: Track
      ) => {
        if (participant instanceof RemoteParticipant) {
          reorderRemoteParticipantsBySubscription(participant);
        }
        if (track.source === TrackSource.Screenshare && track.hasMedia()) {
          setupScreenShare(participant, track);
        }
      };

      const handleParticipantTrackUnpublished = (
        _participant: LocalParticipant | RemoteParticipant,
        track: Track
      ) => {
        if (track.source === TrackSource.Screenshare) {
          tearDownScreenShare();
        }
      };

      const handleParticipantTrackUnsubscribed = (
        participant: LocalParticipant | RemoteParticipant,
        track: Track
      ) => {
        if (participant instanceof RemoteParticipant) {
          reorderRemoteParticipantsBySubscription(participant);
        }
        if (track.source === TrackSource.Screenshare) {
          tearDownScreenShare();
        }
      };

      const handleParticipantDisplayNameChanged = (
        updated: LocalParticipant | RemoteParticipant
      ) => {
        if (updated instanceof RemoteParticipant) {
          setRemoteParticipants((oldParticipantArray) => {
            const found = oldParticipantArray.find(
              (p) => p.connectionId === updated.connectionId
            );
            if (found) {
              found.displayName = updated.displayName;
            }
            return oldParticipantArray;
          });
          // no participant found for this update
        } else {
          setLocalParticipant((local) => {
            if (!local) {
              return null;
            }
            local.displayName = updated.displayName;
            return local;
          });
        }
      };

      const reorderRemoteParticipantsBySubscription = (
        participantWhoChanged: RemoteParticipant
      ) => {
        setRemoteParticipants((oldParticipantArray) => {
          const updatedSubscriptionParticipants = oldParticipantArray.map(
            (oldParticipant) =>
              oldParticipant.connectionId === participantWhoChanged.connectionId
                ? participantWhoChanged
                : oldParticipant
          );

          return [
            ...updatedSubscriptionParticipants.filter((p) => p.isSubscribed()),
            ...updatedSubscriptionParticipants.filter((p) => !p.isSubscribed()),
          ];
        });
      };

      _space.on(SpaceEvent.ParticipantJoined, handleParticipantJoined);
      _space.on(SpaceEvent.ParticipantLeft, handleParticipantLeft);

      _space.on(SpaceEvent.ActiveSpeakersChanged, handleActiveSpeakerChanged);
      _space.on(SpaceEvent.BroadcastStateChanged, handleBroadcastStateChange);

      _space.on(
        SpaceEvent.ParticipantTrackPublished,
        handleParticipantTrackPublished
      );
      _space.on(
        SpaceEvent.ParticipantTrackSubscribed,
        handleParticipantTrackSubscribed
      );
      _space.on(
        SpaceEvent.ParticipantTrackUnpublished,
        handleParticipantTrackUnpublished
      );
      _space.on(
        SpaceEvent.ParticipantTrackUnsubscribed,
        handleParticipantTrackUnsubscribed
      );
      _space.on(
        SpaceEvent.ParticipantDisplayNameChanged,
        handleParticipantDisplayNameChanged
      );

      setSpace(_space);

      try {
        const _localParticipant = await _space.join();
        publishForLocalParticipant(_localParticipant);
        setLocalParticipant(_localParticipant);
        setIsBroadcasting(_space.broadcasting);
        setIsJoined(true);
      } catch (error: any) {
        setJoinError(error.message);
        setIsBroadcasting(false);
        setIsJoined(false);
      }
    },
    [publishForLocalParticipant, router.isReady, router.query.auto_sub_limit]
  );

  const publishMicrophone = useCallback(
    async (deviceId: string) => {
      if (!localParticipant) {
        throw new Error("Join a space before publishing a device.");
      }
      if (microphoneDeviceId !== deviceId) {
        const microphoneTrack = await getMicrophone(deviceId);
        localParticipant.updateTracks([microphoneTrack]);
      } else {
        const publishedMicrophone = localParticipant
          .getAudioTracks()
          .filter((track) => track.source === TrackSource.Microphone)
          .find((track) => track.deviceId === deviceId);
        if (publishedMicrophone) {
          throw new Error("That microphone is already published.");
        }
        const microphoneTrack = await getMicrophone(deviceId);
        await localParticipant.publishTracks([microphoneTrack]);
        if (userWantsMicMuted) {
          microphoneTrack.mute();
        }
      }
    },
    [localParticipant, microphoneDeviceId, getMicrophone, userWantsMicMuted]
  );

  const publishCamera = useCallback(
    async (deviceId: string) => {
      if (!localParticipant) {
        throw new Error("Join a space before publishing a device.");
      }
      if (cameraDeviceId !== deviceId) {
        const cameraTrack = await getCamera(deviceId);
        localParticipant.updateTracks([cameraTrack]);
      } else {
        const publishedCamera = localParticipant
          .getVideoTracks()
          .filter((track) => track.source === TrackSource.Camera)
          .find((track) => track.deviceId === deviceId);
        if (publishedCamera) {
          throw new Error("That camera is already published.");
        }
        const cameraTrack = await getCamera(deviceId);
        localParticipant.publishTracks([cameraTrack]);
      }
    },
    [localParticipant, cameraDeviceId, getCamera]
  );

  const unPublishDevice = useCallback(
    (deviceId: string): void => {
      if (!localParticipant) {
        throw new Error(
          "Join a space and publish a device before un-publishing the device."
        );
      }
      const publishedDevice = localParticipant
        .getTracks()
        .find((track) => track.deviceId === deviceId);
      if (publishedDevice) {
        localParticipant.unpublishTracks([publishedDevice]);
      } else {
        throw new Error("Device to un-published was not found.");
      }
    },
    [localParticipant]
  );

  const startScreenShare = useCallback(async () => {
    try {
      const screenStreams = await getDisplayMedia({
        video: true,
        audio: false,
      });
      const screenStream = screenStreams?.find(
        (track) => track.source === "screenshare"
      );
      if (screenStream) {
        if (localParticipant) {
          return localParticipant
            .publishTracks([screenStream])
            .then((publishedTracks: LocalTrack[]) => {
              if (publishedTracks.length < 1) {
                throw new Error("Failed to publish track.");
              }
              return publishedTracks[0];
            });
        } else {
          throw new Error("Join a space before starting a screen share.");
        }
      }
    } catch (error) {
      if (error instanceof Error) {
        if (error.message === "Permission denied") {
          // do nothing, they pressed cancel
        } else if (error.message === "Permission denied by system") {
          // chrome does not have permission to screen share
          setScreenShareError(error.message);
        } else {
          // unhandled exception
          console.error(error);
        }
      }
    }
  }, [localParticipant, setScreenShareError]);

  const stopScreenShare = useCallback(async () => {
    if (screenShareTrack && screenShareTrack instanceof LocalTrack) {
      if (localParticipant) {
        localParticipant.unpublishTracks([screenShareTrack]);
      } else {
        throw new Error("Join a space before stopping the screen share.");
      }
    } else {
      throw new Error("No screen share to stop.");
    }
  }, [localParticipant, screenShareTrack]);

  const attachScreenShare = useCallback(
    (element: HTMLVideoElement) => {
      screenShareTrack?.attachedElements.forEach((attachedEl) =>
        screenShareTrack.detach(attachedEl)
      );
      screenShareTrack?.attach(element);
    },
    [screenShareTrack]
  );

  const submitAcrScore = useCallback(
    (score: AcrScore) => {
      if (!space) {
        throw new Error(
          "You must join a space before submitting an ACR score."
        );
      }

      try {
        return space.submitAcrScore(score);
      } catch (error) {
        throw new Error(`Error when submitting ACR score: ${error}`);
      }
    },
    [space]
  );

  const leaveSpace = useCallback(() => {
    try {
      space?.removeAllListeners();
      space?.leave();
    } finally {
      setJoinError(null);
      setRemoteParticipants([]);
      setIsBroadcasting(false);
      setLocalParticipant(null);
      setIsJoined(false);
      setSpaceEndsAt(null);
      // Don't call setSpace(null) here, as things like ACR Score submission depend on it
    }
  }, [space]);

  const publishCustomEvent = useCallback(
    async (payload: string) => {
      try {
        return await space?.localParticipant?.publishCustomEvent(payload);
      } catch (error) {
        throw error;
      }
    },
    [space]
  );

  const setDisplayName = useCallback(
    async (name: string) => {
      return await space?.localParticipant?.setDisplayName(name);
    },
    [space]
  );

  return (
    <SpaceContext.Provider
      value={{
        space,
        localParticipant,
        remoteParticipants,

        joinSpace,
        joinError,
        isJoined,

        connectionIds,
        isBroadcasting,
        participantCount,
        publishCamera,
        publishMicrophone,
        unPublishDevice,

        isLocalScreenShareSupported,
        isScreenShareActive,
        isLocalScreenShare,
        screenShareError,
        attachScreenShare,
        startScreenShare,
        stopScreenShare,
        screenShareParticipantConnectionId,
        screenShareParticipantName,

        leaveSpace,
        submitAcrScore,
        spaceEndsAt,

        setDisplayName,
        publishCustomEvent,
      }}
    >
      {children}
    </SpaceContext.Provider>
  );
};


================================================
FILE: context/User.tsx
================================================
import React, { createContext, ReactNode, useCallback, useState } from "react";
import { v4 as uuidv4 } from "uuid";

import { useLocalStorage } from "../hooks/useLocalStorage";

interface UserState {
  id: string;
  participantName: string;
  setParticipantName: (newName: string) => string;
  interactionRequired: boolean;
  setInteractionRequired: (requiresInteraction: boolean) => void;
  userWantsMicMuted: boolean;
  setUserWantsMicMuted: (mute: boolean) => void;
  cameraOff: boolean;
  setCameraOff: (mute: boolean) => void;
  microphoneDeviceId: string;
  setMicrophoneDeviceId: (deviceId: string) => void;
  cameraDeviceId: string;
  setCameraDeviceId: (deviceId: string) => void;
  pinnedConnectionId: string;
  setPinnedConnectionId: (newConnectionId: string) => void;
}

const UserContext = createContext({} as UserState);

export default UserContext;

interface Props {
  children: ReactNode;
}

export const UserProvider = ({ children }: Props) => {
  const [interactionRequired, setInteractionRequired] = useState(true);
  const [participantName, setParticipantName] = useLocalStorage(
    "participantName",
    ""
  );

  // This should never change unless we reload
  const id = uuidv4();

  const [userWantsMicMuted, setUserWantsMicMuted] = useState(false);
  const [cameraOff, setCameraOff] = useState(false);

  const [microphoneDeviceId, setMicrophoneDeviceId] = useLocalStorage(
    "audioDeviceId",
    ""
  );
  const [cameraDeviceId, setCameraDeviceId] = useLocalStorage(
    "videoDeviceId",
    ""
  );
  const [pinnedConnectionId, setPinnedConnectionId] = useState("");

  const handleSetParticipantName = useCallback(
    (name: string) => {
      setParticipantName(name);
      return name;
    },
    [setParticipantName]
  );

  return (
    <UserContext.Provider
      value={{
        id,
        participantName,
        setParticipantName: handleSetParticipantName,
        interactionRequired,
        setInteractionRequired,
        userWantsMicMuted,
        setUserWantsMicMuted,
        cameraOff,
        setCameraOff,
        microphoneDeviceId,
        setMicrophoneDeviceId,
        cameraDeviceId,
        setCameraDeviceId,
        pinnedConnectionId,
        setPinnedConnectionId,
      }}
    >
      {children}
    </UserContext.Provider>
  );
};


================================================
FILE: context/UserMedia.tsx
================================================
import React, {
  createContext,
  ReactNode,
  useCallback,
  useEffect,
  useMemo,
  useState,
} from "react";
import {
  CreateLocalMediaOptions,
  getUserMedia,
  LocalTrack,
  TrackSource,
} from "@mux/spaces-web";

import UserContext from "./User";

import { defaultAudioConstraints } from "shared/defaults";

interface UserMediaState {
  activeCamera?: LocalTrack;
  activeMicrophone?: LocalTrack;

  userMediaError?: string;
  requestPermissionAndPopulateDevices: () => void;
  requestPermissionAndStartDevices: (
    microphoneDeviceId?: string,
    cameraDeviceId?: string
  ) => Promise<void>;

  getCamera: (deviceId: string) => Promise<LocalTrack>;
  cameraDevices: MediaDeviceInfo[];
  activeCameraId?: string;
  stopActiveCamera: () => void;
  changeActiveCamera: (deviceId: string) => Promise<void>;

  getMicrophone: (deviceId: string) => Promise<LocalTrack>;
  microphoneDevices: MediaDeviceInfo[];
  activeMicrophoneId?: string;
  muteActiveMicrophone: () => void;
  unMuteActiveMicrophone: () => void;
  changeActiveMicrophone: (deviceId: string) => Promise<void>;
  getActiveMicrophoneLevel: () => {
    avgDb: number;
    peakDb: number;
  } | null;
}

export const UserMediaContext = createContext({} as UserMediaState);

export default UserMediaContext;

const defaultCameraOption: CreateLocalMediaOptions = {
  video: {},
};

const defaultMicrophoneOption: CreateLocalMediaOptions = {
  audio: { constraints: defaultAudioConstraints },
};

const noCameraOption: CreateLocalMediaOptions = {
  video: false,
};

const noMicrophoneOption: CreateLocalMediaOptions = {
  audio: false,
};

const defaultMicrophoneCameraOptions: CreateLocalMediaOptions = {
  ...defaultCameraOption,
  ...defaultMicrophoneOption,
};

type Props = {
  children: ReactNode;
};

export const UserMediaProvider: React.FC<Props> = ({ children }) => {
  const {
    cameraDeviceId,
    setCameraDeviceId,
    microphoneDeviceId,
    setMicrophoneDeviceId,
    userWantsMicMuted,
  } = React.useContext(UserContext);
  const [microphoneDevices, setMicrophoneDevices] = useState<InputDeviceInfo[]>(
    []
  );
  const [activeMicrophone, setActiveMicrophone] = useState<LocalTrack>();
  const [cameraDevices, setCameraDevices] = useState<InputDeviceInfo[]>([]);
  const [activeCamera, setActiveCamera] = useState<LocalTrack>();
  const [localAudioAnalyser, setLocalAudioAnalyser] = useState<AnalyserNode>();
  const [userMediaError, setUserMediaError] = useState<string>();

  const activeCameraId = useMemo(() => {
    return activeCamera?.deviceId;
  }, [activeCamera]);

  const activeMicrophoneId = useMemo(() => {
    return activeMicrophone?.deviceId;
  }, [activeMicrophone]);

  const setupLocalMicrophoneAnalyser = useCallback((track: LocalTrack) => {
    let stream = new MediaStream([track.track]);

    const audioCtx = new AudioContext();
    const streamSource = audioCtx.createMediaStreamSource(stream);
    const analyser = audioCtx.createAnalyser();
    streamSource.connect(analyser);
    analyser.fftSize = 2048;

    setLocalAudioAnalyser(analyser);
  }, []);

  const loadDevices = useCallback(async () => {
    const availableDevices = await navigator.mediaDevices.enumerateDevices();

    const audioInputDevices = availableDevices.filter(
      (device) => device.kind === "audioinput"
    );
    setMicrophoneDevices(audioInputDevices);

    const videoInputDevices = availableDevices.filter(
      (device) => device.kind === "videoinput"
    );
    setCameraDevices(videoInputDevices);
  }, []);

  const requestPermissionAndPopulateDevices = useCallback(async () => {
    let tracks: LocalTrack[] = [];
    try {
      tracks = await getUserMedia({
        audio: { constraints: { deviceId: microphoneDeviceId } }, // loose constraint, will fail back if missing
        video: { constraints: { deviceId: cameraDeviceId } }, // loose constraint, will fail back if missing
      });
    } catch (e) {
      console.log("Failed to request default devices from your browser.");
    }

    try {
      tracks.forEach((track) => {
        if (track.deviceId) {
          if (track.source === TrackSource.Camera) {
            setCameraDeviceId(track.deviceId);
          } else if (track.source === TrackSource.Microphone) {
            setMicrophoneDeviceId(track.deviceId);
          }
        }
      });
    } catch (e) {
      console.log("Error thrown while stopping devices.");
    }

    await loadDevices();

    // Need to wait to stop the tracks until we've gotten the device list, or they'll have no labels
    tracks.forEach((track) => track.track.stop());
  }, [
    cameraDeviceId,
    loadDevices,
    microphoneDeviceId,
    setCameraDeviceId,
    setMicrophoneDeviceId,
  ]);

  const requestPermissionAndStartDevices = useCallback(
    async (microphoneDeviceId?: string, cameraDeviceId?: string) => {
      let options = { ...defaultMicrophoneCameraOptions };
      if (typeof microphoneDeviceId === "undefined") {
        options["audio"] = false;
      } else if (microphoneDeviceId !== "") {
        options["audio"] = {
          constraints: {
            deviceId: { exact: microphoneDeviceId },
            ...defaultAudioConstraints,
          },
        };
      }
      if (typeof cameraDeviceId === "undefined") {
        options["video"] = false;
      } else if (cameraDeviceId !== "") {
        options["video"] = {
          constraints: {
            deviceId: { exact: cameraDeviceId },
          },
        };
      }

      let tracks: LocalTrack[] = [];
      try {
        tracks = await getUserMedia(options);
      } catch (e: any) {
        if (
          e.name == "NotAllowedError" ||
          e.name == "PermissionDeniedError" ||
          e instanceof DOMException
        ) {
          // permission denied to camera
          setUserMediaError("NotAllowedError");
        } else if (
          e.name == "OverconstrainedError" ||
          e.name == "ConstraintNotSatisfiedError"
        ) {
          tracks = await getUserMedia({ audio: true, video: true });
        } else {
          setUserMediaError(e.name);
        }
      }

      tracks.forEach((track) => {
        switch (track.source) {
          case TrackSource.Microphone:
            setActiveMicrophone(track);
            setupLocalMicrophoneAnalyser(track);
            if (track.deviceId) {
              setMicrophoneDeviceId(track.deviceId);
            }
            if (userWantsMicMuted) {
              track.mute();
            }
            break;
          case TrackSource.Camera:
            setActiveCamera(track);
            if (track.deviceId) {
              setCameraDeviceId(track.deviceId);
            }
            break;
        }
      });

      await loadDevices();
    },
    [
      setupLocalMicrophoneAnalyser,
      setMicrophoneDeviceId,
      setCameraDeviceId,
      userWantsMicMuted,
    ]
  );

  const muteActiveMicrophone = useCallback(() => {
    try {
      activeMicrophone?.mute();
    } catch (error) {
      throw new Error("Select an active microphone before muting.");
    }
  }, [activeMicrophone]);

  const unMuteActiveMicrophone = useCallback(() => {
    try {
      activeMicrophone?.unMute();
    } catch (error) {
      throw new Error("Select an active microphone before muting.");
    }
  }, [activeMicrophone]);

  const getMicrophone = useCallback(
    async (deviceId: string) => {
      let options = {
        ...defaultMicrophoneOption,
        ...noCameraOption,
      };
      if (deviceId !== "") {
        options["audio"] = {
          constraints: {
            deviceId: { exact: deviceId },
            ...defaultAudioConstraints,
          },
        };
      }

      let tracks: LocalTrack[] = [];
      try {
        tracks = await getUserMedia(options);
      } catch (e: any) {
        // May occur if previously set device IDs are no longer available
        if (
          e.name == "NotAllowedError" ||
          e.name == "PermissionDeniedError" ||
          e instanceof DOMException
        ) {
          // permission denied to camera
          setUserMediaError("NotAllowedError");
        } else if (
          e.name == "OverconstrainedError" ||
          e.name == "ConstraintNotSatisfiedError"
        ) {
          setUserMediaError("OverconstrainedError");
        } else {
          setUserMediaError(e.name);
        }
      }

      tracks.forEach((track) => {
        switch (track.source) {
          case TrackSource.Microphone:
            setActiveMicrophone(track);
            setupLocalMicrophoneAnalyser(track);
            if (track.deviceId) {
              setMicrophoneDeviceId(track.deviceId);
            }
            if (userWantsMicMuted) {
              track.mute();
            }
            break;
        }
      });

      return tracks[0];
    },
    [setupLocalMicrophoneAnalyser, setMicrophoneDeviceId, userWantsMicMuted]
  );

  const changeActiveMicrophone = useCallback(
    async (deviceId: string) => {
      await getMicrophone(deviceId);
    },
    [getMicrophone]
  );

  const getActiveMicrophoneLevel = useCallback(() => {
    if (!localAudioAnalyser) {
      return null;
    }

    const sampleBuffer = new Float32Array(localAudioAnalyser.fftSize);
    localAudioAnalyser.getFloatTimeDomainData(sampleBuffer);

    // Compute average power over the interval.
    let sumOfSquares = 0;
    for (let i = 0; i < sampleBuffer.length; i++) {
      sumOfSquares += sampleBuffer[i] ** 2;
    }
    const avgPowerDecibels =
      10 * Math.log10(sumOfSquares / sampleBuffer.length);

    // Compute peak instantaneous power over the interval.
    let peakInstantaneousPower = 0;
    for (let i = 0; i < sampleBuffer.length; i++) {
      const power = sampleBuffer[i] ** 2;
      peakInstantaneousPower = Math.max(power, peakInstantaneousPower);
    }
    const peakInstantaneousPowerDecibels =
      10 * Math.log10(peakInstantaneousPower);

    return {
      avgDb: avgPowerDecibels,
      peakDb: peakInstantaneousPowerDecibels,
    };
  }, [localAudioAnalyser]);

  const getCamera = useCallback(
    async (deviceId: string) => {
      let options = {
        ...defaultCameraOption,
        ...noMicrophoneOption,
      };
      if (deviceId !== "") {
        options["video"] = {
          constraints: {
            deviceId: { exact: deviceId },
          },
        };
      }

      let tracks: LocalTrack[] = [];
      try {
        tracks = await getUserMedia(options);
      } catch (e: any) {
        // May occur if previously set device IDs are no longer available
        if (
          e.name == "NotAllowedError" ||
          e.name == "PermissionDeniedError" ||
          e instanceof DOMException
        ) {
          // permission denied to camera
          setUserMediaError("NotAllowedError");
        } else if (
          e.name == "OverconstrainedError" ||
          e.name == "ConstraintNotSatisfiedError"
        ) {
          setUserMediaError("OverconstrainedError");
        } else {
          setUserMediaError(e.name);
        }
      }

      tracks.forEach((track) => {
        switch (track.source) {
          case TrackSource.Camera:
            setActiveCamera(track);
            if (track.deviceId) {
              setCameraDeviceId(track.deviceId);
            }
            break;
        }
      });

      return tracks[0];
    },
    [setCameraDeviceId]
  );

  const changeActiveCamera = useCallback(
    async (deviceId: string) => {
      await getCamera(deviceId);
    },
    [getCamera]
  );

  const stopActiveCamera = useCallback(() => {
    if (activeCamera) {
      activeCamera.stop();
      setActiveCamera(undefined);
    }
  }, [activeCamera]);

  const onDeviceChange = useCallback(async () => {
    console.log("Detected device change, refreshing device list");
    await loadDevices();
  }, [loadDevices]);

  useEffect(() => {
    if (userWantsMicMuted && !activeMicrophone?.muted) {
      activeMicrophone?.mute();
    } else if (!userWantsMicMuted && activeMicrophone?.muted) {
      activeMicrophone?.unMute();
    }
  }, [userWantsMicMuted, activeMicrophone]);

  useEffect(() => {
    navigator.mediaDevices.addEventListener("devicechange", onDeviceChange);
    return () => {
      navigator.mediaDevices.removeEventListener(
        "devicechange",
        onDeviceChange
      );
    };
  }, [onDeviceChange]);

  return (
    <UserMediaContext.Provider
      value={{
        activeCamera,
        activeMicrophone,

        userMediaError,
        requestPermissionAndPopulateDevices,
        requestPermissionAndStartDevices,

        getCamera,
        cameraDevices,
        activeCameraId,
        stopActiveCamera,
        changeActiveCamera,

        getMicrophone,
        microphoneDevices,
        activeMicrophoneId,
        muteActiveMicrophone,
        unMuteActiveMicrophone,
        changeActiveMicrophone,
        getActiveMicrophoneLevel,
      }}
    >
      {children}
    </UserMediaContext.Provider>
  );
};


================================================
FILE: hooks/useLocalStorage.tsx
================================================
import {
  Dispatch,
  SetStateAction,
  useCallback,
  useEffect,
  useRef,
  useState,
} from "react";

declare global {
  interface WindowEventMap {
    "local-storage": CustomEvent;
  }
}

type SetValue<T> = Dispatch<SetStateAction<T>>;

export function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, SetValue<T>] {
  // Get from local storage then
  // parse stored json or return initialValue
  const readValue = useCallback((): T => {
    // Prevent build error "window is undefined" but keep keep working
    if (typeof window === "undefined") {
      return initialValue;
    }

    try {
      const item = window.localStorage.getItem(key);
      const json = parseJSON(item) as T;
      if (json === undefined) {
        window.localStorage.removeItem(key);
      }
      return item ? json : initialValue;
    } catch (error) {
      console.warn(`Error reading localStorage key “${key}”:`, error);
      return initialValue;
    }
  }, [initialValue, key]);

  // State to store our value
  // Pass initial state function to useState so logic is only executed once
  const [storedValue, setStoredValue] = useState<T>(readValue);

  const setValueRef = useRef<SetValue<T>>();

  setValueRef.current = (value) => {
    // Prevent build error "window is undefined" but keeps working
    if (typeof window == "undefined") {
      console.warn(
        `Tried setting localStorage key “${key}” even though environment is not a client`
      );
    }

    try {
      // Allow value to be a function so we have the same API as useState
      const newValue = value instanceof Function ? value(storedValue) : value;

      // Save state
      setStoredValue(newValue);

      // Save to local storage
      window.localStorage.setItem(key, JSON.stringify(newValue));

      // We dispatch a custom event so every useLocalStorage hook are notified
      window.dispatchEvent(new Event("local-storage"));
    } catch (error) {
      console.warn(`Error setting localStorage key “${key}”:`, error);
    }
  };

  // Return a wrapped version of useState's setter function that ...
  // ... persists the new value to localStorage.
  const setValue: SetValue<T> = useCallback(
    (value) => setValueRef.current?.(value),
    []
  );

  useEffect(() => {
    setStoredValue(readValue());
  }, []);

  return [storedValue, setValue];
}

// A wrapper for "JSON.parse()"" to support "undefined" value

function parseJSON<T>(value: string | null): T | undefined | null {
  if (!value) return null; // don't attempt to parse a null value
  try {
    return value === "undefined" ? undefined : JSON.parse(value ?? "");
  } catch {
    console.log("parsing error on", { value });
    return undefined;
  }
}


================================================
FILE: hooks/useParticipant.ts
================================================
import React, {
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useState,
} from "react";
import {
  LocalParticipant,
  LocalTrack,
  ParticipantEvent,
  RemoteParticipant,
  Track,
  TrackSource,
} from "@mux/spaces-web";

import SpaceContext from "../context/Space";
import UserContext from "../context/User";

export interface Participant {
  id: string;
  isLocal: boolean;
  isSpeaking: boolean;
  hasMicTrack: boolean;
  isMicTrackMuted: boolean;
  isCameraOff: boolean;
  cameraWidth: number;
  cameraHeight: number;
  displayName: string;
  attachVideoElement: (element: HTMLVideoElement) => void;
  attachAudioElement: (element: HTMLAudioElement) => void;
}

export function useParticipant(connectionId: string): Participant {
  const { localParticipant, remoteParticipants } = useContext(SpaceContext);
  let participant: LocalParticipant | RemoteParticipant | undefined =
    remoteParticipants.find((p) => p.connectionId === connectionId);
  if (
    !participant &&
    localParticipant &&
    localParticipant.connectionId === connectionId
  ) {
    participant = localParticipant;
  }

  if (typeof participant === "undefined") {
    throw new Error(`No participant found with connectionId: ${connectionId}`);
  }

  const id = useMemo(() => (participant ? participant.id : ""), [participant]);
  const isLocal = useMemo(
    () => participant instanceof LocalParticipant,
    [participant]
  );
  const [isSpeaking, setIsSpeaking] = useState(false);
  const [cameraTrack, setCameraTrack] = useState<Track>();
  const [microphoneTrack, setMicrophoneTrack] = useState<Track>();
  const [isMicTrackMuted, setIsMicTrackMuted] = useState(false);
  const [isCameraOff, setIsCameraOff] = useState(true);
  const [displayName, setDisplayName] = useState(participant.displayName);
  const { userWantsMicMuted } = React.useContext(UserContext);

  const hasMicTrack = useMemo(() => {
    return !!microphoneTrack;
  }, [microphoneTrack]);

  const cameraDimensions = useMemo(() => {
    return { width: cameraTrack?.width || 0, height: cameraTrack?.height || 0 };
  }, [cameraTrack]);

  const attachVideoElement = useCallback(
    (element: HTMLVideoElement) => {
      cameraTrack?.attachedElements.forEach((attachedEl) =>
        cameraTrack.detach(attachedEl)
      );
      cameraTrack?.attach(element);
    },
    [cameraTrack]
  );

  const attachAudioElement = useCallback(
    (element: HTMLAudioElement) => {
      microphoneTrack?.attachedElements.forEach((attachedEl) =>
        microphoneTrack.detach(attachedEl)
      );
      microphoneTrack?.attach(element);
    },
    [microphoneTrack]
  );

  const handleTrackAdded = useCallback(
    (track: Track) => {
      if (track.hasMedia()) {
        if (track.source === TrackSource.Camera) {
          setCameraTrack(track);
          setIsCameraOff(track.isMuted());
        }
        if (track.source === TrackSource.Microphone) {
          setMicrophoneTrack(track);
          if (isLocal && userWantsMicMuted) {
            (track as LocalTrack).mute();
          }
          if (track.isMuted()) {
            setIsMicTrackMuted(true);
          }
        }
      }
    },
    [setCameraTrack, setMicrophoneTrack]
  );

  const handleSetDisplayName = useCallback(() => {
    setDisplayName(participant ? participant.displayName : "");
  }, [setDisplayName]);

  const handleTrackRemoved = useCallback(
    (track: Track) => {
      if (track.source === TrackSource.Camera) {
        setCameraTrack(undefined);
        setIsCameraOff(true);
      }
      if (track.source === TrackSource.Microphone) {
        setMicrophoneTrack(undefined);
      }
    },
    [setCameraTrack, setMicrophoneTrack]
  );

  useEffect(() => {
    if (isLocal && microphoneTrack instanceof LocalTrack) {
      if (userWantsMicMuted && !microphoneTrack.muted) {
        microphoneTrack.mute();
      } else if (!userWantsMicMuted && microphoneTrack.muted) {
        microphoneTrack.unMute();
      }
    }
  }, [userWantsMicMuted, isLocal, microphoneTrack]);

  useEffect(() => {
    if (!participant) return;

    const onMuted = (track: Track) => {
      if (track.source == TrackSource.Microphone) {
        setIsMicTrackMuted(true);
      } else if (track.source == TrackSource.Camera) {
        setIsCameraOff(true);
      }
    };
    const onUnmuted = (track: Track) => {
      if (track.source == TrackSource.Microphone) {
        setIsMicTrackMuted(false);
      } else if (track.source == TrackSource.Camera) {
        setIsCameraOff(false);
      }
    };
    const onSpeaking = () => {
      setIsSpeaking(true);
    };
    const stoppedSpeaking = () => {
      setIsSpeaking(false);
    };

    participant.on(ParticipantEvent.TrackMuted, onMuted);
    participant.on(ParticipantEvent.TrackUnmuted, onUnmuted);
    participant.on(ParticipantEvent.StartedSpeaking, onSpeaking);
    participant.on(ParticipantEvent.StoppedSpeaking, stoppedSpeaking);
    participant.on(ParticipantEvent.TrackPublished, handleTrackAdded);
    participant.on(ParticipantEvent.TrackUnpublished, handleTrackRemoved);
    participant.on(ParticipantEvent.TrackSubscribed, handleTrackAdded);
    participant.on(ParticipantEvent.TrackUnsubscribed, handleTrackRemoved);
    participant.on(ParticipantEvent.DisplayNameChanged, handleSetDisplayName);

    participant.getAudioTracks().forEach((track) => {
      handleTrackAdded(track);
    });
    participant.getVideoTracks().forEach((track) => {
      handleTrackAdded(track);
    });

    return () => {
      if (!participant) return;

      participant.off(ParticipantEvent.TrackMuted, onMuted);
      participant.off(ParticipantEvent.TrackUnmuted, onUnmuted);
      participant.off(ParticipantEvent.StartedSpeaking, onSpeaking);
      participant.off(ParticipantEvent.StoppedSpeaking, stoppedSpeaking);
      participant.off(ParticipantEvent.TrackPublished, handleTrackAdded);
      participant.off(ParticipantEvent.TrackUnpublished, handleTrackRemoved);
      participant.off(ParticipantEvent.TrackSubscribed, handleTrackAdded);
      participant.off(ParticipantEvent.TrackUnsubscribed, handleTrackRemoved);
      participant.off(
        ParticipantEvent.DisplayNameChanged,
        handleSetDisplayName
      );
    };
  }, [participant, handleTrackAdded, handleTrackRemoved]);

  return {
    id,
    isLocal,
    isSpeaking,
    isCameraOff,
    hasMicTrack,
    isMicTrackMuted,
    cameraWidth: cameraDimensions.width,
    cameraHeight: cameraDimensions.height,
    displayName,
    attachVideoElement,
    attachAudioElement,
  };
}


================================================
FILE: hooks/useSpace.ts
================================================
import { useContext } from "react";
import { AcrScore } from "@mux/spaces-web";

import SpaceContext from "../context/Space";

interface Space {
  joinSpace: (
    jwt: string,
    endsAt?: number,
    displayName?: string
  ) => Promise<void>;
  joinError: string | null;
  isJoined: boolean;

  connectionIds: string[];
  localParticipantConnectionId?: string;
  isBroadcasting: boolean;
  participantCount: number;
  publishCamera: (deviceId: string) => void;
  publishMicrophone: (deviceId: string) => void;
  unPublishDevice: (deviceId: string) => void;

  isLocalScreenShareSupported: boolean;
  isScreenShareActive: boolean;
  isLocalScreenShare: boolean;
  screenShareError: string | null;
  attachScreenShare: (element: HTMLVideoElement) => void;
  startScreenShare: () => void;
  stopScreenShare: () => void;
  screenShareParticipantConnectionId?: string;
  screenShareParticipantName?: string;

  spaceEndsAt: number | null;
  leaveSpace: () => void;
  submitAcrScore: (score: AcrScore) => Promise<void> | undefined;
}

export const useSpace = (): Space => {
  const {
    space,

    joinSpace,
    joinError,
    isJoined,

    connectionIds,
    isBroadcasting,
    participantCount,
    publishCamera,
    publishMicrophone,
    unPublishDevice,

    isLocalScreenShareSupported,
    isScreenShareActive,
    isLocalScreenShare,
    screenShareError,
    attachScreenShare,
    startScreenShare,
    stopScreenShare,
    screenShareParticipantConnectionId,
    screenShareParticipantName,

    spaceEndsAt,
    leaveSpace,
    submitAcrScore,
  } = useContext(SpaceContext);

  return {
    joinSpace,
    joinError,
    isJoined,

    connectionIds,
    localParticipantConnectionId: space?.localParticipant?.connectionId,
    isBroadcasting,
    participantCount,
    publishCamera,
    publishMicrophone,
    unPublishDevice,

    isLocalScreenShareSupported,
    isScreenShareActive,
    isLocalScreenShare,
    screenShareError,
    attachScreenShare,
    startScreenShare,
    stopScreenShare,
    screenShareParticipantConnectionId,
    screenShareParticipantName,

    spaceEndsAt,
    leaveSpace,
    submitAcrScore,
  };
};


================================================
FILE: hooks/useSpaceEvent.tsx
================================================
import { SpaceEvent } from "@mux/spaces-web";
import SpaceContext from "context/Space";
import { useContext, useEffect, useRef } from "react";

/**
 * @param event
 * @param callback called whenever the event is fired. Warning: The callback should be provided using the `useCallback` hook to avoid this hook being called at every re-render of the component.
 * @param startListening if false, the callback will not be registered until this is set to true. Useful for registering callbacks conditionally.
 */
export const useSpaceEvent = (
  event: SpaceEvent,
  callback: (...args: any) => void,
  startListening = true
): void => {
  const { space } = useContext(SpaceContext);

  useEffect(() => {
    if (!startListening) {
      return;
    }

    try {
      space?.on(event, callback);
    } catch (_error) {
      throw new Error(
        `Unable to register useSpaceEvent callback ${callback.name} as the Space does not exist yet.`
      );
    }

    return () => {
      space?.off(event, callback);
    };
  }, [event, callback, space, startListening]);
};


================================================
FILE: hooks/useUserMedia.ts
================================================
import { useContext } from "react";

import UserMediaContext from "../context/UserMedia";

interface UserMedia {
  userMediaError?: string;
  requestPermissionAndPopulateDevices: () => void;
  requestPermissionAndStartDevices: (
    microphoneDeviceId?: string,
    cameraDeviceId?: string
  ) => Promise<void>;

  cameraDevices: MediaDeviceInfo[];
  activeCameraId?: string;
  stopActiveCamera: () => void;
  changeActiveCamera: (deviceId: string) => Promise<void>;

  microphoneDevices: MediaDeviceInfo[];
  activeMicrophoneId?: string;
  muteActiveMicrophone: () => void;
  unMuteActiveMicrophone: () => void;
  changeActiveMicrophone: (deviceId: string) => Promise<void>;
  getActiveMicrophoneLevel: () => {
    avgDb: number;
    peakDb: number;
  } | null;
}

export function useUserMedia(): UserMedia {
  const {
    userMediaError,
    requestPermissionAndPopulateDevices,
    requestPermissionAndStartDevices,

    cameraDevices,
    activeCameraId,
    stopActiveCamera,
    changeActiveCamera,

    microphoneDevices,
    activeMicrophoneId,
    muteActiveMicrophone,
    unMuteActiveMicrophone,
    changeActiveMicrophone,
    getActiveMicrophoneLevel,
  } = useContext(UserMediaContext);

  return {
    userMediaError,
    requestPermissionAndPopulateDevices,
    requestPermissionAndStartDevices,

    cameraDevices,
    activeCameraId,
    stopActiveCamera,
    changeActiveCamera,

    microphoneDevices,
    activeMicrophoneId,
    muteActiveMicrophone,
    unMuteActiveMicrophone,
    changeActiveMicrophone,
    getActiveMicrophoneLevel,
  };
}


================================================
FILE: hooks/useWindowDimension.ts
================================================
import { useEffect, useState } from "react";

type WindowDimensions = {
  width: number | undefined;
  height: number | undefined;
};

const useWindowDimensions = (): WindowDimensions => {
  const [windowDimensions, setWindowDimensions] = useState<WindowDimensions>({
    width: undefined,
    height: undefined,
  });
  useEffect(() => {
    function handleResize(): void {
      setWindowDimensions({
        width: window.innerWidth,
        height: window.innerHeight,
      });
    }
    handleResize();
    window.addEventListener("resize", handleResize);
    return (): void => window.removeEventListener("resize", handleResize);
  }, []); // Empty array ensures that effect is only run on mount

  return windowDimensions;
};

export default useWindowDimensions;


================================================
FILE: lib/constants.ts
================================================
export const MAX_PARTICIPANTS_PER_PAGE = 20;
export const TEMPORARY_SPACE_PASSTHROUGH = "Temporary Meet Space";


================================================
FILE: lib/gallery.ts
================================================
type LayoutDescription = {
  area: number;
  cols: number;
  rows: number;
  width: number;
  height: number;
};

/**
 * Calculate optimal layout (most area used) of a number of boxes within a larger frame.
 * Given number of boxes, aspectRatio of those boxes, and spacing between them.
 *
 * Thanks to Anton Dosov for algorithm shown in this article:
 * https://dev.to/antondosov/building-a-video-gallery-just-like-in-zoom-4mam
 *
 * @param frameWidth width of the space holding the boxes
 * @param frameHeight height of the space holding the boxes
 * @param boxCount number of boxes to place (all same aspect ratio)
 * @param aspectRatio ratio of width to height of the boxes (usually 16/9)
 * @param spacing amount of space (margin) between boxes to spread them out
 * @returns A description of the optimal layout
 */
export function calcOptimalBoxes(
  frameWidth: number,
  frameHeight: number,
  boxCount: number,
  aspectRatio: number,
  spacing: number
): LayoutDescription {
  let bestLayout: LayoutDescription = {
    area: 0,
    cols: 0,
    rows: 0,
    width: 0,
    height: 0,
  };

  // try each possible number of columns to find the one with the highest area (optimum use of space)
  for (let cols = 1; cols <= boxCount; cols++) {
    const rows = Math.ceil(boxCount / cols);
    // pack the frames together by removing the spacing between them
    const packedWidth = frameWidth - spacing * (cols - 1);
    const packedHeight = frameHeight - spacing * (rows - 1);
    const hScale = packedWidth / (cols * aspectRatio);
    const vScale = packedHeight / rows;
    let width;
    let height;
    if (hScale <= vScale) {
      width = Math.floor(packedWidth / cols);
      height = Math.floor(width / aspectRatio);
    } else {
      height = Math.floor(packedHeight / rows);
      width = Math.floor(height * aspectRatio);
    }
    const area = width * height;
    if (area > bestLayout.area) {
      bestLayout = { area, width, height, rows, cols };
    }
  }

  return bestLayout;
}


================================================
FILE: lib/theme.ts
================================================
import localFont from "@next/font/local";
import { extendTheme, defineStyle } from "@chakra-ui/react";

const akkuratFont = localFont({
  src: "./Akkurat-Regular.woff2",
});

const baseMuxButtonStyles = {
  height: "40px",
  borderRadius: "20px",
  padding: "10px 20px 10px 20px",
  borderWidth: "1px",
  fontSize: "14px",
};

const control = defineStyle({
  background: "#0a0a0b",
  width: "60px",
  height: "60px",
  border: "1px solid #3E4247",
  borderRadius: "50%",
  _hover: {
    background: "#242628",
    border: "1px solid #FFFFFF",
  },
  _active: {
    background: "#3E4247",
    border: "1px solid #FFFFFF",
  },
});

const controlMenu = defineStyle({
  background: "#707C89",
  width: "20px",
  height: "20px",
  border: "1px solid #565E67",
  borderRadius: "50%",
  fontWeight: "bold",
  _hover: {
    background: "#3E4247",
    border: "1px solid #FFFFFF",
  },
  _active: {
    background: "#3E4247",
    border: "1px solid #FFFFFF",
  },
});

const muxDefault = defineStyle({
  ...baseMuxButtonStyles,
  background: "#FFFFFF",
  borderColor: "#808C99",
  fontWeight: "normal",
  _hover: {
    borderColor: "#242628",
  },
  _active: {
    background: "#F3F5F6",
    borderColor: "#242628",
  },
});

const muxConfirmation = defineStyle({
  ...baseMuxButtonStyles,
  background: "#00AA3C",
  borderColor: "#00802D",
  color: "#FFFFFF",
  _hover: {
    background: "#00802D",
    borderColor: "#005C20",
    _disabled: {
      fontWeight: "normal",
      background: "#E5E8EB",
      borderColor: "#E5E8EB",
      color: "#707C89",
    },
  },
  _active: {
    background: "#005C20",
    borderColor: "#003D16",
  },
  _disabled: {
    fontWeight: "normal",
    background: "#E5E8EB",
    borderColor: "#E5E8EB",
    color: "#707C89",
  },
});

const muxDestructive = defineStyle({
  ...baseMuxButtonStyles,
  background: "#FDA89B",
  borderColor: "#F87B6D",
  fontWeight: "bold",
  _hover: {
    borderColor: "#F85C54",
    background: "#F87B6D",
  },
  _active: {
    borderColor: "#EA3737",
    background: "#F85C54",
  },
});

export const theme = extendTheme({
  fonts: {
    heading: `${akkuratFont.style.fontFamily}, sans-serif`,
    body: `${akkuratFont.style.fontFamily}, sans-serif`,
  },
  styles: {
    global: () => ({
      body: {
        width: "100%",
        height: "100%",
        position: "fixed",
      },
    }),
  },
  colors: {
    red: {
      50: "#FFE0E3",
      100: "#FFC0C6",
      200: "#FF949E",
      300: "#FF6877",
      400: "#FB3C4E",
      500: "#E22C3E",
      600: "#B71928",
      700: "#950D1A",
      800: "#73040E",
    },

    green: {
      50: "#E0FFFA",
      100: "#BAF8EE",
      200: "#82EDDC",
      300: "#49DFC6",
      400: "#1FC3A8",
      500: "#17A089",
      600: "#047F6B",
      700: "#036353",
      800: "#00473C",
    },

    blue: {
      50: "#DBEFFF",
      100: "#B5E0FF",
      200: "#82CBFF",
      300: "#4FB6FF",
      400: "#1CA0FD",
      500: "#0B85DB",
      600: "#006DB9",
      700: "#005997",
      800: "#003C66",
    },

    purple: {
      50: "#F3E0FF",
      100: "#E7BDFF",
      200: "#CF86F9",
      300: "#BE52FA",
      400: "#9620D8",
      500: "#7A10B6",
      600: "#600494",
      700: "#490072",
      800: "#330050",
    },
    orange: {
      50: "#FB501D",
      100: "#DF491E",
    },
  },
  semanticTokens: {
    colors: {
      gradient: "linear-gradient(90deg, #fb3c4e 0%, #fb2491 100%)",
    },
  },
  components: {
    Menu: {
      parts: ["item"],
      baseStyle: {
        item: {
          background: "#383838",
          _focus: {
            background: "transparent",
          },
          _active: {
            background: "rgba(0,0,0,0.5)",
          },
        },
      },
    },
    Button: {
      variants: {
        control,
        controlMenu,
        muxDefault,
        muxConfirmation,
        muxDestructive,
      },
      baseStyle: { transition: "none" },
    },
  },
});


================================================
FILE: lib/utils.ts
================================================
import { CreateStyled } from "@emotion/styled";

export const transientOptions: Parameters<CreateStyled>[1] = {
  shouldForwardProp: (propName: string) => !propName.startsWith("$"),
};


================================================
FILE: next-env.d.ts
================================================
/// <reference types="next" />
/// <reference types="next/image-types/global" />

// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.


================================================
FILE: next.config.js
================================================
/** @type {import('next').NextConfig} */
const nextConfig = {
  reactStrictMode: true,
};

module.exports = nextConfig;


================================================
FILE: package.json
================================================
{
  "name": "@mux/mux-meet-nextjs",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint",
    "prettier": "prettier --write .",
    "prepare": "husky install"
  },
  "dependencies": {
    "@chakra-ui/react": "^2.6.1",
    "@emotion/react": "^11.11.0",
    "@emotion/styled": "^11.11.0",
    "@mux/mux-node": "^7.3.0",
    "@mux/mux-player-react": "^1.11.0",
    "@mux/spaces-web": "1.2.0",
    "@next/font": "^13.4.1",
    "axios": "^1.4.0",
    "framer-motion": "^8.5.5",
    "http-status-codes": "^2.2.0",
    "jsonwebtoken": "^9.0.0",
    "loglevel": "^1.8.1",
    "moment": "^2.29.4",
    "next": "13.1.6",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "react-hotkeys-hook": "^4.4.0",
    "react-icons": "^4.8.0",
    "react-query": "^3.39.3",
    "sharp": "^0.31.3",
    "use-sound": "^4.0.1",
    "uuid": "^9.0.0"
  },
  "devDependencies": {
    "@types/jsonwebtoken": "^9.0.2",
    "@types/node": "18.11.18",
    "@types/react": "18.0.27",
    "@types/uuid": "^9.0.1",
    "eslint": "8.33.0",
    "eslint-config-next": "13.1.6",
    "eslint-config-prettier": "^8.8.0",
    "husky": "^8.0.3",
    "lint-staged": "^13.2.2",
    "prettier": "2.8.3",
    "typescript": "4.9.5"
  },
  "lint-staged": {
    "**/*": "prettier --write --ignore-unknown"
  }
}


================================================
FILE: pages/_app.tsx
================================================
import type { AppProps } from "next/app";
import { ChakraProvider } from "@chakra-ui/react";
import { QueryClient, QueryClientProvider } from "react-query";

import { theme } from "lib/theme";
import { UserProvider } from "context/User";
import { SpaceProvider } from "context/Space";
import { UserMediaProvider } from "context/UserMedia";

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      refetchOnWindowFocus: false,
    },
  },
});

function MyApp({ Component, pageProps }: AppProps) {
  return (
    <QueryClientProvider client={queryClient}>
      <ChakraProvider theme={theme}>
        <UserProvider>
          <UserMediaProvider>
            <SpaceProvider>
              <Component {...pageProps} />
            </SpaceProvider>
          </UserMediaProvider>
        </UserProvider>
      </ChakraProvider>
    </QueryClientProvider>
  );
}

export default MyApp;


================================================
FILE: pages/_document.tsx
================================================
import { ColorModeScript } from "@chakra-ui/react";
import Document, { Html, Head, Main, NextScript } from "next/document";

export default class MyDocument extends Document {
  render() {
    return (
      <Html lang="en">
        <Head />
        <body>
          <ColorModeScript />
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}


================================================
FILE: pages/api/spaces/[id].ts
================================================
import { StatusCodes } from "http-status-codes";
import { NextApiRequest, NextApiResponse } from "next";
import { muxClient } from "server-lib/services";
import axios from "axios";

const fetchSpace = async (id: string) => {
  let response;

  try {
    response = await muxClient.get(`/video/v1/spaces/${id}`);
  } catch (error) {
    if (axios.isAxiosError(error)) {
      throw new Error(`Error: ${error.response?.status}`);
    }

    throw new Error("Unknown error");
  }

  return response.data.data;
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "GET" && typeof req.query.id === "string") {
    const space = await fetchSpace(req.query.id);
    res.status(StatusCodes.OK).json(space);
  } else {
    res.status(StatusCodes.METHOD_NOT_ALLOWED);
  }
}

export { fetchSpace };


================================================
FILE: pages/api/spaces.ts
================================================
import axios from "axios";
import { StatusCodes } from "http-status-codes";
import { TEMPORARY_SPACE_PASSTHROUGH } from "lib/constants";
import { NextApiRequest, NextApiResponse } from "next";

import { muxClient } from "../../server-lib/services";

type Space = {
  id: string;
  type: string;
  created_at: string;
  status: "active" | "idle";
  passthrough?: string;
};

const fetchSpaces = async (): Promise<Space[]> => {
  let response;

  try {
    response = await muxClient.get(`/video/v1/spaces?limit=100`);
  } catch (error) {
    if (axios.isAxiosError(error)) {
      throw new Error(`Error: ${error.response?.status}`);
    }

    throw new Error("Unknown error");
  }

  return response.data.data;
};

const createSpace = async () => {
  let response;

  try {
    response = await muxClient.post(`/video/v1/spaces`, {
      passthrough: TEMPORARY_SPACE_PASSTHROUGH,
    });
  } catch (error) {
    if (axios.isAxiosError(error)) {
      throw new Error(`Error: ${error.response?.status}`);
    }

    throw new Error("Unknown error");
  }

  return response.data.data;
};

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method === "POST") {
    const activeSpaceLimit = process.env.ACTIVE_SPACE_LIMIT;
    if (activeSpaceLimit) {
      const limit = parseInt(activeSpaceLimit, 10);
      const spaces = await fetchSpaces();
      const activeSpaces = spaces.filter(({ status }) => status === "active");
      if (activeSpaces.length >= limit) {
        return res.status(StatusCodes.INSUFFICIENT_SPACE_ON_RESOURCE).end();
      }
    }

    try {
      const space = await createSpace();
      res.status(StatusCodes.OK).json(space);
    } catch (error) {
      const message = (error as Error).message as string;
      if (message.includes("401")) {
        res.status(StatusCodes.UNAUTHORIZED).end();
      }
    }
  } else {
    res.status(StatusCodes.METHOD_NOT_ALLOWED).end();
  }
}


================================================
FILE: pages/api/token.ts
================================================
import { StatusCodes } from "http-status-codes";
import jwt from "jsonwebtoken";
import { NextApiRequest, NextApiResponse } from "next";

const { MUX_SIGNING_KEY, MUX_PRIVATE_KEY } = process.env;

type ResponseData = {
  spaceJWT: string;
};

function signJWT(spaceId: string, participantId: string): ResponseData {
  const JWT = jwt.sign(
    {
      kid: MUX_SIGNING_KEY ?? "",
      aud: "rt",
      sub: spaceId,
      participant_id: participantId,
    },
    Buffer.from(MUX_PRIVATE_KEY ?? "", "base64"),
    { algorithm: "RS256", expiresIn: "1h" }
  );
  return { spaceJWT: JWT };
}

export default function handler(
  req: NextApiRequest,
  res: NextApiResponse<ResponseData>
) {
  const {
    body: { spaceId, participantId },
    method,
  } = req;
  if (method === "POST") {
    res
      .status(StatusCodes.OK)
      .json(signJWT(spaceId as string, participantId as string));
  } else {
    res.status(StatusCodes.METHOD_NOT_ALLOWED);
  }
}


================================================
FILE: pages/api/webhooks.ts
================================================
import Mux from "@mux/mux-node";
import { StatusCodes } from "http-status-codes";
import { TEMPORARY_SPACE_PASSTHROUGH } from "lib/constants";
import { NextApiRequest, NextApiResponse } from "next";

import { muxClient } from "server-lib/services";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const headers = req.headers;
  const muxSignature = headers["mux-signature"] as string;
  const secret = process.env.WEBHOOK_SECRET;

  if (!secret) {
    console.error("WEBHOOK_SECRET not specified");
    return res.status(StatusCodes.INTERNAL_SERVER_ERROR).end();
  }

  if (
    req.method === "POST" &&
    Mux.Webhooks.verifyHeader(JSON.stringify(req.body), muxSignature, secret)
  ) {
    const { body } = req;

    if (
      body.type === "video.space.idle" &&
      body.data.passthrough.startsWith(TEMPORARY_SPACE_PASSTHROUGH)
    ) {
      try {
        await muxClient.delete(`/video/v1/spaces/${body.data.id}`);
        return res.status(StatusCodes.OK).end();
      } catch (error) {
        return res.status(StatusCodes.BAD_REQUEST).end();
      }
    }

    return res.status(StatusCodes.OK).end();
  }

  return res.status(StatusCodes.METHOD_NOT_ALLOWED).end();
}


================================================
FILE: pages/index.tsx
================================================
import React, { useEffect, useMemo, useRef, useState } from "react";
import Head from "next/head";
import { useRouter } from "next/router";
import {
  Button,
  Box,
  FormControl,
  FormLabel,
  Stack,
  Heading,
  Input,
  FormHelperText,
  Flex,
  Center,
  HStack,
  useDisclosure,
} from "@chakra-ui/react";
import { useMutation } from "react-query";

import UserContext from "context/User";
import { useUserMedia } from "hooks/useUserMedia";

import Header from "components/Header";
import MicrophoneButton from "components/controls/buttons/MicrophoneButton";
import CameraButton from "components/controls/buttons/CameraButton";
import ErrorModal from "components/modals/ErrorModal";

const Home = () => {
  const router = useRouter();
  const didPopulateDevicesRef = useRef(false);
  const user = React.useContext(UserContext);
  const [participantName, setParticipantName] = useState("");
  const [joining, setJoining] = useState(false);
  const [hasBlurredNameInput, setHasBlurredNameInput] = useState(false);
  const { requestPermissionAndPopulateDevices } = useUserMedia();
  const [errorModalTitle, setErrorModalTitle] = useState("");
  const [errorModalMessage, setErrorModalMessage] = useState("");
  const {
    isOpen: isErrorModalOpen,
    onOpen: onErrorModalOpen,
    onClose: onErrorModalClose,
  } = useDisclosure();

  const createSpaceMutation = useMutation(["Spaces"], () =>
    fetch(`/api/spaces`, {
      method: "POST",
      mode: "no-cors",
    }).then((res) => {
      if (res.ok) {
        return res.json();
      } else if (res.status === 401) {
        throw new Error("Not authorized to create space");
      } else if (res.status === 419) {
        throw new Error("Maximum active spaces reached");
      } else {
        throw new Error("Error creating space");
      }
    })
  );

  useEffect(() => {
    if (didPopulateDevicesRef.current === false) {
      didPopulateDevicesRef.current = true;
      requestPermissionAndPopulateDevices();
    }
  }, [requestPermissionAndPopulateDevices]);

  useEffect(() => {
    setParticipantName(user.participantName);
  }, [user.participantName]);

  const invalidParticipantName = useMemo(
    () => !participantName,
    [participantName]
  );

  const disableJoin = useMemo(
    () => invalidParticipantName,
    [invalidParticipantName]
  );

  const isNameInputInvalid = useMemo(
    () => invalidParticipantName && hasBlurredNameInput,
    [invalidParticipantName, hasBlurredNameInput]
  );

  const handleParticipantNameChange = (event: {
    target: { value: string };
  }) => {
    setParticipantName(event.target.value);
  };

  async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
    e.preventDefault();
    setJoining(true);
    user.setParticipantName!(participantName);
    user.setInteractionRequired(false);

    createSpaceMutation.mutate(undefined, {
      onError: (error) => {
        const message = (error as Error).message;

        if (message.includes("Not authorized")) {
          setErrorModalTitle("Not authorized to create a new space");
          setErrorModalMessage(
            "Make sure MUX_TOKEN_ID and MUX_TOKEN_SECRET are set. Refer to the README in https://github.com/muxinc/meet for more details."
          );
          onErrorModalOpen();
        } else if (message.includes("Maximum active spaces reached")) {
          setErrorModalTitle("Maximum active space limit reached");
          setErrorModalMessage(
            "There are too many active spaces being used. Please try again later."
          );
          onErrorModalOpen();
        }

        setJoining(false);
      },
      onSuccess: (newSpace) => {
        if (newSpace) {
          router.push({
            pathname: `/space/${newSpace.id}`,
          });
        }
      },
    });
  }

  return (
    <>
      <Head>
        <title>Mux Meet</title>
        <meta name="description" content="Real-time meetings powered by Mux" />
        <link rel="icon" href="/favicon.png" />
      </Head>

      <Flex direction="column" height="100vh" backgroundColor="#323232">
        <Header />
        <Center height="100%" zIndex={1}>
          <Flex direction="column" align="center">
            <Box background="white" padding="4" borderRadius="4" width="360px">
              <form onSubmit={handleSubmit}>
                <Stack spacing="4">
                  <Heading>Join a Space</Heading>

                  <FormControl
                    isInvalid={isNameInputInvalid}
                    onBlur={() => setHasBlurredNameInput(true)}
                  >
                    <FormLabel>Your Name</FormLabel>
                    <Input
                      maxLength={40}
                      id="participant_name"
                      value={participantName}
                      onChange={handleParticipantNameChange}
                    />
                    <FormHelperText
                      color={!isNameInputInvalid ? "white" : "#E22C3E"}
                    >
                      This cannot be empty.
                    </FormHelperText>
                  </FormControl>

                  <Button
                    type="submit"
                    width="full"
                    isDisabled={disableJoin}
                    isLoading={joining}
                  >
                    Join a New Space
                  </Button>
                </Stack>
              </form>
            </Box>
            <HStack marginTop="1rem">
              <MicrophoneButton />
              <CameraButton />
            </HStack>
          </Flex>
        </Center>
      </Flex>

      <ErrorModal
        title={errorModalTitle}
        message={errorModalMessage}
        isOpen={isErrorModalOpen}
        onClose={onErrorModalClose}
      />
    </>
  );
};

export default Home;


================================================
FILE: pages/space/[id].tsx
================================================
import React, { useCallback, useEffect, useRef, useState } from "react";
import { useMutation } from "react-query";
import Head from "next/head";
import Image from "next/image";
import { useRouter } from "next/router";
import type { NextPage, GetServerSideProps } from "next";
import { Flex } from "@chakra-ui/react";
import styled from "@emotion/styled";
import moment from "moment";

import UserContext from "context/User";
import { useSpace } from "hooks/useSpace";
import { tokenPOST } from "client/token";
import { fetchSpace } from "pages/api/spaces/[id]";

import Stage from "components/Stage";
import UserInteractionPrompt from "components/UserInteractionPrompt";

import starfield from "../../public/starfield-bg.jpg";
import { TEMPORARY_SPACE_PASSTHROUGH } from "lib/constants";

const BackgroundImageWrap = styled.div`
  position: fixed;
  height: 100vh;
  width: 100vw;
  overflow: hidden;
  z-index: 0;
`;

interface Props {
  heliosURL: string;
  spaceBackendURL: string;
  title: string;
  endsAt?: number;
}

const SpacePage: NextPage<Props> = ({
  heliosURL,
  spaceBackendURL,
  title,
  endsAt,
}: Props) => {
  const router = useRouter();
  const { id } = router.query;
  const { isReady: isRouterReady } = router;
  const user = React.useContext(UserContext);
  const { joinSpace, leaveSpace } = useSpace();
  const [canJoinSpace, setCanJoinSpace] = useState(true);
  const participantNameRef = useRef<string>("");

  useEffect(() => {
    setCanJoinSpace((endsAt && moment(endsAt).diff(moment()) > 0) || !endsAt);
  }, [endsAt]);

  useEffect(() => {
    if (spaceBackendURL) {
      (window as any).MUX_SPACES_BACKEND_URL = spaceBackendURL;
    }
    if (heliosURL) {
      (window as any).MUX_SPACES_HELIOS_URL = heliosURL;
    }
  }, [spaceBackendURL, heliosURL]);

  const mutation = useMutation(tokenPOST, {
    onSuccess: async (data) => {
      await joinSpace(
        data.spaceJWT,
        endsAt,
        participantNameRef.current || user.participantName
      );
    },
  });

  const authenticate = useCallback(
    (spaceId: string, participantId: string) => {
      mutation.mutate({
        spaceId,
        participantId,
      });
    },
    [mutation]
  );

  const handleJoin = useCallback(() => {
    if (typeof id === "string" && canJoinSpace) {
      authenticate(id, `${participantNameRef.current}|${user.id}`);
    }
  }, [id, canJoinSpace, authenticate, user.id]);

  useEffect(() => {
    if (!isRouterReady) return;
    if (!id || Array.isArray(id)) {
      console.warn("No space selected");
      return;
    }
    router.events.on("routeChangeStart", leaveSpace);
    router.events.on("routeChangeComplete", handleJoin);
    return () => {
      router.events.off("routeChangeStart", leaveSpace);
      router.events.off("routeChangeComplete", handleJoin);
    };
  }, [
    id,
    user,
    router,
    handleJoin,
    leaveSpace,
    isRouterReady,
    user.participantName,
  ]);

  return (
    <>
      <Head>
        <title>{title}</title>
        <meta name="description" content="This is your space room" />
        <link rel="icon" href="/favicon.png" />
      </Head>

      <Flex height="100vh" overflow="hidden" direction="column">
        <BackgroundImageWrap>
          <Image
            alt="Starfield"
            src={starfield}
            placeholder="blur"
            quality={100}
            fill
            sizes="100vw"
            style={{
              objectFit: "cover",
            }}
          />
        </BackgroundImageWrap>

        {/* required to handle auto play https://developer.chrome.com/blog/autoplay/ */}
        {user.interactionRequired ? (
          <UserInteractionPrompt
            onInteraction={handleJoin}
            participantNameRef={participantNameRef}
          />
        ) : (
          <Stage />
        )}
      </Flex>
    </>
  );
};

const { MUX_SPACES_BACKEND_URL = "", MUX_SPACES_HELIOS_URL = "" } = process.env;
export const getServerSideProps: GetServerSideProps = async (context) => {
  const { id } = context.query;
  let passthrough;
  let createdAt;

  try {
    if (typeof id === "string") {
      ({ passthrough, created_at: createdAt } = await fetchSpace(id));
    }
  } catch (error) {}

  let props: Record<string, any> = {
    heliosURL: MUX_SPACES_HELIOS_URL,
    spaceBackendURL: MUX_SPACES_BACKEND_URL,
    title: passthrough ? `${passthrough} | Mux Meet` : "Mux Meet Space",
  };

  if (
    process.env.SPACE_DURATION_SECONDS &&
    passthrough === TEMPORARY_SPACE_PASSTHROUGH &&
    createdAt
  ) {
    props.endsAt = moment(createdAt * 1000)
      .add(process.env.SPACE_DURATION_SECONDS, "seconds")
      .valueOf();
  }

  return {
    props,
  };
};

export default SpacePage;


================================================
FILE: server-lib/services.ts
================================================
import axios from "axios";

const {
  MUX_TOKEN_ID,
  MUX_TOKEN_SECRET,
  MUX_API_HOST = "api.mux.com",
} = process.env;

const muxOptions = {
  auth: { username: MUX_TOKEN_ID ?? "", password: MUX_TOKEN_SECRET ?? "" },
  baseURL: `https://${MUX_API_HOST}`,
};

const muxClient = axios.create(muxOptions);

export { muxClient };


================================================
FILE: shared/defaults.tsx
================================================
export const defaultAudioConstraints: MediaTrackConstraints = {
  autoGainControl: true,
  echoCancellation: true,
  noiseSuppression: true,
  channelCount: 1,
};


================================================
FILE: shared/toastConfigs.tsx
================================================
import { Box } from "@chakra-ui/react";
import { UseToastOptions } from "@chakra-ui/react";

export enum ToastIds {
  COPY_LINK_TOAST_ID,
  SHARING_SCREEN_TOAST_ID,
  BROADCASTING_SCREEN_TOAST_ID,
  VIEWING_SHARED_SCREEN_TOAST_ID,
}

export const copyLinkToastConfig: UseToastOptions = {
  id: ToastIds.COPY_LINK_TOAST_ID,
  position: "top",
  render: () => {
    return (
      <Box
        color="#0a0a0b"
        background="#cff1fc"
        fontSize="14px"
        padding="15px 50px"
        textAlign="center"
      >
        Share link copied to your clipboard.
      </Box>
    );
  },
};

export const sharingScreenToastConfig: UseToastOptions = {
  id: ToastIds.SHARING_SCREEN_TOAST_ID,
  position: "top",
  duration: null,
};

export const viewingSharedScreenToastConfig: UseToastOptions = {
  id: ToastIds.VIEWING_SHARED_SCREEN_TOAST_ID,
  position: "top",
  duration: null,
};

export const broadcastingToastConfig: UseToastOptions = {
  id: ToastIds.BROADCASTING_SCREEN_TOAST_ID,
  position: "top-left",
  duration: null,
};

export const participantEventToastConfig: UseToastOptions = {
  // Don't set id, as we expect to have multiple on screen at the same time
  isClosable: true,
  position: "top-right",
  duration: 3000,
};


================================================
FILE: tsconfig.json
================================================
{
  "compilerOptions": {
    "baseUrl": ".",
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve",
    "incremental": true
  },
  "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
  "exclude": ["node_modules"]
}
Download .txt
gitextract_6okmy78h/

├── .eslintrc.json
├── .gitignore
├── .husky/
│   └── pre-commit
├── .prettierignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── client/
│   └── token.ts
├── components/
│   ├── Controls.tsx
│   ├── Gallery.tsx
│   ├── GalleryLayout.tsx
│   ├── Header.tsx
│   ├── Meeting.tsx
│   ├── MuteIndicator.tsx
│   ├── Notifications.tsx
│   ├── Participant.tsx
│   ├── ParticipantAudio.tsx
│   ├── ParticipantInfoBar.tsx
│   ├── ParticipantName.tsx
│   ├── Pin.tsx
│   ├── Sounds.tsx
│   ├── Stage.tsx
│   ├── Timer.tsx
│   ├── Toasts.tsx
│   ├── UserInteractionPrompt.tsx
│   ├── controls/
│   │   ├── ControlsCenter.tsx
│   │   ├── ControlsLeft.tsx
│   │   ├── ControlsRight.tsx
│   │   └── buttons/
│   │       ├── CameraButton.tsx
│   │       ├── ChatButton.tsx
│   │       ├── MicrophoneButton.tsx
│   │       ├── ScreenShareButton.tsx
│   │       ├── SendButton.tsx
│   │       └── SettingsButton.tsx
│   ├── icons/
│   │   ├── ChatIcon.tsx
│   │   ├── ChevronIcon.tsx
│   │   ├── ChevronLeftIcon.tsx
│   │   ├── ChevronRightIcon.tsx
│   │   ├── LeaveIcon.tsx
│   │   ├── MuteCameraIcon.tsx
│   │   ├── MuteMicrophoneIcon.tsx
│   │   ├── ScreenShareIcon.tsx
│   │   ├── SendIcon.tsx
│   │   ├── SettingsIcon.tsx
│   │   ├── UnmuteCameraIcon.tsx
│   │   └── UnmuteMicrophoneIcon.tsx
│   ├── modals/
│   │   ├── ACRScoreDialog.tsx
│   │   ├── ErrorModal.tsx
│   │   └── RenameParticipantModal.tsx
│   └── renderers/
│       ├── AudioRenderer.tsx
│       ├── ChatRenderer.tsx
│       ├── ScreenShareRenderer.tsx
│       └── VideoRenderer.tsx
├── context/
│   ├── Chat.tsx
│   ├── Space.tsx
│   ├── User.tsx
│   └── UserMedia.tsx
├── hooks/
│   ├── useLocalStorage.tsx
│   ├── useParticipant.ts
│   ├── useSpace.ts
│   ├── useSpaceEvent.tsx
│   ├── useUserMedia.ts
│   └── useWindowDimension.ts
├── lib/
│   ├── constants.ts
│   ├── gallery.ts
│   ├── theme.ts
│   └── utils.ts
├── next-env.d.ts
├── next.config.js
├── package.json
├── pages/
│   ├── _app.tsx
│   ├── _document.tsx
│   ├── api/
│   │   ├── spaces/
│   │   │   └── [id].ts
│   │   ├── spaces.ts
│   │   ├── token.ts
│   │   └── webhooks.ts
│   ├── index.tsx
│   └── space/
│       └── [id].tsx
├── server-lib/
│   └── services.ts
├── shared/
│   ├── defaults.tsx
│   └── toastConfigs.tsx
└── tsconfig.json
Download .txt
SYMBOL INDEX (108 symbols across 65 files)

FILE: client/token.ts
  type TokenParams (line 3) | interface TokenParams {
  type TokenResponse (line 8) | interface TokenResponse {

FILE: components/Controls.tsx
  function Controls (line 12) | function Controls(): JSX.Element {

FILE: components/Gallery.tsx
  function pushToFront (line 14) | function pushToFront<T>(array: T[], element: T) {
  type Props (line 22) | interface Props {
  function Gallery (line 29) | function Gallery({

FILE: components/GalleryLayout.tsx
  type Props (line 12) | interface Props {

FILE: components/Header.tsx
  function Header (line 8) | function Header(): JSX.Element {

FILE: components/Meeting.tsx
  function Meeting (line 16) | function Meeting(): JSX.Element {

FILE: components/MuteIndicator.tsx
  type Props (line 5) | interface Props {
  function MuteIndicator (line 10) | function MuteIndicator({

FILE: components/Notifications.tsx
  function Notifications (line 11) | function Notifications(): JSX.Element {

FILE: components/Participant.tsx
  type Props (line 12) | interface Props {
  function Participant (line 18) | function Participant({

FILE: components/ParticipantAudio.tsx
  type Props (line 7) | interface Props {

FILE: components/ParticipantInfoBar.tsx
  type Props (line 6) | interface Props {
  function ParticipantInfoBar (line 12) | function ParticipantInfoBar({

FILE: components/ParticipantName.tsx
  type Props (line 4) | interface Props {
  function ParticipantName (line 9) | function ParticipantName({ isSmall, children }: Props): JSX.Element {

FILE: components/Pin.tsx
  type Props (line 7) | interface Props {
  function Pin (line 11) | function Pin({ connectionId }: Props): JSX.Element {

FILE: components/Sounds.tsx
  function Sounds (line 10) | function Sounds(): JSX.Element {
  function JoinedSounds (line 16) | function JoinedSounds(): JSX.Element {

FILE: components/Stage.tsx
  function Stage (line 22) | function Stage(): JSX.Element {

FILE: components/Toasts.tsx
  function Toasts (line 24) | function Toasts(): JSX.Element {

FILE: components/UserInteractionPrompt.tsx
  type Props (line 30) | interface Props {
  function UserInteractionPrompt (line 35) | function UserInteractionPrompt({

FILE: components/controls/ControlsCenter.tsx
  type Props (line 12) | interface Props {
  function ControlsCenter (line 16) | function ControlsCenter({ onLeave }: Props): JSX.Element {

FILE: components/controls/ControlsLeft.tsx
  function ControlsLeft (line 7) | function ControlsLeft(): JSX.Element {

FILE: components/controls/ControlsRight.tsx
  type Props (line 8) | interface Props {
  function ControlsRight (line 27) | function ControlsRight({ onLeave }: Props): JSX.Element {

FILE: components/controls/buttons/CameraButton.tsx
  function CameraButton (line 25) | function CameraButton(): JSX.Element {

FILE: components/controls/buttons/ChatButton.tsx
  type Props (line 8) | interface Props {
  function ChatButton (line 23) | function ChatButton(): JSX.Element {

FILE: components/controls/buttons/MicrophoneButton.tsx
  function MicrophoneButton (line 25) | function MicrophoneButton(): JSX.Element {

FILE: components/controls/buttons/ScreenShareButton.tsx
  function ScreenShareButton (line 8) | function ScreenShareButton(): JSX.Element {

FILE: components/controls/buttons/SendButton.tsx
  type Props (line 8) | interface Props {
  function SendButton (line 20) | function SendButton({

FILE: components/controls/buttons/SettingsButton.tsx
  type Props (line 21) | interface Props {
  function SettingsButton (line 25) | function SettingsButton({ onLeave }: Props): JSX.Element {

FILE: components/icons/ChatIcon.tsx
  function ChatIcon (line 6) | function ChatIcon(): JSX.Element {

FILE: components/icons/ChevronIcon.tsx
  function ChevronIcon (line 6) | function ChevronIcon(): JSX.Element {

FILE: components/icons/ChevronLeftIcon.tsx
  function ChevronLeftIcon (line 6) | function ChevronLeftIcon(): JSX.Element {

FILE: components/icons/ChevronRightIcon.tsx
  function ChevronRightIcon (line 6) | function ChevronRightIcon(): JSX.Element {

FILE: components/icons/LeaveIcon.tsx
  function LeaveIcon (line 3) | function LeaveIcon(): JSX.Element {

FILE: components/icons/MuteCameraIcon.tsx
  function MuteCameraIcon (line 6) | function MuteCameraIcon(): JSX.Element {

FILE: components/icons/MuteMicrophoneIcon.tsx
  function MuteMicrophoneIcon (line 6) | function MuteMicrophoneIcon(): JSX.Element {

FILE: components/icons/ScreenShareIcon.tsx
  function ScreenShareIcon (line 6) | function ScreenShareIcon(): JSX.Element {

FILE: components/icons/SendIcon.tsx
  function SendIcon (line 6) | function SendIcon(): JSX.Element {

FILE: components/icons/SettingsIcon.tsx
  function SettingsIcon (line 5) | function SettingsIcon() {

FILE: components/icons/UnmuteCameraIcon.tsx
  function UnmuteCameraIcon (line 6) | function UnmuteCameraIcon(): JSX.Element {

FILE: components/icons/UnmuteMicrophoneIcon.tsx
  function UnmuteMicrophoneIcon (line 6) | function UnmuteMicrophoneIcon(): JSX.Element {

FILE: components/modals/ACRScoreDialog.tsx
  function RadioCard (line 23) | function RadioCard(props: RadioProps) {
  type Props (line 64) | type Props = Pick<ReturnType<typeof useDisclosure>, "isOpen" | "onClose">;
  function ACRScoreDialog (line 68) | function ACRScoreDialog({ isOpen, onClose }: Props) {

FILE: components/modals/ErrorModal.tsx
  type Props (line 12) | interface Props {
  function ErrorModal (line 19) | function ErrorModal({ title, message, isOpen, onClose }: Props) {

FILE: components/modals/RenameParticipantModal.tsx
  type Props (line 27) | type Props = Pick<ReturnType<typeof useDisclosure>, "isOpen" | "onClose"...
  function RenameParticipantModal (line 29) | function RenameParticipantModal({

FILE: components/renderers/AudioRenderer.tsx
  type AudioTrackProps (line 3) | interface AudioTrackProps {

FILE: components/renderers/ChatRenderer.tsx
  type ChatMessageProps (line 91) | interface ChatMessageProps {
  function ChatRenderer (line 155) | function ChatRenderer({ show }: { show: boolean }): JSX.Element {

FILE: components/renderers/ScreenShareRenderer.tsx
  type Props (line 4) | interface Props {
  function ScreenShareRenderer (line 8) | function ScreenShareRenderer({ attach }: Props): JSX.Element {

FILE: components/renderers/VideoRenderer.tsx
  type Props (line 6) | interface Props {
  function VideoRenderer (line 14) | function VideoRenderer({

FILE: context/Chat.tsx
  type ChatMessagePayloadValue (line 15) | interface ChatMessagePayloadValue {
  type RemoteChatMessage (line 22) | interface RemoteChatMessage extends ChatMessagePayloadValue {
  type LocalChatMessage (line 26) | interface LocalChatMessage extends RemoteChatMessage {
  type SendState (line 30) | enum SendState {
  type ChatMessage (line 36) | type ChatMessage = RemoteChatMessage | LocalChatMessage;
  type IChatContext (line 38) | interface IChatContext {
  type Props (line 52) | type Props = {

FILE: context/Space.tsx
  type SpaceState (line 31) | interface SpaceState {
  type Props (line 73) | type Props = {

FILE: context/User.tsx
  type UserState (line 6) | interface UserState {
  type Props (line 28) | interface Props {

FILE: context/UserMedia.tsx
  type UserMediaState (line 20) | interface UserMediaState {
  type Props (line 74) | type Props = {

FILE: hooks/useLocalStorage.tsx
  type WindowEventMap (line 11) | interface WindowEventMap {
  type SetValue (line 16) | type SetValue<T> = Dispatch<SetStateAction<T>>;
  function useLocalStorage (line 18) | function useLocalStorage<T>(
  function parseJSON (line 90) | function parseJSON<T>(value: string | null): T | undefined | null {

FILE: hooks/useParticipant.ts
  type Participant (line 20) | interface Participant {
  function useParticipant (line 34) | function useParticipant(connectionId: string): Participant {

FILE: hooks/useSpace.ts
  type Space (line 6) | interface Space {

FILE: hooks/useUserMedia.ts
  type UserMedia (line 5) | interface UserMedia {
  function useUserMedia (line 29) | function useUserMedia(): UserMedia {

FILE: hooks/useWindowDimension.ts
  type WindowDimensions (line 3) | type WindowDimensions = {
  function handleResize (line 14) | function handleResize(): void {

FILE: lib/constants.ts
  constant MAX_PARTICIPANTS_PER_PAGE (line 1) | const MAX_PARTICIPANTS_PER_PAGE = 20;
  constant TEMPORARY_SPACE_PASSTHROUGH (line 2) | const TEMPORARY_SPACE_PASSTHROUGH = "Temporary Meet Space";

FILE: lib/gallery.ts
  type LayoutDescription (line 1) | type LayoutDescription = {
  function calcOptimalBoxes (line 23) | function calcOptimalBoxes(

FILE: pages/_app.tsx
  function MyApp (line 18) | function MyApp({ Component, pageProps }: AppProps) {

FILE: pages/_document.tsx
  class MyDocument (line 4) | class MyDocument extends Document {
    method render (line 5) | render() {

FILE: pages/api/spaces.ts
  type Space (line 8) | type Space = {
  function handler (line 50) | async function handler(

FILE: pages/api/spaces/[id].ts
  function handler (line 22) | async function handler(

FILE: pages/api/token.ts
  type ResponseData (line 7) | type ResponseData = {
  function signJWT (line 11) | function signJWT(spaceId: string, participantId: string): ResponseData {
  function handler (line 25) | function handler(

FILE: pages/api/webhooks.ts
  function handler (line 8) | async function handler(

FILE: pages/index.tsx
  function handleSubmit (line 93) | async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {

FILE: pages/space/[id].tsx
  type Props (line 30) | interface Props {

FILE: shared/toastConfigs.tsx
  type ToastIds (line 4) | enum ToastIds {
Condensed preview — 82 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (172K chars).
[
  {
    "path": ".eslintrc.json",
    "chars": 54,
    "preview": "{\n  \"extends\": [\"next/core-web-vitals\", \"prettier\"]\n}\n"
  },
  {
    "path": ".gitignore",
    "chars": 437,
    "preview": "# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n/.pn"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 58,
    "preview": "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\nnpx lint-staged\n"
  },
  {
    "path": ".prettierignore",
    "chars": 194,
    "preview": "# dependencies\n/node_modules\n/.pnp\n.pnp.js\n\n# testing\n/coverage\n\n# next.js\n/.next/\n/out/\n\n# production\n/build\n\n# debug\nn"
  },
  {
    "path": ".prettierrc.json",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "LICENSE",
    "chars": 1060,
    "preview": "MIT License\n\nCopyright (c) 2023 Mux\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof thi"
  },
  {
    "path": "README.md",
    "chars": 5716,
    "preview": "# Mux Meet\n\n> [!WARNING]\n> Mux Real-Time Video has been sunset and is unavailable for new usage. Existing access will en"
  },
  {
    "path": "client/token.ts",
    "chars": 305,
    "preview": "import axios from \"axios\";\n\ninterface TokenParams {\n  spaceId: string;\n  participantId: string;\n}\n\ninterface TokenRespon"
  },
  {
    "path": "components/Controls.tsx",
    "chars": 1642,
    "preview": "import React, { useCallback } from \"react\";\nimport { useRouter } from \"next/router\";\nimport { Flex, useDisclosure } from"
  },
  {
    "path": "components/Gallery.tsx",
    "chars": 4808,
    "preview": "import React, { useCallback, useMemo, useState } from \"react\";\nimport { IconButton, Center, Flex } from \"@chakra-ui/reac"
  },
  {
    "path": "components/GalleryLayout.tsx",
    "chars": 1247,
    "preview": "import React, {\n  Children,\n  cloneElement,\n  isValidElement,\n  ReactNode,\n  useMemo,\n} from \"react\";\nimport { Flex } fr"
  },
  {
    "path": "components/Header.tsx",
    "chars": 1112,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\nimport { AiFillGithub } from \"react-icons/ai\";\nimport { Flex,"
  },
  {
    "path": "components/Meeting.tsx",
    "chars": 2638,
    "preview": "import React, { useContext } from \"react\";\nimport { Center, Flex } from \"@chakra-ui/react\";\n\nimport { useSpace } from \"h"
  },
  {
    "path": "components/MuteIndicator.tsx",
    "chars": 1401,
    "preview": "import React from \"react\";\nimport { Box, Flex, Icon } from \"@chakra-ui/react\";\nimport { IoMicOffOutline } from \"react-ic"
  },
  {
    "path": "components/Notifications.tsx",
    "chars": 2035,
    "preview": "import { useState, useEffect } from \"react\";\nimport { useDisclosure } from \"@chakra-ui/react\";\n\nimport { useSpace } from"
  },
  {
    "path": "components/Participant.tsx",
    "chars": 1827,
    "preview": "import React, { useMemo } from \"react\";\nimport { Box, Center, Flex } from \"@chakra-ui/react\";\n\nimport UserContext from \""
  },
  {
    "path": "components/ParticipantAudio.tsx",
    "chars": 429,
    "preview": "import React from \"react\";\n\nimport { useParticipant } from \"hooks/useParticipant\";\n\nimport AudioRenderer from \"./rendere"
  },
  {
    "path": "components/ParticipantInfoBar.tsx",
    "chars": 1074,
    "preview": "import React from \"react\";\nimport { Flex, Text } from \"@chakra-ui/react\";\n\nimport MuteIndicator from \"./MuteIndicator\";\n"
  },
  {
    "path": "components/ParticipantName.tsx",
    "chars": 630,
    "preview": "import { memo } from \"react\";\nimport { Box, Center, Flex } from \"@chakra-ui/react\";\n\ninterface Props {\n  isSmall: boolea"
  },
  {
    "path": "components/Pin.tsx",
    "chars": 1050,
    "preview": "import React from \"react\";\nimport { IconButton } from \"@chakra-ui/react\";\nimport { MdPushPin, MdOutlinePushPin } from \"r"
  },
  {
    "path": "components/Sounds.tsx",
    "chars": 1074,
    "preview": "import useSound from \"use-sound\";\nimport { SpaceEvent } from \"@mux/spaces-web\";\n\nimport { useSpaceEvent } from \"hooks/us"
  },
  {
    "path": "components/Stage.tsx",
    "chars": 815,
    "preview": "import React from \"react\";\nimport { Center, Heading, Spinner } from \"@chakra-ui/react\";\n\nimport { useSpace } from \"hooks"
  },
  {
    "path": "components/Timer.tsx",
    "chars": 2173,
    "preview": "import { useEffect, useState } from \"react\";\nimport { useRouter } from \"next/router\";\nimport { Tooltip } from \"@chakra-u"
  },
  {
    "path": "components/Toasts.tsx",
    "chars": 5146,
    "preview": "import styled from \"@emotion/styled\";\nimport { useRouter } from \"next/router\";\nimport { useCallback, useEffect, useRef }"
  },
  {
    "path": "components/UserInteractionPrompt.tsx",
    "chars": 3761,
    "preview": "import React, {\n  useState,\n  useEffect,\n  useMemo,\n  useRef,\n  useContext,\n  MutableRefObject,\n} from \"react\";\nimport I"
  },
  {
    "path": "components/controls/ControlsCenter.tsx",
    "chars": 920,
    "preview": "import React from \"react\";\nimport { HStack } from \"@chakra-ui/react\";\n\nimport MicrophoneButton from \"./buttons/Microphon"
  },
  {
    "path": "components/controls/ControlsLeft.tsx",
    "chars": 619,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\nimport { Flex } from \"@chakra-ui/react\";\n\nimport muxLogo from"
  },
  {
    "path": "components/controls/ControlsRight.tsx",
    "chars": 1650,
    "preview": "import React from \"react\";\nimport { Button, Flex, Text } from \"@chakra-ui/react\";\nimport LeaveIcon from \"components/icon"
  },
  {
    "path": "components/controls/buttons/CameraButton.tsx",
    "chars": 3774,
    "preview": "import React, { useCallback } from \"react\";\nimport {\n  Box,\n  ButtonGroup,\n  Flex,\n  IconButton,\n  Menu,\n  MenuButton,\n "
  },
  {
    "path": "components/controls/buttons/ChatButton.tsx",
    "chars": 1118,
    "preview": "import React, { useContext } from \"react\";\nimport { IconButton, Tooltip } from \"@chakra-ui/react\";\n\nimport ChatIcon from"
  },
  {
    "path": "components/controls/buttons/MicrophoneButton.tsx",
    "chars": 6190,
    "preview": "import React, { useCallback, useEffect, useState } from \"react\";\nimport {\n  Box,\n  Flex,\n  IconButton,\n  Menu,\n  MenuBut"
  },
  {
    "path": "components/controls/buttons/ScreenShareButton.tsx",
    "chars": 1000,
    "preview": "import React from \"react\";\nimport { IconButton, Tooltip } from \"@chakra-ui/react\";\n\nimport { useSpace } from \"hooks/useS"
  },
  {
    "path": "components/controls/buttons/SendButton.tsx",
    "chars": 893,
    "preview": "import React, { useContext } from \"react\";\nimport { IconButton, Tooltip } from \"@chakra-ui/react\";\n\nimport styled from \""
  },
  {
    "path": "components/controls/buttons/SettingsButton.tsx",
    "chars": 2097,
    "preview": "import React from \"react\";\nimport {\n  Box,\n  IconButton,\n  Menu,\n  MenuButton,\n  MenuItem,\n  MenuList,\n  useDisclosure,\n"
  },
  {
    "path": "components/icons/ChatIcon.tsx",
    "chars": 328,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport chat from \"../../public/chat.svg\";\n\nexport default fu"
  },
  {
    "path": "components/icons/ChevronIcon.tsx",
    "chars": 261,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport chevronUp from \"../../public/chevronUp.svg\";\n\nexport "
  },
  {
    "path": "components/icons/ChevronLeftIcon.tsx",
    "chars": 256,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport chevronLeft from \"../../public/chevronLeft.svg\";\n\nexp"
  },
  {
    "path": "components/icons/ChevronRightIcon.tsx",
    "chars": 271,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport chevronRight from \"../../public/chevronRight.svg\";\n\ne"
  },
  {
    "path": "components/icons/LeaveIcon.tsx",
    "chars": 969,
    "preview": "import React from \"react\";\n\nexport default function LeaveIcon(): JSX.Element {\n  return (\n    <svg\n      width=\"17\"\n    "
  },
  {
    "path": "components/icons/MuteCameraIcon.tsx",
    "chars": 339,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport cameraOnIcon from \"../../public/cameraOn.svg\";\n\nexpor"
  },
  {
    "path": "components/icons/MuteMicrophoneIcon.tsx",
    "chars": 355,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport microphoneOffIcon from \"../../public/microphoneOff.sv"
  },
  {
    "path": "components/icons/ScreenShareIcon.tsx",
    "chars": 358,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport screenShare from \"../../public/screen-share.svg\";\n\nex"
  },
  {
    "path": "components/icons/SendIcon.tsx",
    "chars": 353,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport send from \"../../public/send.svg\";\n\nexport default fu"
  },
  {
    "path": "components/icons/SettingsIcon.tsx",
    "chars": 306,
    "preview": "import Image from \"next/image\";\n\nimport elipsisIcon from \"../../public/elipsis.svg\";\n\nexport default function SettingsIc"
  },
  {
    "path": "components/icons/UnmuteCameraIcon.tsx",
    "chars": 346,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport cameraOffIcon from \"../../public/cameraOff.svg\";\n\nexp"
  },
  {
    "path": "components/icons/UnmuteMicrophoneIcon.tsx",
    "chars": 371,
    "preview": "import React from \"react\";\nimport Image from \"next/image\";\n\nimport microphoneOnIcon from \"../../public/microphoneOn.svg\""
  },
  {
    "path": "components/modals/ACRScoreDialog.tsx",
    "chars": 4001,
    "preview": "import React, { useCallback, useRef, useState } from \"react\";\nimport {\n  useDisclosure,\n  Button,\n  AlertDialog,\n  Alert"
  },
  {
    "path": "components/modals/ErrorModal.tsx",
    "chars": 752,
    "preview": "import {\n  Button,\n  Modal,\n  ModalBody,\n  ModalCloseButton,\n  ModalContent,\n  ModalFooter,\n  ModalHeader,\n  ModalOverla"
  },
  {
    "path": "components/modals/RenameParticipantModal.tsx",
    "chars": 2954,
    "preview": "import React, {\n  useCallback,\n  useMemo,\n  useRef,\n  useState,\n  useContext,\n} from \"react\";\nimport {\n  Modal,\n  ModalO"
  },
  {
    "path": "components/renderers/AudioRenderer.tsx",
    "chars": 437,
    "preview": "import { useEffect, useRef } from \"react\";\n\ninterface AudioTrackProps {\n  attachFunc: (element: HTMLAudioElement) => voi"
  },
  {
    "path": "components/renderers/ChatRenderer.tsx",
    "chars": 6059,
    "preview": "import React, {\n  FormEvent,\n  KeyboardEvent,\n  UIEvent,\n  useCallback,\n  useContext,\n  useEffect,\n  useRef,\n  useState,"
  },
  {
    "path": "components/renderers/ScreenShareRenderer.tsx",
    "chars": 706,
    "preview": "import React, { useEffect, useRef } from \"react\";\nimport { Flex } from \"@chakra-ui/react\";\n\ninterface Props {\n  attach: "
  },
  {
    "path": "components/renderers/VideoRenderer.tsx",
    "chars": 1476,
    "preview": "import React, { useEffect, useRef, useState } from \"react\";\n\nimport poster from \"../../public/poster.jpg\";\nimport poster"
  },
  {
    "path": "context/Chat.tsx",
    "chars": 4432,
    "preview": "import { CustomEvent, SpaceEvent } from \"@mux/spaces-web\";\nimport { useSpaceEvent } from \"hooks/useSpaceEvent\";\nimport R"
  },
  {
    "path": "context/Space.tsx",
    "chars": 18269,
    "preview": "import React, {\n  createContext,\n  ReactNode,\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n} from \"r"
  },
  {
    "path": "context/User.tsx",
    "chars": 2301,
    "preview": "import React, { createContext, ReactNode, useCallback, useState } from \"react\";\nimport { v4 as uuidv4 } from \"uuid\";\n\nim"
  },
  {
    "path": "context/UserMedia.tsx",
    "chars": 12964,
    "preview": "import React, {\n  createContext,\n  ReactNode,\n  useCallback,\n  useEffect,\n  useMemo,\n  useState,\n} from \"react\";\nimport "
  },
  {
    "path": "hooks/useLocalStorage.tsx",
    "chars": 2714,
    "preview": "import {\n  Dispatch,\n  SetStateAction,\n  useCallback,\n  useEffect,\n  useRef,\n  useState,\n} from \"react\";\n\ndeclare global"
  },
  {
    "path": "hooks/useParticipant.ts",
    "chars": 6540,
    "preview": "import React, {\n  useCallback,\n  useContext,\n  useEffect,\n  useMemo,\n  useState,\n} from \"react\";\nimport {\n  LocalPartici"
  },
  {
    "path": "hooks/useSpace.ts",
    "chars": 2149,
    "preview": "import { useContext } from \"react\";\nimport { AcrScore } from \"@mux/spaces-web\";\n\nimport SpaceContext from \"../context/Sp"
  },
  {
    "path": "hooks/useSpaceEvent.tsx",
    "chars": 1068,
    "preview": "import { SpaceEvent } from \"@mux/spaces-web\";\nimport SpaceContext from \"context/Space\";\nimport { useContext, useEffect, "
  },
  {
    "path": "hooks/useUserMedia.ts",
    "chars": 1565,
    "preview": "import { useContext } from \"react\";\n\nimport UserMediaContext from \"../context/UserMedia\";\n\ninterface UserMedia {\n  userM"
  },
  {
    "path": "hooks/useWindowDimension.ts",
    "chars": 771,
    "preview": "import { useEffect, useState } from \"react\";\n\ntype WindowDimensions = {\n  width: number | undefined;\n  height: number | "
  },
  {
    "path": "lib/constants.ts",
    "chars": 112,
    "preview": "export const MAX_PARTICIPANTS_PER_PAGE = 20;\nexport const TEMPORARY_SPACE_PASSTHROUGH = \"Temporary Meet Space\";\n"
  },
  {
    "path": "lib/gallery.ts",
    "chars": 2003,
    "preview": "type LayoutDescription = {\n  area: number;\n  cols: number;\n  rows: number;\n  width: number;\n  height: number;\n};\n\n/**\n *"
  },
  {
    "path": "lib/theme.ts",
    "chars": 3922,
    "preview": "import localFont from \"@next/font/local\";\nimport { extendTheme, defineStyle } from \"@chakra-ui/react\";\n\nconst akkuratFon"
  },
  {
    "path": "lib/utils.ts",
    "chars": 185,
    "preview": "import { CreateStyled } from \"@emotion/styled\";\n\nexport const transientOptions: Parameters<CreateStyled>[1] = {\n  should"
  },
  {
    "path": "next-env.d.ts",
    "chars": 201,
    "preview": "/// <reference types=\"next\" />\n/// <reference types=\"next/image-types/global\" />\n\n// NOTE: This file should not be edite"
  },
  {
    "path": "next.config.js",
    "chars": 120,
    "preview": "/** @type {import('next').NextConfig} */\nconst nextConfig = {\n  reactStrictMode: true,\n};\n\nmodule.exports = nextConfig;\n"
  },
  {
    "path": "package.json",
    "chars": 1348,
    "preview": "{\n  \"name\": \"@mux/mux-meet-nextjs\",\n  \"private\": true,\n  \"scripts\": {\n    \"dev\": \"next dev\",\n    \"build\": \"next build\",\n"
  },
  {
    "path": "pages/_app.tsx",
    "chars": 903,
    "preview": "import type { AppProps } from \"next/app\";\nimport { ChakraProvider } from \"@chakra-ui/react\";\nimport { QueryClient, Query"
  },
  {
    "path": "pages/_document.tsx",
    "chars": 374,
    "preview": "import { ColorModeScript } from \"@chakra-ui/react\";\nimport Document, { Html, Head, Main, NextScript } from \"next/documen"
  },
  {
    "path": "pages/api/spaces/[id].ts",
    "chars": 849,
    "preview": "import { StatusCodes } from \"http-status-codes\";\nimport { NextApiRequest, NextApiResponse } from \"next\";\nimport { muxCli"
  },
  {
    "path": "pages/api/spaces.ts",
    "chars": 1955,
    "preview": "import axios from \"axios\";\nimport { StatusCodes } from \"http-status-codes\";\nimport { TEMPORARY_SPACE_PASSTHROUGH } from "
  },
  {
    "path": "pages/api/token.ts",
    "chars": 955,
    "preview": "import { StatusCodes } from \"http-status-codes\";\nimport jwt from \"jsonwebtoken\";\nimport { NextApiRequest, NextApiRespons"
  },
  {
    "path": "pages/api/webhooks.ts",
    "chars": 1223,
    "preview": "import Mux from \"@mux/mux-node\";\nimport { StatusCodes } from \"http-status-codes\";\nimport { TEMPORARY_SPACE_PASSTHROUGH }"
  },
  {
    "path": "pages/index.tsx",
    "chars": 5812,
    "preview": "import React, { useEffect, useMemo, useRef, useState } from \"react\";\nimport Head from \"next/head\";\nimport { useRouter } "
  },
  {
    "path": "pages/space/[id].tsx",
    "chars": 4724,
    "preview": "import React, { useCallback, useEffect, useRef, useState } from \"react\";\nimport { useMutation } from \"react-query\";\nimpo"
  },
  {
    "path": "server-lib/services.ts",
    "chars": 328,
    "preview": "import axios from \"axios\";\n\nconst {\n  MUX_TOKEN_ID,\n  MUX_TOKEN_SECRET,\n  MUX_API_HOST = \"api.mux.com\",\n} = process.env;"
  },
  {
    "path": "shared/defaults.tsx",
    "chars": 163,
    "preview": "export const defaultAudioConstraints: MediaTrackConstraints = {\n  autoGainControl: true,\n  echoCancellation: true,\n  noi"
  },
  {
    "path": "shared/toastConfigs.tsx",
    "chars": 1244,
    "preview": "import { Box } from \"@chakra-ui/react\";\nimport { UseToastOptions } from \"@chakra-ui/react\";\n\nexport enum ToastIds {\n  CO"
  },
  {
    "path": "tsconfig.json",
    "chars": 529,
    "preview": "{\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"target\": \"es5\",\n    \"lib\": [\"dom\", \"dom.iterable\", \"esnext\"],\n    \"all"
  }
]

About this extraction

This page contains the full source code of the muxinc/meet GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 82 files (155.8 KB), approximately 41.0k tokens, and a symbol index with 108 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!