From 5168feefe0dc3237b0d6fe7a6d7bab58dfc605b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Wed, 20 Jun 2018 14:23:47 +0200 Subject: [PATCH] Initial work on tool area. Moved chat, settings and participant list into tool area. --- app/lib/RoomClient.js | 72 +++ app/lib/components/Chat/Chat.jsx | 89 ++++ .../components/ParticipantList/ListPeer.jsx | 166 ++++++ .../ParticipantList/ParticipantList.jsx | 80 +++ app/lib/components/Room.jsx | 274 +++++----- app/lib/components/Settings.jsx | 63 +-- app/lib/components/ToolArea/ToolArea.jsx | 125 +++++ .../components/ToolArea/ToolAreaButton.jsx | 60 +++ app/lib/redux/reducers/index.js | 4 +- app/lib/redux/reducers/peers.js | 13 + app/lib/redux/reducers/toolarea.js | 30 ++ app/lib/redux/requestActions.js | 16 + app/lib/redux/roomClientMiddleware.js | 18 + app/lib/redux/stateActions.js | 23 + app/resources/images/icon_tool_area_black.svg | 4 + app/resources/images/icon_tool_area_white.svg | 4 + app/stylus/components/Chat.styl | 15 +- app/stylus/components/Notifications.styl | 4 +- app/stylus/components/ParticipantList.styl | 132 +++++ app/stylus/components/Room.styl | 499 ++++++++++-------- app/stylus/components/Settings.styl | 50 -- app/stylus/components/ToolArea.styl | 99 ++++ app/stylus/index.styl | 2 + 23 files changed, 1390 insertions(+), 452 deletions(-) create mode 100644 app/lib/components/Chat/Chat.jsx create mode 100644 app/lib/components/ParticipantList/ListPeer.jsx create mode 100644 app/lib/components/ParticipantList/ParticipantList.jsx create mode 100644 app/lib/components/ToolArea/ToolArea.jsx create mode 100644 app/lib/components/ToolArea/ToolAreaButton.jsx create mode 100644 app/lib/redux/reducers/toolarea.js create mode 100644 app/resources/images/icon_tool_area_black.svg create mode 100644 app/resources/images/icon_tool_area_white.svg create mode 100644 app/stylus/components/ParticipantList.styl create mode 100644 app/stylus/components/ToolArea.styl diff --git a/app/lib/RoomClient.js b/app/lib/RoomClient.js index 8a474cc..1c35374 100644 --- a/app/lib/RoomClient.js +++ b/app/lib/RoomClient.js @@ -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()'); diff --git a/app/lib/components/Chat/Chat.jsx b/app/lib/components/Chat/Chat.jsx new file mode 100644 index 0000000..e678a3d --- /dev/null +++ b/app/lib/components/Chat/Chat.jsx @@ -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 ( +
+ +
{ onSendMessage(e, displayName); }} + > + +
+
+ ); + } +} + +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; diff --git a/app/lib/components/ParticipantList/ListPeer.jsx b/app/lib/components/ParticipantList/ListPeer.jsx new file mode 100644 index 0000000..2fb666c --- /dev/null +++ b/app/lib/components/ParticipantList/ListPeer.jsx @@ -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 ( +
+ +
+ {peer.displayName} +
+
+ { screenConsumer ? +
+ { + e.stopPropagation(); + screenVisible ? + onDisableScreen(peer.name) : onEnableScreen(peer.name); + }} + /> + :null + } +
+ { + e.stopPropagation(); + micEnabled ? onMuteMic(peer.name) : onUnmuteMic(peer.name); + }} + /> + +
+ { + e.stopPropagation(); + videoVisible ? + onDisableWebcam(peer.name) : onEnableWebcam(peer.name); + }} + /> +
+
+ ); +}; + +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; diff --git a/app/lib/components/ParticipantList/ParticipantList.jsx b/app/lib/components/ParticipantList/ParticipantList.jsx new file mode 100644 index 0000000..a212509 --- /dev/null +++ b/app/lib/components/ParticipantList/ParticipantList.jsx @@ -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 ( +
+
    + { + peers.map((peer) => + { + return ( +
  • + +
  • + ); + }) + } +
+
+ ); + } +} + +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; diff --git a/app/lib/components/Room.jsx b/app/lib/components/Room.jsx index 4675277..8ae0258 100644 --- a/app/lib/components/Room.jsx +++ b/app/lib/components/Room.jsx @@ -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 (
- - - -
-
-

{room.state}

-
- -
-
- - { - // 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 - -
-
- - -
- + + +
+
+

{room.state}

+
+ +
+
+ + { + // 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 + +
+
+ + -
-
- { - switch (screenState) + className={classnames('me-container', { + 'active-speaker' : amActiveSpeaker + })} + > + +
+ +
+
{ - 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; - } - } - }} - /> + }} + /> -
onToggleSettings()} - /> +
onLogin()} + /> -
onLogin()} - /> +
onToggleHand(!me.raiseHand)} + /> -
onToggleHand(!me.raiseHand)} - /> +
onLeaveMeeting()} + /> +
-
onLeaveMeeting()} +
- - - - +
+ {toolAreaOpen ? + + :null + } +
); @@ -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) diff --git a/app/lib/components/Settings.jsx b/app/lib/components/Settings.jsx index c41d65c..e4bacb0 100644 --- a/app/lib/components/Settings.jsx +++ b/app/lib/components/Settings.jsx @@ -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 ( - -
-
-
- Settings -
-
- - - - Advanced mode -
-
- onToggleSettings()} - > - Close - -
-
+
+
+ + + + Advanced mode
- +
); } } @@ -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 diff --git a/app/lib/components/ToolArea/ToolArea.jsx b/app/lib/components/ToolArea/ToolArea.jsx new file mode 100644 index 0000000..3ec7cf7 --- /dev/null +++ b/app/lib/components/ToolArea/ToolArea.jsx @@ -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 ( +
+
+ + { + setToolTab('chat'); + }} + checked={toolarea.currentToolTab === 'chat'} + /> + + +
+ +
+ + + { + setToolTab('users'); + }} + checked={toolarea.currentToolTab === 'users'} + /> + + +
+ +
+ + + { + setToolTab('settings'); + }} + checked={toolarea.currentToolTab === 'settings'} + /> + + +
+ +
+ + + { + setToolTab('layout'); + }} + checked={toolarea.currentToolTab === 'layout'} + /> + + +
+

