added RoomContainer abstraction

This commit is contained in:
Jon 2021-07-20 14:00:05 +01:00
parent 8461c3e028
commit db91e7f84e
27 changed files with 284 additions and 140 deletions

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useMemo } from 'react';
import React, { useMemo } from 'react';
import ExpiryTimer from '@dailyjs/shared/components/ExpiryTimer';
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
import { useCallUI } from '@dailyjs/shared/hooks/useCallUI';

View File

@ -1,64 +1,16 @@
import React from 'react';
import { Audio } from '@dailyjs/shared/components/Audio';
import { BasicTray } from '@dailyjs/shared/components/Tray';
import {
WaitingRoomModal,
WaitingRoomNotification,
} from '@dailyjs/shared/components/WaitingRoom';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider';
import useJoinSound from '@dailyjs/shared/hooks/useJoinSound';
import { VideoGrid } from '../VideoGrid';
import { Header } from './Header';
import RoomContainer from './RoomContainer';
export const Room = () => {
const { setShowModal, showModal } = useWaitingRoom();
const { localParticipant } = useParticipants();
useJoinSound();
return (
<div className="room">
<Header />
<main>
<VideoGrid />
</main>
{/* Show waiting room notification & modal if call owner */}
{localParticipant?.isOwner && (
<>
<WaitingRoomNotification />
{showModal && (
<WaitingRoomModal onClose={() => setShowModal(false)} />
)}
</>
)}
<BasicTray />
<Audio />
<style jsx>{`
.room {
flex-flow: column nowrap;
width: 100%;
height: 100%;
display: flex;
}
main {
flex: 1 1 auto;
position: relative;
overflow: hidden;
min-height: 0px;
height: 100%;
padding: var(--spacing-xxxs);
box-sizing: border-box;
}
`}</style>
</div>
);
};
export const Room = () => (
<RoomContainer>
<Header />
<main>
<VideoGrid />
</main>
</RoomContainer>
);
export default Room;

View File

@ -0,0 +1,62 @@
import React, { useMemo } from 'react';
import { Audio } from '@dailyjs/shared/components/Audio';
import { BasicTray } from '@dailyjs/shared/components/Tray';
import { useParticipants } from '@dailyjs/shared/contexts/ParticipantsProvider';
import useJoinSound from '@dailyjs/shared/hooks/useJoinSound';
import PropTypes from 'prop-types';
import WaitingRoom from '../WaitingRoom';
const RoomContainer = ({ children }) => {
const { localParticipant } = useParticipants();
const isOwner = !!localParticipant?.isOwner;
useJoinSound();
const roomComponents = useMemo(
() => (
<>
{/* Show waiting room notification & modal if call owner */}
{isOwner && <WaitingRoom />}
{/* Tray buttons */}
<BasicTray />
{/* Audio tags */}
<Audio />
</>
),
[isOwner]
);
return (
<div className="room">
{children}
{roomComponents}
<style jsx>{`
.room {
flex-flow: column nowrap;
width: 100%;
height: 100%;
display: flex;
}
.room > :global(main) {
flex: 1 1 auto;
position: relative;
overflow: hidden;
min-height: 0px;
height: 100%;
padding: var(--spacing-xxxs);
box-sizing: border-box;
}
`}</style>
</div>
);
};
RoomContainer.propTypes = {
children: PropTypes.node,
};
export default React.memo(RoomContainer, () => true);

View File

@ -0,0 +1,18 @@
import React from 'react';
import {
WaitingRoomModal,
WaitingRoomNotification,
} from '@dailyjs/shared/components/WaitingRoom';
import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider';
export const WaitingRoom = () => {
const { setShowModal, showModal } = useWaitingRoom();
return (
<>
<WaitingRoomNotification />
{showModal && <WaitingRoomModal onClose={() => setShowModal(false)} />}
</>
);
};
export default WaitingRoom;

View File

@ -0,0 +1 @@
export { WaitingRoom as default } from './WaitingRoom';

View File

@ -15,6 +15,7 @@ export default async function handler(req, res) {
properties: {
exp: Math.round(Date.now() / 1000) + (expiryMinutes || 5) * 60, // expire in x minutes
eject_at_room_exp: true,
enable_knocking: privacy !== 'public',
},
}),
};

View File

