commit cda372562d04ab793e5bbe5c23bd8ceb2031ca01 Author: Jon Date: Thu Jun 10 18:17:23 2021 +0100 initial commit diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..51a6ed2 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,34 @@ +{ + "extends": ["airbnb", "airbnb/hooks", "prettier"], + "parser": "babel-eslint", + "env": { + "browser": true, + "node": true, + "es6": true + }, + "rules": { + "no-console": 0, + "react/jsx-props-no-spreading": 0, + "react/forbid-prop-types": 0, + "react/require-default-props": 0, + "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], + "import/no-extraneous-dependencies": 0, + "import/order": [ + "error", + { + "groups": ["builtin", "external", "parent", "sibling", "index"], + "pathGroups": [ + { + "pattern": "react", + "group": "external", + "position": "before" + } + ], + "pathGroupsExcludedImportTypes": ["react"], + "alphabetize": { + "order": "asc" + } + } + ] + } +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8be2a9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# 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 \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..6eb7cc4 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,6 @@ +{ + "semi": true, + "tabWidth": 2, + "useTabs": false, + "singleQuote": true + } \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..461d170 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +help@daily.co. All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..19027ab --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,72 @@ +# Contributing + +Thank you for looking into contributing to`daily-demos`! We want these projects to help people experiment with Daily more quickly. We especially welcome any contributions that help us make existing demos easier to understand, improve demos' instructions and descriptions, and we're especially excited about any new demos that highlight unique ways to use the [Daily API](https://docs.daily.co/reference). + +**Before contributing:** + +- [Read our code of conduct](#read-our-code-of-conduct) + +**How to contribute:** + +- [Open or claim an issue](#open-or-claim-an-issue) +- [Open a pull request](#open-a-pull-request) +- [Contribute a new demo project](#contribute-a-new-demo-project) + +## Before contributing + +### Run prebuilt-ui locally + +Please follow the instructions in `README.md`. + +### Read our code of conduct + +We use the [Contributor Covenant](https://www.contributor-covenant.org/) for our Code of Conduct. Before contributing, [please read it](CODE_OF_CONDUCT.md). + +## How to contribute + +### Open or claim an issue + +#### Open an issue + +Today we work off two main issue templates: _bug reports_ and _demo/feature requests_. + +_Bug reports_ + +Before creating a new bug report, please do two things: + +1. If you want to report a bug you experienced while on a Daily call, try out these [troubleshooting tips](https://help.daily.co/en/articles/2303117-top-troubleshooting-tips) to see if that takes care of the bug. +2. If you're still seeing the error, check to see if somebody else has [already filed the issue](https://github.com/daily-co/daily-demos/issues) before creating a new one. + +If you've done those two things and need to create an issue, we'll ask you to tell us: + +- What you expected to happen +- What actually happened +- Steps to reproduce the error +- Screenshots that illustrate where and what we should be looking for when we reproduce +- System information, like your device, OS, and browser +- Any additional context that you think could help us work through this + +_Demo/feature requests_ + +We're always happy to hear about new ways you'd like to use Daily. If you'd like a demo that we don't have yet, we'll ask you to let us know: + +- If the demo will help you solve a particular problem +- Alternative solutions you've considered +- Any additional context that might help us understand this ask + +#### Claim an issue + +All issues labeled `good-first-issue` are up for grabs. If you'd like to tackle an existing issue, feel free to assign yourself, and please leave a comment letting everyone know that you're on it. + +### Open a pull request + +- If it's been a minute or if you haven't yet cloned, forked, or branched a repository, GitHub has some [docs to help](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests). +- When creating commit messages and pull request titles, please follow the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) standard. + +### Contribute a new demo project + +If you've built a project on Daily that you want to share with other developers, we'd be more than happy to help spread the word. + +To add a new demo project: + +Open a PR in [awesome-daily](#) and add a link to your project. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5b8d9e0 --- /dev/null +++ b/README.md @@ -0,0 +1,39 @@ +![Daily Examples](./logo.png) + +# [Daily](https://daily.co) | Examples + +This repository uses [yarn workspaces](https://classic.yarnpkg.com/en/docs/workspaces/) so it's required to have yarn [installed](https://classic.yarnpkg.com/en/docs/install). + +## Getting started + +Setup dependencies via `yarn install`. + +Run an example via `yarn workspace @dailyjs/basic-call dev` (replacing `basic-call` with the name of the demo) + +Please note: these demos are intended as educational resources for using the Daily platform as well as showcasing common usage patterns and best practices. That said, they are not intended to be used as production ready applications. + +--- + +## Contents + +## [Daily JS (Web)](./dailyjs/) + +Examples that showcase the Daily CallObject using our Javascript library + +## [Prebuilt UI](./dailyjs/prebuilt-ui) + +Work in progress, examples coming soon + +## [React Native (Mobile)](./dailyjs/react-native) + +Work in progress, examples coming soon + +--- + +## Contributions + +We welcome all contributions that make it easier for developers to get to know Daily through these demos. If you'd like to open or claim an issue, add your own demo, or contribute in another way, please read CONTRIBUTING.md. + +## Contact us + +We're always happy to help developers building on Daily. Reach out to us any time at help@daily.co, or chat with us. diff --git a/dailyjs/README.md b/dailyjs/README.md new file mode 100644 index 0000000..78f5f9b --- /dev/null +++ b/dailyjs/README.md @@ -0,0 +1,117 @@ +# Daily JS Examples + +Run an examples via `yarn workspace @dailyjs/basic-call dev` (replacing `basic-call` with the name of the demo) from the project root + +Note: please ensure your rooms are setup to use [web sockets](https://docs.daily.co/reference#domain-configuration) + +Note: examples are served using [nextjs](https://nextjs.org/) + +--- + +## Getting started + +``` +// run locally, from project root +yarn +yarn workspace @dailyjs/[example-to-run] dev +``` + +We recommend starting with the [basic call](./basic-call) example, showcasing the common flow of a call Daily call, device management and error handling. + +## Shared code + +These examples re-use some common components, contexts, hooks and libraries. These can be found in the [shared](./shared) folder. + +--- + +## Examples + +### [πŸ€™ Basic call](./basic-call) + +Simple call demo derived from our prebuilt UI codebase + +--- + +## Coming soon + +### [πŸ”Œ Device management](./device-management) + +Manage media device, handle errors and set initial device mute state + +### [πŸ§‘β€πŸ€β€πŸ§‘ 1-to-1 calls](./examples/1-to-1-calls) + +Simple p2p (single partcipant) call demo + +### [🎚️ Audio gain monitor](./examples/audio-monitor) + +Use AudioContext to create a audio gain meter + +### [πŸ‘± Hair check](./examples/hair-check) + +Check your audio and video before joining a call + +### [πŸšͺ Knock for access](./examples/knock-for-access) + +Pre-authenticate and request access before joining a call. For owners, approve / deny requests + +### [βœ‹ Raise your hand](./examples/knock-for-access) + +Use sendAppMessage to raise your hand to speak (stateless) + +### [πŸ”₯ Reactions](./examples/reactions) + +Use sendAppMessage to send emoji reactions during a call + +### [πŸ’¬ Text Chat](./examples/text-chat) + +Create a fully featured text chat (stateless) + +### [⏭️ Pagination](./examples/pagination) + +Optimise call performance by paginating (and pausing / resuming tracks) + +### [πŸ’― Large calls](./examples/large-calls) + +Optimisations for larger calls (100+ particpants) + +### [πŸŽ™οΈ Live streaminng](./examples/live-streaming) + +Example for how to broadcast and composite your calls via RTMP + +### [⏺️ Cloud recording & composition](./examples/cloud-recording) + +Record your call to Daily's cloud and composite the results via the REST API + +### [πŸ“Ό Local recording](./examples/local-recording) + +Record calls to your local computer + +### [πŸ–₯️ Screen sharing](./examples/audio-sharing) + +Share your screen with participants during a call + +### [πŸ”Š Audio sharing](./examples/audio-sharing) + +Share tab audio during a call and allow participants to manage audio mix + +### [πŸŽ›οΈ Quality controls](./examples/quality-controls) + +Optimise or enhance video and audio quality + +## Stateful examples + +### [User spotlighting](./examples/user-spotlighting-firebase) + +Set a participant to be spotlighted during a call + +### [Breakout rooms](./examples/breakout-rooms-firebase) + +Allow participants to form or join breakout rooms + +--- + +## Other + +### [shared](./examples/shared) + +Common components, context, hooks and libraries used across example projects diff --git a/dailyjs/basic-call/.babelrc b/dailyjs/basic-call/.babelrc new file mode 100644 index 0000000..a6f4434 --- /dev/null +++ b/dailyjs/basic-call/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["inline-react-svg"] +} diff --git a/dailyjs/basic-call/.gitignore b/dailyjs/basic-call/.gitignore new file mode 100644 index 0000000..058f0ec --- /dev/null +++ b/dailyjs/basic-call/.gitignore @@ -0,0 +1,35 @@ +# 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 \ No newline at end of file diff --git a/dailyjs/basic-call/README.md b/dailyjs/basic-call/README.md new file mode 100644 index 0000000..037ae34 --- /dev/null +++ b/dailyjs/basic-call/README.md @@ -0,0 +1,51 @@ +# Basic call + +![Basic Call](./image.png) + +## What does this demo do? + +- Built on [NextJS](https://nextjs.org/) +- Create a Daily instance using call object mode +- Manage user media devices +- Render UI based on the call state +- Handle media and call errors +- Obtain call access token via Daily REST API +- Handle preauthentication, knock for access and auto join + +Please note: this demo is not currently mobile optimised + +### Getting started + +``` +# set both DAILY_API_KEY and DAILY_DOMAIN +mv env.example env.local + +# from project root... +yarn +yarn workspace @dailyjs/basic-call dev +``` + +## How does this example work? + +This demo puts to work the following [shared libraries](../shared): + +**[MediaProvider.js](../shared/contexts/MediaProvider.js)** +Convenience context that provides an interface to media devices throughout app + +**[useDevices.js](../shared/hooks/useDevices.js)** +Hook for managing the enumeration and status of client media devices) + +**[CallProvider.js](../shared/contexts/CallProvider.js)** +Primary call context that manages Daily call state, participant state and call object interaction + +**[useCallMachine.js](../shared/hooks/useCallMachine.js)** +Abstraction hook that manages Daily call state and error handling + +**[ParticipantProvider.js](../shared/contexts/ParticipantProvider.js)** +Manages participant state and abstracts common selectors / derived data + +## Deploy your own + +Deploy the example using Vercel + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/git/external?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples%2Ftree%2Fmain%2Fdailyjs%2Fbasic-call&env=DAILY_DOMAIN,DAILY_API_KEY&project-name=dailyjs-basic-call&demo-title=Daily%20Basic%20Call%20Demo) diff --git a/dailyjs/basic-call/components/App/App.js b/dailyjs/basic-call/components/App/App.js new file mode 100644 index 0000000..ecd99bc --- /dev/null +++ b/dailyjs/basic-call/components/App/App.js @@ -0,0 +1,94 @@ +import React, { useCallback, useEffect, useMemo } from 'react'; +import Loader from '@dailyjs/shared/components/Loader'; +import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; +import { + CALL_STATE_ENDED, + CALL_STATE_JOINED, + CALL_STATE_JOINING, + CALL_STATE_LOBBY, + CALL_STATE_NOT_FOUND, + CALL_STATE_NOT_BEFORE, + CALL_STATE_READY, + CALL_STATE_REDIRECTING, +} from '@dailyjs/shared/contexts/useCallMachine'; + +import { useRouter } from 'next/router'; +import HairCheck from '../HairCheck'; +import MessageCard from '../MessageCard'; +import Room from '../Room'; +import { Modals } from './Modals'; + +export const App = () => { + const { state, leave } = useCallState(); + const router = useRouter(); + + useEffect(() => { + console.log(`%cπŸ”€ App state changed: ${state}`, `color: gray;`); + }, [state]); + + const renderState = useCallback(() => { + // Show loader when state is undefined or ready to join + if (!state || [CALL_STATE_READY, CALL_STATE_JOINING].includes(state)) { + return ; + } + + // Update the UI based on the state of our call + switch (state) { + case CALL_STATE_NOT_FOUND: + router.replace('/not-found'); + return null; + case CALL_STATE_NOT_BEFORE: + return ( + + This room has `nbf` set, meaning you cannot join the call before the + owner + + ); + case CALL_STATE_READY: + case CALL_STATE_LOBBY: + return ; + case CALL_STATE_JOINED: + return leave()} />; + case CALL_STATE_REDIRECTING: + case CALL_STATE_ENDED: + // Note: you could set a manual redirect here but we'll show just an exit screen + return ( + window.location.reload()}> + You have left the call. We hope you had fun! + + ); + default: + break; + } + + return ( + + An unknown error has occured in the call loop. This should not happen! + + ); + }, [leave, router, state]); + + // Memoize children to avoid unnecassary renders from HOC + return useMemo( + () => ( +
+ {renderState()} + + +
+ ), + [renderState] + ); +}; + +export default App; diff --git a/dailyjs/basic-call/components/App/Modals.js b/dailyjs/basic-call/components/App/Modals.js new file mode 100644 index 0000000..ced3862 --- /dev/null +++ b/dailyjs/basic-call/components/App/Modals.js @@ -0,0 +1,10 @@ +import React from 'react'; +import DeviceSelectModal from '@dailyjs/shared/components/DeviceSelectModal/DeviceSelectModal'; + +export const Modals = () => ( + <> + + +); + +export default Modals; diff --git a/dailyjs/basic-call/components/App/index.js b/dailyjs/basic-call/components/App/index.js new file mode 100644 index 0000000..8ab4977 --- /dev/null +++ b/dailyjs/basic-call/components/App/index.js @@ -0,0 +1,2 @@ +export { App as default } from './App'; +export { Modals } from './Modals'; diff --git a/dailyjs/basic-call/components/Audio/Audio.js b/dailyjs/basic-call/components/Audio/Audio.js new file mode 100644 index 0000000..788737d --- /dev/null +++ b/dailyjs/basic-call/components/Audio/Audio.js @@ -0,0 +1,44 @@ +import React, { useRef, useEffect } from 'react'; +import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; +import useAudioTrack from '@dailyjs/shared/hooks/useAudioTrack'; +import PropTypes from 'prop-types'; + +const AudioItem = React.memo(({ participant }) => { + const audioRef = useRef(null); + const audioTrack = useAudioTrack(participant); + + useEffect(() => { + if (!audioTrack || !audioRef.current) return; + + // sanity check to make sure this is an audio track + if (audioTrack && audioTrack !== 'audio') return; + + audioRef.current.srcObject = new MediaStream([audioTrack]); + }, [audioTrack]); + + return ( + <> + + + ); +}); + +AudioItem.propTypes = { + participant: PropTypes.object, +}; + +export const Audio = React.memo(() => { + const { allParticipants } = useParticipants(); + + return ( + <> + {allParticipants.map( + (p) => !p.isLocal && + )} + + ); +}); + +export default Audio; diff --git a/dailyjs/basic-call/components/Audio/index.js b/dailyjs/basic-call/components/Audio/index.js new file mode 100644 index 0000000..de613b3 --- /dev/null +++ b/dailyjs/basic-call/components/Audio/index.js @@ -0,0 +1,2 @@ +export { Audio as default } from './Audio'; +export { Audio } from './Audio'; diff --git a/dailyjs/basic-call/components/HairCheck/HairCheck.js b/dailyjs/basic-call/components/HairCheck/HairCheck.js new file mode 100644 index 0000000..fc8a711 --- /dev/null +++ b/dailyjs/basic-call/components/HairCheck/HairCheck.js @@ -0,0 +1,323 @@ +import React, { useState, useEffect, useCallback, useMemo } from 'react'; +import Button from '@dailyjs/shared/components/Button'; +import { TextInput } from '@dailyjs/shared/components/Input'; +import Loader from '@dailyjs/shared/components/Loader'; +import { MuteButton } from '@dailyjs/shared/components/MuteButtons'; +import { Tile } from '@dailyjs/shared/components/Tile'; +import { ACCESS_STATE_LOBBY } from '@dailyjs/shared/constants'; +import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; +import { useMediaDevices } from '@dailyjs/shared/contexts/MediaDeviceProvider'; +import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; +import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; +import { + DEVICE_STATE_BLOCKED, + DEVICE_STATE_NOT_FOUND, + DEVICE_STATE_IN_USE, + DEVICE_STATE_PENDING, + DEVICE_STATE_LOADING, + DEVICE_STATE_GRANTED, +} from '@dailyjs/shared/contexts/useDevices'; +import IconSettings from '@dailyjs/shared/icons/settings-sm.svg'; + +import { useDeepCompareMemo } from 'use-deep-compare'; + +export const HairCheck = () => { + const { callObject } = useCallState(); + const { localParticipant } = useParticipants(); + const { deviceState, camError, micError, isCamMuted, isMicMuted } = + useMediaDevices(); + const { showDeviceModal, setShowDeviceModal } = useUIState(); + const [waiting, setWaiting] = useState(false); + const [joining, setJoining] = useState(false); + const [userName, setUserName] = useState(''); + + // Tell Daily to initialise devices (even through we're not yet in a call) + useEffect(() => { + if (!callObject) return; + callObject.startCamera(); + }, [callObject]); + + const joinCall = useCallback(async () => { + if (!callObject) return; + + // Disable join controls + setJoining(true); + + // Set the local participants name + await callObject.setUserName(userName); + + // Attempt to jin the call + const { access } = callObject.accessState(); + await callObject.join(); + + // If we we're in the lobby, wait for the owner to let us in + if (access?.level === ACCESS_STATE_LOBBY) { + setWaiting(true); + const { granted } = await callObject.requestAccess({ + name: localParticipant?.name, + access: { + level: 'full', + }, + }); + + if (granted) { + console.log('πŸ‘‹ Access granted'); + } else { + console.log('❌ Access denied'); + } + } + }, [callObject, userName, localParticipant]); + + // Memoize the to prevent unnecassary re-renders + const tileMemo = useDeepCompareMemo( + () => ( + + ), + [localParticipant] + ); + + const isLoading = useMemo( + () => deviceState === DEVICE_STATE_LOADING, + [deviceState] + ); + + const hasError = useMemo(() => { + if ( + !deviceState || + [ + DEVICE_STATE_LOADING, + DEVICE_STATE_PENDING, + DEVICE_STATE_GRANTED, + ].includes(deviceState) + ) { + return false; + } + return true; + }, [deviceState]); + + const camErrorVerbose = useMemo(() => { + switch (camError) { + case DEVICE_STATE_BLOCKED: + return 'Camera blocked by user'; + case DEVICE_STATE_NOT_FOUND: + return 'Camera not found'; + case DEVICE_STATE_IN_USE: + return 'Device in use'; + default: + return 'unknown'; + } + }, [camError]); + + return ( + <> +
+ Daily.co +
+
+

Ready to join?

+
+
+
+ + + {isLoading && ( +
+ Loading devices, please wait... +
+ )} + {hasError && ( + <> + {camError && ( +
{camErrorVerbose}
+ )} + {micError && ( +
{micError}
+ )} + + )} +
+
+ + +
+ {tileMemo} +
+
+ {waiting ? ( +
+ Waiting for host to grant access +
+ ) : ( + <> + setUserName(e.target.value)} + /> + + + )} +
+
+ + +
+ + ); +}; + +export default HairCheck; diff --git a/dailyjs/basic-call/components/HairCheck/index.js b/dailyjs/basic-call/components/HairCheck/index.js new file mode 100644 index 0000000..b7e878a --- /dev/null +++ b/dailyjs/basic-call/components/HairCheck/index.js @@ -0,0 +1 @@ +export { HairCheck as default } from './HairCheck'; diff --git a/dailyjs/basic-call/components/Intro/Intro.js b/dailyjs/basic-call/components/Intro/Intro.js new file mode 100644 index 0000000..706ea4d --- /dev/null +++ b/dailyjs/basic-call/components/Intro/Intro.js @@ -0,0 +1,74 @@ +import React, { useEffect, useState } from 'react'; +import { Button } from '@dailyjs/shared/components/Button'; +import { + Card, + CardBody, + CardFooter, + CardHeader, +} from '@dailyjs/shared/components/Card'; +import Field from '@dailyjs/shared/components/Field'; +import { TextInput, BooleanInput } from '@dailyjs/shared/components/Input'; +import { Well } from '@dailyjs/shared/components/Well'; +import PropTypes from 'prop-types'; + +/** + * Intro + * --- + * Specify which room we would like to join + */ +export const Intro = ({ room, error, domain, onJoin, fetching = false }) => { + const [roomName, setRoomName] = useState(); + const [owner, setOwner] = useState(false); + const [fetchToken, setFetchToken] = useState(false); + + useEffect(() => { + setRoomName(room); + }, [room]); + + return ( + + Daily Basic Call Example + + {error && ( + + Failed to obtain token

{error}

+
+ )} + + setRoomName(e.target.value)} + required + /> + + + setFetchToken(e.target.checked)} /> + + + setOwner(e.target.checked)} /> + +
+ + + +
+ ); +}; + +Intro.propTypes = { + room: PropTypes.string, + error: PropTypes.string, + domain: PropTypes.string.isRequired, + onJoin: PropTypes.func.isRequired, + fetching: PropTypes.bool, +}; + +export default Intro; diff --git a/dailyjs/basic-call/components/Intro/NotConfigured.js b/dailyjs/basic-call/components/Intro/NotConfigured.js new file mode 100644 index 0000000..75a32fd --- /dev/null +++ b/dailyjs/basic-call/components/Intro/NotConfigured.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { Card, CardBody, CardHeader } from '@dailyjs/shared/components/Card'; + +export const NotConfigured = () => ( + + Environmental variables not set + +

+ Please ensure you have set both the DAILY_API_KEY and{' '} + DAILY_DOMAIN environmental variables. An example can be + found in the provided env.example file. +

+

+ If you do not yet have a Daily developer account, please{' '} + + create one now + + . You can find your Daily API key on the{' '} + + developer page + {' '} + of the dashboard. +

+
+
+); + +export default NotConfigured; diff --git a/dailyjs/basic-call/components/Intro/index.js b/dailyjs/basic-call/components/Intro/index.js new file mode 100644 index 0000000..7994320 --- /dev/null +++ b/dailyjs/basic-call/components/Intro/index.js @@ -0,0 +1,3 @@ +export { Intro as default } from './Intro'; +export { Intro } from './Intro'; +export { NotConfigured } from './NotConfigured'; diff --git a/dailyjs/basic-call/components/MessageCard/MessageCard.js b/dailyjs/basic-call/components/MessageCard/MessageCard.js new file mode 100644 index 0000000..ec63a43 --- /dev/null +++ b/dailyjs/basic-call/components/MessageCard/MessageCard.js @@ -0,0 +1,39 @@ +import React from 'react'; +import Button from '@dailyjs/shared/components/Button'; +import { + Card, + CardBody, + CardFooter, + CardHeader, +} from '@dailyjs/shared/components/Card'; +import PropTypes from 'prop-types'; + +export const MessageCard = ({ header, children, error = false, onBack }) => ( + + {header && {header}} + {children && {children}} + + {onBack ? ( + + ) : ( + + )} + + {error && ( + + )} + +); + +MessageCard.propTypes = { + header: PropTypes.string, + children: PropTypes.node, + error: PropTypes.bool, + onBack: PropTypes.func, +}; + +export default MessageCard; diff --git a/dailyjs/basic-call/components/MessageCard/index.js b/dailyjs/basic-call/components/MessageCard/index.js new file mode 100644 index 0000000..a6cbe58 --- /dev/null +++ b/dailyjs/basic-call/components/MessageCard/index.js @@ -0,0 +1 @@ +export { MessageCard as default } from './MessageCard'; diff --git a/dailyjs/basic-call/components/Room/Header.js b/dailyjs/basic-call/components/Room/Header.js new file mode 100644 index 0000000..401d272 --- /dev/null +++ b/dailyjs/basic-call/components/Room/Header.js @@ -0,0 +1,49 @@ +import React, { useMemo } from 'react'; +import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; + +export const Header = () => { + const { participantCount } = useParticipants(); + + return useMemo( + () => ( +
+ Daily +
Basic call demo
+
+ {`${participantCount} ${ + participantCount > 1 ? 'participants' : 'participant' + }`} +
+ + +
+ ), + [participantCount] + ); +}; + +export default Header; diff --git a/dailyjs/basic-call/components/Room/Room.js b/dailyjs/basic-call/components/Room/Room.js new file mode 100644 index 0000000..ec98886 --- /dev/null +++ b/dailyjs/basic-call/components/Room/Room.js @@ -0,0 +1,97 @@ +import React from 'react'; +import { useCallState } from '@dailyjs/shared/contexts/CallProvider'; +import { useMediaDevices } from '@dailyjs/shared/contexts/MediaDeviceProvider'; +import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; +import { ReactComponent as IconAdd } from '@dailyjs/shared/icons/add-md.svg'; +import { ReactComponent as IconCameraOff } from '@dailyjs/shared/icons/camera-off-md.svg'; +import { ReactComponent as IconCameraOn } from '@dailyjs/shared/icons/camera-on-md.svg'; +import { ReactComponent as IconLeave } from '@dailyjs/shared/icons/leave-md.svg'; +import { ReactComponent as IconMicOff } from '@dailyjs/shared/icons/mic-off-md.svg'; +import { ReactComponent as IconMicOn } from '@dailyjs/shared/icons/mic-on-md.svg'; +import { ReactComponent as IconSettings } from '@dailyjs/shared/icons/settings-md.svg'; +import PropTypes from 'prop-types'; + +import { Audio } from '../Audio'; +import { VideoGrid } from '../VideoGrid'; +import { Header } from './Header'; +import { Tray, TrayButton } from './Tray'; + +export const Room = ({ onLeave }) => { + const { callObject, addFakeParticipant } = useCallState(); + const { setShowDeviceModal } = useUIState(); + const { isCamMuted, isMicMuted } = useMediaDevices(); + + const toggleCamera = (newState) => { + if (!callObject) return false; + return callObject.setLocalVideo(newState); + }; + + const toggleMic = (newState) => { + if (!callObject) return false; + return callObject.setLocalAudio(newState); + }; + + return ( +
+
+ +
+ +
+ + + toggleCamera(isCamMuted)} + orange={isCamMuted} + > + {isCamMuted ? : } + + toggleMic(isMicMuted)} + orange={isMicMuted} + > + {isMicMuted ? : } + + setShowDeviceModal(true)}> + + + addFakeParticipant()}> + + + + + + + + +
+ ); +}; + +Room.propTypes = { + onLeave: PropTypes.func.isRequired, +}; + +export default Room; diff --git a/dailyjs/basic-call/components/Room/Tray.js b/dailyjs/basic-call/components/Room/Tray.js new file mode 100644 index 0000000..e6fcf1c --- /dev/null +++ b/dailyjs/basic-call/components/Room/Tray.js @@ -0,0 +1,65 @@ +import React from 'react'; +import Button from '@dailyjs/shared/components/Button'; +import PropTypes from 'prop-types'; + +export const TrayButton = ({ children, label, onClick, orange = false }) => ( +
+ + {label} + + +
+); + +TrayButton.propTypes = { + children: PropTypes.node, + onClick: PropTypes.func, + orange: PropTypes.bool, + label: PropTypes.string.isRequired, +}; + +export const Tray = ({ children }) => ( +
+ {children} + +
+); + +Tray.propTypes = { + children: PropTypes.node, +}; + +export default Tray; diff --git a/dailyjs/basic-call/components/Room/index.js b/dailyjs/basic-call/components/Room/index.js new file mode 100644 index 0000000..ebab667 --- /dev/null +++ b/dailyjs/basic-call/components/Room/index.js @@ -0,0 +1 @@ +export { Room as default } from './Room'; diff --git a/dailyjs/basic-call/components/VideoGrid/VideoGrid.js b/dailyjs/basic-call/components/VideoGrid/VideoGrid.js new file mode 100644 index 0000000..223b424 --- /dev/null +++ b/dailyjs/basic-call/components/VideoGrid/VideoGrid.js @@ -0,0 +1,163 @@ +import React, { useState, useMemo, useEffect, useRef } from 'react'; +import Tile from '@dailyjs/shared/components/Tile'; +import { DEFAULT_ASPECT_RATIO } from '@dailyjs/shared/constants'; +import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider'; +import { useTracks } from '@dailyjs/shared/contexts/TracksProvider'; +import { useDeepCompareMemo } from 'use-deep-compare'; + +export const VideoGrid = React.memo( + () => { + const containerRef = useRef(); + const { allParticipants } = useParticipants(); + const { resumeVideoTrack } = useTracks(); + const [dimensions, setDimensions] = useState({ + width: 1, + height: 1, + }); + + useEffect(() => { + let frame; + const handleResize = () => { + if (frame) cancelAnimationFrame(frame); + frame = requestAnimationFrame(() => + setDimensions({ + width: containerRef.current?.clientWidth, + height: containerRef.current?.clientHeight, + }) + ); + }; + handleResize(); + window.addEventListener('resize', handleResize); + window.addEventListener('orientationchange', handleResize); + return () => { + window.removeEventListener('resize', handleResize); + window.removeEventListener('orientationchange', handleResize); + }; + }, []); + + const layout = useMemo(() => { + const aspectRatio = DEFAULT_ASPECT_RATIO; + const tileCount = allParticipants.length || 0; + const w = dimensions.width; + const h = dimensions.height; + + // brute-force search layout where video occupy the largest area of the container + let bestLayout = { + area: 0, + cols: 0, + rows: 0, + width: 0, + height: 0, + }; + + for (let cols = 0; cols <= tileCount; cols += 1) { + const rows = Math.ceil(tileCount / cols); + const hScale = w / (cols * aspectRatio); + const vScale = h / rows; + let width; + let height; + if (hScale <= vScale) { + width = Math.floor(w / cols); + height = Math.floor(width / aspectRatio); + } else { + height = Math.floor(h / rows); + width = Math.floor(height * aspectRatio); + } + const area = width * height; + if (area > bestLayout.area) { + bestLayout = { + area, + width, + height, + rows, + cols, + }; + } + } + + return bestLayout; + }, [dimensions, allParticipants]); + + const tiles = useDeepCompareMemo( + () => + allParticipants.map((p) => ( + + )), + [layout, allParticipants] + ); + + /** + * Set bandwidth layer based on amount of visible participants + */ + useEffect(() => { + if ( + typeof rtcpeers === 'undefined' || + // eslint-disable-next-line no-undef + rtcpeers?.getCurrentType() !== 'sfu' + ) + return; + + // eslint-disable-next-line no-undef + const sfu = rtcpeers.soup; + const count = allParticipants.length; + + allParticipants.forEach(({ id }) => { + if (count < 5) { + // High quality video for calls with < 5 people per page + sfu.setPreferredLayerForTrack(id, 'cam-video', 2); + } else if (count < 10) { + // Medium quality video for calls with < 10 people per page + sfu.setPreferredLayerForTrack(id, 'cam-video', 1); + } else { + // Low quality video for calls with 10 or more people per page + sfu.setPreferredLayerForTrack(id, 'cam-video', 0); + } + }); + }, [allParticipants]); + + useEffect(() => { + allParticipants.forEach( + (p) => p.id !== 'local' && resumeVideoTrack(p.id) + ); + }, [allParticipants, resumeVideoTrack]); + + if (!allParticipants.length) { + return null; + } + + return ( +
+
{tiles}
+ +
+ ); + }, + () => true +); + +export default VideoGrid; diff --git a/dailyjs/basic-call/components/VideoGrid/index.js b/dailyjs/basic-call/components/VideoGrid/index.js new file mode 100644 index 0000000..5285614 --- /dev/null +++ b/dailyjs/basic-call/components/VideoGrid/index.js @@ -0,0 +1,2 @@ +export { VideoGrid as default } from './VideoGrid'; +export { VideoGrid } from './VideoGrid'; diff --git a/dailyjs/basic-call/env.example b/dailyjs/basic-call/env.example new file mode 100644 index 0000000..09c847a --- /dev/null +++ b/dailyjs/basic-call/env.example @@ -0,0 +1,8 @@ +# Domain excluding 'https://' and 'daily.co' e.g. 'somedomain' +DAILY_DOMAIN= + +# Obtained from https://dashboard.staging.daily.co/developers +DAILY_API_KEY= + +# Daily REST API endpoint +DAILY_REST_DOMAIN=https://api.daily.co/v1 \ No newline at end of file diff --git a/dailyjs/basic-call/image.png b/dailyjs/basic-call/image.png new file mode 100644 index 0000000..5d8604d Binary files /dev/null and b/dailyjs/basic-call/image.png differ diff --git a/dailyjs/basic-call/next.config.js b/dailyjs/basic-call/next.config.js new file mode 100644 index 0000000..c223d3f --- /dev/null +++ b/dailyjs/basic-call/next.config.js @@ -0,0 +1,4 @@ +const withPlugins = require('next-compose-plugins'); +const withTM = require('next-transpile-modules')(['@dailyjs/shared']); + +module.exports = withPlugins([withTM]); diff --git a/dailyjs/basic-call/package.json b/dailyjs/basic-call/package.json new file mode 100644 index 0000000..01031e1 --- /dev/null +++ b/dailyjs/basic-call/package.json @@ -0,0 +1,23 @@ +{ + "name": "@dailyjs/basic-call", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start" + }, + "dependencies": { + "@dailyjs/shared": "*", + "debounce": "^1.2.1", + "next": "^10.2.0", + "pluralize": "^8.0.0", + "react": "^17.0.2", + "react-dom": "^17.0.2" + }, + "devDependencies": { + "babel-plugin-module-resolver": "^4.1.0", + "next-compose-plugins": "^2.2.1", + "next-transpile-modules": "^6.4.0" + } +} diff --git a/dailyjs/basic-call/pages/_app.js b/dailyjs/basic-call/pages/_app.js new file mode 100644 index 0000000..3237e3a --- /dev/null +++ b/dailyjs/basic-call/pages/_app.js @@ -0,0 +1,33 @@ +import React from 'react'; +import GlobalStyle from '@dailyjs/shared/components/GlobalStyle'; +import Head from 'next/head'; +import PropTypes from 'prop-types'; + +function App({ Component, pageProps }) { + return ( + <> + + Daily - Basic Call Example + + + + + + + ); +} + +App.defaultProps = { + Component: null, + pageProps: {}, +}; + +App.propTypes = { + Component: PropTypes.elementType, + pageProps: PropTypes.object, +}; + +export default App; diff --git a/dailyjs/basic-call/pages/api/token.js b/dailyjs/basic-call/pages/api/token.js new file mode 100644 index 0000000..617a9cf --- /dev/null +++ b/dailyjs/basic-call/pages/api/token.js @@ -0,0 +1,38 @@ +/* + * This is an example server-side function that generates a meeting token + * server-side. You could replace this on your own back-end to include + * custom user authentication, etc. + */ +export default async function handler(req, res) { + const { roomName, isOwner } = req.body; + + if (req.method === 'POST' && roomName) { + console.log(`Getting token for room '${roomName}' as owner: ${isOwner}`); + + const options = { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.DAILY_API_KEY}`, + }, + body: JSON.stringify({ + properties: { room_name: roomName, is_owner: isOwner }, + }), + }; + + const dailyRes = await fetch( + `${process.env.DAILY_REST_DOMAIN}/meeting-tokens`, + options + ); + + const { token, error } = await dailyRes.json(); + + if (error) { + return res.status(500).json({ error }); + } + + return res.status(200).json({ token, domain: process.env.DAILY_DOMAIN }); + } + + return res.status(500); +} diff --git a/dailyjs/basic-call/pages/index.js b/dailyjs/basic-call/pages/index.js new file mode 100644 index 0000000..3f0250e --- /dev/null +++ b/dailyjs/basic-call/pages/index.js @@ -0,0 +1,121 @@ +import React, { useState, useCallback } from 'react'; +import { CallProvider } from '@dailyjs/shared/contexts/CallProvider'; +import { MediaDeviceProvider } from '@dailyjs/shared/contexts/MediaDeviceProvider'; +import { ParticipantsProvider } from '@dailyjs/shared/contexts/ParticipantsProvider'; +import { TracksProvider } from '@dailyjs/shared/contexts/TracksProvider'; +import { UIStateProvider } from '@dailyjs/shared/contexts/UIStateProvider'; +import PropTypes from 'prop-types'; +import App from '../components/App'; +import { Intro, NotConfigured } from '../components/Intro'; + +/** + * Index page + * --- + * - Checks configuration variables are set in local env + * - Optionally obtain a meeting token from Daily REST API (./pages/api/token) + * - Set call owner status + * - Finally, renders the main application loop + */ +export default function Index({ domain, isConfigured = false }) { + const [roomName, setRoomName] = useState(''); + const [fetchingToken, setFetchingToken] = useState(false); + const [token, setToken] = useState(); + const [tokenError, setTokenError] = useState(); + + const getMeetingToken = useCallback(async (room, isOwner = false) => { + if (!room) { + return false; + } + + setFetchingToken(true); + + // Fetch token from serverside method (provided by Next) + const res = await fetch('/api/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ roomName: room, isOwner }), + }); + const resJson = await res.json(); + + if (!resJson?.token) { + setTokenError(resJson?.error || true); + setFetchingToken(false); + return false; + } + + console.log(`πŸͺ™ Token received`); + + setFetchingToken(false); + setToken(resJson.token); + + // Setting room name will change ready state + setRoomName(room); + + return true; + }, []); + + const isReady = !!(isConfigured && roomName); + + if (!isReady) { + return ( +
+ {!isConfigured ? ( + + ) : ( + + fetchToken ? getMeetingToken(room, isOwner) : setRoomName(room) + } + /> + )} + + +
+ ); + } + + /** + * Main call UI + */ + return ( + + + + + + + + + + + + ); +} + +Index.propTypes = { + isConfigured: PropTypes.bool.isRequired, + domain: PropTypes.string, +}; + +export async function getStaticProps() { + // Check that both domain and key env vars are set + const isConfigured = + !!process.env.DAILY_DOMAIN && !!process.env.DAILY_API_KEY; + + // Pass through domain as prop + return { + props: { domain: process.env.DAILY_DOMAIN || null, isConfigured }, + }; +} diff --git a/dailyjs/basic-call/pages/not-found.js b/dailyjs/basic-call/pages/not-found.js new file mode 100644 index 0000000..239a97c --- /dev/null +++ b/dailyjs/basic-call/pages/not-found.js @@ -0,0 +1,21 @@ +import React from 'react'; +import MessageCard from '../components/MessageCard'; + +export default function RoomNotFound() { + return ( +
+ + The room you are trying to join does not exist. Have you created the + room using the Daily REST API or the dashboard? + + +
+ ); +} diff --git a/dailyjs/basic-call/public/images/daily-logo.svg b/dailyjs/basic-call/public/images/daily-logo.svg new file mode 100644 index 0000000..534a18a --- /dev/null +++ b/dailyjs/basic-call/public/images/daily-logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/dailyjs/basic-call/public/images/pattern-bg.png b/dailyjs/basic-call/public/images/pattern-bg.png new file mode 100644 index 0000000..01e0d0d Binary files /dev/null and b/dailyjs/basic-call/public/images/pattern-bg.png differ diff --git a/dailyjs/shared/components/Button/Button.js b/dailyjs/shared/components/Button/Button.js new file mode 100644 index 0000000..64187d2 --- /dev/null +++ b/dailyjs/shared/components/Button/Button.js @@ -0,0 +1,240 @@ +import React, { forwardRef } from 'react'; +import classnames from 'classnames'; +import Link from 'next/link'; +import PropTypes from 'prop-types'; +import theme from '../../styles/defaultTheme'; +import { hexa } from '../../styles/global'; + +export const Button = forwardRef( + ( + { + children, + className, + disabled = false, + fullWidth, + href, + IconAfter = null, + IconBefore = null, + loading = false, + size = 'medium', + type = 'button', + variant = 'primary', + shadow = false, + ...rest + }, + ref + ) => { + const cx = classnames('button', className, size, variant, { + disabled, + fullWidth, + loading, + shadow, + }); + + const content = ( + <> + {IconBefore && } + {children} + {IconAfter && } + + ); + + return ( + <> + {href ? ( + + + {content} + + + ) : ( + + )} + + + ); + } +); + +Button.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + disabled: PropTypes.bool, + fullWidth: PropTypes.bool, + href: PropTypes.string, + IconAfter: PropTypes.node, + IconBefore: PropTypes.node, + loading: PropTypes.bool, + size: PropTypes.string, + type: PropTypes.oneOf(['button', 'reset', 'submit']), + variant: PropTypes.string, + shadow: PropTypes.bool, +}; + +export default Button; diff --git a/dailyjs/shared/components/Button/index.js b/dailyjs/shared/components/Button/index.js new file mode 100644 index 0000000..e229288 --- /dev/null +++ b/dailyjs/shared/components/Button/index.js @@ -0,0 +1,2 @@ +export { Button } from './Button'; +export { Button as default } from './Button'; diff --git a/dailyjs/shared/components/Card/Card.js b/dailyjs/shared/components/Card/Card.js new file mode 100644 index 0000000..607d6a5 --- /dev/null +++ b/dailyjs/shared/components/Card/Card.js @@ -0,0 +1,75 @@ +import React from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; + +export const Card = ({ children }) => ( +
+ {children} + +
+); + +Card.propTypes = { + children: PropTypes.node, +}; + +export const CardHeader = ({ children }) => ( +
+

{children}

+ +
+); +CardHeader.propTypes = { + children: PropTypes.node, +}; + +export const CardBody = ({ children }) => ( +
+ {children} + +
+); +CardBody.propTypes = { + children: PropTypes.node, +}; + +export const CardFooter = ({ children, divider = false }) => ( +
+ {children} + +
+); +CardFooter.propTypes = { + children: PropTypes.node, + divider: PropTypes.bool, +}; + +export default Card; diff --git a/dailyjs/shared/components/Card/index.js b/dailyjs/shared/components/Card/index.js new file mode 100644 index 0000000..2ce9882 --- /dev/null +++ b/dailyjs/shared/components/Card/index.js @@ -0,0 +1 @@ +export { Card, CardHeader, CardBody, CardFooter } from './Card'; diff --git a/dailyjs/shared/components/DeviceSelect/DeviceSelect.js b/dailyjs/shared/components/DeviceSelect/DeviceSelect.js new file mode 100644 index 0000000..c089280 --- /dev/null +++ b/dailyjs/shared/components/DeviceSelect/DeviceSelect.js @@ -0,0 +1,76 @@ +import React from 'react'; +import { useMediaDevices } from '@dailyjs/shared/contexts/MediaDeviceProvider'; +import { Field } from '../Field'; +import { SelectInput } from '../Input'; + +export const DeviceSelect = () => { + const { + cams, + mics, + speakers, + currentDevices, + setCamDevice, + setMicDevice, + setSpeakersDevice, + } = useMediaDevices(); + + if (!currentDevices) { + return
Loading devices...
; + } + + return ( + <> + + setCamDevice(cams[e.target.value])} + value={cams.findIndex( + (i) => i.deviceId === currentDevices.camera.deviceId + )} + > + {cams.map(({ deviceId, label }, i) => ( + + ))} + + + + + setMicDevice(mics[e.target.value])} + value={mics.findIndex( + (i) => i.deviceId === currentDevices.mic.deviceId + )} + > + {mics.map(({ deviceId, label }, i) => ( + + ))} + + + + {/** + * Note: Safari does not support selection audio output devices + */} + {speakers.length > 0 && ( + + setSpeakersDevice(speakers[e.target.value])} + value={speakers.findIndex( + (i) => i.deviceId === currentDevices.speaker.deviceId + )} + > + {speakers.map(({ deviceId, label }, i) => ( + + ))} + + + )} + + ); +}; + +export default DeviceSelect; diff --git a/dailyjs/shared/components/DeviceSelect/index.js b/dailyjs/shared/components/DeviceSelect/index.js new file mode 100644 index 0000000..8929c5c --- /dev/null +++ b/dailyjs/shared/components/DeviceSelect/index.js @@ -0,0 +1,2 @@ +export { DeviceSelect as default } from './DeviceSelect'; +export { DeviceSelect } from './DeviceSelect'; diff --git a/dailyjs/shared/components/DeviceSelectModal/DeviceSelectModal.js b/dailyjs/shared/components/DeviceSelectModal/DeviceSelectModal.js new file mode 100644 index 0000000..9f87bc4 --- /dev/null +++ b/dailyjs/shared/components/DeviceSelectModal/DeviceSelectModal.js @@ -0,0 +1,27 @@ +import React from 'react'; +import Modal from '@dailyjs/shared/components/Modal'; +import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider'; +import { Button } from '../Button'; +import { DeviceSelect } from '../DeviceSelect'; + +export const DeviceSelectModal = () => { + const { showDeviceModal, setShowDeviceModal } = useUIState(); + + return ( + setShowDeviceModal(false)} + actions={[ + , + , + ]} + > + + + ); +}; + +export default DeviceSelectModal; diff --git a/dailyjs/shared/components/DeviceSelectModal/index.js b/dailyjs/shared/components/DeviceSelectModal/index.js new file mode 100644 index 0000000..3ea8747 --- /dev/null +++ b/dailyjs/shared/components/DeviceSelectModal/index.js @@ -0,0 +1,2 @@ +export { DeviceSelectModal as default } from './DeviceSelectModal'; +export { DeviceSelectModal } from './DeviceSelectModal'; diff --git a/dailyjs/shared/components/Field/Field.js b/dailyjs/shared/components/Field/Field.js new file mode 100644 index 0000000..e2289c9 --- /dev/null +++ b/dailyjs/shared/components/Field/Field.js @@ -0,0 +1,28 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export const Field = ({ label, children }) => ( +
+ {label &&
{label}
} +
{children}
+ + +
+); + +Field.propTypes = { + label: PropTypes.string, + children: PropTypes.node, +}; + +export default Field; diff --git a/dailyjs/shared/components/Field/index.js b/dailyjs/shared/components/Field/index.js new file mode 100644 index 0000000..5308bf3 --- /dev/null +++ b/dailyjs/shared/components/Field/index.js @@ -0,0 +1,2 @@ +export { Field as default } from './Field'; +export { Field } from './Field'; diff --git a/dailyjs/shared/components/GlobalStyle/GlobalStyle.js b/dailyjs/shared/components/GlobalStyle/GlobalStyle.js new file mode 100644 index 0000000..afc1b2a --- /dev/null +++ b/dailyjs/shared/components/GlobalStyle/GlobalStyle.js @@ -0,0 +1,121 @@ +import React from 'react'; +import theme from '../../styles/defaultTheme'; +import { hexa } from '../../styles/global'; + +export const GlobalStyle = () => ( + +); + +export default GlobalStyle; diff --git a/dailyjs/shared/components/GlobalStyle/index.js b/dailyjs/shared/components/GlobalStyle/index.js new file mode 100644 index 0000000..bfd861a --- /dev/null +++ b/dailyjs/shared/components/GlobalStyle/index.js @@ -0,0 +1 @@ +export { GlobalStyle as default } from './GlobalStyle'; diff --git a/dailyjs/shared/components/Input/Input.js b/dailyjs/shared/components/Input/Input.js new file mode 100644 index 0000000..7935c16 --- /dev/null +++ b/dailyjs/shared/components/Input/Input.js @@ -0,0 +1,271 @@ +import React, { useState } from 'react'; +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import theme from '../../styles/defaultTheme'; +import { hexa } from '../../styles/global'; + +const InputContainer = ({ children, prefix, className }) => ( +
+ {prefix && {prefix}} + {children} + +
+); + +InputContainer.propTypes = { + children: PropTypes.node, + className: PropTypes.string, + prefix: PropTypes.string, +}; + +export const TextInput = ({ onChange, prefix, variant, ...rest }) => { + const cx = classNames('input-container', variant, { prefix }); + + return ( + + + + ); +}; + +TextInput.propTypes = { + onChange: PropTypes.func, + prefix: PropTypes.string, + variant: PropTypes.string, +}; + +export const BooleanInput = ({ + value = false, + onChange = () => null, + disabled = false, + ...rest +}) => { + const [checked, setChecked] = useState(value); + + return ( + // eslint-disable-next-line +