Tab Three Content

+

Lorem ipsum dolor sit amet, consectetur adipisicing elit.

+
+
+
+ ); + } +} + +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; diff --git a/app/lib/components/ToolArea/ToolAreaButton.jsx b/app/lib/components/ToolArea/ToolAreaButton.jsx new file mode 100644 index 0000000..b158ba4 --- /dev/null +++ b/app/lib/components/ToolArea/ToolAreaButton.jsx @@ -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 ( +
+
toggleToolArea()} + /> +
+ ); + } +} + +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; diff --git a/app/lib/redux/reducers/index.js b/app/lib/redux/reducers/index.js index 779f775..e5c89a3 100644 --- a/app/lib/redux/reducers/index.js +++ b/app/lib/redux/reducers/index.js @@ -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; diff --git a/app/lib/redux/reducers/peers.js b/app/lib/redux/reducers/peers.js index b7001b9..d7174d5 100644 --- a/app/lib/redux/reducers/peers.js +++ b/app/lib/redux/reducers/peers.js @@ -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; diff --git a/app/lib/redux/reducers/toolarea.js b/app/lib/redux/reducers/toolarea.js new file mode 100644 index 0000000..00ad64a --- /dev/null +++ b/app/lib/redux/reducers/toolarea.js @@ -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; diff --git a/app/lib/redux/requestActions.js b/app/lib/redux/requestActions.js index ff865fa..5d9a240 100644 --- a/app/lib/redux/requestActions.js +++ b/app/lib/redux/requestActions.js @@ -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 { diff --git a/app/lib/redux/roomClientMiddleware.js b/app/lib/redux/roomClientMiddleware.js index 64a1059..61aa34b 100644 --- a/app/lib/redux/roomClientMiddleware.js +++ b/app/lib/redux/roomClientMiddleware.js @@ -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); diff --git a/app/lib/redux/stateActions.js b/app/lib/redux/stateActions.js index 4fb47fe..6c7270a 100644 --- a/app/lib/redux/stateActions.js +++ b/app/lib/redux/stateActions.js @@ -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 { diff --git a/app/resources/images/icon_tool_area_black.svg b/app/resources/images/icon_tool_area_black.svg new file mode 100644 index 0000000..1acc639 --- /dev/null +++ b/app/resources/images/icon_tool_area_black.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/resources/images/icon_tool_area_white.svg b/app/resources/images/icon_tool_area_white.svg new file mode 100644 index 0000000..c860ab2 --- /dev/null +++ b/app/resources/images/icon_tool_area_white.svg @@ -0,0 +1,4 @@ + + + + diff --git a/app/stylus/components/Chat.styl b/app/stylus/components/Chat.styl index 45f5b63..05794b5 100644 --- a/app/stylus/components/Chat.styl +++ b/app/stylus/components/Chat.styl @@ -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; @@ -27,7 +27,7 @@ &.focus { outline: none; } - + &.on { background-color: rgba(#fff, 0.7); } @@ -44,10 +44,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; @@ -102,8 +105,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 { diff --git a/app/stylus/components/Notifications.styl b/app/stylus/components/Notifications.styl index 35d86bf..329fa64 100644 --- a/app/stylus/components/Notifications.styl +++ b/app/stylus/components/Notifications.styl @@ -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; diff --git a/app/stylus/components/ParticipantList.styl b/app/stylus/components/ParticipantList.styl new file mode 100644 index 0000000..1f0bd98 --- /dev/null +++ b/app/stylus/components/ParticipantList.styl @@ -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; + } +} diff --git a/app/stylus/components/Room.styl b/app/stylus/components/Room.styl index ec3e43c..aa62aa0 100644 --- a/app/stylus/components/Room.styl +++ b/app/stylus/components/Room.styl @@ -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); } } diff --git a/app/stylus/components/Settings.styl b/app/stylus/components/Settings.styl index a58d04a..b5f818d 100644 --- a/app/stylus/components/Settings.styl +++ b/app/stylus/components/Settings.styl @@ -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; - } - } - } } diff --git a/app/stylus/components/ToolArea.styl b/app/stylus/components/ToolArea.styl new file mode 100644 index 0000000..cd177f2 --- /dev/null +++ b/app/stylus/components/ToolArea.styl @@ -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; + } + } +} diff --git a/app/stylus/index.styl b/app/stylus/index.styl index 9f9fec5..4d9561c 100644 --- a/app/stylus/index.styl +++ b/app/stylus/index.styl @@ -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