@ -81,7 +81,7 @@ export const FlyingEmojisOverlay = () => {
// Remove all event listeners on unmount to prevent console warnings
useEffect(
() => () =>
overlayRef.current.childNodes.forEach((n) =>
overlayRef.current?.childNodes.forEach((n) =>
n.removeEventListener('animationend', handleRemoveFlyingEmoji)
),
[handleRemoveFlyingEmoji]

View File

@ -0,0 +1 @@
// Note: I am here because next-transpile-modules requires a mainfile

View File

@ -1 +1,9 @@
# Live Fitness
Brings together
- Live streaming
- Recording
- Flying emojis
- Pagination / track management
- Text chat

View File

@ -0,0 +1,16 @@
import React from 'react';
import App from '@dailyjs/basic-call/components/App';
import FlyingEmojiOverlay from '@dailyjs/flying-emojis/components/FlyingEmojis';
import { ChatProvider } from '@dailyjs/text-chat/contexts/ChatProvider';
export const LiveFitnessApp = () => (
<>
<ChatProvider>
<FlyingEmojiOverlay />
<App />
</ChatProvider>
</>
);
export default LiveFitnessApp;

View File

@ -0,0 +1 @@
export { LiveFitnessApp as default } from './App';

View File

@ -0,0 +1,12 @@
import React from 'react';
import FlyingEmojiTrayButton from '@dailyjs/flying-emojis/components/Tray';
import ChatTrayButton from '@dailyjs/text-chat/components/Tray';
export const Tray = () => (
<>
<FlyingEmojiTrayButton />
<ChatTrayButton />
</>
);
export default Tray;

View File

@ -0,0 +1 @@
export { Tray as default } from './Tray';

View File

@ -2,6 +2,8 @@ const withPlugins = require('next-compose-plugins');
const withTM = require('next-transpile-modules')([
'@dailyjs/shared',
'@dailyjs/basic-call',
'@dailyjs/flying-emojis',
'@dailyjs/text-chat',
]);
const packageJson = require('./package.json');

View File

@ -12,6 +12,8 @@
"dependencies": {
"@dailyjs/shared": "*",
"@dailyjs/basic-call": "*",
"@dailyjs/flying-emojis": "*",
"@dailyjs/text-chat": "*",
"next": "^11.0.0",
"pluralize": "^8.0.0",
"react": "^17.0.2",

View File

@ -1,6 +1,4 @@
import React, { useState, useEffect } from 'react';
import App from '@dailyjs/basic-call/components/App';
import Loader from '@dailyjs/shared/components/Loader';
import { CallProvider } from '@dailyjs/shared/contexts/CallProvider';
import { MediaDeviceProvider } from '@dailyjs/shared/contexts/MediaDeviceProvider';
import { ParticipantsProvider } from '@dailyjs/shared/contexts/ParticipantsProvider';
@ -10,12 +8,19 @@ import { WaitingRoomProvider } from '@dailyjs/shared/contexts/WaitingRoomProvide
import getDemoProps from '@dailyjs/shared/lib/demoProps';
import { useRouter } from 'next/router';
import PropTypes from 'prop-types';
import App from '../components/App';
/**
* Room page
* ---
*/
export default function Room({ room, instructor, domain }) {
export default function Room({
room,
instructor,
domain,
customTrayComponent,
asides,
}) {
const [token, setToken] = useState();
const [tokenError, setTokenError] = useState();
@ -65,11 +70,15 @@ export default function Room({ room, instructor, domain }) {
return <div>Fetching token...</div>;
}
if (tokenError) {
return <div>Token error</div>;
}
/**
* Main call UI
*/
return (
<UIStateProvider>
<UIStateProvider customTrayComponent={customTrayComponent} asides={asides}>
<CallProvider domain={domain} room={room} token={token}>
<ParticipantsProvider>
<TracksProvider>
@ -89,6 +98,8 @@ Room.propTypes = {
room: PropTypes.string.isRequired,
domain: PropTypes.string.isRequired,
instructor: PropTypes.bool,
customTrayComponent: PropTypes.node,
asides: PropTypes.arrayOf(PropTypes.func),
};
export async function getServerSideProps(context) {

View File

@ -1,30 +1,9 @@
import React from 'react';
import GlobalHead from '@dailyjs/shared/components/GlobalHead';
import GlobalStyle from '@dailyjs/shared/components/GlobalStyle';
import Head from 'next/head';
import PropTypes from 'prop-types';
import App from '@dailyjs/basic-call/pages/_app';
import ChatAside from '@dailyjs/text-chat/components/ChatAside';
import Tray from '../components/Tray';
function App({ Component, pageProps }) {
return (
<>
<Head>
<title>Daily - {process.env.PROJECT_TITLE}</title>
</Head>
<GlobalHead />
<GlobalStyle />
<Component {...pageProps} />
</>
);
}
App.defaultProps = {
Component: null,
pageProps: {},
};
App.propTypes = {
Component: PropTypes.elementType,
pageProps: PropTypes.object,
};
App.customTrayComponent = <Tray />;
App.asides = [ChatAside];
export default App;

View File

@ -1,13 +0,0 @@
import React from 'react';
import App from '@dailyjs/basic-call/components/App';
import { LiveStreamingProvider } from '../../contexts/LiveStreamingProvider';
// Extend our basic call app component with the live streaming context
export const AppWithLiveStreaming = () => (
<LiveStreamingProvider>
<App />
</LiveStreamingProvider>
);
export default AppWithLiveStreaming;

View File

@ -1 +0,0 @@
export { AppWithLiveStreaming as default } from './App';

View File

@ -12,15 +12,13 @@ export const Tray = () => {
const { isStreaming } = useLiveStreaming();
return (
<>
<TrayButton
label={isStreaming ? 'Live' : 'Stream'}
orange={isStreaming}
onClick={() => openModal(LIVE_STREAMING_MODAL)}
>
<IconStream />
</TrayButton>
</>
<TrayButton
label={isStreaming ? 'Live' : 'Stream'}
orange={isStreaming}
onClick={() => openModal(LIVE_STREAMING_MODAL)}
>
<IconStream />
</TrayButton>
);
};

View File

@ -32,15 +32,13 @@ export const Tray = () => {
].includes(recordingState);
return (
<>
<TrayButton
label={isRecording ? 'Recording' : 'Record'}
orange={isRecording}
onClick={() => openModal(RECORDING_MODAL)}
>
<IconRecord />
</TrayButton>
</>
<TrayButton
label={isRecording ? 'Recording' : 'Record'}
orange={isRecording}
onClick={() => openModal(RECORDING_MODAL)}
>
<IconRecord />
</TrayButton>
);
};

View File

@ -15,7 +15,7 @@ export const MessageCard = ({
hideBack = false,
onBack,
}) => (
<Card>
<Card className={error && 'error'}>
{header && <CardHeader>{header}</CardHeader>}
{children && <CardBody>{children}</CardBody>}
{!hideBack && (
@ -27,13 +27,11 @@ export const MessageCard = ({
)}
</CardFooter>
)}
{error && (
<style jsx>{`
.card {
border: 3px solid var(--red-default);
}
`}</style>
)}
<style jsx>{`
:global(.card.error) {
border: 3px solid var(--red-default);
}
`}</style>
</Card>
);

View File

@ -0,0 +1,38 @@
import React from 'react';
import Button from '@dailyjs/shared/components/Button';
import PropTypes from 'prop-types';
export const TrayButton = ({ children, label, onClick, orange = false }) => (
<div className={orange ? 'tray-button orange' : 'tray-button'}>
<Button onClick={() => onClick()} variant="dark" size="large-square">
{children}
</Button>
<span>{label}</span>
<style jsx>{`
.tray-button {
text-align: center;
user-select: none;
}
.tray-button.orange :global(.button) {
color: var(--secondary-dark);
}
span {
color: white;
font-weight: var(--weight-medium);
font-size: 12px;
}
`}</style>
</div>
);
TrayButton.propTypes = {
children: PropTypes.node,
onClick: PropTypes.func,
orange: PropTypes.bool,
label: PropTypes.string.isRequired,
};
export default TrayButton;

View File

@ -0,0 +1,24 @@
import React from 'react';
import { TrayButton } from '@dailyjs/shared/components/Tray';
import { useAudioLevel } from '@dailyjs/shared/hooks/useAudioLevel';
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 PropTypes from 'prop-types';
export const TrayMicButton = ({ isMuted, onClick }) => {
const audioLevel = useAudioLevel('local');
return (
<TrayButton label="Mic" onClick={onClick} orange={isMuted}>
{isMuted ? <IconMicOff /> : <IconMicOn />}
</TrayButton>
);
};
TrayMicButton.propTypes = {
isMuted: PropTypes.bool,
onClick: PropTypes.func.isRequired,
};
export default TrayMicButton;

View File

@ -0,0 +1,36 @@
import { useEffect, useState } from 'react';
export const useAudioLevel = (sessionId) => {
const [audioLevel, setAudioLevel] = useState(0);
useEffect(() => {
if (!sessionId) {
return false;
}
const i = setInterval(async () => {
try {
if (!(window.rtcpeers && window.rtcpeers.sfu)) {
return;
}
const consumer =
window.rtcpeers.sfu.consumers[`${sessionId}/cam-audio`];
if (!(consumer && consumer.getStats)) {
return;
}
const level = Array.from((await consumer.getStats()).values()).find(
(s) => 'audioLevel' in s
).audioLevel;
setAudioLevel(level);
} catch (e) {
console.error(e);
}
}, 2000);
return () => clearInterval(i);
}, [sessionId]);
return audioLevel;
};
export default useAudioLevel;

View File

@ -11,17 +11,15 @@ export const Tray = () => {
const { hasNewMessages } = useChat();
return (
<>
<TrayButton
label="Chat"
bubble={hasNewMessages}
onClick={() => {
toggleAside(CHAT_ASIDE);
}}
>
<IconChat />
</TrayButton>
</>
<TrayButton
label="Chat"
bubble={hasNewMessages}
onClick={() => {
toggleAside(CHAT_ASIDE);
}}
>
<IconChat />
</TrayButton>
);
};

View File

@ -0,0 +1 @@
// Note: I am here because next-transpile-modules requires a mainfile