diff --git a/custom/basic-call/components/Call/Container.js b/custom/basic-call/components/Call/Container.js index 22e0d08..5e9cb68 100644 --- a/custom/basic-call/components/Call/Container.js +++ b/custom/basic-call/components/Call/Container.js @@ -2,7 +2,7 @@ import React, { useMemo } from 'react'; import { Audio } from '@custom/shared/components/Audio'; import { BasicTray } from '@custom/shared/components/Tray'; import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider'; -import useJoinSound from '@custom/shared/hooks/useJoinSound'; +import { useJoinSound } from '@custom/shared/hooks/useJoinSound'; import PropTypes from 'prop-types'; import { WaitingRoom } from './WaitingRoom'; diff --git a/custom/fitness-demo/.babelrc b/custom/fitness-demo/.babelrc new file mode 100644 index 0000000..a6f4434 --- /dev/null +++ b/custom/fitness-demo/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["inline-react-svg"] +} diff --git a/custom/fitness-demo/.gitignore b/custom/fitness-demo/.gitignore new file mode 100644 index 0000000..058f0ec --- /dev/null +++ b/custom/fitness-demo/.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/custom/fitness-demo/README.md b/custom/fitness-demo/README.md new file mode 100644 index 0000000..1bec2c5 --- /dev/null +++ b/custom/fitness-demo/README.md @@ -0,0 +1,55 @@ +# Fitness Demo + +![Fitness Demo](./image.png) + +### Live example + +**[See it in action here ➑️](https://custom-fitness-demo.vercel.app)** + +--- + +## 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 @custom/fitness-demo dev +``` + +## How does this example work? + +This demo puts to work the following [shared libraries](../shared): + +**[MediaDeviceProvider.js](../shared/contexts/MediaDeviceProvider.js)** +Convenience context that provides an interface to media devices throughout app + +**[useDevices.js](../shared/contexts/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/contexts/useCallMachine.js)** +Abstraction hook that manages Daily call state and error handling + +**[ParticipantsProvider.js](../shared/contexts/ParticipantsProvider.js)** +Manages participant state and abstracts common selectors / derived data + +## Deploy your own on Vercel + +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples) diff --git a/custom/fitness-demo/components/App/App.js b/custom/fitness-demo/components/App/App.js new file mode 100644 index 0000000..a86b453 --- /dev/null +++ b/custom/fitness-demo/components/App/App.js @@ -0,0 +1,63 @@ +import React, { useMemo } from 'react'; +import { LiveStreamingProvider } from '@custom/live-streaming/contexts/LiveStreamingProvider'; +import { RecordingProvider } from '@custom/recording/contexts/RecordingProvider'; +import ExpiryTimer from '@custom/shared/components/ExpiryTimer'; +import { useCallState } from '@custom/shared/contexts/CallProvider'; +import { useCallUI } from '@custom/shared/hooks/useCallUI'; +import PropTypes from 'prop-types'; + +import { ChatProvider } from '../../contexts/ChatProvider'; +import { ClassStateProvider } from '../../contexts/ClassStateProvider'; +import Room from '../Call/Room'; +import { Asides } from './Asides'; +import { Modals } from './Modals'; + +export const App = ({ customComponentForState }) => { + const { roomExp, state } = useCallState(); + + const componentForState = useCallUI({ + state, + room: , + ...customComponentForState, + }); + + // Memoize children to avoid unnecassary renders from HOC + return useMemo( + () => ( + <> + + + + + {roomExp && } +
+ {componentForState()} + + + +
+
+
+
+
+ + ), + [componentForState, roomExp] + ); +}; + +App.propTypes = { + customComponentForState: PropTypes.any, +}; + +export default App; \ No newline at end of file diff --git a/custom/fitness-demo/components/App/AsideHeader.js b/custom/fitness-demo/components/App/AsideHeader.js new file mode 100644 index 0000000..ca354dc --- /dev/null +++ b/custom/fitness-demo/components/App/AsideHeader.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { PEOPLE_ASIDE } from '@custom/shared/components/Aside/PeopleAside'; +import { useUIState } from '@custom/shared/contexts/UIStateProvider'; +import { CHAT_ASIDE } from '../Call/ChatAside'; + +export const AsideHeader = () => { + const { showAside, setShowAside } = useUIState(); + + return ( + <> +
+
setShowAside(PEOPLE_ASIDE)} + > +

People

+
+
setShowAside(CHAT_ASIDE)} + > +

Chat

