Merge pull request #2 from daily-demos/dailyjs/basic-call/lobby

added initial waiting room components and contexts
This commit is contained in:
Jon Taylor 2021-06-15 18:03:11 +01:00 committed by GitHub
commit 1573daedaf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 390 additions and 15 deletions

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react';
import React, { useState, useEffect, useMemo } from 'react';
import Button from '@dailyjs/shared/components/Button';
import { TextInput } from '@dailyjs/shared/components/Input';
import Loader from '@dailyjs/shared/components/Loader';
@ -37,7 +37,7 @@ export const HairCheck = () => {
callObject.startCamera();
}, [callObject]);
const joinCall = useCallback(async () => {
const joinCall = async () => {
if (!callObject) return;
// Disable join controls
@ -54,7 +54,7 @@ export const HairCheck = () => {
if (access?.level === ACCESS_STATE_LOBBY) {
setWaiting(true);
const { granted } = await callObject.requestAccess({
name: localParticipant?.name,
name: userName,
access: {
level: 'full',
},
@ -66,7 +66,7 @@ export const HairCheck = () => {
console.log('❌ Access denied');
}
}
}, [callObject, userName, localParticipant]);
};
// Memoize the to prevent unnecassary re-renders
const tileMemo = useDeepCompareMemo(

View File

@ -1,7 +1,17 @@
import React from 'react';
import {
WaitingRoomModal,
WaitingRoomNotification,
} from '@dailyjs/shared/components/WaitingRoom';
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';
<<<<<<< HEAD
import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider';
=======
>>>>>>> e47ada8fa4389bbfbeb7c97a6d80731a33d24b01
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';
@ -19,6 +29,7 @@ export const Room = ({ onLeave }) => {
const { callObject } = useCallState();
const { setShowDeviceModal } = useUIState();
const { isCamMuted, isMicMuted } = useMediaDevices();
const { setShowModal, showModal } = useWaitingRoom();
const toggleCamera = (newState) => {
if (!callObject) return false;
@ -38,6 +49,16 @@ export const Room = ({ onLeave }) => {
<VideoGrid />
</main>
{/* Show waiting room notification & modal if call owner */}
{localParticipant?.isOwner && (
<>
<WaitingRoomNotification />
{showModal && (
<WaitingRoomModal onClose={() => setShowModal(false)} />
)}
</>
)}
<Tray>
<TrayButton
label="Camera"
@ -58,6 +79,7 @@ export const Room = ({ onLeave }) => {
</TrayButton>
<span className="divider" />
<TrayButton label="Leave" onClick={onLeave} orange>
<IconLeave />
</TrayButton>

View File

@ -4,6 +4,7 @@ import { MediaDeviceProvider } from '@dailyjs/shared/contexts/MediaDeviceProvide
import { ParticipantsProvider } from '@dailyjs/shared/contexts/ParticipantsProvider';
import { TracksProvider } from '@dailyjs/shared/contexts/TracksProvider';
import { UIStateProvider } from '@dailyjs/shared/contexts/UIStateProvider';
import { WaitingRoomProvider } from '@dailyjs/shared/contexts/WaitingRoomProvider';
import PropTypes from 'prop-types';
import App from '../components/App';
import { Intro, NotConfigured } from '../components/Intro';
@ -95,7 +96,9 @@ export default function Index({ domain, isConfigured = false }) {
<ParticipantsProvider>
<TracksProvider>
<MediaDeviceProvider>
<App />
<WaitingRoomProvider>
<App />
</WaitingRoomProvider>
</MediaDeviceProvider>
</TracksProvider>
</ParticipantsProvider>

View File

@ -121,6 +121,28 @@ export const Button = forwardRef(
cursor: not-allowed;
}
.button.error {
background: var(--secondary-default);
border-color: var(--secondary-default);
}
.button.error:focus {
box-shadow: 0 0 0px 3px ${hexa(theme.secondary.default, 0.35)};
}
.button.error:hover {
border-color: var(--secondary-dark);
}
.button.success {
background: var(--green-default);
border-color: var(--green-default);
}
.button.success:focus {
box-shadow: 0 0 0px 3px ${hexa(theme.green.default, 0.35)};
}
.button.success:hover {
border-color: var(--green-dark);
}
.button.shadow {
box-shadow: 0 0 4px 0 rgb(0 0 0 / 8%), 0 4px 4px 0 rgb(0 0 0 / 4%);
}
@ -129,6 +151,10 @@ export const Button = forwardRef(
box-shadow: 0 0 4px 0 rgb(0 0 0 / 8%), 0 4px 4px 0 rgb(0 0 0 / 12%);
}
.button.small {
height: 42px;
}
.button.medium-square {
padding: 0px;
height: 48px;

View File

@ -2,8 +2,8 @@ import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
export const Card = ({ children }) => (
<div className="card">
export const Card = ({ children, className }) => (
<div className={classNames('card', className)}>
{children}
<style jsx>{`
background: white;
@ -16,6 +16,7 @@ export const Card = ({ children }) => (
Card.propTypes = {
children: PropTypes.node,
className: PropTypes.string,
};
export const CardHeader = ({ children }) => (
@ -53,14 +54,19 @@ CardBody.propTypes = {
children: PropTypes.node,
};
export const CardFooter = ({ children, divider = false }) => (
<footer className={classNames('card-footer', { divider })}>
export const CardFooter = ({ children, divider = false, flex = false }) => (
<footer className={classNames('card-footer', { divider, flex })}>
{children}
<style jsx>{`
display: flex;
margin: 0;
.card-footer {
display: flex;
}
&.divider {
:global(.card-footer.flex > *) {
flex: 1;
}
.card-footer.divider {
border-top: 1px solid var(--gray-light);
padding-top: var(--spacing-md);
}
@ -70,6 +76,7 @@ export const CardFooter = ({ children, divider = false }) => (
CardFooter.propTypes = {
children: PropTypes.node,
divider: PropTypes.bool,
flex: PropTypes.bool,
};
export default Card;

View File

@ -121,9 +121,9 @@ export const Modal = ({
.backdrop .modal :global(.card-footer) {
border-top: 0px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
grid-column-gap: var(--spacing-sm);
display: flex;
column-gap: var(--spacing-xs);
margin-top: var(--spacing-sm);
}
.isVisible {

View File

@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import { useWaitingRoom } from '../../contexts/WaitingRoomProvider';
import { Button } from '../Button';
export const WaitingParticipantRow = ({ participant }) => {
const { grantAccess, denyAccess } = useWaitingRoom();
const handleAllowClick = () => {
grantAccess(participant.id);
};
const handleDenyClick = () => {
denyAccess(participant.id);
};
return (
<div className="waiting-room-row">
{participant.name}
<div className="actions">
<Button onClick={handleAllowClick} size="small" variant="success">
Allow
</Button>
<Button onClick={handleDenyClick} size="small" variant="error">
Deny
</Button>
</div>
<style jsx>{`
.waiting-room-row {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid var(--gray-light);
padding-bottom: var(--spacing-xxs);
margin-bottom: var(--spacing-xxs);
}
.waiting-room-row:last-child {
border-bottom: 0px;
padding-bottom: 0px;
margin-bottom: 0px;
}
.actions {
display: flex;
gap: var(--spacing-xxs);
}
`}</style>
</div>
);
};
WaitingParticipantRow.propTypes = {
participant: PropTypes.object,
};
export default WaitingParticipantRow;

View File

@ -0,0 +1,45 @@
import React from 'react';
import Modal from '@dailyjs/shared/components/Modal';
import { useWaitingRoom } from '@dailyjs/shared/contexts/WaitingRoomProvider';
import PropTypes from 'prop-types';
import { Button } from '../Button';
import { WaitingParticipantRow } from './WaitingParticipantRow';
export const WaitingRoomModal = ({ onClose }) => {
const { denyAccess, grantAccess, waitingParticipants } = useWaitingRoom();
const handleAllowAllClick = (close) => {
grantAccess('all');
close();
};
const handleDenyAllClick = (close) => {
denyAccess('all');
close();
};
return (
<Modal
title="Waiting room"
isOpen
onClose={() => onClose()}
actions={[
<Button fullWidth onClick={handleAllowAllClick} variant="success">
Allow all
</Button>,
<Button fullWidth onClick={handleDenyAllClick} variant="error">
Deny all
</Button>,
]}
>
{waitingParticipants.map((p) => (
<WaitingParticipantRow participant={p} />
))}
</Modal>
);
};
WaitingRoomModal.propTypes = {
onClose: PropTypes.func,
};
export default WaitingRoomModal;

View File

@ -0,0 +1,101 @@
import React, { useEffect, useState } from 'react';
import { useCallState } from '../../contexts/CallProvider';
import { useWaitingRoom } from '../../contexts/WaitingRoomProvider';
import { Button } from '../Button';
import { Card, CardBody, CardFooter } from '../Card';
export const WaitingRoomNotification = () => {
const { callObject } = useCallState();
const {
denyAccess,
grantAccess,
showModal,
setShowModal,
waitingParticipants,
} = useWaitingRoom();
const [showNotification, setShowNotification] = useState(false);
/**
* Show notification when waiting participants change.
*/
useEffect(() => {
if (showModal) return false;
const handleWaitingParticipantAdded = () => {
setShowNotification(
Object.keys(callObject.waitingParticipants()).length > 0
);
};
callObject.on('waiting-participant-added', handleWaitingParticipantAdded);
return () => {
callObject.off(
'waiting-participant-added',
handleWaitingParticipantAdded
);
};
}, [callObject, showModal]);
/**
* Hide notification when people panel is opened.
*/
useEffect(() => {
if (showModal) setShowNotification(false);
}, [showModal]);
if (!showNotification || waitingParticipants.length === 0) return null;
const hasMultiplePeopleWaiting = waitingParticipants.length > 1;
const handleViewAllClick = () => {
setShowModal(true);
setShowNotification(false);
};
const handleAllowClick = () => {
grantAccess(waitingParticipants[0].id);
};
const handleDenyClick = () => {
denyAccess(hasMultiplePeopleWaiting ? 'all' : waitingParticipants[0].id);
};
// const handleClose = () => setShowNotification(false);
return (
<Card className="waiting-room-notification">
<CardBody>
{hasMultiplePeopleWaiting
? `${waitingParticipants.length} people would like to join the call`
: `${waitingParticipants[0].name} would like to join the call`}
</CardBody>
<CardFooter>
{hasMultiplePeopleWaiting ? (
<Button onClick={handleViewAllClick} size="small" variant="success">
View all
</Button>
) : (
<Button onClick={handleAllowClick} size="small" variant="success">
Allow
</Button>
)}
<Button onClick={handleDenyClick} size="small" variant="error">
{hasMultiplePeopleWaiting ? 'Deny All' : 'Deny'}
</Button>
</CardFooter>
<style jsx>{`
:global(.waiting-room-notification) {
position: absolute;
right: var(--spacing-sm);
top: var(--spacing-sm);
z-index: 999;
box-shadow: var(--shadow-depth-2);
}
:global(.waiting-room-notification .card-footer) {
display: flex;
column-gap: var(--spacing-xxs);
}
`}</style>
</Card>
);
};
export default WaitingRoomNotification;

View File

@ -0,0 +1,3 @@
export { WaitingRoomModal } from './WaitingRoomModal';
export { WaitingRoomNotification } from './WaitingRoomNotification';
export { WaitingParticipantRow } from './WaitingParticipantRow';

View File

@ -0,0 +1,111 @@
import React, {
createContext,
useCallback,
useContext,
useEffect,
useState,
} from 'react';
import PropTypes from 'prop-types';
import { useCallState } from './CallProvider';
const WaitingRoomContext = createContext(null);
export const WaitingRoomProvider = ({ children }) => {
const { callObject } = useCallState();
const [waitingParticipants, setWaitingParticipants] = useState([]);
const [showModal, setShowModal] = useState(false);
const handleWaitingParticipantEvent = useCallback(() => {
if (!callObject) return;
const waiting = Object.entries(callObject.waitingParticipants());
console.log(`🚪 ${waiting.length} participant(s) waiting for access`);
setWaitingParticipants((wp) =>
waiting.map(([pid, p]) => {
const prevWP = wp.find(({ id }) => id === pid);
return {
...p,
joinDate: prevWP?.joinDate ?? new Date(),
};
})
);
}, [callObject]);
useEffect(() => {
if (waitingParticipants.length === 0) {
setShowModal(false);
}
}, [waitingParticipants]);
useEffect(() => {
if (!callObject) return false;
console.log('🚪 Waiting room provider listening for requests');
const events = [
'waiting-participant-added',
'waiting-participant-updated',
'waiting-participant-removed',
];
events.forEach((e) => callObject.on(e, handleWaitingParticipantEvent));
return () =>
events.forEach((e) => callObject.off(e, handleWaitingParticipantEvent));
}, [callObject, handleWaitingParticipantEvent]);
const updateWaitingParticipant = (id, grantRequestedAccess) => {
if (!waitingParticipants.some((p) => p.id === id)) return;
callObject.updateWaitingParticipant(id, {
grantRequestedAccess,
});
setWaitingParticipants((wp) => wp.filter((p) => p.id !== id));
};
const updateAllWaitingParticipants = (grantRequestedAccess) => {
if (!waitingParticipants.length) return;
callObject.updateWaitingParticipants({
'*': {
grantRequestedAccess,
},
});
setWaitingParticipants([]);
};
const grantAccess = (id = 'all') => {
if (id === 'all') {
updateAllWaitingParticipants(true);
return;
}
updateWaitingParticipant(id, true);
};
const denyAccess = (id = 'all') => {
if (id === 'all') {
updateAllWaitingParticipants(false);
return;
}
updateWaitingParticipant(id, false);
};
return (
<WaitingRoomContext.Provider
value={{
denyAccess,
grantAccess,
setShowModal,
showModal,
waitingParticipants,
}}
>
{children}
</WaitingRoomContext.Provider>
);
};
WaitingRoomProvider.propTypes = {
children: PropTypes.node,
};
export const useWaitingRoom = () => useContext(WaitingRoomContext);