expiry timer

This commit is contained in:
Jon 2021-07-20 11:05:11 +01:00
parent 242b6154f1
commit 1f7ebf8e22
12 changed files with 385 additions and 100 deletions

View File

@ -1,4 +1,5 @@
import React, { useState, useEffect, useMemo } from 'react';
import ExpiryTimer from '@dailyjs/shared/components/ExpiryTimer';
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
import { useCallUI } from '@dailyjs/shared/hooks/useCallUI';
@ -8,19 +9,6 @@ import { Modals } from './Modals';
export const App = () => {
const { roomExp, state } = useCallState();
const [secs, setSecs] = useState();
// If room has an expiry time, we'll calculate how many seconds until expiry
useEffect(() => {
if (!roomExp) {
return false;
}
const i = setInterval(() => {
const timeLeft = Math.round((roomExp - Date.now()) / 1000);
setSecs(`${Math.floor(timeLeft / 60)}:${`0${timeLeft % 60}`.slice(-2)}`);
}, 1000);
return () => clearInterval(i);
}, [roomExp]);
const componentForState = useCallUI({
state,
@ -28,48 +16,29 @@ export const App = () => {
});
// Memoize children to avoid unnecassary renders from HOC
const memoizedApp = useMemo(
return useMemo(
() => (
<div className="app">
{componentForState()}
<Modals />
<Asides />
<style jsx>{`
color: white;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
<>
{roomExp && <ExpiryTimer expiry={roomExp} />}
<div className="app">
{componentForState()}
<Modals />
<Asides />
<style jsx>{`
color: white;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
.loader {
margin: 0 auto;
}
`}</style>
</div>
.loader {
margin: 0 auto;
}
`}</style>
</div>
</>
),
[componentForState]
);
return (
<>
{roomExp && <div className="countdown">{secs}</div>} {memoizedApp}
<style jsx>{`
.countdown {
position: fixed;
top: 0px;
right: 0px;
width: 48px;
text-align: center;
padding: 4px 0;
font-size: 0.875rem;
font-weight: var(--weight-medium);
border-radius: 0 0 0 var(--radius-sm);
background: var(--blue-dark);
color: white;
z-index: 999;
}
`}</style>
</>
[componentForState, roomExp]
);
};

View File

@ -1,4 +1,6 @@
export default async function handler(req, res) {
const { privacy, expiryMinutes } = req.body;
if (req.method === 'POST') {
console.log(`Creating room on domain ${process.env.DAILY_DOMAIN}`);
@ -9,8 +11,9 @@ export default async function handler(req, res) {
Authorization: `Bearer ${process.env.DAILY_API_KEY}`,
},
body: JSON.stringify({
privacy: privacy || 'public',
properties: {
exp: Math.round(Date.now() / 1000) + 5 * 60, // expire in 5 minutes
exp: Math.round(Date.now() / 1000) + (expiryMinutes || 5) * 60, // expire in x minutes
eject_at_room_exp: true,
},
}),

View File

@ -1,17 +1,23 @@
import React, { useState } from 'react';
import Button from '@dailyjs/shared/components/Button';
import Loader from '@dailyjs/shared/components/Loader';
import { Well } from '@dailyjs/shared/components/Well';
import PropTypes from 'prop-types';
export const Splash = ({ domain, onJoin, isConfigured }) => {
// const [joinAsInstructor, setJoinAsInstructor] = useState(false);
/**
* Splash
* ---
* - Checks our app is configured properly
* - Creates a new Daily room for this session
* - Calls the onJoin method with the room name and instructor (owner) status
*/
export const Splash = ({ onJoin, isConfigured }) => {
const [fetching, setFetching] = useState(false);
const [error, setError] = useState(false);
const [room, setRoom] = useState('');
async function createRoom() {
// Create a room
async function createRoom(asInstructor) {
// Create a new room for this class
setError(false);
setFetching(true);
@ -23,13 +29,16 @@ export const Splash = ({ domain, onJoin, isConfigured }) => {
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
privacy: 'private',
expiryMinutes: 10,
}),
});
const resJson = await res.json();
if (resJson.name) {
setFetching(false);
setRoom(resJson.name);
onJoin(resJson.name, asInstructor);
return;
}
@ -39,25 +48,111 @@ export const Splash = ({ domain, onJoin, isConfigured }) => {
return (
<div className="container">
<aside />
<aside>
<a href="https://unsplash.com/@jordannix?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">
Photo by Jordan Nix on Unsplash
</a>
</aside>
<main>
<header className="branding">
<img src="/assets/daily-logo-dark.svg" alt="Daily" />
</header>
<img
src="/assets/daily-logo-dark.svg"
alt="Daily"
className="branding"
/>
<div className="inner">
{!isConfigured ? (
<div>
You must set <code>Stuff</code>
</div>
) : (
<Button onClick={() => createRoom()}>
{fetching ? 'Creating room...' : 'Join'}
</Button>
)}
{(() => {
// Application is not yet configured (there are missing globals, such as domain and dev key)
if (!isConfigured)
return (
<>
<h2>Not configured</h2>
<p>
Please ensure you have set both the{' '}
<code>DAILY_API_KEY</code> and <code>DAILY_DOMAIN</code>{' '}
environmental variables. An example can be found in the
provided <code>env.example</code> file.
</p>
<p>
If you do not yet have a Daily developer account, please{' '}
<a
href="https://dashboard.daily.co/signup"
target="_blank"
rel="noreferrer"
>
create one now
</a>{' '}
. You can find your Daily API key on the{' '}
<a
href="https://dashboard.daily.co/developers"
target="_blank"
rel="noreferrer"
>
developer page
</a>
of the dashboard.
</p>
</>
);
// There was an error creating the room
if (error)
return (
<div>
<Well variant="error">{error}</Well>
An error occured when trying to create a demo room. Please
check that your environmental variables are correct and try
again.
</div>
);
// Loader whilst we create the room
if (fetching)
return (
<>
<Loader /> <p>Creating room, please wait...</p>
</>
);
// Introductory splash screen
return (
<>
<h2>Live fitness example</h2>
<p>
This example demonstrates how to use Daily JS to create a live
class experience. Please be sure to reference the project
readme first.
</p>
<p>
Note: all rooms created with this demo will have a 5 minute
expiry time. If you would like to create a longer running
room, please set the <code>DAILY_ROOM</code> environmental
variable to use your own custom room.
</p>
<hr />
<footer>
<Button
fullWidth
onClick={() => createRoom(true)}
disabled={fetching}
>
Join as instructor
</Button>
<Button
fullWidth
onClick={() => createRoom(false)}
variant="outline-gray"
disabled={fetching}
>
Join as student
</Button>
</footer>
</>
);
})()}
</div>
</main>
<style jsx>
{`
.container {
@ -74,19 +169,43 @@ export const Splash = ({ domain, onJoin, isConfigured }) => {
box-sizing: border-box;
}
p {
color: var(--text-mid);
}
main .inner {
display: flex;
flex: 1;
align-items: center;
flex: 0;
margin: auto 0;
}
.branding {
flex: 0;
width: 108px;
}
hr {
margin: var(--spacing-md) 0;
}
aside {
background: url(/images/fitness-bg.jpg) no-repeat;
background-size: cover;
color: white;
font-size: 0.875rem;
display: flex;
align-items: flex-end;
padding: var(--spacing-xs);
box-sizing: border-box;
}
aside a {
color: white;
opacity: 0.65;
}
footer {
display: flex;
gap: var(--spacing-xxs);
}
`}
</style>
@ -95,7 +214,6 @@ export const Splash = ({ domain, onJoin, isConfigured }) => {
};
Splash.propTypes = {
domain: PropTypes.string,
onJoin: PropTypes.func,
isConfigured: PropTypes.bool,
};

View File

@ -0,0 +1,101 @@
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';
import { TracksProvider } from '@dailyjs/shared/contexts/TracksProvider';
import { UIStateProvider } from '@dailyjs/shared/contexts/UIStateProvider';
import { WaitingRoomProvider } from '@dailyjs/shared/contexts/WaitingRoomProvider';
import getDemoProps from '@dailyjs/shared/lib/demoProps';
import { useRouter } from 'next/router';
import PropTypes from 'prop-types';
/**
* Room page
* ---
*/
export default function Room({ room, instructor, domain }) {
const [token, setToken] = useState();
const [tokenError, setTokenError] = useState();
const router = useRouter();
// Redirect to a 404 if we do not have a room
useEffect(
() => (!room || !domain) && router.replace('/not-found'),
[room, domain, router]
);
// Fetch a meeting token
useEffect(() => {
if (token || !room) {
return;
}
// We're using a simple Next serverless function to generate meeting tokens
// which could be replaced with your own serverside method that authenticates
// users / sets room owner status, user ID, user names etc
async function getToken() {
console.log(`🪙 Fetching meeting token for room '${room}'`);
const res = await fetch('/api/token', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ roomName: room, isOwner: !!instructor }),
});
const resJson = await res.json();
if (!resJson?.token) {
setTokenError(resJson?.error || true);
}
console.log(`🪙 Meeting token received`);
setToken(resJson.token);
}
getToken();
}, [token, room, instructor]);
if (!token) {
return <div>Fetching token...</div>;
}
/**
* Main call UI
*/
return (
<UIStateProvider>
<CallProvider domain={domain} room={room} token={token}>
<ParticipantsProvider>
<TracksProvider>
<MediaDeviceProvider>
<WaitingRoomProvider>
<App />
</WaitingRoomProvider>
</MediaDeviceProvider>
</TracksProvider>
</ParticipantsProvider>
</CallProvider>
</UIStateProvider>
);
}
Room.propTypes = {
room: PropTypes.string.isRequired,
domain: PropTypes.string.isRequired,
instructor: PropTypes.bool,
};
export async function getServerSideProps(context) {
const { room, instructor } = context.query;
const defaultProps = getDemoProps();
return {
props: { room, instructor: !!instructor, ...defaultProps },
};
}

View File

@ -8,25 +8,22 @@ import Splash from '../components/Splash';
* Index page
* ---
*/
export default function Index({ domain, isConfigured = false }) {
export default function Index({ isConfigured = false }) {
const router = useRouter();
function joinRoom(room, joinAsInstructor) {
// redirect to room....
console.log(room);
console.log(joinAsInstructor);
router.replace(`/${room}/`);
// Redirect to room page
router.replace({
pathname: `/${room}`,
query: { instructor: !!joinAsInstructor },
});
}
return (
<Splash domain={domain} onJoin={joinRoom} isConfigured={!!isConfigured} />
);
return <Splash onJoin={joinRoom} isConfigured={!!isConfigured} />;
}
Index.propTypes = {
isConfigured: PropTypes.bool.isRequired,
domain: PropTypes.string,
};
export async function getStaticProps() {

View File

@ -0,0 +1,21 @@
import React from 'react';
import MessageCard from '@dailyjs/shared/components/MessageCard';
export default function RoomNotFound() {
return (
<div className="not-found">
<MessageCard error header="Room not found">
The room you are trying to join does not exist. Have you created the
room using the Daily REST API or the dashboard?
</MessageCard>
<style jsx>{`
display: grid;
align-items: center;
justify-content: center;
grid-template-columns: 620px;
width: 100%;
height: 100vh;
`}</style>
</div>
);
}

View File

@ -0,0 +1,47 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
export const ExpiryTimer = ({ expiry }) => {
const [secs, setSecs] = useState('--:--');
// If room has an expiry time, we'll calculate how many seconds until expiry
useEffect(() => {
if (!expiry) {
return false;
}
const i = setInterval(() => {
const timeLeft = Math.round((expiry - Date.now()) / 1000);
setSecs(`${Math.floor(timeLeft / 60)}:${`0${timeLeft % 60}`.slice(-2)}`);
}, 1000);
return () => clearInterval(i);
}, [expiry]);
return (
<div className="countdown">
{secs}
<style jsx>{`
.countdown {
position: fixed;
top: 0px;
right: 0px;
width: 48px;
text-align: center;
padding: 4px 0;
font-size: 0.875rem;
font-weight: var(--weight-medium);
border-radius: 0 0 0 var(--radius-sm);
background: var(--secondary-dark);
color: white;
z-index: 999;
}
`}</style>
</div>
);
};
ExpiryTimer.propTypes = {
expiry: PropTypes.number,
};
export default ExpiryTimer;

View File

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

View File

@ -213,7 +213,7 @@ export const HairCheck = () => {
}
.haircheck .panel {
width: 720px;
width: 580px;
text-align: center;
}
@ -228,7 +228,7 @@ export const HairCheck = () => {
position: relative;
color: white;
border: 3px solid rgba(255, 255, 255, 0.1);
max-width: 520px;
max-width: 480px;
margin: 0 auto;
border-radius: var(--radius-md) var(--radius-md) 0 0;
border-bottom: 0px;
@ -279,7 +279,7 @@ export const HairCheck = () => {
left: 0px;
right: 0px;
z-index: 99;
padding: var(--spacing-sm);
padding: var(--spacing-xs);
box-sizing: border-box;
display: flex;
align-items: center;
@ -289,8 +289,8 @@ export const HairCheck = () => {
.haircheck .content :global(.device-button) {
position: absolute;
top: var(--spacing-sm);
right: var(--spacing-sm);
top: var(--spacing-xxs);
right: var(--spacing-xxs);
}
.haircheck .overlay-message {
@ -303,7 +303,7 @@ export const HairCheck = () => {
.haircheck footer {
position: relative;
border: 3px solid rgba(255, 255, 255, 0.1);
max-width: 520px;
max-width: 480px;
margin: 0 auto;
border-radius: 0 0 var(--radius-md) var(--radius-md);
padding: calc(6px + var(--spacing-md)) var(--spacing-sm)
@ -312,7 +312,7 @@ export const HairCheck = () => {
display: grid;
grid-template-columns: 1fr auto;
grid-column-gap: var(--spacing-sm);
grid-column-gap: var(--spacing-xs);
}
.waiting {

View File

@ -1,11 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';
export const Loader = ({ color = 'currentColor', size = 24 }) => (
export const Loader = ({
color = 'currentColor',
size = 24,
centered = false,
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 44 44"
className="loader"
className={centered ? 'loader centered' : 'loader'}
>
<g fill="none" fillRule="evenodd" strokeWidth="2">
<circle cx="22" cy="22" r="19.4775">
@ -60,6 +64,13 @@ export const Loader = ({ color = 'currentColor', size = 24 }) => (
stroke: ${color};
width: ${size}px;
}
.centered {
position: absolute;
top: 50%;
margin-top: ${size / 2}px;
left: 50%;
margin-left: ${size / 2}px;
}
`}</style>
</svg>
);
@ -67,6 +78,7 @@ export const Loader = ({ color = 'currentColor', size = 24 }) => (
Loader.propTypes = {
size: PropTypes.number,
color: PropTypes.string,
centered: PropTypes.bool,
};
export default Loader;

View File

@ -5,8 +5,8 @@ import { useDeepCompareMemo } from 'use-deep-compare';
export const UIStateContext = createContext();
export const UIStateProvider = ({
asides,
modals,
asides = [],
modals = [],
customTrayComponent,
children,
}) => {

View File

@ -10,6 +10,8 @@ import {
CALL_STATE_NOT_BEFORE,
CALL_STATE_READY,
CALL_STATE_REDIRECTING,
CALL_STATE_NOT_ALLOWED,
CALL_STATE_EXPIRED,
} from '@dailyjs/shared/contexts/useCallMachine';
import { useRouter } from 'next/router';
import HairCheck from '../components/HairCheck';
@ -39,6 +41,13 @@ export const useCallUI = ({
case CALL_STATE_NOT_FOUND:
router.replace(notFoundRedirect);
return null;
case CALL_STATE_NOT_ALLOWED:
return (
<MessageCard error header="Access denied">
You are not allowed to join this meeting. Please make sure you have
a valid meeting token.
</MessageCard>
);
case CALL_STATE_NOT_BEFORE:
return (
<MessageCard error header="Cannot join before owner">
@ -46,6 +55,13 @@ export const useCallUI = ({
owner
</MessageCard>
);
case CALL_STATE_EXPIRED:
return (
<MessageCard error header="Room expired">
The room you are trying to join has expired. Please create or join
another room.
</MessageCard>
);
case CALL_STATE_LOBBY:
return haircheck ? haircheck() : <HairCheck />;
case CALL_STATE_JOINED: