Merge branch 'feature-toolarea' into develop
commit
8ee85e6384
|
|
@ -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()');
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,137 +59,143 @@ class Room extends React.Component
|
|||
return (
|
||||
<Appear duration={300}>
|
||||
<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
|
||||
className={classnames('me-container', {
|
||||
'active-speaker' : amActiveSpeaker
|
||||
})}
|
||||
className='room-wrapper'
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className='sidebar'>
|
||||
<div
|
||||
className={classnames('button', 'screen', screenState)}
|
||||
data-tip={screenTip}
|
||||
data-type='dark'
|
||||
onClick={() =>
|
||||
{
|
||||
switch (screenState)
|
||||
className={classnames('me-container', {
|
||||
'active-speaker' : amActiveSpeaker
|
||||
})}
|
||||
>
|
||||
<Me
|
||||
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();
|
||||
break;
|
||||
case 'on':
|
||||
{
|
||||
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
|
||||
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
|
||||
})}
|
||||
data-tip='Login'
|
||||
data-type='dark'
|
||||
onClick={() => onLogin()}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'login', 'off', {
|
||||
disabled : me.loginInProgress
|
||||
})}
|
||||
data-tip='Login'
|
||||
data-type='dark'
|
||||
onClick={() => onLogin()}
|
||||
/>
|
||||
<div
|
||||
className={classnames('button', 'raise-hand', {
|
||||
on : me.raiseHand,
|
||||
disabled : me.raiseHandInProgress
|
||||
})}
|
||||
data-tip='Raise hand'
|
||||
data-type='dark'
|
||||
onClick={() => onToggleHand(!me.raiseHand)}
|
||||
/>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'raise-hand', {
|
||||
on : me.raiseHand,
|
||||
disabled : me.raiseHandInProgress
|
||||
})}
|
||||
data-tip='Raise hand'
|
||||
data-type='dark'
|
||||
onClick={() => onToggleHand(!me.raiseHand)}
|
||||
/>
|
||||
<div
|
||||
className={classnames('button', 'leave-meeting')}
|
||||
data-tip='Leave meeting'
|
||||
data-type='dark'
|
||||
onClick={() => onLeaveMeeting()}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={classnames('button', 'leave-meeting')}
|
||||
data-tip='Leave meeting'
|
||||
data-type='dark'
|
||||
onClick={() => onLeaveMeeting()}
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
delayShow={100}
|
||||
delayHide={100}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Settings
|
||||
onToggleSettings={onToggleSettings}
|
||||
/>
|
||||
|
||||
<ReactTooltip
|
||||
effect='solid'
|
||||
delayShow={100}
|
||||
delayHide={100}
|
||||
/>
|
||||
<div
|
||||
className='toolarea-wrapper'
|
||||
style={{
|
||||
width : toolAreaOpen ? '20%' : '0%'
|
||||
}}
|
||||
>
|
||||
{toolAreaOpen ?
|
||||
<ToolArea
|
||||
advancedMode={room.advancedMode}
|
||||
/>
|
||||
:null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</Appear>
|
||||
);
|
||||
|
|
@ -199,18 +204,18 @@ class Room extends React.Component
|
|||
|
||||
Room.propTypes =
|
||||
{
|
||||
room : appPropTypes.Room.isRequired,
|
||||
me : appPropTypes.Me.isRequired,
|
||||
amActiveSpeaker : 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
|
||||
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,
|
||||
onToggleHand : PropTypes.func.isRequired,
|
||||
onLeaveMeeting : PropTypes.func.isRequired,
|
||||
onLogin : PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = (state) =>
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,43 +50,28 @@ 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}
|
||||
options={webcams}
|
||||
onChange={handleChangeWebcam}
|
||||
placeholder={webcamText}
|
||||
/>
|
||||
<Dropdown
|
||||
disabled={!me.canChangeAudioDevice}
|
||||
options={audioDevices}
|
||||
onChange={handleChangeAudioDevice}
|
||||
placeholder={audioDevicesText}
|
||||
/>
|
||||
<input
|
||||
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 data-component='Settings'>
|
||||
<div className='settings'>
|
||||
<Dropdown
|
||||
disabled={!me.canChangeWebcam}
|
||||
options={webcams}
|
||||
onChange={handleChangeWebcam}
|
||||
placeholder={webcamText}
|
||||
/>
|
||||
<Dropdown
|
||||
disabled={!me.canChangeAudioDevice}
|
||||
options={audioDevices}
|
||||
onChange={handleChangeAudioDevice}
|
||||
placeholder={audioDevicesText}
|
||||
/>
|
||||
<input
|
||||
type='checkbox'
|
||||
defaultChecked={room.advancedMode}
|
||||
onChange={onToggleAdvancedMode}
|
||||
/>
|
||||
<span>Advanced mode</span>
|
||||
</div>
|
||||
</Appear>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,268 +1,283 @@
|
|||
[data-component='Room'] {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
AppearFadeIn(300ms);
|
||||
|
||||
> .state {
|
||||
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;
|
||||
> .room-wrapper {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition: width 0.3s;
|
||||
|
||||
> .room-link {
|
||||
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);
|
||||
> .state {
|
||||
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);
|
||||
|
||||
> 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() {
|
||||
top: 20px;
|
||||
left: 20px;
|
||||
width: 124px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
width: 110px;
|
||||
}
|
||||
|
||||
> .icon {
|
||||
flex: 0 0 auto;
|
||||
border-radius: 100%;
|
||||
|
||||
+desktop() {
|
||||
padding: 10px 20px;
|
||||
font-size: 16px;
|
||||
margin: 5px;
|
||||
margin-right: 0;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
padding: 6px 10px;
|
||||
font-size: 14px;
|
||||
margin: 4px;
|
||||
margin-right: 0;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
text-decoration: underline;
|
||||
&.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
> .me-container {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);
|
||||
transition-property: border-color;
|
||||
transition-duration: 0.15s;
|
||||
> .room-link-wrapper {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
|
||||
&.active-speaker {
|
||||
border-color: #fff;
|
||||
> .room-link {
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
|
||||
+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;
|
||||
> .me-container {
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
overflow: hidden;
|
||||
box-shadow: 0px 5px 12px 2px rgba(#111, 0.5);
|
||||
transition-property: border-color;
|
||||
transition-duration: 0.15s;
|
||||
border-radius: 100%;
|
||||
|
||||
&.active-speaker {
|
||||
border-color: #fff;
|
||||
}
|
||||
|
||||
+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;
|
||||
}
|
||||
|
||||
+mobile() {
|
||||
height: 32px;
|
||||
left: 10px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
&.on {
|
||||
background-color: rgba(#fff, 0.7);
|
||||
}
|
||||
> .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%;
|
||||
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.login {
|
||||
&.off {
|
||||
background-image: url('/resources/images/icon_login_white.svg');
|
||||
+desktop() {
|
||||
height: 36px;
|
||||
width: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
&.settings {
|
||||
&.off {
|
||||
background-image: url('/resources/images/icon_settings_white.svg');
|
||||
+mobile() {
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
&.on {
|
||||
background-image: url('/resources/images/icon_settings_black.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.screen {
|
||||
&.on {
|
||||
background-image: url('/resources/images/no-share-screen-black.svg');
|
||||
background-color: rgba(#fff, 0.7);
|
||||
}
|
||||
|
||||
&.off {
|
||||
background-image: url('/resources/images/share-screen-white.svg');
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&.unsupported {
|
||||
background-image: url('/resources/images/no-share-screen-white.svg');
|
||||
background-color: rgba(#d42241, 0.7);
|
||||
&.login {
|
||||
&.off {
|
||||
background-image: url('/resources/images/icon_login_white.svg');
|
||||
}
|
||||
}
|
||||
|
||||
&.need-extension {
|
||||
background-image: url('/resources/images/share-screen-extension.svg');
|
||||
}
|
||||
}
|
||||
&.raise-hand {
|
||||
background-image: url('/resources/images/icon-hand-white.svg');
|
||||
&.settings {
|
||||
&.off {
|
||||
background-image: url('/resources/images/icon_settings_white.svg');
|
||||
}
|
||||
|
||||
&.on {
|
||||
background-image: url('/resources/images/icon-hand-black.svg');
|
||||
&.on {
|
||||
background-image: url('/resources/images/icon_settings_black.svg');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.leave-meeting {
|
||||
background-image: url('/resources/images/leave-meeting.svg');
|
||||
&.screen {
|
||||
&.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 {
|
||||
|
|
@ -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); }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue