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()
{
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 ClipboardButton from 'react-clipboard.js';
import * as appPropTypes from './appPropTypes';
import * as stateActions from '../redux/stateActions';
import * as requestActions from '../redux/requestActions';
import { Appear } from './transitions';
import Me from './Me';
import Peers from './Peers';
import Notifications from './Notifications';
import ChatWidget from './ChatWidget';
import Settings from './Settings';
import ToolAreaButton from './ToolArea/ToolAreaButton';
import ToolArea from './ToolArea/ToolArea';
class Room extends React.Component
{
@ -21,10 +20,10 @@ class Room extends React.Component
const {
room,
me,
toolAreaOpen,
amActiveSpeaker,
screenProducer,
onRoomLinkCopy,
onToggleSettings,
onLogin,
onShareScreen,
onUnShareScreen,
@ -60,8 +59,14 @@ class Room extends React.Component
return (
<Appear duration={300}>
<div data-component='Room'>
<div
className='room-wrapper'
style={{
width : toolAreaOpen ? '80%' : '100%'
}}
>
<Notifications />
<ChatWidget />
<ToolAreaButton />
<div className='state' data-tip='Server status'>
<div className={classnames('icon', room.state)} />
@ -145,16 +150,6 @@ class Room extends React.Component
}}
/>
<div
className={classnames('button', 'settings', {
on : room.showSettings,
off : !room.showSettings
})}
data-tip='Open settings'
data-type='dark'
onClick={() => onToggleSettings()}
/>
<div
className={classnames('button', 'login', 'off', {
disabled : me.loginInProgress
@ -182,16 +177,26 @@ class Room extends React.Component
/>
</div>
<Settings
onToggleSettings={onToggleSettings}
/>
<ReactTooltip
effect='solid'
delayShow={100}
delayHide={100}
/>
</div>
<div
className='toolarea-wrapper'
style={{
width : toolAreaOpen ? '20%' : '0%'
}}
>
{toolAreaOpen ?
<ToolArea
advancedMode={room.advancedMode}
/>
:null
}
</div>
</div>
</Appear>
);
}
@ -202,12 +207,12 @@ Room.propTypes =
room : appPropTypes.Room.isRequired,
me : appPropTypes.Me.isRequired,
amActiveSpeaker : PropTypes.bool.isRequired,
toolAreaOpen : PropTypes.bool.isRequired,
screenProducer : appPropTypes.Producer,
onRoomLinkCopy : PropTypes.func.isRequired,
onShareScreen : PropTypes.func.isRequired,
onUnShareScreen : PropTypes.func.isRequired,
onNeedExtension : PropTypes.func.isRequired,
onToggleSettings : PropTypes.func.isRequired,
onToggleHand : PropTypes.func.isRequired,
onLeaveMeeting : PropTypes.func.isRequired,
onLogin : PropTypes.func.isRequired
@ -222,6 +227,7 @@ const mapStateToProps = (state) =>
return {
room : state.room,
me : state.me,
toolAreaOpen : state.toolarea.toolAreaOpen,
amActiveSpeaker : state.me.name === state.room.activeSpeakerName,
screenProducer : screenProducer
};
@ -237,10 +243,6 @@ const mapDispatchToProps = (dispatch) =>
text : 'Room link copied to the clipboard'
}));
},
onToggleSettings : () =>
{
dispatch(stateActions.toggleSettings());
},
onToggleHand : (enable) =>
{
if (enable)

View File

@ -4,7 +4,6 @@ import * as appPropTypes from './appPropTypes';
import * as requestActions from '../redux/requestActions';
import * as stateActions from '../redux/stateActions';
import PropTypes from 'prop-types';
import { Appear } from './transitions';
import Dropdown from 'react-dropdown';
class Settings extends React.Component
@ -21,13 +20,9 @@ class Settings extends React.Component
me,
handleChangeWebcam,
handleChangeAudioDevice,
onToggleSettings,
onToggleAdvancedMode
} = this.props;
if (!room.showSettings)
return null;
let webcams;
let webcamText;
@ -55,12 +50,7 @@ class Settings extends React.Component
audioDevices = [];
return (
<Appear duration={500}>
<div data-component='Settings'>
<div className='dialog'>
<div className='header'>
<span>Settings</span>
</div>
<div className='settings'>
<Dropdown
disabled={!me.canChangeWebcam}
@ -81,17 +71,7 @@ class Settings extends React.Component
/>
<span>Advanced mode</span>
</div>
<div className='footer'>
<span
className='button'
onClick={() => onToggleSettings()}
>
Close
</span>
</div>
</div>
</div>
</Appear>
);
}
}
@ -100,7 +80,6 @@ Settings.propTypes =
{
me : appPropTypes.Me.isRequired,
room : appPropTypes.Room.isRequired,
onToggleSettings : PropTypes.func.isRequired,
handleChangeWebcam : PropTypes.func.isRequired,
handleChangeAudioDevice : 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 chatmessages from './chatmessages';
import chatbehavior from './chatbehavior';
import toolarea from './toolarea';
const reducers = combineReducers(
{
@ -17,7 +18,8 @@ const reducers = combineReducers(
consumers,
notifications,
chatmessages,
chatbehavior
chatbehavior,
toolarea
});
export default reducers;

View File

@ -60,6 +60,19 @@ const peers = (state = initialState, action) =>
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':
{
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 = () =>
{
return {

View File

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

View File

@ -1,9 +1,9 @@
[data-component='Notifications'] {
position: fixed;
position: absolute;
z-index: 9999;
pointer-events: none;
top: 0;
right: 0;
right: 65px;
bottom: 0;
padding: 20px;
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,9 +1,13 @@
[data-component='Room'] {
position: relative;
AppearFadeIn(300ms);
> .room-wrapper {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
AppearFadeIn(300ms);
transition: width 0.3s;
> .state {
position: fixed;
@ -263,6 +267,17 @@
}
}
}
}
> .toolarea-wrapper {
position: fixed;
top: 0;
right: 0;
width: 20%;
height: 100%;
background-color: #FFF;
transition: width 0.3s;
}
}
.Dropdown-root {
@ -360,6 +375,60 @@
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 {
50% { background-color: rgba(orange, 0.75); }
}

View File

@ -1,53 +1,3 @@
[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/Chat';
@import './components/Settings';
@import './components/ToolArea';
@import './components/ParticipantList';
}
// Hack to detect in JS the current media query