Merge branch 'feature-toolarea' into develop

master
Håvar Aambø Fosstveit 2018-06-21 09:56:21 +02:00
commit 8ee85e6384
23 changed files with 1389 additions and 451 deletions

View File

@ -732,6 +732,78 @@ export default class RoomClient
}); });
} }
pausePeerScreen(peerName)
{
logger.debug('pausePeerScreen() [peerName:"%s"]', peerName);
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, true));
return Promise.resolve()
.then(() =>
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'screen')
continue;
consumer.pause('pause-screen');
}
}
}
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
})
.catch((error) =>
{
logger.error('pausePeerScreen() failed: %o', error);
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
});
}
resumePeerScreen(peerName)
{
logger.debug('resumePeerScreen() [peerName:"%s"]', peerName);
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, true));
return Promise.resolve()
.then(() =>
{
for (const peer of this._room.peers)
{
if (peer.name === peerName)
{
for (const consumer of peer.consumers)
{
if (consumer.appData.source !== 'screen' || !consumer.supported)
continue;
consumer.resume();
}
}
}
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
})
.catch((error) =>
{
logger.error('resumePeerScreen() failed: %o', error);
this._dispatch(
stateActions.setPeerScreenInProgress(peerName, false));
});
}
enableAudioOnly() enableAudioOnly()
{ {
logger.debug('enableAudioOnly()'); logger.debug('enableAudioOnly()');

View File

@ -0,0 +1,89 @@
import React, { Component } from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as stateActions from '../../redux/stateActions';
import * as requestActions from '../../redux/requestActions';
import MessageList from './MessageList';
class Chat extends Component
{
render()
{
const {
senderPlaceHolder,
onSendMessage,
disabledInput,
autofocus,
displayName
} = this.props;
return (
<div data-component='Chat'>
<MessageList />
<form
data-component='Sender'
onSubmit={(e) => { onSendMessage(e, displayName); }}
>
<input
type='text'
className='new-message'
name='message'
placeholder={senderPlaceHolder}
disabled={disabledInput}
autoFocus={autofocus}
autoComplete='off'
/>
</form>
</div>
);
}
}
Chat.propTypes =
{
senderPlaceHolder : PropTypes.string,
onSendMessage : PropTypes.func,
disabledInput : PropTypes.bool,
autofocus : PropTypes.bool,
displayName : PropTypes.string
};
Chat.defaultProps =
{
senderPlaceHolder : 'Type a message...',
autofocus : true,
displayName : null
};
const mapStateToProps = (state) =>
{
return {
disabledInput : state.chatbehavior.disabledInput,
displayName : state.me.displayName
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
onSendMessage : (event, displayName) =>
{
event.preventDefault();
const userInput = event.target.message.value;
if (userInput)
{
dispatch(stateActions.addUserMessage(userInput));
dispatch(requestActions.sendChatMessage(userInput, displayName));
}
event.target.message.value = '';
}
};
};
const ChatContainer = connect(
mapStateToProps,
mapDispatchToProps
)(Chat);
export default ChatContainer;

View File

@ -0,0 +1,166 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as appPropTypes from '../appPropTypes';
import * as requestActions from '../../redux/requestActions';
const ListPeer = (props) =>
{
const {
peer,
micConsumer,
webcamConsumer,
screenConsumer,
onMuteMic,
onUnmuteMic,
onDisableWebcam,
onEnableWebcam,
onDisableScreen,
onEnableScreen
} = props;
const micEnabled = (
Boolean(micConsumer) &&
!micConsumer.locallyPaused &&
!micConsumer.remotelyPaused
);
const videoVisible = (
Boolean(webcamConsumer) &&
!webcamConsumer.locallyPaused &&
!webcamConsumer.remotelyPaused
);
const screenVisible = (
Boolean(screenConsumer) &&
!screenConsumer.locallyPaused &&
!screenConsumer.remotelyPaused
);
return (
<div data-component='ListPeer'>
<img className='avatar' />
<div className='peer-info'>
{peer.displayName}
</div>
<div className='controls'>
{ screenConsumer ?
<div
className={classnames('button', 'screen', {
on : screenVisible,
off : !screenVisible,
disabled : peer.peerScreenInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
screenVisible ?
onDisableScreen(peer.name) : onEnableScreen(peer.name);
}}
/>
:null
}
<div
className={classnames('button', 'mic', {
on : micEnabled,
off : !micEnabled,
disabled : peer.peerAudioInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name);
}}
/>
<div
className={classnames('button', 'webcam', {
on : videoVisible,
off : !videoVisible,
disabled : peer.peerVideoInProgress
})}
onClick={(e) =>
{
e.stopPropagation();
videoVisible ?
onDisableWebcam(peer.name) : onEnableWebcam(peer.name);
}}
/>
</div>
</div>
);
};
ListPeer.propTypes =
{
advancedMode : PropTypes.bool,
peer : appPropTypes.Peer.isRequired,
micConsumer : appPropTypes.Consumer,
webcamConsumer : appPropTypes.Consumer,
screenConsumer : appPropTypes.Consumer,
onMuteMic : PropTypes.func.isRequired,
onUnmuteMic : PropTypes.func.isRequired,
onEnableWebcam : PropTypes.func.isRequired,
onDisableWebcam : PropTypes.func.isRequired,
onEnableScreen : PropTypes.func.isRequired,
onDisableScreen : PropTypes.func.isRequired
};
const mapStateToProps = (state, { name }) =>
{
const peer = state.peers[name];
const consumersArray = peer.consumers
.map((consumerId) => state.consumers[consumerId]);
const micConsumer =
consumersArray.find((consumer) => consumer.source === 'mic');
const webcamConsumer =
consumersArray.find((consumer) => consumer.source === 'webcam');
const screenConsumer =
consumersArray.find((consumer) => consumer.source === 'screen');
return {
peer,
micConsumer,
webcamConsumer,
screenConsumer
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
onMuteMic : (peerName) =>
{
dispatch(requestActions.mutePeerAudio(peerName));
},
onUnmuteMic : (peerName) =>
{
dispatch(requestActions.unmutePeerAudio(peerName));
},
onEnableWebcam : (peerName) =>
{
dispatch(requestActions.resumePeerVideo(peerName));
},
onDisableWebcam : (peerName) =>
{
dispatch(requestActions.pausePeerVideo(peerName));
},
onEnableScreen : (peerName) =>
{
dispatch(requestActions.resumePeerScreen(peerName));
},
onDisableScreen : (peerName) =>
{
dispatch(requestActions.pausePeerScreen(peerName));
}
};
};
const ListPeerContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ListPeer);
export default ListPeerContainer;

View File

@ -0,0 +1,80 @@
import React from 'react';
import { connect } from 'react-redux';
import * as appPropTypes from '../appPropTypes';
import * as requestActions from '../../redux/requestActions';
import * as stateActions from '../../redux/stateActions';
import PropTypes from 'prop-types';
import ListPeer from './ListPeer';
class ParticipantList extends React.Component
{
constructor(props)
{
super(props);
}
render()
{
const {
advancedMode,
peers
} = this.props;
return (
<div data-component='ParticipantList'>
<ul className='list'>
{
peers.map((peer) =>
{
return (
<li key={peer.name} className='list-item'>
<ListPeer name={peer.name} advancedMode={advancedMode} />
</li>
);
})
}
</ul>
</div>
);
}
}
ParticipantList.propTypes =
{
advancedMode : PropTypes.bool,
peers : PropTypes.arrayOf(appPropTypes.Peer).isRequired
};
const mapStateToProps = (state) =>
{
const peersArray = Object.values(state.peers);
return {
peers : peersArray
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
handleChangeWebcam : (device) =>
{
dispatch(requestActions.changeWebcam(device.value));
},
handleChangeAudioDevice : (device) =>
{
dispatch(requestActions.changeAudioDevice(device.value));
},
onToggleAdvancedMode : () =>
{
dispatch(stateActions.toggleAdvancedMode());
}
};
};
const ParticipantListContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ParticipantList);
export default ParticipantListContainer;

View File

@ -5,14 +5,13 @@ import PropTypes from 'prop-types';
import classnames from 'classnames'; import classnames from 'classnames';
import ClipboardButton from 'react-clipboard.js'; import ClipboardButton from 'react-clipboard.js';
import * as appPropTypes from './appPropTypes'; import * as appPropTypes from './appPropTypes';
import * as stateActions from '../redux/stateActions';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import { Appear } from './transitions'; import { Appear } from './transitions';
import Me from './Me'; import Me from './Me';
import Peers from './Peers'; import Peers from './Peers';
import Notifications from './Notifications'; import Notifications from './Notifications';
import ChatWidget from './ChatWidget'; import ToolAreaButton from './ToolArea/ToolAreaButton';
import Settings from './Settings'; import ToolArea from './ToolArea/ToolArea';
class Room extends React.Component class Room extends React.Component
{ {
@ -21,10 +20,10 @@ class Room extends React.Component
const { const {
room, room,
me, me,
toolAreaOpen,
amActiveSpeaker, amActiveSpeaker,
screenProducer, screenProducer,
onRoomLinkCopy, onRoomLinkCopy,
onToggleSettings,
onLogin, onLogin,
onShareScreen, onShareScreen,
onUnShareScreen, onUnShareScreen,
@ -60,137 +59,143 @@ class Room extends React.Component
return ( return (
<Appear duration={300}> <Appear duration={300}>
<div data-component='Room'> <div data-component='Room'>
<Notifications />
<ChatWidget />
<div className='state' data-tip='Server status'>
<div className={classnames('icon', room.state)} />
<p className={classnames('text', room.state)}>{room.state}</p>
</div>
<div className='room-link-wrapper'>
<div className='room-link'>
<ClipboardButton
component='a'
className='link'
button-href={room.url}
button-target='_blank'
data-tip='Click to copy room link'
data-clipboard-text={room.url}
onSuccess={onRoomLinkCopy}
onClick={(event) =>
{
// If this is a 'Open in new window/tab' don't prevent
// click default action.
if (
event.ctrlKey || event.shiftKey || event.metaKey ||
// Middle click (IE > 9 and everyone else).
(event.button && event.button === 1)
)
{
return;
}
event.preventDefault();
}}
>
invitation link
</ClipboardButton>
</div>
</div>
<Peers
advancedMode={room.advancedMode}
/>
<div <div
className={classnames('me-container', { className='room-wrapper'
'active-speaker' : amActiveSpeaker style={{
})} width : toolAreaOpen ? '80%' : '100%'
}}
> >
<Me <Notifications />
<ToolAreaButton />
<div className='state' data-tip='Server status'>
<div className={classnames('icon', room.state)} />
<p className={classnames('text', room.state)}>{room.state}</p>
</div>
<div className='room-link-wrapper'>
<div className='room-link'>
<ClipboardButton
component='a'
className='link'
button-href={room.url}
button-target='_blank'
data-tip='Click to copy room link'
data-clipboard-text={room.url}
onSuccess={onRoomLinkCopy}
onClick={(event) =>
{
// If this is a 'Open in new window/tab' don't prevent
// click default action.
if (
event.ctrlKey || event.shiftKey || event.metaKey ||
// Middle click (IE > 9 and everyone else).
(event.button && event.button === 1)
)
{
return;
}
event.preventDefault();
}}
>
invitation link
</ClipboardButton>
</div>
</div>
<Peers
advancedMode={room.advancedMode} advancedMode={room.advancedMode}
/> />
</div>
<div className='sidebar'>
<div <div
className={classnames('button', 'screen', screenState)} className={classnames('me-container', {
data-tip={screenTip} 'active-speaker' : amActiveSpeaker
data-type='dark' })}
onClick={() => >
{ <Me
switch (screenState) advancedMode={room.advancedMode}
/>
</div>
<div className='sidebar'>
<div
className={classnames('button', 'screen', screenState)}
data-tip={screenTip}
data-type='dark'
onClick={() =>
{ {
case 'on': switch (screenState)
{ {
onUnShareScreen(); case 'on':
break; {
onUnShareScreen();
break;
}
case 'off':
{
onShareScreen();
break;
}
case 'need-extension':
{
onNeedExtension();
break;
}
default:
{
break;
}
} }
case 'off': }}
{ />
onShareScreen();
break;
}
case 'need-extension':
{
onNeedExtension();
break;
}
default:
{
break;
}
}
}}
/>
<div <div
className={classnames('button', 'settings', { className={classnames('button', 'login', 'off', {
on : room.showSettings, disabled : me.loginInProgress
off : !room.showSettings })}
})} data-tip='Login'
data-tip='Open settings' data-type='dark'
data-type='dark' onClick={() => onLogin()}
onClick={() => onToggleSettings()} />
/>
<div <div
className={classnames('button', 'login', 'off', { className={classnames('button', 'raise-hand', {
disabled : me.loginInProgress on : me.raiseHand,
})} disabled : me.raiseHandInProgress
data-tip='Login' })}
data-type='dark' data-tip='Raise hand'
onClick={() => onLogin()} data-type='dark'
/> onClick={() => onToggleHand(!me.raiseHand)}
/>
<div <div
className={classnames('button', 'raise-hand', { className={classnames('button', 'leave-meeting')}
on : me.raiseHand, data-tip='Leave meeting'
disabled : me.raiseHandInProgress data-type='dark'
})} onClick={() => onLeaveMeeting()}
data-tip='Raise hand' />
data-type='dark' </div>
onClick={() => onToggleHand(!me.raiseHand)}
/>
<div <ReactTooltip
className={classnames('button', 'leave-meeting')} effect='solid'
data-tip='Leave meeting' delayShow={100}
data-type='dark' delayHide={100}
onClick={() => onLeaveMeeting()}
/> />
</div> </div>
<div
<Settings className='toolarea-wrapper'
onToggleSettings={onToggleSettings} style={{
/> width : toolAreaOpen ? '20%' : '0%'
}}
<ReactTooltip >
effect='solid' {toolAreaOpen ?
delayShow={100} <ToolArea
delayHide={100} advancedMode={room.advancedMode}
/> />
:null
}
</div>
</div> </div>
</Appear> </Appear>
); );
@ -199,18 +204,18 @@ class Room extends React.Component
Room.propTypes = Room.propTypes =
{ {
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
amActiveSpeaker : PropTypes.bool.isRequired, amActiveSpeaker : PropTypes.bool.isRequired,
screenProducer : appPropTypes.Producer, toolAreaOpen : PropTypes.bool.isRequired,
onRoomLinkCopy : PropTypes.func.isRequired, screenProducer : appPropTypes.Producer,
onShareScreen : PropTypes.func.isRequired, onRoomLinkCopy : PropTypes.func.isRequired,
onUnShareScreen : PropTypes.func.isRequired, onShareScreen : PropTypes.func.isRequired,
onNeedExtension : PropTypes.func.isRequired, onUnShareScreen : PropTypes.func.isRequired,
onToggleSettings : PropTypes.func.isRequired, onNeedExtension : PropTypes.func.isRequired,
onToggleHand : PropTypes.func.isRequired, onToggleHand : PropTypes.func.isRequired,
onLeaveMeeting : PropTypes.func.isRequired, onLeaveMeeting : PropTypes.func.isRequired,
onLogin : PropTypes.func.isRequired onLogin : PropTypes.func.isRequired
}; };
const mapStateToProps = (state) => const mapStateToProps = (state) =>
@ -222,6 +227,7 @@ const mapStateToProps = (state) =>
return { return {
room : state.room, room : state.room,
me : state.me, me : state.me,
toolAreaOpen : state.toolarea.toolAreaOpen,
amActiveSpeaker : state.me.name === state.room.activeSpeakerName, amActiveSpeaker : state.me.name === state.room.activeSpeakerName,
screenProducer : screenProducer screenProducer : screenProducer
}; };
@ -237,10 +243,6 @@ const mapDispatchToProps = (dispatch) =>
text : 'Room link copied to the clipboard' text : 'Room link copied to the clipboard'
})); }));
}, },
onToggleSettings : () =>
{
dispatch(stateActions.toggleSettings());
},
onToggleHand : (enable) => onToggleHand : (enable) =>
{ {
if (enable) if (enable)

View File

@ -4,7 +4,6 @@ import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions'; import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions'; import * as stateActions from '../redux/stateActions';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Appear } from './transitions';
import Dropdown from 'react-dropdown'; import Dropdown from 'react-dropdown';
class Settings extends React.Component class Settings extends React.Component
@ -21,13 +20,9 @@ class Settings extends React.Component
me, me,
handleChangeWebcam, handleChangeWebcam,
handleChangeAudioDevice, handleChangeAudioDevice,
onToggleSettings,
onToggleAdvancedMode onToggleAdvancedMode
} = this.props; } = this.props;
if (!room.showSettings)
return null;
let webcams; let webcams;
let webcamText; let webcamText;
@ -55,43 +50,28 @@ class Settings extends React.Component
audioDevices = []; audioDevices = [];
return ( return (
<Appear duration={500}> <div data-component='Settings'>
<div data-component='Settings'> <div className='settings'>
<div className='dialog'> <Dropdown
<div className='header'> disabled={!me.canChangeWebcam}
<span>Settings</span> options={webcams}
</div> onChange={handleChangeWebcam}
<div className='settings'> placeholder={webcamText}
<Dropdown />
disabled={!me.canChangeWebcam} <Dropdown
options={webcams} disabled={!me.canChangeAudioDevice}
onChange={handleChangeWebcam} options={audioDevices}
placeholder={webcamText} onChange={handleChangeAudioDevice}
/> placeholder={audioDevicesText}
<Dropdown />
disabled={!me.canChangeAudioDevice} <input
options={audioDevices} type='checkbox'
onChange={handleChangeAudioDevice} defaultChecked={room.advancedMode}
placeholder={audioDevicesText} onChange={onToggleAdvancedMode}
/> />
<input <span>Advanced mode</span>
type='checkbox'
defaultChecked={room.advancedMode}
onChange={onToggleAdvancedMode}
/>
<span>Advanced mode</span>
</div>
<div className='footer'>
<span
className='button'
onClick={() => onToggleSettings()}
>
Close
</span>
</div>
</div>
</div> </div>
</Appear> </div>
); );
} }
} }
@ -100,7 +80,6 @@ Settings.propTypes =
{ {
me : appPropTypes.Me.isRequired, me : appPropTypes.Me.isRequired,
room : appPropTypes.Room.isRequired, room : appPropTypes.Room.isRequired,
onToggleSettings : PropTypes.func.isRequired,
handleChangeWebcam : PropTypes.func.isRequired, handleChangeWebcam : PropTypes.func.isRequired,
handleChangeAudioDevice : PropTypes.func.isRequired, handleChangeAudioDevice : PropTypes.func.isRequired,
onToggleAdvancedMode : PropTypes.func.isRequired onToggleAdvancedMode : PropTypes.func.isRequired

View File

@ -0,0 +1,125 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import * as stateActions from '../../redux/stateActions';
import ParticipantList from '../ParticipantList/ParticipantList';
import Chat from '../Chat/Chat';
import Settings from '../Settings';
class ToolArea extends React.Component
{
constructor(props)
{
super(props);
}
render()
{
const {
toolarea,
setToolTab
} = this.props;
return (
<div data-component='ToolArea'>
<div className='tabs'>
<input
type='radio'
name='tabs'
id='tab-chat'
onChange={() =>
{
setToolTab('chat');
}}
checked={toolarea.currentToolTab === 'chat'}
/>
<label htmlFor='tab-chat'>Chat</label>
<div className='tab'>
<Chat />
</div>
<input
type='radio'
name='tabs'
id='tab-users'
onChange={() =>
{
setToolTab('users');
}}
checked={toolarea.currentToolTab === 'users'}
/>
<label htmlFor='tab-users'>Users</label>
<div className='tab'>
<ParticipantList />
</div>
<input
type='radio'
name='tabs'
id='tab-settings'
onChange={() =>
{
setToolTab('settings');
}}
checked={toolarea.currentToolTab === 'settings'}
/>
<label htmlFor='tab-settings'>Settings</label>
<div className='tab'>
<Settings />
</div>
<input
type='radio'
name='tabs'
id='tab-layout'
onChange={() =>
{
setToolTab('layout');
}}
checked={toolarea.currentToolTab === 'layout'}
/>
<label htmlFor='tab-layout'>Layout</label>
<div className='tab'>
<h1>Tab Three Content</h1>
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit.</p>
</div>
</div>
</div>
);
}
}
ToolArea.propTypes =
{
advancedMode : PropTypes.bool,
toolarea : PropTypes.object.isRequired,
setToolTab : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
toolarea : state.toolarea
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
setToolTab : (toolTab) =>
{
dispatch(stateActions.setToolTab(toolTab));
}
};
};
const ToolAreaContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ToolArea);
export default ToolAreaContainer;