+
+
+ + + ) +}; + +export default AsideHeader; \ No newline at end of file diff --git a/custom/fitness-demo/components/App/Asides.js b/custom/fitness-demo/components/App/Asides.js new file mode 100644 index 0000000..855bc0e --- /dev/null +++ b/custom/fitness-demo/components/App/Asides.js @@ -0,0 +1,20 @@ +import React from 'react'; +import { NetworkAside } from '@custom/shared/components/Aside'; +import { useUIState } from '@custom/shared/contexts/UIStateProvider'; +import { PeopleAside } from '../Call/PeopleAside'; + +export const Asides = () => { + const { asides } = useUIState(); + + return ( + <> + + + {asides.map((AsideComponent) => ( + + ))} + + ); +}; + +export default Asides; diff --git a/custom/fitness-demo/components/App/Modals.js b/custom/fitness-demo/components/App/Modals.js new file mode 100644 index 0000000..ba5c27c --- /dev/null +++ b/custom/fitness-demo/components/App/Modals.js @@ -0,0 +1,18 @@ +import React from 'react'; +import DeviceSelectModal from '@custom/shared/components/DeviceSelectModal/DeviceSelectModal'; +import { useUIState } from '@custom/shared/contexts/UIStateProvider'; + +export const Modals = () => { + const { modals } = useUIState(); + + return ( + <> + + {modals.map((ModalComponent) => ( + + ))} + + ); +}; + +export default Modals; diff --git a/custom/fitness-demo/components/App/index.js b/custom/fitness-demo/components/App/index.js new file mode 100644 index 0000000..7e7372b --- /dev/null +++ b/custom/fitness-demo/components/App/index.js @@ -0,0 +1 @@ +export { App as default } from './App'; diff --git a/custom/fitness-demo/components/Call/ChatAside.js b/custom/fitness-demo/components/Call/ChatAside.js new file mode 100644 index 0000000..d2caa5f --- /dev/null +++ b/custom/fitness-demo/components/Call/ChatAside.js @@ -0,0 +1,172 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { Aside } from '@custom/shared/components/Aside'; +import Button from '@custom/shared/components/Button'; +import { TextInput } from '@custom/shared/components/Input'; +import { useUIState } from '@custom/shared/contexts/UIStateProvider'; +import { ReactComponent as IconEmoji } from '@custom/shared/icons/emoji-sm.svg'; +import { useMessageSound } from '@custom/text-chat/hooks/useMessageSound'; +import { useChat } from '../../contexts/ChatProvider'; +import AsideHeader from '../App/AsideHeader'; + +export const CHAT_ASIDE = 'chat'; + +export const ChatAside = () => { + const { showAside, setShowAside } = useUIState(); + const { sendMessage, chatHistory, hasNewMessages, setHasNewMessages } = + useChat(); + const [newMessage, setNewMessage] = useState(''); + const playMessageSound = useMessageSound(); + const [showEmojis, setShowEmojis] = useState(false); + + const emojis = ['😍', '😭', 'πŸ˜‚', 'πŸ‘‹', 'πŸ™']; + const chatWindowRef = useRef(); + + useEffect(() => { + // Clear out any new message notifications if we're showing the chat screen + if (showAside === CHAT_ASIDE) { + setHasNewMessages(false); + } + }, [showAside, chatHistory?.length, setHasNewMessages]); + + useEffect(() => { + if (hasNewMessages && showAside !== CHAT_ASIDE) { + playMessageSound(); + } + }, [playMessageSound, showAside, hasNewMessages]); + + useEffect(() => { + if (chatWindowRef.current) { + chatWindowRef.current.scrollTop = chatWindowRef.current.scrollHeight; + } + }, [chatHistory?.length]); + + if (!showAside || showAside !== CHAT_ASIDE) { + return null; + } + + return ( + + ); +}; + +export default ChatAside; diff --git a/custom/fitness-demo/components/Call/Container.js b/custom/fitness-demo/components/Call/Container.js new file mode 100644 index 0000000..5e9cb68 --- /dev/null +++ b/custom/fitness-demo/components/Call/Container.js @@ -0,0 +1,49 @@ +import React, { useMemo } from 'react'; +import { Audio } from '@custom/shared/components/Audio'; +import { BasicTray } from '@custom/shared/components/Tray'; +import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider'; +import { useJoinSound } from '@custom/shared/hooks/useJoinSound'; +import PropTypes from 'prop-types'; +import { WaitingRoom } from './WaitingRoom'; + +export const Container = ({ children }) => { + const { isOwner } = useParticipants(); + + useJoinSound(); + + const roomComponents = useMemo( + () => ( + <> + {/* Show waiting room notification & modal if call owner */} + {isOwner && } + {/* Tray buttons */} + + {/* Audio tags */} +