Salle de conférence expérimentale
This commit is contained in:
@ -14,6 +14,7 @@ import { createClient } from '../util/apollo';
|
||||
import { ApolloProvider } from '@apollo/client';
|
||||
import { LogoutPage } from './LogoutPage';
|
||||
import { UnauthorizedPage } from './UnauthorizedPage/UnauthorizedPage';
|
||||
import { ConferencePage } from './ConferencePage/ConferencePage';
|
||||
|
||||
export interface AppProps {
|
||||
|
||||
@ -44,6 +45,7 @@ export const App: FunctionComponent<AppProps> = () => {
|
||||
<Route path="/" exact component={HomePage} />
|
||||
<Route path="/unauthorized" exact component={UnauthorizedPage} />
|
||||
<PrivateRoute path="/profile" exact component={ProfilePage} />
|
||||
<PrivateRoute path="/conference" exact component={ConferencePage} />
|
||||
<PrivateRoute path="/workgroups/:id" exact component={WorkgroupPage} />
|
||||
<PrivateRoute path="/decisions/:id" component={DecisionSupportFilePage} />
|
||||
<PrivateRoute path="/dashboard" exact component={DashboardPage} />
|
||||
|
130
client/src/components/ConferencePage/ConferencePage.tsx
Normal file
130
client/src/components/ConferencePage/ConferencePage.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import React, { FunctionComponent, useEffect } from 'react';
|
||||
import { useUserProfile } from '../../gql/queries/profile';
|
||||
import { useConference } from '../../hooks/useConference';
|
||||
import { Page } from '../Page';
|
||||
import { Gravatar } from './Gravatar';
|
||||
|
||||
export interface ConferencePageProps {
|
||||
|
||||
}
|
||||
|
||||
const StatusHandRaised = 'hand-raised';
|
||||
const StatusThumbsUp = 'thumbs-up';
|
||||
const StatusThumbsDown = 'thumbs-down';
|
||||
const StatusNoVote = 'no-vote';
|
||||
|
||||
export const ConferencePage:FunctionComponent<ConferencePageProps> = () => {
|
||||
const { user } = useUserProfile();
|
||||
const { id, data, peers, setNickname, setEmail, ping, setStatus } = useConference();
|
||||
|
||||
const currentStatus = data.statuses[id];
|
||||
|
||||
useEffect(() => {
|
||||
if (peers.length === 0) return;
|
||||
if (!id || (!user.name && !user.email)) return;
|
||||
setNickname(user.name || user.email.split('@')[0]);
|
||||
setEmail(user.email);
|
||||
}, [user.name, user.email, peers.length]);
|
||||
|
||||
useEffect(() => {
|
||||
ping();
|
||||
const intervalId = setInterval(() => ping(), 30000);
|
||||
return () => clearInterval(intervalId);
|
||||
}, []);
|
||||
|
||||
const onStatusChange = (status: string) => {
|
||||
setStatus(currentStatus === status ? '' : status);
|
||||
};
|
||||
|
||||
return (
|
||||
<Page title="Conference">
|
||||
<div className="container is-fluid">
|
||||
<section className="mt-5">
|
||||
<h3 className="is-size-3">Mes actions</h3>
|
||||
<div className="buttons has-addons">
|
||||
<button
|
||||
className={`button is-medium ${currentStatus === StatusHandRaised ? 'is-info is-selected' : ''}`}
|
||||
onClick={onStatusChange.bind(null, StatusHandRaised)}>
|
||||
<span className="icon">
|
||||
<i className="fa fa-hand-paper"></i>
|
||||
</span>
|
||||
<span>Lever la main</span>
|
||||
</button>
|
||||
<button
|
||||
className={`button is-medium ${currentStatus === StatusThumbsUp ? 'is-success is-selected' : ''}`}
|
||||
onClick={onStatusChange.bind(null, StatusThumbsUp)}>
|
||||
<span className="icon">
|
||||
<i className="fa fa-thumbs-up"></i>
|
||||
</span>
|
||||
<span>Voter pour</span>
|
||||
</button>
|
||||
<button
|
||||
className={`button is-medium ${currentStatus === StatusNoVote ? 'is-warning is-selected' : ''}`}
|
||||
onClick={onStatusChange.bind(null, StatusNoVote)}>
|
||||
<span className="icon">
|
||||
<i className="fa fa-mitten"></i>
|
||||
</span>
|
||||
<span>Ne se prononce pas</span>
|
||||
</button>
|
||||
<button
|
||||
className={`button is-medium ${currentStatus === StatusThumbsDown ? 'is-danger is-selected' : ''}`}
|
||||
onClick={onStatusChange.bind(null, StatusThumbsDown)}>
|
||||
<span className="icon">
|
||||
<i className="fa fa-thumbs-down"></i>
|
||||
</span>
|
||||
<span>Voter contre</span>
|
||||
</button>
|
||||
</div>
|
||||
<h3 className="is-size-3">Assemblée</h3>
|
||||
<div className="columns mt-1">
|
||||
<UserCard className="column is-narrow"
|
||||
nickname={data.nicknames[id]}
|
||||
status={currentStatus}
|
||||
email={user.email} />
|
||||
{
|
||||
peers.map(p => {
|
||||
const nickname = data.nicknames[p] || '???';
|
||||
const email = data.emails[p] || '';
|
||||
return (
|
||||
<UserCard key={`peer-${p}`} className="column is-narrow"
|
||||
nickname={nickname}
|
||||
status={data.statuses[p]}
|
||||
email={email} />
|
||||
)
|
||||
})
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Page>
|
||||
);
|
||||
}
|
||||
|
||||
export interface UserCardProps {
|
||||
nickname: string
|
||||
email: string
|
||||
className?: string
|
||||
status: string
|
||||
};
|
||||
|
||||
export const UserCard:FunctionComponent<UserCardProps> = ({ nickname, email, className, status }) => {
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className="box">
|
||||
<div className="has-text-centered">
|
||||
<div className="mb-1">
|
||||
{ !status ? <span className="icon"><i className="far fa-2x fa-meh-blank"></i></span> : null }
|
||||
{ status === StatusHandRaised ? <span className="icon has-text-info"><i className="fa fa-2x fa-hand-paper"></i></span> : null }
|
||||
{ status === StatusThumbsUp ? <span className="icon has-text-success"><i className="fa fa-2x fa-thumbs-up"></i></span> : null }
|
||||
{ status === StatusNoVote ? <span className="icon has-text-warning"><i className="fa fa-2x fa-mitten"></i></span> : null }
|
||||
{ status === StatusThumbsDown ? <span className="icon has-text-danger"><i className="fa fa-2x fa-thumbs-down"></i></span> : null }
|
||||
</div>
|
||||
<figure className="image is-128x128 is-inline-block">
|
||||
<Gravatar className="is-rounded" email={email} />
|
||||
</figure>
|
||||
<h4 className="is-size-4">{nickname}</h4>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
20
client/src/components/ConferencePage/Gravatar.tsx
Normal file
20
client/src/components/ConferencePage/Gravatar.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import React, { FunctionComponent, useEffect, useState } from 'react';
|
||||
import md5 from 'crypto-js/md5';
|
||||
|
||||
export interface GravatarProps {
|
||||
className?: string
|
||||
email: string
|
||||
}
|
||||
|
||||
const defaultAvatarUrl = 'https://www.gravatar.com/avatar/00000000000000000000000000000000?d=mp&s=128';
|
||||
|
||||
export const Gravatar:FunctionComponent<GravatarProps> = ({ className, email }) => {
|
||||
const [ avatarUrl, setAvatarUrl ] = useState(defaultAvatarUrl);
|
||||
useEffect(() => {
|
||||
const hash = md5(email.trim().toLowerCase());
|
||||
setAvatarUrl(`https://www.gravatar.com/avatar/${hash}?d=mp&s=128`);
|
||||
}, [email]);
|
||||
return (
|
||||
<img className={className} src={avatarUrl} />
|
||||
);
|
||||
}
|
@ -31,6 +31,20 @@ export function Navbar() {
|
||||
</a>
|
||||
</div>
|
||||
<div className={`navbar-menu ${isActive ? 'is-active' : ''}`}>
|
||||
<div className="navbar-start">
|
||||
{
|
||||
loggedIn ?
|
||||
<React.Fragment>
|
||||
<Link to="/dashboard" className="navbar-item">
|
||||
<i className="fa fa-columns"></i> Tableau de bord
|
||||
</Link>
|
||||
<Link to="/conference" className="navbar-item">
|
||||
<i className="fa fa-users"></i> Conférence
|
||||
</Link>
|
||||
</React.Fragment> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
<div className="navbar-end">
|
||||
<div className="navbar-item">
|
||||
<div className="buttons">
|
||||
|
85
client/src/hooks/useConference.tsx
Normal file
85
client/src/hooks/useConference.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import * as Y from 'yjs'
|
||||
import { WebrtcProvider, } from 'y-webrtc'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
|
||||
export function useConference() {
|
||||
const docRef = useRef(new Y.Doc());
|
||||
|
||||
const [ state, setState ] = useState({
|
||||
data: {
|
||||
heartbeats: {},
|
||||
emails: {},
|
||||
nicknames: {},
|
||||
statuses: {},
|
||||
},
|
||||
peers: [],
|
||||
id: null,
|
||||
});
|
||||
|
||||
const setData = (key: string, value: any) => {
|
||||
setState(state => ({...state, data: { ...state.data, [key]: value }}));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const doc = docRef.current;
|
||||
const roomName = `${window.location.protocol}//${window.location.host}/daddy/conference`;
|
||||
const provider = new WebrtcProvider(roomName, docRef.current);
|
||||
|
||||
const onPeers = (evt) => {
|
||||
let peers = [...state.peers];
|
||||
peers = peers.filter(p => evt.removed.indexOf(p) === -1);
|
||||
peers.push(...evt.added);
|
||||
setState(state => ({ ...state, id: provider.room.peerId, peers }));
|
||||
};
|
||||
|
||||
provider.on('peers', onPeers);
|
||||
|
||||
const heartbeats = doc.getMap('heartbeats');
|
||||
heartbeats.observe(evt => setData('heartbeats', evt.currentTarget.toJSON()));
|
||||
|
||||
const nicknames = doc.getMap('nicknames');
|
||||
nicknames.observe(evt => setData('nicknames', evt.currentTarget.toJSON()));
|
||||
|
||||
const emails = doc.getMap('emails');
|
||||
emails.observe(evt => setData('emails', evt.currentTarget.toJSON()));
|
||||
|
||||
const statuses = doc.getMap('statuses');
|
||||
statuses.observe(evt => setData('statuses', evt.currentTarget.toJSON()));
|
||||
|
||||
return () => {
|
||||
provider.off('peers', onPeers);
|
||||
provider.destroy();
|
||||
docRef.current.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
data: state.data,
|
||||
peers: state.peers,
|
||||
id: state.id,
|
||||
|
||||
setStatus: (status: string) => {
|
||||
const doc = docRef.current;
|
||||
const statuses = doc.getMap('statuses');
|
||||
statuses.set(state.id, status);
|
||||
},
|
||||
|
||||
ping: () => {
|
||||
const doc = docRef.current;
|
||||
const heartbeats = doc.getMap('heartbeats');
|
||||
heartbeats.set(state.id, (new Date()).toJSON());
|
||||
},
|
||||
|
||||
setNickname: (nickname: string) => {
|
||||
const doc = docRef.current;
|
||||
const nicknames = doc.getMap('nicknames');
|
||||
nicknames.set(state.id, nickname);
|
||||
},
|
||||
|
||||
setEmail: (email: string) => {
|
||||
const doc = docRef.current;
|
||||
const emails = doc.getMap('emails');
|
||||
emails.set(state.id, email);
|
||||
},
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user