View File

@ -0,0 +1,60 @@
import React from 'react';
import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import * as stateActions from '../../redux/stateActions';
class ToolAreaButton extends React.Component
{
render()
{
const {
toolAreaOpen,
toggleToolArea
} = this.props;
return (
<div data-component='ToolAreaButton'>
<div
className={classnames('button', 'toolarea-button', {
on : toolAreaOpen
})}
data-tip='Toggle tool area'
data-type='dark'
data-for='globaltip'
onClick={() => toggleToolArea()}
/>
</div>
);
}
}
ToolAreaButton.propTypes =
{
toolAreaOpen : PropTypes.bool.isRequired,
toggleToolArea : PropTypes.func.isRequired
};
const mapStateToProps = (state) =>
{
return {
toolAreaOpen : state.toolarea.toolAreaOpen
};
};
const mapDispatchToProps = (dispatch) =>
{
return {
toggleToolArea : () =>
{
dispatch(stateActions.toggleToolArea());
}
};
};
const ToolAreaButtonContainer = connect(
mapStateToProps,
mapDispatchToProps
)(ToolAreaButton);
export default ToolAreaButtonContainer;

View File

@ -7,6 +7,7 @@ import consumers from './consumers';
import notifications from './notifications'; import notifications from './notifications';
import chatmessages from './chatmessages'; import chatmessages from './chatmessages';
import chatbehavior from './chatbehavior'; import chatbehavior from './chatbehavior';
import toolarea from './toolarea';
const reducers = combineReducers( const reducers = combineReducers(
{ {
@ -17,7 +18,8 @@ const reducers = combineReducers(
consumers, consumers,
notifications, notifications,
chatmessages, chatmessages,
chatbehavior chatbehavior,
toolarea
}); });
export default reducers; export default reducers;

View File

@ -60,6 +60,19 @@ const peers = (state = initialState, action) =>
return { ...state, [newPeer.name]: newPeer }; return { ...state, [newPeer.name]: newPeer };
} }
case 'SET_PEER_SCREEN_IN_PROGRESS':
{
const { peerName, flag } = action.payload;
const peer = state[peerName];
if (!peer)
throw new Error('no Peer found');
const newPeer = { ...peer, peerScreenInProgress: flag };
return { ...state, [newPeer.name]: newPeer };
}
case 'SET_PEER_RAISE_HAND_STATE': case 'SET_PEER_RAISE_HAND_STATE':
{ {
const { peerName, raiseHandState } = action.payload; const { peerName, raiseHandState } = action.payload;

View File

@ -0,0 +1,30 @@
const initialState =
{
toolAreaOpen : false,
currentToolTab : 'chat' // chat, settings, layout, users
};
const toolarea = (state = initialState, action) =>
{
switch (action.type)
{
case 'TOGGLE_TOOL_AREA':
{
const toolAreaOpen = !state.toolAreaOpen;
return { ...state, toolAreaOpen };
}
case 'SET_TOOL_TAB':
{
const { toolTab } = action.payload;
return { ...state, currentToolTab: toolTab };
}
default:
return state;
}
};
export default toolarea;

View File

@ -119,6 +119,22 @@ export const resumePeerVideo = (peerName) =>
}; };
}; };
export const pausePeerScreen = (peerName) =>
{
return {
type : 'PAUSE_PEER_SCREEN',
payload : { peerName }
};
};
export const resumePeerScreen = (peerName) =>
{
return {
type : 'RESUME_PEER_SCREEN',
payload : { peerName }
};
};
export const userLogin = () => export const userLogin = () =>
{ {
return { return {

View File

@ -149,6 +149,24 @@ export default ({ dispatch, getState }) => (next) =>
break; break;
} }
case 'PAUSE_PEER_SCREEN':
{
const { peerName } = action.payload;
client.pausePeerScreen(peerName);
break;
}
case 'RESUME_PEER_SCREEN':
{
const { peerName } = action.payload;
client.resumePeerScreen(peerName);
break;
}
case 'RAISE_HAND': case 'RAISE_HAND':
{ {
client.sendRaiseHandState(true); client.sendRaiseHandState(true);

View File

@ -125,6 +125,14 @@ export const setPeerAudioInProgress = (peerName, flag) =>
}; };
}; };
export const setPeerScreenInProgress = (peerName, flag) =>
{
return {
type : 'SET_PEER_SCREEN_IN_PROGRESS',
payload : { peerName, flag }
};
};
export const setMyRaiseHandState = (flag) => export const setMyRaiseHandState = (flag) =>
{ {
return { return {
@ -148,6 +156,21 @@ export const toggleSettings = () =>
}; };
}; };
export const toggleToolArea = () =>
{
return {
type : 'TOGGLE_TOOL_AREA'
};
};
export const setToolTab = (toolTab) =>
{
return {
type : 'SET_TOOL_TAB',
payload : { toolTab }
};
};
export const setMyRaiseHandStateInProgress = (flag) => export const setMyRaiseHandStateInProgress = (flag) =>
{ {
return { return {

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 0 24 24" width="48">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>

After

Width:  |  Height:  |  Size: 195 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFFFFF" height="48" viewBox="0 0 24 24" width="48">
<path d="M0 0h24v24H0z" fill="none"/>
<path d="M3 18h18v-2H3v2zm0-5h18v-2H3v2zm0-7v2h18V6H3z"/>
</svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@ -1,10 +1,10 @@
[data-component='ChatWidget'] { [data-component='ChatWidget'] {
position: absolute;
bottom: 0; bottom: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 0 10px 10px 0; margin: 0 10px 10px 0;
max-width: 300px; max-width: 300px;
position: fixed;
right: 0; right: 0;
width: 90vw; width: 90vw;
z-index: 9999; z-index: 9999;
@ -56,10 +56,13 @@
box-shadow: 0px 2px 10px 1px #000; box-shadow: 0px 2px 10px 1px #000;
} }
[data-component='Chat'] {
height: 100%;
}
[data-component='MessageList'] { [data-component='MessageList'] {
background-color: rgba(#fff, 0.9); background-color: rgba(#fff, 0.9);
height: 50vh; height: 91vmin;
max-height: 350px;
overflow-y: scroll; overflow-y: scroll;
padding-top: 5px; padding-top: 5px;
border-radius: 5px 5px 0px 0px; border-radius: 5px 5px 0px 0px;
@ -114,8 +117,8 @@
align-items: center; align-items: center;
display: flex; display: flex;
background-color: rgba(#fff, 0.9); background-color: rgba(#fff, 0.9);
height: 35px; height: 6vmin;
padding: 5px; padding: 0.5vmin;
border-radius: 0 0 5px 5px; border-radius: 0 0 5px 5px;
> .new-message { > .new-message {

View File

@ -1,9 +1,9 @@
[data-component='Notifications'] { [data-component='Notifications'] {
position: fixed; position: absolute;
z-index: 9999; z-index: 9999;
pointer-events: none; pointer-events: none;
top: 0; top: 0;
right: 0; right: 65px;
bottom: 0; bottom: 0;
padding: 20px; padding: 20px;
display: flex; display: flex;

View File

@ -0,0 +1,132 @@
[data-component='ParticipantList'] {
width: 100%;
> .list {
box-shadow: 0 4px 10px 0 rgba(0,0,0,0.2), \
0 4px 20px 0 rgba(0,0,0,0.19);
> .list-item {
padding: 0.5vmin;
border-bottom: 1px solid #ddd;
width: 100%;
overflow: hidden;
}
}
}
[data-component='ListPeer'] {
> .controls {
float: right;
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
> .button {
flex: 0 0 auto;
margin: 0.2vmin;
border-radius: 2px;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#000, 0.5);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
+desktop() {
width: 24px;
height: 24px;
opacity: 0.85;
&:hover {
opacity: 1;
}
}
+mobile() {
width: 22px;
height: 22px;
}
&.unsupported {
pointer-events: none;
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
&.on {
background-color: rgba(#fff, 0.7);
}
&.mic {
&.on {
background-image: url('/resources/images/icon_mic_black_on.svg');
}
&.off {
background-image: url('/resources/images/icon_remote_mic_white_off.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/icon_mic_white_unsupported.svg');
}
}
&.webcam {
&.on {
background-image: url('/resources/images/icon_webcam_black_on.svg');
}
&.off {
background-image: url('/resources/images/icon_remote_webcam_white_off.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/icon_webcam_white_unsupported.svg');
}
}
&.screen {
&.on {
background-image: url('/resources/images/share-screen-black.svg');
}
&.off {
background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
}
&.unsupported {
background-image: url('/resources/images/no-share-screen-white.svg');
}
}
}
}
> .avatar {
padding: 8px 16px;
float: left;
width: auto;
border: none;
display: block;
outline: 0;
border-radius: 50%;
vertical-align: middle;
}
> .peer-info {
font-size: 1.4vmin;
float: left;
width: auto;
border: none;
display: block;
outline: 0;
padding: 0.6vmin;
}
}

View File

@ -1,268 +1,283 @@
[data-component='Room'] { [data-component='Room'] {
position: relative;
height: 100%;
width: 100%;
AppearFadeIn(300ms); AppearFadeIn(300ms);
> .state { > .room-wrapper {
position: fixed;
z-index: 100;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
border-radius: 25px;
background-color: rgba(#fff, 0.2);
+desktop() {
top: 20px;
left: 20px;
width: 124px;
}
+mobile() {
top: 10px;
left: 10px;
width: 110px;
}
> .icon {
flex: 0 0 auto;
border-radius: 100%;
+desktop() {
margin: 5px;
margin-right: 0;
height: 20px;
width: 20px;
}
+mobile() {
margin: 4px;
margin-right: 0;
height: 16px;
width: 16px;
}
&.new, &.closed {
background-color: rgba(#aaa, 0.5);
}
&.connecting {
animation: Room-info-state-connecting .75s infinite linear;
}
&.connected {
background-color: rgba(#30bd18, 0.75);
+mobile() {
display: none;
}
}
}
> .text {
flex: 100 0 auto;
user-select: none;
pointer-events: none;
text-align: center;
text-transform: uppercase;
font-family: 'Roboto';
font-weight: 400;
color: rgba(#fff, 0.75);
+desktop() {
font-size: 12px;
}
+mobile() {
font-size: 10px;
}
&.connected {
+mobile() {
display: none;
}
}
}
}
> .room-link-wrapper {
pointer-events: none;
position: absolute; position: absolute;
z-index: 1;
top: 0; top: 0;
left: 0; left: 0;
right: 0; height: 100%;
display: flex; width: 100%;
flex-direction: row; transition: width 0.3s;
justify-content: center;
> .room-link { > .state {
width: auto; position: fixed;
background-color: rgba(#fff, 0.75); z-index: 100;
border-bottom-right-radius: 4px; display: flex;
border-bottom-left-radius: 4px; flex-direction: row;
box-shadow: 0px 3px 12px 2px rgba(#111, 0.4); justify-content: center;
align-items: center;
border-radius: 25px;
background-color: rgba(#fff, 0.2);
> a.link { +desktop() {
display: block;; top: 20px;
user-select: none; left: 20px;
pointer-events: auto; width: 124px;
color: #104758; }
font-weight: 400;
cursor: pointer; +mobile() {
text-decoration: none; top: 10px;
transition-property: opacity; left: 10px;
transition-duration: 0.25s; width: 110px;
opacity: 0.8; }
> .icon {
flex: 0 0 auto;
border-radius: 100%;
+desktop() { +desktop() {
padding: 10px 20px; margin: 5px;
font-size: 16px; margin-right: 0;
height: 20px;
width: 20px;
} }
+mobile() { +mobile() {
padding: 6px 10px; margin: 4px;
font-size: 14px; margin-right: 0;
height: 16px;
width: 16px;
} }
&:hover { &.new, &.closed {
opacity: 1; background-color: rgba(#aaa, 0.5);
text-decoration: underline; }
&.connecting {
animation: Room-info-state-connecting .75s infinite linear;
}
&.connected {
background-color: rgba(#30bd18, 0.75);
+mobile() {
display: none;
}
}
}
> .text {
flex: 100 0 auto;
user-select: none;
pointer-events: none;
text-align: center;
text-transform: uppercase;
font-family: 'Roboto';
font-weight: 400;
color: rgba(#fff, 0.75);
+desktop() {
font-size: 12px;
}
+mobile() {
font-size: 10px;
}
&.connected {
+mobile() {
display: none;
}
} }
} }
} }
}
> .me-container { > .room-link-wrapper {
position: fixed; pointer-events: none;
z-index: 100; position: absolute;
overflow: hidden; z-index: 1;
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5); top: 0;
transition-property: border-color; left: 0;
transition-duration: 0.15s; right: 0;
display: flex;
flex-direction: row;
justify-content: center;
&.active-speaker { > .room-link {
border-color: #fff; width: auto;
background-color: rgba(#fff, 0.75);
border-bottom-right-radius: 4px;
border-bottom-left-radius: 4px;
box-shadow: 0px 3px 12px 2px rgba(#111, 0.4);
> a.link {
display: block;;
user-select: none;
pointer-events: auto;
color: #104758;
font-weight: 400;
cursor: pointer;
text-decoration: none;
transition-property: opacity;
transition-duration: 0.25s;
opacity: 0.8;
+desktop() {
padding: 10px 20px;
font-size: 16px;
}
+mobile() {
padding: 6px 10px;
font-size: 14px;
}
&:hover {
opacity: 1;
text-decoration: underline;
}
}
}
} }
+desktop() { > .me-container {
height: 200px; position: fixed;
width: 235px; z-index: 100;
bottom: 20px; overflow: hidden;
left: 20px; box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);
border: 1px solid rgba(#fff, 0.15); transition-property: border-color;
}
+mobile() {
height: 175px;
width: 150px;
bottom: 10px;
left: 10px;
border: 1px solid rgba(#fff, 0.25);
}
}
> .sidebar {
position: fixed;
z-index: 101;
top: calc(50% - 60px);
height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
+desktop() {
left: 20px;
width: 36px;
}
+mobile() {
left: 10px;
width: 32px;
}
> .button {
flex: 0 0 auto;
margin: 4px 0;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#fff, 0.3);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s; transition-duration: 0.15s;
border-radius: 100%;
&.active-speaker {
border-color: #fff;
}
+desktop() { +desktop() {
height: 36px; height: 200px;
width: 235px;
bottom: 20px;
left: 20px;
border: 1px solid rgba(#fff, 0.15);
}
+mobile() {
height: 175px;
width: 150px;
bottom: 10px;
left: 10px;
border: 1px solid rgba(#fff, 0.25);
}
}
> .sidebar {
position: fixed;
z-index: 101;
top: calc(50% - 60px);
height: 120px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
+desktop() {
left: 20px;
width: 36px; width: 36px;
} }
+mobile() { +mobile() {
height: 32px; left: 10px;
width: 32px; width: 32px;
} }
&.on { > .button {
background-color: rgba(#fff, 0.7); flex: 0 0 auto;
} margin: 4px 0;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#fff, 0.3);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
border-radius: 100%;
&.disabled { +desktop() {
pointer-events: none; height: 36px;
opacity: 0.5; width: 36px;
}
&.login {
&.off {
background-image: url('/resources/images/icon_login_white.svg');
} }
}
&.settings { +mobile() {
&.off { height: 32px;
background-image: url('/resources/images/icon_settings_white.svg'); width: 32px;
} }
&.on { &.on {
background-image: url('/resources/images/icon_settings_black.svg'); background-color: rgba(#fff, 0.7);
}
}
&.screen {
&.on {
background-image: url('/resources/images/no-share-screen-black.svg');
} }
&.off { &.disabled {
background-image: url('/resources/images/share-screen-white.svg'); pointer-events: none;
opacity: 0.5;
} }
&.unsupported { &.login {
background-image: url('/resources/images/no-share-screen-white.svg'); &.off {
background-color: rgba(#d42241, 0.7); background-image: url('/resources/images/icon_login_white.svg');
}
} }
&.need-extension { &.settings {
background-image: url('/resources/images/share-screen-extension.svg'); &.off {
} background-image: url('/resources/images/icon_settings_white.svg');
} }
&.raise-hand {
background-image: url('/resources/images/icon-hand-white.svg');
&.on { &.on {
background-image: url('/resources/images/icon-hand-black.svg'); background-image: url('/resources/images/icon_settings_black.svg');
}
} }
}
&.leave-meeting { &.screen {
background-image: url('/resources/images/leave-meeting.svg'); &.on {
background-image: url('/resources/images/no-share-screen-black.svg');
}
&.off {
background-image: url('/resources/images/share-screen-white.svg');
}
&.unsupported {
background-image: url('/resources/images/no-share-screen-white.svg');
background-color: rgba(#d42241, 0.7);
}
&.need-extension {
background-image: url('/resources/images/share-screen-extension.svg');
}
}
&.raise-hand {
background-image: url('/resources/images/icon-hand-white.svg');
&.on {
background-image: url('/resources/images/icon-hand-black.svg');
}
}
&.leave-meeting {
background-image: url('/resources/images/leave-meeting.svg');
}
} }
} }
} }
> .toolarea-wrapper {
position: fixed;
top: 0;
right: 0;
width: 20%;
height: 100%;
background-color: #FFF;
transition: width 0.3s;
}
} }
.Dropdown-root { .Dropdown-root {
@ -360,6 +375,60 @@
padding: 8px 10px; padding: 8px 10px;
} }
.react-tabs__tab-list {
border-bottom: 1px solid #aaa;
margin: 0 0 10px;
padding: 0;
}
.react-tabs__tab {
display: inline-block;
border: 1px solid transparent;
border-bottom: none;
bottom: -1px;
position: relative;
list-style: none;
padding: 6px 12px;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.react-tabs__tab--selected {
background: #fff;
border-color: #aaa;
color: black;
border-radius: 5px 5px 0 0;
}
.react-tabs__tab--disabled {
color: GrayText;
cursor: default;
}
.react-tabs__tab:focus {
box-shadow: 0 0 5px hsl(208, 99%, 50%);
border-color: hsl(208, 99%, 50%);
outline: none;
}
.react-tabs__tab:focus:after {
content: "";
position: absolute;
height: 5px;
left: -4px;
right: -4px;
bottom: -5px;
background: #fff;
}
.react-tabs__tab-panel {
display: none;
}
.react-tabs__tab-panel--selected {
display: block;
}
@keyframes Room-info-state-connecting { @keyframes Room-info-state-connecting {
50% { background-color: rgba(orange, 0.75); } 50% { background-color: rgba(orange, 0.75); }
} }

View File

@ -1,53 +1,3 @@
[data-component='Settings'] { [data-component='Settings'] {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 19999;
background-color: rgba(000, 000, 000, 0.5);
AppearFadeIn(500ms);
> .dialog {
position: absolute;
width: 40vmin;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
background-color: #fff;
border-radius: 4px;
box-shadow: 0px 3px 12px 2px rgba(#111, 0.4);
padding: 1vmin;
> .header {
> span {
font-size: 2vmin;
font-weight: 400;
}
}
> .settings {
}
> .footer {
bottom: 0;
right: 0;
left: 0;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
> .button {
flex: 0 0 auto;
margin: 1vmin;
background-color: rgba(#000, 0.8);
color: #fff;
cursor: pointer;
border-radius: 4px;
padding: 0.5vmin;
}
}
}
} }

View File

@ -0,0 +1,99 @@
[data-component='ToolAreaButton'] {
position: absolute;
z-index: 101;
top: 20px;
right: 20px;
height: 36px;
width: 36px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
> .button {
flex: 0 0 auto;
margin: 4px 0;
background-position: center;
background-size: 75%;
background-repeat: no-repeat;
background-color: rgba(#fff, 0.3);
cursor: pointer;
transition-property: opacity, background-color;
transition-duration: 0.15s;
border-radius: 100%;
+desktop() {
height: 36px;
width: 36px;
}
+mobile() {
height: 32px;
width: 32px;
}
&.on {
background-color: rgba(#fff, 0.7);
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
&.toolarea-button {
background-image: url('/resources/images/icon_tool_area_white.svg');
&.on {
background-image: url('/resources/images/icon_tool_area_black.svg');
}
}
}
}
[data-component='ToolArea'] {
width: 100%;
height: 100%;
> .tabs {
display: flex;
flex-wrap: wrap;
height: 100%;
> label {
order: 1;
display: block;
padding: 1vmin 0 1vmin 0;
cursor: pointer;
background: rgba(#000, 0.3);
font-weight: bold;
transition: background ease 0.2s;
text-align: center;
width: 25%;
font-size: 1.3vmin;
height: 3vmin;
}
> .tab {
order: 99;
flex-grow: 1;
width: 100%;
height: 100%;
display: none;
padding: 1vmin;
background: #fff;
}
> input[type="radio"] {
display: none;
}
> input[type="radio"]:checked + label {
background: #fff;
}
> input[type="radio"]:checked + label + .tab {
display: block;
}
}
}

View File

@ -43,6 +43,8 @@ body {
@import './components/Notifications'; @import './components/Notifications';
@import './components/Chat'; @import './components/Chat';
@import './components/Settings'; @import './components/Settings';
@import './components/ToolArea';
@import './components/ParticipantList';
} }
// Hack to detect in JS the current media query // Hack to detect in JS the current media query