From 7c134039212fd0b60eb7a84e5663ead46ce4f800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Wed, 6 May 2020 23:39:40 +0200 Subject: [PATCH 01/28] Add help and about modal --- app/src/actions/roomActions.js | 12 +++ app/src/components/Controls/About.js | 133 ++++++++++++++++++++++++++ app/src/components/Controls/Help.js | 99 +++++++++++++++++++ app/src/components/Controls/TopBar.js | 54 +++++++++++ app/src/components/Room.js | 9 ++ app/src/reducers/room.js | 16 ++++ 6 files changed, 323 insertions(+) create mode 100644 app/src/components/Controls/About.js create mode 100644 app/src/components/Controls/Help.js diff --git a/app/src/actions/roomActions.js b/app/src/actions/roomActions.js index b90bf1b..7c44835 100644 --- a/app/src/actions/roomActions.js +++ b/app/src/actions/roomActions.js @@ -70,6 +70,18 @@ export const setExtraVideoOpen = (extraVideoOpen) => payload : { extraVideoOpen } }); +export const setHelpOpen = (helpOpen) => + ({ + type : 'SET_HELP_OPEN', + payload : { helpOpen } + }); + +export const setAboutOpen = (aboutOpen) => + ({ + type : 'SET_ABOUT_OPEN', + payload : { aboutOpen } + }); + export const setSettingsTab = (tab) => ({ type : 'SET_SETTINGS_TAB', diff --git a/app/src/components/Controls/About.js b/app/src/components/Controls/About.js new file mode 100644 index 0000000..d361a8c --- /dev/null +++ b/app/src/components/Controls/About.js @@ -0,0 +1,133 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { withStyles } from '@material-ui/core/styles'; +import { withRoomContext } from '../../RoomContext'; +import * as roomActions from '../../actions/roomActions'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import Dialog from '@material-ui/core/Dialog'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; +import Link from '@material-ui/core/Link'; +import Button from '@material-ui/core/Button'; + +const styles = (theme) => + ({ + dialogPaper : + { + width : '30vw', + [theme.breakpoints.down('lg')] : + { + width : '40vw' + }, + [theme.breakpoints.down('md')] : + { + width : '50vw' + }, + [theme.breakpoints.down('sm')] : + { + width : '70vw' + }, + [theme.breakpoints.down('xs')] : + { + width : '90vw' + } + }, + logo : + { + marginRight : 'auto' + }, + link : + { + display : 'block', + textAlign : 'center' + } + }); + +const About = ({ + aboutOpen, + handleCloseAbout, + classes +}) => +{ + return ( + handleCloseAbout(false)} + classes={{ + paper : classes.dialogPaper + }} + > + + + + + + Contributions to this work were made on behalf of the GÉANT + project, a project that has received funding from the + European Union’s Horizon 2020 research and innovation + programme under Grant Agreement No. 731122 (GN4-2). + On behalf of GÉANT project, GÉANT Association is the sole + owner of the copyright in all material which was developed + by a member of the GÉANT project.
+
+ GÉANT Vereniging (Association) is registered with the + Chamber of Commerce in Amsterdam with registration number + 40535155 and operates in the UK as a branch of GÉANT + Vereniging. Registered office: Hoekenrode 3, 1102BR + Amsterdam, The Netherlands. UK branch address: City House, + 126-130 Hills Road, Cambridge CB2 1PQ, UK. +
+ + https://edumeet.org + +
+ + { window.config.logo && Logo } + + +
+ ); +}; + +About.propTypes = +{ + roomClient : PropTypes.object.isRequired, + aboutOpen : PropTypes.bool.isRequired, + handleCloseAbout : PropTypes.func.isRequired, + classes : PropTypes.object.isRequired +}; + +const mapStateToProps = (state) => + ({ + aboutOpen : state.room.aboutOpen + }); + +const mapDispatchToProps = { + handleCloseAbout : roomActions.setAboutOpen +}; + +export default withRoomContext(connect( + mapStateToProps, + mapDispatchToProps, + null, + { + areStatesEqual : (next, prev) => + { + return ( + prev.room.aboutOpen === next.room.aboutOpen + ); + } + } +)(withStyles(styles)(About))); \ No newline at end of file diff --git a/app/src/components/Controls/Help.js b/app/src/components/Controls/Help.js new file mode 100644 index 0000000..67be03f --- /dev/null +++ b/app/src/components/Controls/Help.js @@ -0,0 +1,99 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { withStyles } from '@material-ui/core/styles'; +import { withRoomContext } from '../../RoomContext'; +import * as roomActions from '../../actions/roomActions'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; + +import Dialog from '@material-ui/core/Dialog'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import DialogActions from '@material-ui/core/DialogActions'; +import Button from '@material-ui/core/Button'; + +const styles = (theme) => + ({ + dialogPaper : + { + width : '30vw', + [theme.breakpoints.down('lg')] : + { + width : '40vw' + }, + [theme.breakpoints.down('md')] : + { + width : '50vw' + }, + [theme.breakpoints.down('sm')] : + { + width : '70vw' + }, + [theme.breakpoints.down('xs')] : + { + width : '90vw' + } + } + }); + +const Help = ({ + helpOpen, + handleCloseHelp, + classes +}) => +{ + return ( + { handleCloseHelp(false); }} + classes={{ + paper : classes.dialogPaper + }} + > + + + + + + + + ); +}; + +Help.propTypes = +{ + roomClient : PropTypes.object.isRequired, + helpOpen : PropTypes.bool.isRequired, + handleCloseHelp : PropTypes.func.isRequired, + classes : PropTypes.object.isRequired +}; + +const mapStateToProps = (state) => + ({ + helpOpen : state.room.helpOpen + }); + +const mapDispatchToProps = { + handleCloseHelp : roomActions.setHelpOpen +}; + +export default withRoomContext(connect( + mapStateToProps, + mapDispatchToProps, + null, + { + areStatesEqual : (next, prev) => + { + return ( + prev.room.helpOpen === next.room.helpOpen + ); + } + } +)(withStyles(styles)(Help))); \ No newline at end of file diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index 6422788..028b883 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -36,6 +36,8 @@ import VideoCallIcon from '@material-ui/icons/VideoCall'; import Button from '@material-ui/core/Button'; import Tooltip from '@material-ui/core/Tooltip'; import MoreIcon from '@material-ui/icons/MoreVert'; +import HelpIcon from '@material-ui/icons/Help'; +import InfoIcon from '@material-ui/icons/Info'; const styles = (theme) => ({ @@ -192,6 +194,8 @@ const TopBar = (props) => onFullscreen, setSettingsOpen, setExtraVideoOpen, + setHelpOpen, + setAboutOpen, setLockDialogOpen, toggleToolArea, openUsersTab, @@ -483,6 +487,46 @@ const TopBar = (props) => />

+ + { + handleMenuClose(); + setHelpOpen(!room.helpOpen); + }} + > + +

+ +

+
+ + { + handleMenuClose(); + setAboutOpen(!room.aboutOpen); + }} + > + +

+ +

+
} @@ -694,6 +738,8 @@ TopBar.propTypes = setToolbarsVisible : PropTypes.func.isRequired, setSettingsOpen : PropTypes.func.isRequired, setExtraVideoOpen : PropTypes.func.isRequired, + setHelpOpen : PropTypes.func.isRequired, + setAboutOpen : PropTypes.func.isRequired, setLockDialogOpen : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired, openUsersTab : PropTypes.func.isRequired, @@ -741,6 +787,14 @@ const mapDispatchToProps = (dispatch) => { dispatch(roomActions.setExtraVideoOpen(extraVideoOpen)); }, + setHelpOpen : (helpOpen) => + { + dispatch(roomActions.setHelpOpen(helpOpen)); + }, + setAboutOpen : (aboutOpen) => + { + dispatch(roomActions.setAboutOpen(aboutOpen)); + }, setLockDialogOpen : (lockDialogOpen) => { dispatch(roomActions.setLockDialogOpen(lockDialogOpen)); diff --git a/app/src/components/Room.js b/app/src/components/Room.js index cdda66f..505fc69 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -25,6 +25,8 @@ import Settings from './Settings/Settings'; import TopBar from './Controls/TopBar'; import WakeLock from 'react-wakelock-react16'; import ExtraVideo from './Controls/ExtraVideo'; +import Help from './Controls/Help'; +import About from './Controls/About'; const TIMEOUT = 5 * 1000; @@ -222,6 +224,13 @@ class Room extends React.PureComponent { room.extraVideoOpen && } + { room.helpOpen && + + } + { room.aboutOpen && + + } + ); } diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index 6d47d42..1a4b99e 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -22,6 +22,8 @@ const initialState = spotlights : [], settingsOpen : false, extraVideoOpen : false, + helpOpen : false, + aboutOpen : false, currentSettingsTab : 'media', // media, appearence, advanced lockDialogOpen : false, joined : false, @@ -130,6 +132,20 @@ const room = (state = initialState, action) => return { ...state, extraVideoOpen }; } + case 'SET_HELP_OPEN': + { + const { helpOpen } = action.payload; + + return { ...state, helpOpen }; + } + + case 'SET_ABOUT_OPEN': + { + const { aboutOpen } = action.payload; + + return { ...state, aboutOpen }; + } + case 'SET_SETTINGS_TAB': { const { tab } = action.payload; From cfdaceed223039a4d9a1d727610ae9b7f1ef1a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 7 May 2020 13:35:29 +0200 Subject: [PATCH 02/28] Add posibility to have separate media buttons, fixes #309 --- app/public/config/config.example.js | 19 +- app/src/actions/settingsActions.js | 5 + app/src/components/Containers/Me.js | 212 ++++---- app/src/components/Containers/Peer.js | 470 +++++++++--------- .../components/Controls/ButtonControlBar.js | 256 ++++++++++ app/src/components/MeetingViews/Democratic.js | 52 +- app/src/components/Room.js | 9 + .../components/Settings/AppearenceSettings.js | 11 + app/src/components/Settings/MediaSettings.js | 6 +- app/src/reducers/settings.js | 8 + app/src/translations/cn.json | 4 + app/src/translations/cs.json | 4 + app/src/translations/de.json | 4 + app/src/translations/dk.json | 4 + app/src/translations/el.json | 4 + app/src/translations/en.json | 4 + app/src/translations/es.json | 4 + app/src/translations/fr.json | 4 + app/src/translations/hr.json | 4 + app/src/translations/hu.json | 4 + app/src/translations/it.json | 4 + app/src/translations/lv.json | 4 + app/src/translations/nb.json | 4 + app/src/translations/pl.json | 4 + app/src/translations/pt.json | 4 + app/src/translations/ro.json | 4 + app/src/translations/tr.json | 4 + app/src/translations/uk.json | 4 + 28 files changed, 739 insertions(+), 381 deletions(-) create mode 100644 app/src/components/Controls/ButtonControlBar.js diff --git a/app/public/config/config.example.js b/app/public/config/config.example.js index b591c16..38334f3 100644 --- a/app/public/config/config.example.js +++ b/app/public/config/config.example.js @@ -52,18 +52,21 @@ var config = noiseSuppression : true, sampleSize : 16 }, - background : 'images/background.jpg', - defaultLayout : 'democratic', // democratic, filmstrip - lastN : 4, - mobileLastN : 1, + background : 'images/background.jpg', + defaultLayout : 'democratic', // democratic, filmstrip + // If true, will show media control buttons in separate + // control bar, not in the ME container. + buttonControlBar : false, + lastN : 4, + mobileLastN : 1, // Highest number of speakers user can select - maxLastN : 5, + maxLastN : 5, // If truthy, users can NOT change number of speakers visible - lockLastN : false, + lockLastN : false, // Add file and uncomment for adding logo to appbar // logo : 'images/logo.svg', - title : 'Multiparty meeting', - theme : + title : 'Multiparty meeting', + theme : { palette : { diff --git a/app/src/actions/settingsActions.js b/app/src/actions/settingsActions.js index 63c12bf..4b7dc6d 100644 --- a/app/src/actions/settingsActions.js +++ b/app/src/actions/settingsActions.js @@ -38,6 +38,11 @@ export const togglePermanentTopBar = () => type : 'TOGGLE_PERMANENT_TOPBAR' }); +export const toggleButtonControlBar = () => + ({ + type : 'TOGGLE_BUTTON_CONTROL_BAR' + }); + export const toggleShowNotifications = () => ({ type : 'TOGGLE_SHOW_NOTIFICATIONS' diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index f368089..8279a61 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -77,7 +77,29 @@ const styles = (theme) => { position : 'relative', width : '100%', - height : '100%' + height : '100%', + '& p' : + { + position : 'absolute', + float : 'left', + top : '50%', + left : '50%', + transform : 'translate(-50%, -50%)', + color : 'rgba(255, 255, 255, 0.5)', + fontSize : '7em', + zIndex : 30, + margin : 0, + opacity : 0, + transition : 'opacity 0.1s ease-in-out', + '&.hover' : + { + opacity : 1 + }, + '&.smallContainer' : + { + fontSize : '3em' + } + } }, controls : { @@ -100,27 +122,6 @@ const styles = (theme) => '&.hover' : { opacity : 1 - }, - '& p' : - { - position : 'absolute', - float : 'left', - top : '50%', - left : '50%', - transform : 'translate(-50%, -50%)', - color : 'rgba(255, 255, 255, 0.5)', - fontSize : '7em', - margin : 0, - opacity : 0, - transition : 'opacity 0.1s ease-in-out', - '&.hover' : - { - opacity : 1 - }, - '&.smallContainer' : - { - fontSize : '3em' - } } }, ptt : @@ -330,47 +331,46 @@ const Me = (props) => /> } -
setHover(true)} - onMouseOut={() => setHover(false)} - onTouchStart={() => - { - if (touchTimeout) - clearTimeout(touchTimeout); - - setHover(true); - }} - onTouchEnd={() => - { - if (touchTimeout) - clearTimeout(touchTimeout); - - touchTimeout = setTimeout(() => - { - setHover(false); - }, 2000); - }} > -

+

+ { !settings.buttonControlBar && +
- -

+ onMouseOver={() => setHover(true)} + onMouseOut={() => setHover(false)} + onTouchStart={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); - - -
+ setHover(true); + }} + onTouchEnd={() => + { + if (touchTimeout) + clearTimeout(touchTimeout); + + touchTimeout = setTimeout(() => + { + setHover(false); + }, 2000); + }} + > + + { smallContainer ? } } -
-
- -
+ + { smallContainer ? } } -
-
- { me.browser.platform !== 'mobile' && - -
+ + { me.browser.platform !== 'mobile' && + { smallContainer ? } } -
-
- } -
-
+ + } + +
+ }

-
- { smallContainer ? - - { - roomClient.disableExtraVideo(producer.id); - }} - > - + { smallContainer ? + + { + roomClient.disableExtraVideo(producer.id); + }} + > + - - : - - { - roomClient.disableExtraVideo(producer.id); - }} - > - - - } -
+ + : + + { + roomClient.disableExtraVideo(producer.id); + }} + > + + + }
diff --git a/app/src/components/Containers/Peer.js b/app/src/components/Containers/Peer.js index 4fc495e..ab90a13 100644 --- a/app/src/components/Containers/Peer.js +++ b/app/src/components/Containers/Peer.js @@ -249,55 +249,53 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
- { smallContainer ? - - { - micEnabled ? - roomClient.modifyPeerConsumer(peer.id, 'mic', true) : - roomClient.modifyPeerConsumer(peer.id, 'mic', false); - }} - > - { micEnabled ? - - : - - } - - : - - { - micEnabled ? - roomClient.modifyPeerConsumer(peer.id, 'mic', true) : - roomClient.modifyPeerConsumer(peer.id, 'mic', false); - }} - > - { micEnabled ? - - : - - } - - } -
+ { smallContainer ? + + { + micEnabled ? + roomClient.modifyPeerConsumer(peer.id, 'mic', true) : + roomClient.modifyPeerConsumer(peer.id, 'mic', false); + }} + > + { micEnabled ? + + : + + } + + : + + { + micEnabled ? + roomClient.modifyPeerConsumer(peer.id, 'mic', true) : + roomClient.modifyPeerConsumer(peer.id, 'mic', false); + }} + > + { micEnabled ? + + : + + } + + } { browser.platform !== 'mobile' && @@ -308,48 +306,46 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
- { smallContainer ? - - { - toggleConsumerWindow(webcamConsumer); - }} - > - - - : - - { - toggleConsumerWindow(webcamConsumer); - }} - > - - - } -
+ { smallContainer ? + + { + toggleConsumerWindow(webcamConsumer); + }} + > + + + : + + { + toggleConsumerWindow(webcamConsumer); + }} + > + + + } } @@ -360,42 +356,40 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
- { smallContainer ? - - { - toggleConsumerFullscreen(webcamConsumer); - }} - > - - - : - - { - toggleConsumerFullscreen(webcamConsumer); - }} - > - - - } -
+ { smallContainer ? + + { + toggleConsumerFullscreen(webcamConsumer); + }} + > + + + : + + { + toggleConsumerFullscreen(webcamConsumer); + }} + > + + + } @@ -507,48 +501,46 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
- { smallContainer ? - - { - toggleConsumerWindow(consumer); - }} - > - - - : - - { - toggleConsumerWindow(consumer); - }} - > - - - } -
+ { smallContainer ? + + { + toggleConsumerWindow(consumer); + }} + > + + + : + + { + toggleConsumerWindow(consumer); + }} + > + + + } } @@ -559,42 +551,40 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
- { smallContainer ? - - { - toggleConsumerFullscreen(consumer); - }} - > - - - : - - { - toggleConsumerFullscreen(consumer); - }} - > - - - } -
+ { smallContainer ? + + { + toggleConsumerFullscreen(consumer); + }} + > + + + : + + { + toggleConsumerFullscreen(consumer); + }} + > + + + } @@ -694,26 +684,24 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
- - { - toggleConsumerWindow(screenConsumer); - }} - > - - -
+ + { + toggleConsumerWindow(screenConsumer); + }} + > + + } @@ -724,23 +712,21 @@ const Peer = (props) => })} placement={smallScreen ? 'top' : 'left'} > -
- - { - toggleConsumerFullscreen(screenConsumer); - }} - > - - -
+ + { + toggleConsumerFullscreen(screenConsumer); + }} + > + + + ({ + root : + { + position : 'fixed', + display : 'flex', + [theme.breakpoints.up('md')] : + { + top : '50%', + transform : 'translate(0%, -50%)', + flexDirection : 'column', + justifyContent : 'center', + alignItems : 'center', + left : theme.spacing(1) + }, + [theme.breakpoints.down('sm')] : + { + flexDirection : 'row', + bottom : theme.spacing(1), + left : '50%', + transform : 'translate(-50%, -0%)' + } + }, + fab : + { + margin : theme.spacing(1) + }, + show : + { + opacity : 1, + transition : 'opacity .5s' + }, + hide : + { + opacity : 0, + transition : 'opacity .5s' + } + }); + +const ButtonControlBar = (props) => +{ + const { + roomClient, + toolbarsVisible, + me, + micProducer, + webcamProducer, + screenProducer, + classes, + theme + } = props; + + let micState; + + let micTip; + + if (!me.canSendMic || !micProducer) + { + micState = 'unsupported'; + micTip = 'Audio unsupported'; + } + else if (!micProducer.locallyPaused && !micProducer.remotelyPaused) + { + micState = 'on'; + micTip = 'Mute audio'; + } + else + { + micState = 'off'; + micTip = 'Unmute audio'; + } + + let webcamState; + + let webcamTip; + + if (!me.canSendWebcam) + { + webcamState = 'unsupported'; + webcamTip = 'Video unsupported'; + } + else if (webcamProducer) + { + webcamState = 'on'; + webcamTip = 'Stop video'; + } + else + { + webcamState = 'off'; + webcamTip = 'Start video'; + } + + let screenState; + + let screenTip; + + if (!me.canShareScreen) + { + screenState = 'unsupported'; + screenTip = 'Screen sharing not supported'; + } + else if (screenProducer) + { + screenState = 'on'; + screenTip = 'Stop screen sharing'; + } + else + { + screenState = 'off'; + screenTip = 'Start screen sharing'; + } + + const smallScreen = useMediaQuery(theme.breakpoints.down('sm')); + + return ( +
+ + + { + micState === 'on' ? + roomClient.muteMic() : + roomClient.unmuteMic(); + }} + > + { micState === 'on' ? + + : + + } + + + + + { + webcamState === 'on' ? + roomClient.disableWebcam() : + roomClient.enableWebcam(); + }} + > + { webcamState === 'on' ? + + : + + } + + + + + { + switch (screenState) + { + case 'on': + { + roomClient.disableScreenSharing(); + break; + } + case 'off': + { + roomClient.enableScreenSharing(); + break; + } + default: + { + break; + } + } + }} + > + { screenState === 'on' || screenState === 'unsupported' ? + + :null + } + { screenState === 'off' ? + + :null + } + + +
+ ); +}; + +ButtonControlBar.propTypes = +{ + roomClient : PropTypes.any.isRequired, + toolbarsVisible : PropTypes.bool.isRequired, + me : appPropTypes.Me.isRequired, + micProducer : appPropTypes.Producer, + webcamProducer : appPropTypes.Producer, + screenProducer : appPropTypes.Producer, + classes : PropTypes.object.isRequired, + theme : PropTypes.object.isRequired +}; + +const mapStateToProps = (state) => + ({ + toolbarsVisible : state.room.toolbarsVisible, + ...meProducersSelector(state), + me : state.me + }); + +export default withRoomContext(connect( + mapStateToProps, + null, + null, + { + areStatesEqual : (next, prev) => + { + return ( + prev.room.toolbarsVisible === next.room.toolbarsVisible && + prev.producers === next.producers && + prev.me === next.me + ); + } + } +)(withStyles(styles, { withTheme: true })(ButtonControlBar))); \ No newline at end of file diff --git a/app/src/components/MeetingViews/Democratic.js b/app/src/components/MeetingViews/Democratic.js index e2a703e..f2fff1a 100644 --- a/app/src/components/MeetingViews/Democratic.js +++ b/app/src/components/MeetingViews/Democratic.js @@ -11,10 +11,9 @@ import Peer from '../Containers/Peer'; import Me from '../Containers/Me'; const RATIO = 1.334; -const PADDING_V = 50; -const PADDING_H = 0; +const PADDING = 60; -const styles = () => +const styles = (theme) => ({ root : { @@ -36,6 +35,14 @@ const styles = () => { paddingTop : 60, transition : 'padding .5s' + }, + buttonControlBar : + { + paddingLeft : 60, + [theme.breakpoints.down('sm')] : + { + paddingLeft : 0 + } } }); @@ -66,9 +73,11 @@ class Democratic extends React.PureComponent return; } - const width = this.peersRef.current.clientWidth - PADDING_H; - const height = this.peersRef.current.clientHeight - - (this.props.toolbarsVisible || this.props.permanentTopBar ? PADDING_V : PADDING_H); + const width = + this.peersRef.current.clientWidth - (this.props.buttonControlBar ? PADDING : 0); + const height = + this.peersRef.current.clientHeight - + (this.props.toolbarsVisible || this.props.permanentTopBar ? PADDING : 0); let x, y, space; @@ -130,6 +139,7 @@ class Democratic extends React.PureComponent spotlightsPeers, toolbarsVisible, permanentTopBar, + buttonControlBar, classes } = this.props; @@ -144,7 +154,8 @@ class Democratic extends React.PureComponent className={classnames( classes.root, toolbarsVisible || permanentTopBar ? - classes.showingToolBar : classes.hiddenToolBar + classes.showingToolBar : classes.hiddenToolBar, + buttonControlBar ? classes.buttonControlBar : null )} ref={this.peersRef} > @@ -172,21 +183,23 @@ class Democratic extends React.PureComponent Democratic.propTypes = { - advancedMode : PropTypes.bool, - boxes : PropTypes.number, - spotlightsPeers : PropTypes.array.isRequired, - toolbarsVisible : PropTypes.bool.isRequired, - permanentTopBar : PropTypes.bool, - classes : PropTypes.object.isRequired + advancedMode : PropTypes.bool, + boxes : PropTypes.number, + spotlightsPeers : PropTypes.array.isRequired, + toolbarsVisible : PropTypes.bool.isRequired, + permanentTopBar : PropTypes.bool.isRequired, + buttonControlBar : PropTypes.bool.isRequired, + classes : PropTypes.object.isRequired }; const mapStateToProps = (state) => { return { - boxes : videoBoxesSelector(state), - spotlightsPeers : spotlightPeersSelector(state), - toolbarsVisible : state.room.toolbarsVisible, - permanentTopBar : state.settings.permanentTopBar + boxes : videoBoxesSelector(state), + spotlightsPeers : spotlightPeersSelector(state), + toolbarsVisible : state.room.toolbarsVisible, + permanentTopBar : state.settings.permanentTopBar, + buttonControlBar : state.settings.buttonControlBar }; }; @@ -203,8 +216,9 @@ export default connect( prev.consumers === next.consumers && prev.room.spotlights === next.room.spotlights && prev.room.toolbarsVisible === next.room.toolbarsVisible && - prev.settings.permanentTopBar === next.settings.permanentTopBar + prev.settings.permanentTopBar === next.settings.permanentTopBar && + prev.settings.buttonControlBar === next.settings.buttonControlBar ); } } -)(withStyles(styles)(Democratic)); +)(withStyles(styles, { withTheme: true })(Democratic)); diff --git a/app/src/components/Room.js b/app/src/components/Room.js index 7562ee4..c216f46 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -25,6 +25,7 @@ import Settings from './Settings/Settings'; import TopBar from './Controls/TopBar'; import WakeLock from 'react-wakelock-react16'; import ExtraVideo from './Controls/ExtraVideo'; +import ButtonControlBar from './Controls/ButtonControlBar'; const TIMEOUT = 5 * 1000; @@ -143,6 +144,7 @@ class Room extends React.PureComponent browser, advancedMode, showNotifications, + buttonControlBar, toolAreaOpen, toggleToolArea, classes, @@ -214,6 +216,10 @@ class Room extends React.PureComponent + { buttonControlBar && + + } + { room.lockDialogOpen && } @@ -236,6 +242,7 @@ Room.propTypes = browser : PropTypes.object.isRequired, advancedMode : PropTypes.bool.isRequired, showNotifications : PropTypes.bool.isRequired, + buttonControlBar : PropTypes.bool.isRequired, toolAreaOpen : PropTypes.bool.isRequired, setToolbarsVisible : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired, @@ -249,6 +256,7 @@ const mapStateToProps = (state) => browser : state.me.browser, advancedMode : state.settings.advancedMode, showNotifications : state.settings.showNotifications, + buttonControlBar : state.settings.buttonControlBar, toolAreaOpen : state.toolarea.toolAreaOpen }); @@ -276,6 +284,7 @@ export default connect( prev.me.browser === next.me.browser && prev.settings.advancedMode === next.settings.advancedMode && prev.settings.showNotifications === next.settings.showNotifications && + prev.settings.buttonControlBar === next.settings.buttonControlBar && prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen ); } diff --git a/app/src/components/Settings/AppearenceSettings.js b/app/src/components/Settings/AppearenceSettings.js index a34a5e1..a859ab4 100644 --- a/app/src/components/Settings/AppearenceSettings.js +++ b/app/src/components/Settings/AppearenceSettings.js @@ -30,6 +30,7 @@ const AppearenceSettings = ({ settings, onTogglePermanentTopBar, onToggleHiddenControls, + onToggleButtonControlBar, onToggleShowNotifications, handleChangeMode, classes @@ -102,6 +103,14 @@ const AppearenceSettings = ({ defaultMessage : 'Hidden media controls' })} /> + } + label={intl.formatMessage({ + id : 'settings.buttonControlBar', + defaultMessage : 'Separate media controls' + })} + /> } @@ -120,6 +129,7 @@ AppearenceSettings.propTypes = settings : PropTypes.object.isRequired, onTogglePermanentTopBar : PropTypes.func.isRequired, onToggleHiddenControls : PropTypes.func.isRequired, + onToggleButtonControlBar : PropTypes.func.isRequired, onToggleShowNotifications : PropTypes.func.isRequired, handleChangeMode : PropTypes.func.isRequired, classes : PropTypes.object.isRequired @@ -135,6 +145,7 @@ const mapDispatchToProps = { onTogglePermanentTopBar : settingsActions.togglePermanentTopBar, onToggleHiddenControls : settingsActions.toggleHiddenControls, onToggleShowNotifications : settingsActions.toggleShowNotifications, + onToggleButtonControlBar : settingsActions.toggleButtonControlBar, handleChangeMode : roomActions.setDisplayMode }; diff --git a/app/src/components/Settings/MediaSettings.js b/app/src/components/Settings/MediaSettings.js index d215181..28a69a2 100644 --- a/app/src/components/Settings/MediaSettings.js +++ b/app/src/components/Settings/MediaSettings.js @@ -265,7 +265,7 @@ const MediaSettings = ({ />} label={intl.formatMessage({ id : 'settings.echoCancellation', - defaultMessage : 'Echo Cancellation' + defaultMessage : 'Echo cancellation' })} /> } label={intl.formatMessage({ id : 'settings.autoGainControl', - defaultMessage : 'Auto Gain Control' + defaultMessage : 'Auto gain control' })} /> } label={intl.formatMessage({ id : 'settings.noiseSuppression', - defaultMessage : 'Noise Suppression' + defaultMessage : 'Noise suppression' })} /> diff --git a/app/src/reducers/settings.js b/app/src/reducers/settings.js index 7186fdc..f306726 100644 --- a/app/src/reducers/settings.js +++ b/app/src/reducers/settings.js @@ -18,6 +18,7 @@ const initialState = hiddenControls : false, showNotifications : true, notificationSounds : true, + buttonControlBar : window.config.buttonControlBar || false, ...window.config.defaultAudio }; @@ -145,6 +146,13 @@ const settings = (state = initialState, action) => return { ...state, permanentTopBar }; } + case 'TOGGLE_BUTTON_CONTROL_BAR': + { + const buttonControlBar = !state.buttonControlBar; + + return { ...state, buttonControlBar }; + } + case 'TOGGLE_HIDDEN_CONTROLS': { const hiddenControls = !state.hiddenControls; diff --git a/app/src/translations/cn.json b/app/src/translations/cn.json index 26724a4..7da15eb 100644 --- a/app/src/translations/cn.json +++ b/app/src/translations/cn.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "无法保存文件", "filesharing.startingFileShare": "正在尝试共享文件", diff --git a/app/src/translations/cs.json b/app/src/translations/cs.json index b80ab14..ee859d2 100644 --- a/app/src/translations/cs.json +++ b/app/src/translations/cs.json @@ -130,6 +130,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Není možné uložit soubor", "filesharing.startingFileShare": "Pokouším se sdílet soubor", diff --git a/app/src/translations/de.json b/app/src/translations/de.json index 3eabb28..428d104 100644 --- a/app/src/translations/de.json +++ b/app/src/translations/de.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Fehler beim Speichern der Datei", "filesharing.startingFileShare": "Starte Teilen der Datei", diff --git a/app/src/translations/dk.json b/app/src/translations/dk.json index cab3586..c74026f 100644 --- a/app/src/translations/dk.json +++ b/app/src/translations/dk.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Kan ikke gemme fil", "filesharing.startingFileShare": "Forsøger at dele filen", diff --git a/app/src/translations/el.json b/app/src/translations/el.json index 5a83427..10047cf 100644 --- a/app/src/translations/el.json +++ b/app/src/translations/el.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου", "filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου", diff --git a/app/src/translations/en.json b/app/src/translations/en.json index 2b40a30..475acd2 100644 --- a/app/src/translations/en.json +++ b/app/src/translations/en.json @@ -131,6 +131,10 @@ "settings.hiddenControls": "Hidden media controls", "settings.notificationSounds": "Notification sounds", "settings.showNotifications": "Show notifications", + "settings.buttonControlBar": "Separate media controls", + "settings.echoCancellation": "Echo cancellation", + "settings.autoGainControl": "Auto gain control", + "settings.noiseSuppression": "Noise suppression", "filesharing.saveFileError": "Unable to save file", "filesharing.startingFileShare": "Attempting to share file", diff --git a/app/src/translations/es.json b/app/src/translations/es.json index 63c2f7d..6c757cd 100644 --- a/app/src/translations/es.json +++ b/app/src/translations/es.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "No ha sido posible guardar el fichero", "filesharing.startingFileShare": "Intentando compartir el fichero", diff --git a/app/src/translations/fr.json b/app/src/translations/fr.json index 4471861..73493f5 100644 --- a/app/src/translations/fr.json +++ b/app/src/translations/fr.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Impossible d'enregistrer le fichier", "filesharing.startingFileShare": "Début du transfert de fichier", diff --git a/app/src/translations/hr.json b/app/src/translations/hr.json index 682dfbe..22db8fa 100644 --- a/app/src/translations/hr.json +++ b/app/src/translations/hr.json @@ -131,6 +131,10 @@ "settings.hiddenControls": "Skrivene kontrole medija", "settings.notificationSounds": "Zvuk obavijesti", "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Nije moguće spremiti datoteku", "filesharing.startingFileShare": "Pokušaj dijeljenja datoteke", diff --git a/app/src/translations/hu.json b/app/src/translations/hu.json index 262f117..8c68590 100644 --- a/app/src/translations/hu.json +++ b/app/src/translations/hu.json @@ -131,6 +131,10 @@ "settings.hiddenControls": "Média Gombok automatikus elrejtése", "settings.notificationSounds": "Értesítések hangjelzéssel", "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "A file-t nem sikerült elmenteni", "filesharing.startingFileShare": "Fájl megosztása", diff --git a/app/src/translations/it.json b/app/src/translations/it.json index 27b3194..ba849d9 100644 --- a/app/src/translations/it.json +++ b/app/src/translations/it.json @@ -130,6 +130,10 @@ "settings.hiddenControls": "Controlli media nascosti", "settings.notificationSounds": "Suoni di notifica", "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Impossibile salvare file", "filesharing.startingFileShare": "Tentativo di condivisione file", diff --git a/app/src/translations/lv.json b/app/src/translations/lv.json index d355c84..6d4e97b 100644 --- a/app/src/translations/lv.json +++ b/app/src/translations/lv.json @@ -125,6 +125,10 @@ "settings.hiddenControls": "Slēpto mediju vadība", "settings.notificationSounds": "Paziņojumu skaņas", "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Nav iespējams saglabāt failu", "filesharing.startingFileShare": "Tiek mēģināts kopīgot failu", diff --git a/app/src/translations/nb.json b/app/src/translations/nb.json index 59edd6c..ea9b0b8 100644 --- a/app/src/translations/nb.json +++ b/app/src/translations/nb.json @@ -131,6 +131,10 @@ "settings.hiddenControls": "Skjul media knapper", "settings.notificationSounds": "Varslingslyder", "settings.showNotifications": "Vis varslinger", + "settings.buttonControlBar": "Separate media knapper", + "settings.echoCancellation": "Echokansellering", + "settings.autoGainControl": "Auto gain kontroll", + "settings.noiseSuppression": "Støy reduksjon", "filesharing.saveFileError": "Klarte ikke å lagre fil", "filesharing.startingFileShare": "Starter fildeling", diff --git a/app/src/translations/pl.json b/app/src/translations/pl.json index 1413c73..859a29f 100644 --- a/app/src/translations/pl.json +++ b/app/src/translations/pl.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Nie można zapisać pliku", "filesharing.startingFileShare": "Próba udostępnienia pliku", diff --git a/app/src/translations/pt.json b/app/src/translations/pt.json index 3b0a078..7faed7c 100644 --- a/app/src/translations/pt.json +++ b/app/src/translations/pt.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Impossível de gravar o ficheiro", "filesharing.startingFileShare": "Tentando partilha de ficheiro", diff --git a/app/src/translations/ro.json b/app/src/translations/ro.json index e666ddf..3188e91 100644 --- a/app/src/translations/ro.json +++ b/app/src/translations/ro.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat", "filesharing.startingFileShare": "Partajarea fișierului", diff --git a/app/src/translations/tr.json b/app/src/translations/tr.json index 16aaae0..aa6a8cb 100644 --- a/app/src/translations/tr.json +++ b/app/src/translations/tr.json @@ -128,6 +128,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Dosya kaydedilemiyor", "filesharing.startingFileShare": "Paylaşılan dosyaya erişiliyor", diff --git a/app/src/translations/uk.json b/app/src/translations/uk.json index fe9dd52..4932fc4 100644 --- a/app/src/translations/uk.json +++ b/app/src/translations/uk.json @@ -131,6 +131,10 @@ "settings.hiddenControls": null, "settings.notificationSounds": null, "settings.showNotifications": null, + "settings.buttonControlBar": null, + "settings.echoCancellation": null, + "settings.autoGainControl": null, + "settings.noiseSuppression": null, "filesharing.saveFileError": "Неможливо зберегти файл", "filesharing.startingFileShare": "Спроба поділитися файлом", From 4c6d9291bf767384a67fea6b449eb05d942e7511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 7 May 2020 14:57:42 +0200 Subject: [PATCH 03/28] Fix regression on advanced mode, ref #309 --- app/src/components/Containers/Me.js | 43 +++++++++++++++-------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index 8279a61..ead95b1 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -77,28 +77,28 @@ const styles = (theme) => { position : 'relative', width : '100%', - height : '100%', - '& p' : + height : '100%' + }, + meTag : + { + position : 'absolute', + float : 'left', + top : '50%', + left : '50%', + transform : 'translate(-50%, -50%)', + color : 'rgba(255, 255, 255, 0.5)', + fontSize : '7em', + zIndex : 30, + margin : 0, + opacity : 0, + transition : 'opacity 0.1s ease-in-out', + '&.hover' : { - position : 'absolute', - float : 'left', - top : '50%', - left : '50%', - transform : 'translate(-50%, -50%)', - color : 'rgba(255, 255, 255, 0.5)', - fontSize : '7em', - zIndex : 30, - margin : 0, - opacity : 0, - transition : 'opacity 0.1s ease-in-out', - '&.hover' : - { - opacity : 1 - }, - '&.smallContainer' : - { - fontSize : '3em' - } + opacity : 1 + }, + '&.smallContainer' : + { + fontSize : '3em' } }, controls : @@ -333,6 +333,7 @@ const Me = (props) => }

Date: Thu, 7 May 2020 15:15:44 +0200 Subject: [PATCH 04/28] Respect hide media buttons setting on button control bar, ref #309 --- app/public/config/config.example.js | 2 ++ app/src/components/Controls/ButtonControlBar.js | 10 +++++++++- app/src/components/Room.js | 2 +- 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/public/config/config.example.js b/app/public/config/config.example.js index 38334f3..4bbc13f 100644 --- a/app/public/config/config.example.js +++ b/app/public/config/config.example.js @@ -57,6 +57,8 @@ var config = // If true, will show media control buttons in separate // control bar, not in the ME container. buttonControlBar : false, + // Timeout for autohiding topbar and button control bar + hideTimeout : 3000, lastN : 4, mobileLastN : 1, // Highest number of speakers user can select diff --git a/app/src/components/Controls/ButtonControlBar.js b/app/src/components/Controls/ButtonControlBar.js index a70c43e..2ee5b7e 100644 --- a/app/src/components/Controls/ButtonControlBar.js +++ b/app/src/components/Controls/ButtonControlBar.js @@ -60,6 +60,7 @@ const ButtonControlBar = (props) => const { roomClient, toolbarsVisible, + hiddenControls, me, micProducer, webcamProducer, @@ -133,7 +134,11 @@ const ButtonControlBar = (props) => return (

@@ -224,6 +229,7 @@ ButtonControlBar.propTypes = { roomClient : PropTypes.any.isRequired, toolbarsVisible : PropTypes.bool.isRequired, + hiddenControls : PropTypes.bool.isRequired, me : appPropTypes.Me.isRequired, micProducer : appPropTypes.Producer, webcamProducer : appPropTypes.Producer, @@ -235,6 +241,7 @@ ButtonControlBar.propTypes = const mapStateToProps = (state) => ({ toolbarsVisible : state.room.toolbarsVisible, + hiddenControls : state.settings.hiddenControls, ...meProducersSelector(state), me : state.me }); @@ -248,6 +255,7 @@ export default withRoomContext(connect( { return ( prev.room.toolbarsVisible === next.room.toolbarsVisible && + prev.settings.hiddenControls === next.settings.hiddenControls && prev.producers === next.producers && prev.me === next.me ); diff --git a/app/src/components/Room.js b/app/src/components/Room.js index c216f46..b6027ba 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -27,7 +27,7 @@ import WakeLock from 'react-wakelock-react16'; import ExtraVideo from './Controls/ExtraVideo'; import ButtonControlBar from './Controls/ButtonControlBar'; -const TIMEOUT = 5 * 1000; +const TIMEOUT = window.config.hideTimeout || 5000; const styles = (theme) => ({ From 2373bf44d8733e101d0f8cec4c948290d893cc91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 7 May 2020 21:03:07 +0200 Subject: [PATCH 05/28] Allow serving other than root of site, fixes #249 --- app/src/RoomClient.js | 8 ++++++-- app/src/index.js | 12 ++++++++++-- server/server.js | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index df276ae..a48a428 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -129,7 +129,8 @@ export default class RoomClient produce, forceTcp, displayName, - muted + muted, + basePath } = {}) { if (!peerId) @@ -152,6 +153,9 @@ export default class RoomClient // Whether we force TCP this._forceTcp = forceTcp; + // URL basepath + this._basePath = basePath; + // Use displayName if (displayName) store.dispatch(settingsActions.setDisplayName(displayName)); @@ -1719,7 +1723,7 @@ export default class RoomClient this._screenSharing = ScreenShare.create(this._device); - this._signalingSocket = io(this._signalingUrl); + this._signalingSocket = io(this._signalingUrl, { path: this._basePath }); this._spotlights = new Spotlights(this._maxSpotlights, this._signalingSocket); diff --git a/app/src/index.js b/app/src/index.js index cdefb40..e89d06d 100644 --- a/app/src/index.js +++ b/app/src/index.js @@ -115,6 +115,13 @@ function run() const forceTcp = parameters.get('forceTcp') === 'true'; const displayName = parameters.get('displayName'); const muted = parameters.get('muted') === 'true'; + + const { pathname } = window.location; + + let basePath = pathname.substring(0, pathname.lastIndexOf('/')); + + if (!basePath) + basePath = '/'; // Get current device. const device = deviceInfo(); @@ -134,7 +141,8 @@ function run() produce, forceTcp, displayName, - muted + muted, + basePath }); global.CLIENT = roomClient; @@ -146,7 +154,7 @@ function run() } persistor={persistor}> - + }> diff --git a/server/server.js b/server/server.js index fcde0b1..9fb775b 100755 --- a/server/server.js +++ b/server/server.js @@ -494,7 +494,7 @@ function isPathAlreadyTaken(url) */ async function runWebSocketServer() { - io = require('socket.io')(mainListener); + io = require('socket.io')(mainListener, { path: '/' }); io.use( sharedSession(session, { From ce8141eed7f5a97326df79806c184e45cc8b8bae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 7 May 2020 21:03:22 +0200 Subject: [PATCH 06/28] Cleanup --- server/lib/Peer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/lib/Peer.js b/server/lib/Peer.js index 163e648..8e11ca2 100644 --- a/server/lib/Peer.js +++ b/server/lib/Peer.js @@ -64,10 +64,10 @@ class Peer extends EventEmitter // Iterate and close all mediasoup Transport associated to this Peer, so all // its Producers and Consumers will also be closed. - this.transports.forEach((transport) => + for (const transport of this.transports.values()) { transport.close(); - }); + } if (this.socket) this.socket.disconnect(true); From 16a59f1167aee0b561281b3dfb889d0f253b7e0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Thu, 7 May 2020 22:14:48 +0200 Subject: [PATCH 07/28] Don't show video mute icon for peer that does not have video visible. Fixes #302 --- .../MeetingDrawer/ParticipantList/ListPeer.js | 58 ++++++++++--------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js index d8b3fb3..7e163f0 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ListPeer.js @@ -126,7 +126,7 @@ const ListPeer = (props) => } - { screenConsumer && + { screenConsumer && spotlight && } - - - { - e.stopPropagation(); - - webcamEnabled ? - roomClient.modifyPeerConsumer(peer.id, 'webcam', true) : - roomClient.modifyPeerConsumer(peer.id, 'webcam', false); - }} + placement='bottom' > - { webcamEnabled ? - - : - - } - - + + { + e.stopPropagation(); + + webcamEnabled ? + roomClient.modifyPeerConsumer(peer.id, 'webcam', true) : + roomClient.modifyPeerConsumer(peer.id, 'webcam', false); + }} + > + { webcamEnabled ? + + : + + } + + + } Date: Thu, 7 May 2020 23:22:46 +0200 Subject: [PATCH 08/28] Revert server change, seems to be a problem with it. Ref #249 --- server/server.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/server.js b/server/server.js index 9fb775b..fcde0b1 100755 --- a/server/server.js +++ b/server/server.js @@ -494,7 +494,7 @@ function isPathAlreadyTaken(url) */ async function runWebSocketServer() { - io = require('socket.io')(mainListener, { path: '/' }); + io = require('socket.io')(mainListener); io.use( sharedSession(session, { From 837aa1ace26b86b377d5394f9517a20ca9cfb60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 8 May 2020 00:11:39 +0200 Subject: [PATCH 09/28] Revert change for now, ref #249 --- app/src/RoomClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index a48a428..dba7c64 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -1723,7 +1723,7 @@ export default class RoomClient this._screenSharing = ScreenShare.create(this._device); - this._signalingSocket = io(this._signalingUrl, { path: this._basePath }); + this._signalingSocket = io(this._signalingUrl); this._spotlights = new Spotlights(this._maxSpotlights, this._signalingSocket); From 3b779bd81d1a2c61400667618c5bb90f9255c8f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Fri, 8 May 2020 00:20:35 +0200 Subject: [PATCH 10/28] Add shotcuts to help and add labels --- app/src/components/Controls/Help.js | 75 +++++++++++++++++++++++++-- app/src/components/Controls/TopBar.js | 8 +-- app/src/translations/cn.json | 5 +- app/src/translations/cs.json | 3 ++ app/src/translations/de.json | 3 ++ app/src/translations/dk.json | 3 ++ app/src/translations/el.json | 3 ++ app/src/translations/en.json | 3 ++ app/src/translations/es.json | 3 ++ app/src/translations/fr.json | 3 ++ app/src/translations/hr.json | 3 ++ app/src/translations/hu.json | 3 ++ app/src/translations/it.json | 5 +- app/src/translations/lv.json | 3 ++ app/src/translations/nb.json | 3 ++ app/src/translations/pl.json | 3 ++ app/src/translations/pt.json | 3 ++ app/src/translations/ro.json | 3 ++ app/src/translations/tr.json | 3 ++ app/src/translations/uk.json | 3 ++ 20 files changed, 132 insertions(+), 9 deletions(-) diff --git a/app/src/components/Controls/Help.js b/app/src/components/Controls/Help.js index 67be03f..3091ec4 100644 --- a/app/src/components/Controls/Help.js +++ b/app/src/components/Controls/Help.js @@ -4,13 +4,26 @@ import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../RoomContext'; import * as roomActions from '../../actions/roomActions'; import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; +import { useIntl, FormattedMessage } from 'react-intl'; import Dialog from '@material-ui/core/Dialog'; import DialogTitle from '@material-ui/core/DialogTitle'; import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogContentText from '@material-ui/core/DialogContentText'; import Button from '@material-ui/core/Button'; +import Paper from '@material-ui/core/Paper'; +import Tabs from '@material-ui/core/Tabs'; +import Tab from '@material-ui/core/Tab'; +const shortcuts=[ + { key: 'm', label: 'device.muteAudio', defaultMessage: 'Mute Audio' }, + { key: 'v', label: 'device.stopVideo', defaultMessage: 'Mute Video' }, + { key: '1', label: 'label.democratic', defaultMessage: 'Democratic View' }, + { key: '2', label: 'label.filmstrip', defaultMessage: 'Filmstrip View' }, + { key: 'space', label: 'me.mutedPTT', defaultMessage: 'Push SPACE to talk' }, + { key: 'a', label: 'label.advanced', defaultMessage: 'Show advanced information' } +]; const styles = (theme) => ({ dialogPaper : @@ -31,7 +44,27 @@ const styles = (theme) => [theme.breakpoints.down('xs')] : { width : '90vw' - } + }, + display : 'flex', + flexDirection : 'column' + }, + paper : { + padding : theme.spacing(1), + textAlign : 'center', + color : theme.palette.text.secondary, + whiteSpace : 'nowrap', + marginRight : theme.spacing(3), + marginBottom : theme.spacing(1), + minWidth : theme.spacing(8) + }, + shortcuts : { + display : 'flex', + flexDirection : 'row', + alignItems : 'center' + }, + tabsHeader : + { + flexGrow : 1 } }); @@ -41,6 +74,8 @@ const Help = ({ classes }) => { + const intl = useIntl(); + return ( + + + + + + {shortcuts.map((value, index) => + { + return ( +
+ + {value.key} + + +
+ ); + })} + +
+
); diff --git a/app/src/reducers/peers.js b/app/src/reducers/peers.js index 3e2c6a0..6c4fd1f 100644 --- a/app/src/reducers/peers.js +++ b/app/src/reducers/peers.js @@ -68,6 +68,18 @@ const peer = (state = {}, action) => return { ...state, roles }; } + case 'STOP_PEER_AUDIO_IN_PROGRESS': + return { + ...state, + stopPeerAudioInProgress : action.payload.flag + }; + + case 'STOP_PEER_VIDEO_IN_PROGRESS': + return { + ...state, + stopPeerVideoInProgress : action.payload.flag + }; + default: return state; } @@ -102,6 +114,8 @@ const peers = (state = {}, action) => case 'ADD_CONSUMER': case 'ADD_PEER_ROLE': case 'REMOVE_PEER_ROLE': + case 'STOP_PEER_AUDIO_IN_PROGRESS': + case 'STOP_PEER_VIDEO_IN_PROGRESS': { const oldPeer = state[action.payload.peerId]; diff --git a/server/lib/Room.js b/server/lib/Room.js index 02ba41c..77f00ee 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -1369,6 +1369,29 @@ class Room extends EventEmitter break; } + case 'moderator:mute': + { + if ( + !peer.roles.some( + (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) + ) + ) + throw new Error('peer not authorized'); + + const { peerId } = request.data; + + const mutePeer = this._peers[peerId]; + + if (!mutePeer) + throw new Error(`peer with id "${peerId}" not found`); + + this._notification(mutePeer.socket, 'moderator:mute'); + + cb(); + + break; + } + case 'moderator:muteAll': { if ( @@ -1386,6 +1409,29 @@ class Room extends EventEmitter break; } + case 'moderator:stopVideo': + { + if ( + !peer.roles.some( + (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) + ) + ) + throw new Error('peer not authorized'); + + const { peerId } = request.data; + + const stopVideoPeer = this._peers[peerId]; + + if (!stopVideoPeer) + throw new Error(`peer with id "${peerId}" not found`); + + this._notification(stopVideoPeer.socket, 'moderator:stopVideo'); + + cb(); + + break; + } + case 'moderator:stopAllVideo': { if ( From 0de99d6da8a4cec323a6cd8dcfb63b79bbe181ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Fri, 8 May 2020 13:24:39 +0200 Subject: [PATCH 12/28] Update Hungarian language --- app/src/translations/hu.json | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/translations/hu.json b/app/src/translations/hu.json index 1b2db12..6ed3eac 100644 --- a/app/src/translations/hu.json +++ b/app/src/translations/hu.json @@ -133,11 +133,11 @@ "settings.lastn": "A látható videók száma", "settings.hiddenControls": "Média Gombok automatikus elrejtése", "settings.notificationSounds": "Értesítések hangjelzéssel", - "settings.showNotifications": null, - "settings.buttonControlBar": null, - "settings.echoCancellation": null, - "settings.autoGainControl": null, - "settings.noiseSuppression": null, + "settings.showNotifications": "Értesítések megjelenítése", + "settings.buttonControlBar": "Médiavezérlő gombok leválasztása", + "settings.echoCancellation": "Visszhangelnyomás", + "settings.autoGainControl": "Automatikus hangerősítés", + "settings.noiseSuppression": "Zajelnyomás", "filesharing.saveFileError": "A file-t nem sikerült elmenteni", "filesharing.startingFileShare": "Fájl megosztása", From 220d4dd99dbcb4c821aab3351431d0921304c67d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A9sz=C3=A1ros=20Mih=C3=A1ly?= Date: Fri, 8 May 2020 14:46:30 +0200 Subject: [PATCH 13/28] fix in lang hu --- app/src/translations/hu.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/translations/hu.json b/app/src/translations/hu.json index 6ed3eac..be0342d 100644 --- a/app/src/translations/hu.json +++ b/app/src/translations/hu.json @@ -136,7 +136,7 @@ "settings.showNotifications": "Értesítések megjelenítése", "settings.buttonControlBar": "Médiavezérlő gombok leválasztása", "settings.echoCancellation": "Visszhangelnyomás", - "settings.autoGainControl": "Automatikus hangerősítés", + "settings.autoGainControl": "Automatikus hangerő", "settings.noiseSuppression": "Zajelnyomás", "filesharing.saveFileError": "A file-t nem sikerült elmenteni", From a49258e840b1813bf8aa21736d90db3241722381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 8 May 2020 16:19:55 +0200 Subject: [PATCH 14/28] New option for handling permissions in rooms. Set allowWhenRoleMissing to permit actions before a peer with that permission joins. Ref #303 --- app/src/RoomClient.js | 10 +- app/src/actions/roomActions.js | 12 +- .../AccessControl/LockDialog/ListLobbyPeer.js | 28 ++- .../AccessControl/LockDialog/LockDialog.js | 26 +- app/src/components/Containers/Me.js | 37 +-- app/src/components/Controls/TopBar.js | 56 +++-- .../MeetingDrawer/Chat/ChatInput.js | 28 ++- .../MeetingDrawer/Chat/ChatModerator.js | 26 +- .../MeetingDrawer/FileSharing/FileSharing.js | 28 ++- .../FileSharing/FileSharingModerator.js | 26 +- .../ParticipantList/ParticipantList.js | 33 +-- app/src/components/Selectors.js | 53 ++++ app/src/permissions.js | 20 ++ app/src/reducers/room.js | 26 +- server/access.js | 11 + server/config/config.example.js | 52 ++-- server/lib/Room.js | 233 ++++++++---------- server/permissions.js | 20 ++ 18 files changed, 437 insertions(+), 288 deletions(-) create mode 100644 app/src/permissions.js create mode 100644 server/access.js create mode 100644 server/permissions.js diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index cffdd2a..2b2b387 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -2847,8 +2847,8 @@ export default class RoomClient roles, peers, tracker, - permissionsFromRoles, - userRoles, + roomPermissions, + allowWhenRoleMissing, chatHistory, fileHistory, lastNHistory, @@ -2874,8 +2874,10 @@ export default class RoomClient store.dispatch(meActions.loggedIn(authenticated)); - store.dispatch(roomActions.setUserRoles(userRoles)); - store.dispatch(roomActions.setPermissionsFromRoles(permissionsFromRoles)); + store.dispatch(roomActions.setRoomPermissions(roomPermissions)); + + if (allowWhenRoleMissing) + store.dispatch(roomActions.setAllowWhenRoleMissing(allowWhenRoleMissing)); const myRoles = store.getState().me.roles; diff --git a/app/src/actions/roomActions.js b/app/src/actions/roomActions.js index 7c44835..cbfde37 100644 --- a/app/src/actions/roomActions.js +++ b/app/src/actions/roomActions.js @@ -177,14 +177,14 @@ export const setClearFileSharingInProgress = (flag) => payload : { flag } }); -export const setUserRoles = (userRoles) => +export const setRoomPermissions = (roomPermissions) => ({ - type : 'SET_USER_ROLES', - payload : { userRoles } + type : 'SET_ROOM_PERMISSIONS', + payload : { roomPermissions } }); -export const setPermissionsFromRoles = (permissionsFromRoles) => +export const setAllowWhenRoleMissing = (allowWhenRoleMissing) => ({ - type : 'SET_PERMISSIONS_FROM_ROLES', - payload : { permissionsFromRoles } + type : 'SET_ALLOW_WHEN_ROLE_MISSING', + payload : { allowWhenRoleMissing } }); diff --git a/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js b/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js index 9e73e82..050994d 100644 --- a/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js +++ b/app/src/components/AccessControl/LockDialog/ListLobbyPeer.js @@ -5,6 +5,8 @@ import PropTypes from 'prop-types'; import classnames from 'classnames'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import ListItem from '@material-ui/core/ListItem'; import ListItemText from '@material-ui/core/ListItemText'; import IconButton from '@material-ui/core/IconButton'; @@ -85,28 +87,32 @@ ListLobbyPeer.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state, { id }) => +const makeMapStateToProps = (initialState, { id }) => { - return { - peer : state.lobbyPeers[id], - promotionInProgress : state.room.lobbyPeersPromotionInProgress, - canPromote : - state.me.roles.some((role) => - state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) + const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER); + + const mapStateToProps = (state) => + { + return { + peer : state.lobbyPeers[id], + promotionInProgress : state.room.lobbyPeersPromotionInProgress, + canPromote : hasPermission(state) + }; }; + + return mapStateToProps; }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && - prev.room.lobbyPeersPromotionInProgress === - next.room.lobbyPeersPromotionInProgress && + prev.room === next.room && + prev.peers === next.peers && // For checking permissions prev.me.roles === next.me.roles && prev.lobbyPeers === next.lobbyPeers ); diff --git a/app/src/components/AccessControl/LockDialog/LockDialog.js b/app/src/components/AccessControl/LockDialog/LockDialog.js index 4d6cd24..c3dbd6a 100644 --- a/app/src/components/AccessControl/LockDialog/LockDialog.js +++ b/app/src/components/AccessControl/LockDialog/LockDialog.js @@ -1,8 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; import { - lobbyPeersKeySelector + lobbyPeersKeySelector, + makePermissionSelector } from '../../Selectors'; +import { permissions } from '../../../permissions'; import * as appPropTypes from '../../appPropTypes'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; @@ -140,15 +142,20 @@ LockDialog.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - room : state.room, - lobbyPeers : lobbyPeersKeySelector(state), - canPromote : - state.me.roles.some((role) => - state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) + const hasPermission = makePermissionSelector(permissions.PROMOTE_PEER); + + const mapStateToProps = (state) => + { + return { + room : state.room, + lobbyPeers : lobbyPeersKeySelector(state), + canPromote : hasPermission(state) + }; }; + + return mapStateToProps; }; const mapDispatchToProps = { @@ -157,7 +164,7 @@ const mapDispatchToProps = { }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, mapDispatchToProps, null, { @@ -166,6 +173,7 @@ export default withRoomContext(connect( return ( prev.room === next.room && prev.me.roles === next.me.roles && + prev.peers === next.peers && prev.lobbyPeers === next.lobbyPeers ); } diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index ead95b1..aa3a9a3 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -1,6 +1,10 @@ import React, { useState } from 'react'; import { connect } from 'react-redux'; -import { meProducersSelector } from '../Selectors'; +import { + meProducersSelector, + makePermissionSelector +} from '../Selectors'; +import { permissions } from '../../permissions'; import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import PropTypes from 'prop-types'; @@ -807,32 +811,37 @@ Me.propTypes = theme : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - me : state.me, - ...meProducersSelector(state), - settings : state.settings, - activeSpeaker : state.me.id === state.room.activeSpeakerId, - canShareScreen : - state.me.roles.some((role) => - state.room.permissionsFromRoles.SHARE_SCREEN.includes(role)) + const hasPermission = makePermissionSelector(permissions.SHARE_SCREEN); + + const mapStateToProps = (state) => + { + return { + me : state.me, + ...meProducersSelector(state), + settings : state.settings, + activeSpeaker : state.me.id === state.room.activeSpeakerId, + canShareScreen : hasPermission(state) + }; }; + + return mapStateToProps; }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me === next.me && + prev.peers === next.peers && prev.producers === next.producers && - prev.settings === next.settings && - prev.room.activeSpeakerId === next.room.activeSpeakerId + prev.settings === next.settings ); } } diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index 43ee13f..b880c36 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -4,8 +4,10 @@ import PropTypes from 'prop-types'; import { lobbyPeersKeySelector, peersLengthSelector, - raisedHandsSelector + raisedHandsSelector, + makePermissionSelector } from '../Selectors'; +import { permissions } from '../../permissions'; import * as appPropTypes from '../appPropTypes'; import { withRoomContext } from '../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; @@ -751,27 +753,35 @@ TopBar.propTypes = theme : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - room : state.room, - peersLength : peersLengthSelector(state), - lobbyPeers : lobbyPeersKeySelector(state), - permanentTopBar : state.settings.permanentTopBar, - loggedIn : state.me.loggedIn, - loginEnabled : state.me.loginEnabled, - myPicture : state.me.picture, - unread : state.toolarea.unreadMessages + - state.toolarea.unreadFiles + raisedHandsSelector(state), - canProduceExtraVideo : - state.me.roles.some((role) => - state.room.permissionsFromRoles.EXTRA_VIDEO.includes(role)), - canLock : - state.me.roles.some((role) => - state.room.permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role)), - canPromote : - state.me.roles.some((role) => - state.room.permissionsFromRoles.PROMOTE_PEER.includes(role)) - }); +const makeMapStateToProps = () => +{ + const hasExtraVideoPermission = + makePermissionSelector(permissions.EXTRA_VIDEO); + + const hasLockPermission = + makePermissionSelector(permissions.CHANGE_ROOM_LOCK); + + const hasPromotionPermission = + makePermissionSelector(permissions.PROMOTE_PEER); + + const mapStateToProps = (state) => + ({ + room : state.room, + peersLength : peersLengthSelector(state), + lobbyPeers : lobbyPeersKeySelector(state), + permanentTopBar : state.settings.permanentTopBar, + loggedIn : state.me.loggedIn, + loginEnabled : state.me.loginEnabled, + myPicture : state.me.picture, + unread : state.toolarea.unreadMessages + + state.toolarea.unreadFiles + raisedHandsSelector(state), + canProduceExtraVideo : hasExtraVideoPermission(state), + canLock : hasLockPermission(state), + canPromote : hasPromotionPermission(state) + }); + + return mapStateToProps; +}; const mapDispatchToProps = (dispatch) => ({ @@ -811,7 +821,7 @@ const mapDispatchToProps = (dispatch) => }); export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, mapDispatchToProps, null, { diff --git a/app/src/components/MeetingDrawer/Chat/ChatInput.js b/app/src/components/MeetingDrawer/Chat/ChatInput.js index 480bb26..bb07f98 100644 --- a/app/src/components/MeetingDrawer/Chat/ChatInput.js +++ b/app/src/components/MeetingDrawer/Chat/ChatInput.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import Paper from '@material-ui/core/Paper'; import InputBase from '@material-ui/core/InputBase'; import IconButton from '@material-ui/core/IconButton'; @@ -119,26 +121,32 @@ ChatInput.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - displayName : state.settings.displayName, - picture : state.me.picture, - canChat : - state.me.roles.some((role) => - state.room.permissionsFromRoles.SEND_CHAT.includes(role)) - }); +const makeMapStateToProps = () => +{ + const hasPermission = makePermissionSelector(permissions.SEND_CHAT); + + const mapStateToProps = (state) => + ({ + displayName : state.settings.displayName, + picture : state.me.picture, + canChat : hasPermission(state) + }); + + return mapStateToProps; +}; export default withRoomContext( connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me.roles === next.me.roles && + prev.peers === next.peers && prev.settings.displayName === next.settings.displayName && prev.me.picture === next.me.picture ); diff --git a/app/src/components/MeetingDrawer/Chat/ChatModerator.js b/app/src/components/MeetingDrawer/Chat/ChatModerator.js index a35675b..d7b1cad 100644 --- a/app/src/components/MeetingDrawer/Chat/ChatModerator.js +++ b/app/src/components/MeetingDrawer/Chat/ChatModerator.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { withRoomContext } from '../../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import { useIntl, FormattedMessage } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import Button from '@material-ui/core/Button'; const styles = (theme) => @@ -76,16 +78,21 @@ ChatModerator.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - isChatModerator : - state.me.roles.some((role) => - state.room.permissionsFromRoles.MODERATE_CHAT.includes(role)), - room : state.room - }); +const makeMapStateToProps = () => +{ + const hasPermission = makePermissionSelector(permissions.MODERATE_CHAT); + + const mapStateToProps = (state) => + ({ + isChatModerator : hasPermission(state), + room : state.room + }); + + return mapStateToProps; +}; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { @@ -93,7 +100,8 @@ export default withRoomContext(connect( { return ( prev.room === next.room && - prev.me === next.me + prev.me === next.me && + prev.peers === next.peers ); } } diff --git a/app/src/components/MeetingDrawer/FileSharing/FileSharing.js b/app/src/components/MeetingDrawer/FileSharing/FileSharing.js index 78ba569..8af1d8b 100644 --- a/app/src/components/MeetingDrawer/FileSharing/FileSharing.js +++ b/app/src/components/MeetingDrawer/FileSharing/FileSharing.js @@ -4,6 +4,8 @@ import { connect } from 'react-redux'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; import { useIntl } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import FileList from './FileList'; import FileSharingModerator from './FileSharingModerator'; import Paper from '@material-ui/core/Paper'; @@ -131,30 +133,36 @@ FileSharing.propTypes = { classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - canShareFiles : state.me.canShareFiles, - browser : state.me.browser, - tabOpen : state.toolarea.currentToolTab === 'files', - canShare : - state.me.roles.some((role) => - state.room.permissionsFromRoles.SHARE_FILE.includes(role)) + const hasPermission = makePermissionSelector(permissions.SHARE_FILE); + + const mapStateToProps = (state) => + { + return { + canShareFiles : state.me.canShareFiles, + browser : state.me.browser, + tabOpen : state.toolarea.currentToolTab === 'files', + canShare : hasPermission(state) + }; }; + + return mapStateToProps; }; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me.browser === next.me.browser && prev.me.roles === next.me.roles && prev.me.canShareFiles === next.me.canShareFiles && + prev.peers === next.peers && prev.toolarea.currentToolTab === next.toolarea.currentToolTab ); } diff --git a/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js b/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js index e38e54c..05f35e8 100644 --- a/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js +++ b/app/src/components/MeetingDrawer/FileSharing/FileSharingModerator.js @@ -4,6 +4,8 @@ import PropTypes from 'prop-types'; import { withRoomContext } from '../../../RoomContext'; import { withStyles } from '@material-ui/core/styles'; import { useIntl, FormattedMessage } from 'react-intl'; +import { permissions } from '../../../permissions'; +import { makePermissionSelector } from '../../Selectors'; import Button from '@material-ui/core/Button'; const styles = (theme) => @@ -76,16 +78,21 @@ FileSharingModerator.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => - ({ - isFileSharingModerator : - state.me.roles.some((role) => - state.room.permissionsFromRoles.MODERATE_FILES.includes(role)), - room : state.room - }); +const makeMapStateToProps = () => +{ + const hasPermission = makePermissionSelector(permissions.MODERATE_FILES); + + const mapStateToProps = (state) => + ({ + isFileSharingModerator : hasPermission(state), + room : state.room + }); + + return mapStateToProps; +}; export default withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { @@ -93,7 +100,8 @@ export default withRoomContext(connect( { return ( prev.room === next.room && - prev.me === next.me + prev.me === next.me && + prev.peers === next.peers ); } } diff --git a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js index 07e5d24..a416a64 100644 --- a/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js +++ b/app/src/components/MeetingDrawer/ParticipantList/ParticipantList.js @@ -1,8 +1,10 @@ import React from 'react'; import { connect } from 'react-redux'; import { - participantListSelector + participantListSelector, + makePermissionSelector } from '../../Selectors'; +import { permissions } from '../../../permissions'; import classnames from 'classnames'; import { withStyles } from '@material-ui/core/styles'; import { withRoomContext } from '../../../RoomContext'; @@ -160,31 +162,34 @@ ParticipantList.propTypes = classes : PropTypes.object.isRequired }; -const mapStateToProps = (state) => +const makeMapStateToProps = () => { - return { - isModerator : - state.me.roles.some((role) => - state.room.permissionsFromRoles.MODERATE_ROOM.includes(role)), - participants : participantListSelector(state), - spotlights : state.room.spotlights, - selectedPeerId : state.room.selectedPeerId + const hasPermission = makePermissionSelector(permissions.MODERATE_ROOM); + + const mapStateToProps = (state) => + { + return { + isModerator : hasPermission(state), + participants : participantListSelector(state), + spotlights : state.room.spotlights, + selectedPeerId : state.room.selectedPeerId + }; }; + + return mapStateToProps; }; const ParticipantListContainer = withRoomContext(connect( - mapStateToProps, + makeMapStateToProps, null, null, { areStatesEqual : (next, prev) => { return ( - prev.room.permissionsFromRoles === next.room.permissionsFromRoles && + prev.room === next.room && prev.me.roles === next.me.roles && - prev.peers === next.peers && - prev.room.spotlights === next.room.spotlights && - prev.room.selectedPeerId === next.room.selectedPeerId + prev.peers === next.peers ); } } diff --git a/app/src/components/Selectors.js b/app/src/components/Selectors.js index b8e5e41..8ac1176 100644 --- a/app/src/components/Selectors.js +++ b/app/src/components/Selectors.js @@ -1,5 +1,8 @@ import { createSelector } from 'reselect'; +const meRolesSelect = (state) => state.me.roles; +const roomPermissionsSelect = (state) => state.room.roomPermissions; +const roomAllowWhenRoleMissing = (state) => state.room.allowWhenRoleMissing; const producersSelect = (state) => state.producers; const consumersSelect = (state) => state.consumers; const spotlightsSelector = (state) => state.room.spotlights; @@ -217,3 +220,53 @@ export const makePeerConsumerSelector = () => } ); }; + +// Very important that the Components that use this +// selector need to check at least these state changes: +// +// areStatesEqual : (next, prev) => +// { +// return ( +// prev.room.roomPermissions === next.room.roomPermissions && +// prev.room.allowWhenRoleMissing === next.room.allowWhenRoleMissing && +// prev.peers === next.peers && +// prev.me.roles === next.me.roles +// ); +// } +export const makePermissionSelector = (permission) => +{ + return createSelector( + meRolesSelect, + roomPermissionsSelect, + roomAllowWhenRoleMissing, + peersValueSelector, + (roles, roomPermissions, allowWhenRoleMissing, peers) => + { + if (!roomPermissions) + return false; + + const permitted = roles.some((role) => + roomPermissions[permission].includes(role) + ); + + if (permitted) + return true; + + if (!allowWhenRoleMissing) + return false; + + // Allow if config is set, and no one is present + if (allowWhenRoleMissing.includes(permission) && + peers.filter( + (peer) => + peer.roles.some( + (role) => roomPermissions[permission].includes(role) + ) + ).length === 0 + ) + return true; + + return false; + } + ); +}; \ No newline at end of file diff --git a/app/src/permissions.js b/app/src/permissions.js new file mode 100644 index 0000000..864bdbd --- /dev/null +++ b/app/src/permissions.js @@ -0,0 +1,20 @@ +export const permissions = { + // The role(s) have permission to lock/unlock a room + CHANGE_ROOM_LOCK : 'CHANGE_ROOM_LOCK', + // The role(s) have permission to promote a peer from the lobby + PROMOTE_PEER : 'PROMOTE_PEER', + // The role(s) have permission to send chat messages + SEND_CHAT : 'SEND_CHAT', + // The role(s) have permission to moderate chat + MODERATE_CHAT : 'MODERATE_CHAT', + // The role(s) have permission to share screen + SHARE_SCREEN : 'SHARE_SCREEN', + // The role(s) have permission to produce extra video + EXTRA_VIDEO : 'EXTRA_VIDEO', + // The role(s) have permission to share files + SHARE_FILE : 'SHARE_FILE', + // The role(s) have permission to moderate files + MODERATE_FILES : 'MODERATE_FILES', + // The role(s) have permission to moderate room (e.g. kick user) + MODERATE_ROOM : 'MODERATE_ROOM' +}; \ No newline at end of file diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index 1a4b99e..595ca4e 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -33,18 +33,8 @@ const initialState = closeMeetingInProgress : false, clearChatInProgress : false, clearFileSharingInProgress : false, - userRoles : { NORMAL: 'normal' }, // Default role - permissionsFromRoles : { - CHANGE_ROOM_LOCK : [], - PROMOTE_PEER : [], - SEND_CHAT : [], - MODERATE_CHAT : [], - SHARE_SCREEN : [], - EXTRA_VIDEO : [], - SHARE_FILE : [], - MODERATE_FILES : [], - MODERATE_ROOM : [] - } + roomPermissions : null, + allowWhenRoleMissing : null }; const room = (state = initialState, action) => @@ -240,18 +230,18 @@ const room = (state = initialState, action) => case 'CLEAR_FILE_SHARING_IN_PROGRESS': return { ...state, clearFileSharingInProgress: action.payload.flag }; - case 'SET_USER_ROLES': + case 'SET_ROOM_PERMISSIONS': { - const { userRoles } = action.payload; + const { roomPermissions } = action.payload; - return { ...state, userRoles }; + return { ...state, roomPermissions }; } - case 'SET_PERMISSIONS_FROM_ROLES': + case 'SET_ALLOW_WHEN_ROLE_MISSING': { - const { permissionsFromRoles } = action.payload; + const { allowWhenRoleMissing } = action.payload; - return { ...state, permissionsFromRoles }; + return { ...state, allowWhenRoleMissing }; } default: diff --git a/server/access.js b/server/access.js new file mode 100644 index 0000000..479e9e2 --- /dev/null +++ b/server/access.js @@ -0,0 +1,11 @@ +module.exports = { + // The role(s) will gain access to the room + // even if it is locked (!) + BYPASS_ROOM_LOCK : 'BYPASS_ROOM_LOCK', + // The role(s) will gain access to the room without + // going into the lobby. If you want to restrict access to your + // server to only directly allow authenticated users, you could + // add the userRoles.AUTHENTICATED to the user in the userMapping + // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] + BYPASS_LOBBY : 'BYPASS_LOBBY' +}; \ No newline at end of file diff --git a/server/config/config.example.js b/server/config/config.example.js index dda9e2f..e08f910 100644 --- a/server/config/config.example.js +++ b/server/config/config.example.js @@ -1,5 +1,23 @@ const os = require('os'); const userRoles = require('../userRoles'); + +const { + BYPASS_ROOM_LOCK, + BYPASS_LOBBY +} = require('../access'); + +const { + CHANGE_ROOM_LOCK, + PROMOTE_PEER, + SEND_CHAT, + MODERATE_CHAT, + SHARE_SCREEN, + EXTRA_VIDEO, + SHARE_FILE, + MODERATE_FILES, + MODERATE_ROOM +} = require('../permissions'); + // const AwaitQueue = require('awaitqueue'); // const axios = require('axios'); @@ -216,44 +234,50 @@ module.exports = accessFromRoles : { // The role(s) will gain access to the room // even if it is locked (!) - BYPASS_ROOM_LOCK : [ userRoles.ADMIN ], + [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], // The role(s) will gain access to the room without // going into the lobby. If you want to restrict access to your // server to only directly allow authenticated users, you could // add the userRoles.AUTHENTICATED to the user in the userMapping // function, and change to BYPASS_LOBBY : [ userRoles.AUTHENTICATED ] - BYPASS_LOBBY : [ userRoles.NORMAL ] + [BYPASS_LOBBY] : [ userRoles.NORMAL ] }, permissionsFromRoles : { // The role(s) have permission to lock/unlock a room - CHANGE_ROOM_LOCK : [ userRoles.NORMAL ], + [CHANGE_ROOM_LOCK] : [ userRoles.MODERATOR ], // The role(s) have permission to promote a peer from the lobby - PROMOTE_PEER : [ userRoles.NORMAL ], + [PROMOTE_PEER] : [ userRoles.NORMAL ], // The role(s) have permission to send chat messages - SEND_CHAT : [ userRoles.NORMAL ], + [SEND_CHAT] : [ userRoles.NORMAL ], // The role(s) have permission to moderate chat - MODERATE_CHAT : [ userRoles.MODERATOR ], + [MODERATE_CHAT] : [ userRoles.MODERATOR ], // The role(s) have permission to share screen - SHARE_SCREEN : [ userRoles.NORMAL ], + [SHARE_SCREEN] : [ userRoles.NORMAL ], // The role(s) have permission to produce extra video - EXTRA_VIDEO : [ userRoles.NORMAL ], + [EXTRA_VIDEO] : [ userRoles.NORMAL ], // The role(s) have permission to share files - SHARE_FILE : [ userRoles.NORMAL ], + [SHARE_FILE] : [ userRoles.NORMAL ], // The role(s) have permission to moderate files - MODERATE_FILES : [ userRoles.MODERATOR ], + [MODERATE_FILES] : [ userRoles.MODERATOR ], // The role(s) have permission to moderate room (e.g. kick user) - MODERATE_ROOM : [ userRoles.MODERATOR ] + [MODERATE_ROOM] : [ userRoles.MODERATOR ] }, + // Array of permissions. If no peer with the permission in question + // is in the room, all peers are permitted to do the action. The peers + // that are allowed because of this rule will not be able to do this + // action as soon as a peer with the permission joins. In this example + // everyone will be able to lock/unlock room until a MODERATOR joins. + allowWhenRoleMissing : [ CHANGE_ROOM_LOCK ], // When truthy, the room will be open to all users when as long as there // are allready users in the room - activateOnHostJoin : true, + activateOnHostJoin : true, // When set, maxUsersPerRoom defines how many users can join // a single room. If not set, there is no limit. // maxUsersPerRoom : 20, // Room size before spreading to new router - routerScaleSize : 40, + routerScaleSize : 40, // Mediasoup settings - mediasoup : + mediasoup : { numWorkers : Object.keys(os.cpus()).length, // mediasoup Worker settings. diff --git a/server/lib/Room.js b/server/lib/Room.js index 77f00ee..3ab281b 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -5,29 +5,47 @@ const Lobby = require('./Lobby'); const { v4: uuidv4 } = require('uuid'); const jwt = require('jsonwebtoken'); const userRoles = require('../userRoles'); + +const { + BYPASS_ROOM_LOCK, + BYPASS_LOBBY +} = require('../access'); + +const permissions = require('../permissions'), { + CHANGE_ROOM_LOCK, + PROMOTE_PEER, + SEND_CHAT, + MODERATE_CHAT, + SHARE_SCREEN, + EXTRA_VIDEO, + SHARE_FILE, + MODERATE_FILES, + MODERATE_ROOM +} = permissions; + const config = require('../config/config'); const logger = new Logger('Room'); // In case they are not configured properly -const accessFromRoles = +const roomAccess = { - BYPASS_ROOM_LOCK : [ userRoles.ADMIN ], - BYPASS_LOBBY : [ userRoles.NORMAL ], + [BYPASS_ROOM_LOCK] : [ userRoles.ADMIN ], + [BYPASS_LOBBY] : [ userRoles.NORMAL ], ...config.accessFromRoles }; -const permissionsFromRoles = +const roomPermissions = { - CHANGE_ROOM_LOCK : [ userRoles.NORMAL ], - PROMOTE_PEER : [ userRoles.NORMAL ], - SEND_CHAT : [ userRoles.NORMAL ], - MODERATE_CHAT : [ userRoles.MODERATOR ], - SHARE_SCREEN : [ userRoles.NORMAL ], - EXTRA_VIDEO : [ userRoles.NORMAL ], - SHARE_FILE : [ userRoles.NORMAL ], - MODERATE_FILES : [ userRoles.MODERATOR ], - MODERATE_ROOM : [ userRoles.MODERATOR ], + [CHANGE_ROOM_LOCK] : [ userRoles.NORMAL ], + [PROMOTE_PEER] : [ userRoles.NORMAL ], + [SEND_CHAT] : [ userRoles.NORMAL ], + [MODERATE_CHAT] : [ userRoles.MODERATOR ], + [SHARE_SCREEN] : [ userRoles.NORMAL ], + [EXTRA_VIDEO] : [ userRoles.NORMAL ], + [SHARE_FILE] : [ userRoles.NORMAL ], + [MODERATE_FILES] : [ userRoles.MODERATOR ], + [MODERATE_ROOM] : [ userRoles.MODERATOR ], ...config.permissionsFromRoles }; @@ -221,9 +239,8 @@ class Room extends EventEmitter // Returning user if (returning) this._peerJoining(peer, true); - else if ( // Has a role that is allowed to bypass room lock - peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) - ) + // Has a role that is allowed to bypass room lock + else if (this._hasAccess(peer, BYPASS_ROOM_LOCK)) this._peerJoining(peer); else if ( 'maxUsersPerRoom' in config && @@ -239,7 +256,7 @@ class Room extends EventEmitter else { // Has a role that is allowed to bypass lobby - peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) ? + this._hasAccess(peer, BYPASS_LOBBY) ? this._peerJoining(peer) : this._handleGuest(peer); } @@ -271,11 +288,7 @@ class Room extends EventEmitter this._peerJoining(promotedPeer); - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id }); } @@ -283,9 +296,8 @@ class Room extends EventEmitter this._lobby.on('peerRolesChanged', (peer) => { - if ( // Has a role that is allowed to bypass room lock - peer.roles.some((role) => accessFromRoles.BYPASS_ROOM_LOCK.includes(role)) - ) + // Has a role that is allowed to bypass room lock + if (this._hasAccess(peer, BYPASS_ROOM_LOCK)) { this._lobby.promotePeer(peer.id); @@ -294,7 +306,7 @@ class Room extends EventEmitter if ( // Has a role that is allowed to bypass lobby !this._locked && - peer.roles.some((role) => accessFromRoles.BYPASS_LOBBY.includes(role)) + this._hasAccess(peer, BYPASS_LOBBY) ) { this._lobby.promotePeer(peer.id); @@ -307,11 +319,7 @@ class Room extends EventEmitter { const { id, displayName } = changedPeer; - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName }); } @@ -321,11 +329,7 @@ class Room extends EventEmitter { const { id, picture } = changedPeer; - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture }); } @@ -337,11 +341,7 @@ class Room extends EventEmitter const { id } = closedPeer; - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:peerClosed', { peerId: id }); } @@ -401,7 +401,7 @@ class Room extends EventEmitter ); } - async dump() + dump() { return { roomId : this._roomId, @@ -447,11 +447,7 @@ class Room extends EventEmitter { this._lobby.parkPeer(parkPeer); - for ( - const peer of this._getPeersWithPermission({ - permission : permissionsFromRoles.PROMOTE_PEER - }) - ) + for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) { this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id }); } @@ -602,7 +598,7 @@ class Room extends EventEmitter // Got permission to promote peers, notify peer of // peers in lobby - if (permissionsFromRoles.PROMOTE_PEER.includes(newRole)) + if (roomPermissions.PROMOTE_PEER.includes(newRole)) { const lobbyPeers = this._lobby.peerList(); @@ -670,12 +666,9 @@ class Room extends EventEmitter .map((joinedPeer) => (joinedPeer.peerInfo)); let lobbyPeers = []; - - if ( // Allowed to promote peers, notify about lobbypeers - peer.roles.some((role) => - permissionsFromRoles.PROMOTE_PEER.includes(role) - ) - ) + + // Allowed to promote peers, notify about lobbypeers + if (this._hasPermission(peer, PROMOTE_PEER)) lobbyPeers = this._lobby.peerList(); cb(null, { @@ -683,8 +676,9 @@ class Room extends EventEmitter peers : peerInfos, tracker : config.fileTracker, authenticated : peer.authenticated, - permissionsFromRoles : permissionsFromRoles, + roomPermissions : roomPermissions, userRoles : userRoles, + allowWhenRoleMissing : config.allowWhenRoleMissing, chatHistory : this._chatHistory, fileHistory : this._fileHistory, lastNHistory : this._lastN, @@ -711,7 +705,7 @@ class Room extends EventEmitter } // Notify the new Peer to all other Peers. - for (const otherPeer of this._getJoinedPeers({ excludePeer: peer })) + for (const otherPeer of this._getJoinedPeers(peer)) { this._notification( otherPeer.socket, @@ -821,15 +815,13 @@ class Room extends EventEmitter if ( appData.source === 'screen' && - !peer.roles.some( - (role) => permissionsFromRoles.SHARE_SCREEN.includes(role)) + !this._hasPermission(peer, SHARE_SCREEN) ) throw new Error('peer not authorized'); if ( appData.source === 'extravideo' && - !peer.roles.some( - (role) => permissionsFromRoles.EXTRA_VIDEO.includes(role)) + !this._hasPermission(peer, EXTRA_VIDEO) ) throw new Error('peer not authorized'); @@ -882,7 +874,7 @@ class Room extends EventEmitter cb(null, { id: producer.id }); // Optimization: Create a server-side Consumer for each Peer. - for (const otherPeer of this._getJoinedPeers({ excludePeer: peer })) + for (const otherPeer of this._getJoinedPeers(peer)) { this._createConsumer( { @@ -1144,9 +1136,7 @@ class Room extends EventEmitter case 'chatMessage': { - if ( - !peer.roles.some((role) => permissionsFromRoles.SEND_CHAT.includes(role)) - ) + if (!this._hasPermission(peer, SEND_CHAT)) throw new Error('peer not authorized'); const { chatMessage } = request.data; @@ -1167,11 +1157,7 @@ class Room extends EventEmitter case 'moderator:clearChat': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_CHAT.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_CHAT)) throw new Error('peer not authorized'); this._chatHistory = []; @@ -1187,11 +1173,7 @@ class Room extends EventEmitter case 'lockRoom': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role) - ) - ) + if (!this._hasPermission(peer, CHANGE_ROOM_LOCK)) throw new Error('peer not authorized'); this._locked = true; @@ -1209,11 +1191,7 @@ class Room extends EventEmitter case 'unlockRoom': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.CHANGE_ROOM_LOCK.includes(role) - ) - ) + if (!this._hasPermission(peer, CHANGE_ROOM_LOCK)) throw new Error('peer not authorized'); this._locked = false; @@ -1271,11 +1249,7 @@ class Room extends EventEmitter case 'promotePeer': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.PROMOTE_PEER.includes(role) - ) - ) + if (!this._hasPermission(peer, PROMOTE_PEER)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1290,11 +1264,7 @@ class Room extends EventEmitter case 'promoteAllPeers': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.PROMOTE_PEER.includes(role) - ) - ) + if (!this._hasPermission(peer, PROMOTE_PEER)) throw new Error('peer not authorized'); this._lobby.promoteAllPeers(); @@ -1307,11 +1277,7 @@ class Room extends EventEmitter case 'sendFile': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.SHARE_FILE.includes(role) - ) - ) + if (!this._hasPermission(peer, SHARE_FILE)) throw new Error('peer not authorized'); const { magnetUri } = request.data; @@ -1332,11 +1298,7 @@ class Room extends EventEmitter case 'moderator:clearFileSharing': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_FILES.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_FILES)) throw new Error('peer not authorized'); this._fileHistory = []; @@ -1371,11 +1333,7 @@ class Room extends EventEmitter case 'moderator:mute': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1394,11 +1352,7 @@ class Room extends EventEmitter case 'moderator:muteAll': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); // Spread to others @@ -1411,11 +1365,7 @@ class Room extends EventEmitter case 'moderator:stopVideo': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1434,11 +1384,7 @@ class Room extends EventEmitter case 'moderator:stopAllVideo': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); // Spread to others @@ -1451,11 +1397,7 @@ class Room extends EventEmitter case 'moderator:closeMeeting': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); this._notification(peer.socket, 'moderator:kick', null, true); @@ -1470,11 +1412,7 @@ class Room extends EventEmitter case 'moderator:kickPeer': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1495,11 +1433,7 @@ class Room extends EventEmitter case 'moderator:lowerHand': { - if ( - !peer.roles.some( - (role) => permissionsFromRoles.MODERATE_ROOM.includes(role) - ) - ) + if (!this._hasPermission(peer, MODERATE_ROOM)) throw new Error('peer not authorized'); const { peerId } = request.data; @@ -1677,16 +1611,41 @@ class Room extends EventEmitter } } + _hasPermission(peer, permission) + { + const hasPermission = peer.roles.some((role) => + roomPermissions[permission].includes(role) + ); + + if (hasPermission) + return true; + + // Allow if config is set, and no one is present + if ( + 'allowWhenRoleMissing' in config && + config.allowWhenRoleMissing.includes(permission) && + this._getPeersWithPermission(permission).length === 0 + ) + return true; + + return false; + } + + _hasAccess(peer, access) + { + return peer.roles.some((role) => roomAccess[access].includes(role)); + } + /** * Helper to get the list of joined peers. */ - _getJoinedPeers({ excludePeer = undefined } = {}) + _getJoinedPeers(excludePeer = undefined) { return Object.values(this._peers) .filter((peer) => peer.joined && peer !== excludePeer); } - _getPeersWithPermission({ permission = null, excludePeer = undefined, joined = true }) + _getPeersWithPermission(permission = null, excludePeer = undefined, joined = true) { return Object.values(this._peers) .filter( @@ -1694,7 +1653,7 @@ class Room extends EventEmitter peer.joined === joined && peer !== excludePeer && peer.roles.some( - (role) => permission.includes(role) + (role) => roomPermissions[permission].includes(role) ) ); } diff --git a/server/permissions.js b/server/permissions.js new file mode 100644 index 0000000..dd3bdbb --- /dev/null +++ b/server/permissions.js @@ -0,0 +1,20 @@ +module.exports = { + // The role(s) have permission to lock/unlock a room + CHANGE_ROOM_LOCK : 'CHANGE_ROOM_LOCK', + // The role(s) have permission to promote a peer from the lobby + PROMOTE_PEER : 'PROMOTE_PEER', + // The role(s) have permission to send chat messages + SEND_CHAT : 'SEND_CHAT', + // The role(s) have permission to moderate chat + MODERATE_CHAT : 'MODERATE_CHAT', + // The role(s) have permission to share screen + SHARE_SCREEN : 'SHARE_SCREEN', + // The role(s) have permission to produce extra video + EXTRA_VIDEO : 'EXTRA_VIDEO', + // The role(s) have permission to share files + SHARE_FILE : 'SHARE_FILE', + // The role(s) have permission to moderate files + MODERATE_FILES : 'MODERATE_FILES', + // The role(s) have permission to moderate room (e.g. kick user) + MODERATE_ROOM : 'MODERATE_ROOM' +}; \ No newline at end of file From d09e7f5565c6aaf7d416b69e4a4ecee41374dbfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 8 May 2020 16:20:15 +0200 Subject: [PATCH 15/28] Some error checks --- server/server.js | 68 +++++++++++++++++++++++++++--------------------- 1 file changed, 38 insertions(+), 30 deletions(-) diff --git a/server/server.js b/server/server.js index fcde0b1..27dc12c 100755 --- a/server/server.js +++ b/server/server.js @@ -368,38 +368,45 @@ async function setupAuth() app.get( '/auth/callback', passport.authenticate('oidc', { failureRedirect: '/auth/login' }), - async (req, res) => + async (req, res, next) => { - const state = JSON.parse(base64.decode(req.query.state)); - - const { peerId, roomId } = state; - - req.session.peerId = peerId; - req.session.roomId = roomId; - - let peer = peers.get(peerId); - - if (!peer) // User has no socket session yet, make temporary - peer = new Peer({ id: peerId, roomId }); - - if (peer.roomId !== roomId) // The peer is mischievous - throw new Error('peer authenticated with wrong room'); - - if (typeof config.userMapping === 'function') + try { - await config.userMapping({ - peer, - roomId, - userinfo : req.user._userinfo - }); + const state = JSON.parse(base64.decode(req.query.state)); + + const { peerId, roomId } = state; + + req.session.peerId = peerId; + req.session.roomId = roomId; + + let peer = peers.get(peerId); + + if (!peer) // User has no socket session yet, make temporary + peer = new Peer({ id: peerId, roomId }); + + if (peer.roomId !== roomId) // The peer is mischievous + throw new Error('peer authenticated with wrong room'); + + if (typeof config.userMapping === 'function') + { + await config.userMapping({ + peer, + roomId, + userinfo : req.user._userinfo + }); + } + + peer.authenticated = true; + + res.send(loginHelper({ + displayName : peer.displayName, + picture : peer.picture + })); + } + catch (error) + { + return next(error); } - - peer.authenticated = true; - - res.send(loginHelper({ - displayName : peer.displayName, - picture : peer.picture - })); } ); } @@ -586,7 +593,8 @@ async function runWebSocketServer() { logger.error('room creation or room joining failed [error:"%o"]', error); - socket.disconnect(true); + if (socket) + socket.disconnect(true); return; }); From 717c0053e54a9e9964571e7a376b53ff04bb48a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 8 May 2020 22:01:28 +0200 Subject: [PATCH 16/28] Better handling of reconnect. Clear the state properly and handle spotlights. --- app/src/RoomClient.js | 17 ++++++----------- app/src/Spotlights.js | 9 +++++++++ app/src/actions/consumerActions.js | 5 +++++ app/src/actions/peerActions.js | 5 +++++ app/src/actions/roomActions.js | 5 +++++ app/src/reducers/consumers.js | 5 +++++ app/src/reducers/peers.js | 11 +++++++++-- app/src/reducers/room.js | 5 +++++ 8 files changed, 49 insertions(+), 13 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index 2b2b387..afbfdc9 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -940,14 +940,10 @@ export default class RoomClient { if (consumer.kind === 'video') { - if (spotlights.indexOf(consumer.appData.peerId) > -1) - { + if (spotlights.includes(consumer.appData.peerId)) await this._resumeConsumer(consumer); - } else - { await this._pauseConsumer(consumer); - } } } } @@ -1516,9 +1512,7 @@ export default class RoomClient if (consumer.appData.peerId === peerId && consumer.appData.source === type) { if (mute) - { await this._pauseConsumer(consumer); - } else await this._resumeConsumer(consumer); } @@ -1846,6 +1840,11 @@ export default class RoomClient this._recvTransport = null; } + this._spotlights.clearSpotlights(); + + store.dispatch(peerActions.clearPeers()); + store.dispatch(consumerActions.clearConsumers()); + store.dispatch(roomActions.clearSpotlights()); store.dispatch(roomActions.setRoomState('connecting')); }); @@ -2561,8 +2560,6 @@ export default class RoomClient case 'moderator:mute': { - // const { peerId } = notification.data; - if (this._micProducer && !this._micProducer.paused) { this.muteMic(); @@ -2581,8 +2578,6 @@ export default class RoomClient case 'moderator:stopVideo': { - // const { peerId } = notification.data; - this.disableWebcam(); this.disableScreenSharing(); diff --git a/app/src/Spotlights.js b/app/src/Spotlights.js index 30dc48a..3d0673f 100644 --- a/app/src/Spotlights.js +++ b/app/src/Spotlights.js @@ -95,6 +95,15 @@ export default class Spotlights extends EventEmitter }); } + clearSpotlights() + { + this._started = false; + + this._peerList = []; + this._selectedSpotlights = []; + this._currentSpotlights = []; + } + _newPeer(id) { logger.debug( diff --git a/app/src/actions/consumerActions.js b/app/src/actions/consumerActions.js index 249d156..659a609 100644 --- a/app/src/actions/consumerActions.js +++ b/app/src/actions/consumerActions.js @@ -10,6 +10,11 @@ export const removeConsumer = (consumerId, peerId) => payload : { consumerId, peerId } }); +export const clearConsumers = () => + ({ + type : 'CLEAR_CONSUMERS' + }); + export const setConsumerPaused = (consumerId, originator) => ({ type : 'SET_CONSUMER_PAUSED', diff --git a/app/src/actions/peerActions.js b/app/src/actions/peerActions.js index 738928c..5672b47 100644 --- a/app/src/actions/peerActions.js +++ b/app/src/actions/peerActions.js @@ -10,6 +10,11 @@ export const removePeer = (peerId) => payload : { peerId } }); +export const clearPeers = () => + ({ + type : 'CLEAR_PEERS' + }); + export const setPeerDisplayName = (displayName, peerId) => ({ type : 'SET_PEER_DISPLAY_NAME', diff --git a/app/src/actions/roomActions.js b/app/src/actions/roomActions.js index cbfde37..5ae45e3 100644 --- a/app/src/actions/roomActions.js +++ b/app/src/actions/roomActions.js @@ -130,6 +130,11 @@ export const setSpotlights = (spotlights) => payload : { spotlights } }); +export const clearSpotlights = () => + ({ + type : 'CLEAR_SPOTLIGHTS' + }); + export const toggleJoined = () => ({ type : 'TOGGLE_JOINED' diff --git a/app/src/reducers/consumers.js b/app/src/reducers/consumers.js index 68a4a4a..6be31ae 100644 --- a/app/src/reducers/consumers.js +++ b/app/src/reducers/consumers.js @@ -110,6 +110,11 @@ const consumers = (state = initialState, action) => return { ...state, [consumerId]: newConsumer }; } + case 'CLEAR_CONSUMERS': + { + return initialState; + } + default: return state; } diff --git a/app/src/reducers/peers.js b/app/src/reducers/peers.js index 6c4fd1f..32d6fef 100644 --- a/app/src/reducers/peers.js +++ b/app/src/reducers/peers.js @@ -1,4 +1,6 @@ -const peer = (state = {}, action) => +const initialState = {}; + +const peer = (state = initialState, action) => { switch (action.type) { @@ -85,7 +87,7 @@ const peer = (state = {}, action) => } }; -const peers = (state = {}, action) => +const peers = (state = initialState, action) => { switch (action.type) { @@ -139,6 +141,11 @@ const peers = (state = {}, action) => return { ...state, [oldPeer.id]: peer(oldPeer, action) }; } + case 'CLEAR_PEERS': + { + return initialState; + } + default: return state; } diff --git a/app/src/reducers/room.js b/app/src/reducers/room.js index 595ca4e..d784219 100644 --- a/app/src/reducers/room.js +++ b/app/src/reducers/room.js @@ -212,6 +212,11 @@ const room = (state = initialState, action) => return { ...state, spotlights }; } + case 'CLEAR_SPOTLIGHTS': + { + return { ...state, spotlights: [] }; + } + case 'SET_LOBBY_PEERS_PROMOTION_IN_PROGRESS': return { ...state, lobbyPeersPromotionInProgress: action.payload.flag }; From c73ee5c26b4e77a21a529d75c8d71f8c347a27ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 8 May 2020 22:02:06 +0200 Subject: [PATCH 17/28] Fix missing picture on peer in lobby. --- app/src/RoomClient.js | 18 ++++++++++++------ server/lib/Lobby.js | 2 +- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index afbfdc9..bce2f03 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -2124,15 +2124,21 @@ export default class RoomClient lobbyPeers.forEach((peer) => { store.dispatch( - lobbyPeerActions.addLobbyPeer(peer.peerId)); + lobbyPeerActions.addLobbyPeer(peer.id)); + store.dispatch( lobbyPeerActions.setLobbyPeerDisplayName( peer.displayName, - peer.peerId + peer.id ) ); + store.dispatch( - lobbyPeerActions.setLobbyPeerPicture(peer.picture)); + lobbyPeerActions.setLobbyPeerPicture( + peer.picture, + peer.id + ) + ); }); store.dispatch( @@ -2930,11 +2936,11 @@ export default class RoomClient (lobbyPeers.length > 0) && lobbyPeers.forEach((peer) => { store.dispatch( - lobbyPeerActions.addLobbyPeer(peer.peerId)); + lobbyPeerActions.addLobbyPeer(peer.id)); store.dispatch( - lobbyPeerActions.setLobbyPeerDisplayName(peer.displayName, peer.peerId)); + lobbyPeerActions.setLobbyPeerDisplayName(peer.displayName, peer.id)); store.dispatch( - lobbyPeerActions.setLobbyPeerPicture(peer.picture)); + lobbyPeerActions.setLobbyPeerPicture(peer.picture, peer.id)); }); (accessCode != null) && store.dispatch( diff --git a/server/lib/Lobby.js b/server/lib/Lobby.js index 0bcfb66..43fc30d 100644 --- a/server/lib/Lobby.js +++ b/server/lib/Lobby.js @@ -46,7 +46,7 @@ class Lobby extends EventEmitter return Object.values(this._peers).map((peer) => ({ - peerId : peer.id, + id : peer.id, displayName : peer.displayName, picture : peer.picture })); From a9e9a1c1fad7c7f9575b7f9873b5351e47fd3454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Fri, 8 May 2020 22:11:48 +0200 Subject: [PATCH 18/28] Cleanup of join logic, and making sure that lobbyPeers are sent to all peers if last peer with PROMOTE_PEER leaves and allowWhenRoleMissing contains PROMOTE_PEER, fixes #303 --- server/lib/Room.js | 255 ++++++++++++++++++++++++++++----------------- 1 file changed, 157 insertions(+), 98 deletions(-) diff --git a/server/lib/Room.js b/server/lib/Room.js index 3ab281b..74accc4 100644 --- a/server/lib/Room.js +++ b/server/lib/Room.js @@ -1,4 +1,5 @@ const EventEmitter = require('events').EventEmitter; +const AwaitQueue = require('awaitqueue'); const axios = require('axios'); const Logger = require('./Logger'); const Lobby = require('./Lobby'); @@ -49,6 +50,8 @@ const roomPermissions = ...config.permissionsFromRoles }; +const roomAllowWhenRoleMissing = config.allowWhenRoleMissing || []; + const ROUTER_SCALE_SIZE = config.routerScaleSize || 40; class Room extends EventEmitter @@ -115,6 +118,9 @@ class Room extends EventEmitter // Closed flag. this._closed = false; + // Joining queue + this._queue = new AwaitQueue(); + // Locked flag. this._locked = false; @@ -166,6 +172,10 @@ class Room extends EventEmitter this._closed = true; + this._queue.close(); + + this._queue = null; + if (this._selfDestructTimeout) clearTimeout(this._selfDestructTimeout); @@ -288,7 +298,7 @@ class Room extends EventEmitter this._peerJoining(promotedPeer); - for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:promotedPeer', { peerId: id }); } @@ -319,7 +329,7 @@ class Room extends EventEmitter { const { id, displayName } = changedPeer; - for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:changeDisplayName', { peerId: id, displayName }); } @@ -329,7 +339,7 @@ class Room extends EventEmitter { const { id, picture } = changedPeer; - for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:changePicture', { peerId: id, picture }); } @@ -341,7 +351,7 @@ class Room extends EventEmitter const { id } = closedPeer; - for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'lobby:peerClosed', { peerId: id }); } @@ -447,114 +457,91 @@ class Room extends EventEmitter { this._lobby.parkPeer(parkPeer); - for (const peer of this._getPeersWithPermission(PROMOTE_PEER)) + for (const peer of this._getAllowedPeers(PROMOTE_PEER)) { this._notification(peer.socket, 'parkedPeer', { peerId: parkPeer.id }); } } - async _peerJoining(peer, returning = false) + _peerJoining(peer, returning = false) { - peer.socket.join(this._roomId); - - // If we don't have this peer, add to end - !this._lastN.includes(peer.id) && this._lastN.push(peer.id); - - this._peers[peer.id] = peer; - - // Assign routerId - peer.routerId = await this._getRouterId(); - - this._handlePeer(peer); - - if (returning) + this._queue.push(async () => { - this._notification(peer.socket, 'roomBack'); - } - else - { - const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true }); + peer.socket.join(this._roomId); - peer.socket.handshake.session.token = token; + // If we don't have this peer, add to end + !this._lastN.includes(peer.id) && this._lastN.push(peer.id); - peer.socket.handshake.session.save(); + this._peers[peer.id] = peer; - let turnServers; - - if ('turnAPIURI' in config) + // Assign routerId + peer.routerId = await this._getRouterId(); + + this._handlePeer(peer); + + if (returning) { - try - { - const { data } = await axios.get( - config.turnAPIURI, - { - params : { - ...config.turnAPIparams, - 'api_key' : config.turnAPIKey, - 'ip' : peer.socket.request.connection.remoteAddress - } - }); - - turnServers = [ { - urls : data.uris, - username : data.username, - credential : data.password - } ]; - } - catch (error) - { - if ('backupTurnServers' in config) - turnServers = config.backupTurnServers; - - logger.error('_peerJoining() | error on REST turn [error:"%o"]', error); - } + this._notification(peer.socket, 'roomBack'); } - else if ('backupTurnServers' in config) + else { - turnServers = config.backupTurnServers; + const token = jwt.sign({ id: peer.id }, this._uuid, { noTimestamp: true }); + + peer.socket.handshake.session.token = token; + + peer.socket.handshake.session.save(); + + let turnServers; + + if ('turnAPIURI' in config) + { + try + { + const { data } = await axios.get( + config.turnAPIURI, + { + params : { + ...config.turnAPIparams, + 'api_key' : config.turnAPIKey, + 'ip' : peer.socket.request.connection.remoteAddress + } + }); + + turnServers = [ { + urls : data.uris, + username : data.username, + credential : data.password + } ]; + } + catch (error) + { + if ('backupTurnServers' in config) + turnServers = config.backupTurnServers; + + logger.error('_peerJoining() | error on REST turn [error:"%o"]', error); + } + } + else if ('backupTurnServers' in config) + { + turnServers = config.backupTurnServers; + } + + this._notification(peer.socket, 'roomReady', { turnServers }); } - - this._notification(peer.socket, 'roomReady', { turnServers }); - } + }) + .catch((error) => + { + logger.error('_peerJoining() [error:"%o"]', error); + }); } _handlePeer(peer) { logger.debug('_handlePeer() [peer:"%s"]', peer.id); - peer.socket.on('request', (request, cb) => - { - logger.debug( - 'Peer "request" event [method:"%s", peerId:"%s"]', - request.method, peer.id); - - this._handleSocketRequest(peer, request, cb) - .catch((error) => - { - logger.error('"request" failed [error:"%o"]', error); - - cb(error); - }); - }); - peer.on('close', () => { - if (this._closed) - return; - - // If the Peer was joined, notify all Peers. - if (peer.joined) - this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true); - - // Remove from lastN - this._lastN = this._lastN.filter((id) => id !== peer.id); - - delete this._peers[peer.id]; - - // If this is the last Peer in the room and - // lobby is empty, close the room after a while. - if (this.checkEmpty() && this._lobby.checkEmpty()) - this.selfDestructCountdown(); + this._handlePeerClose(peer); }); peer.on('displayNameChanged', ({ oldDisplayName }) => @@ -620,6 +607,69 @@ class Room extends EventEmitter role : oldRole }, true, true); }); + + peer.socket.on('request', (request, cb) => + { + logger.debug( + 'Peer "request" event [method:"%s", peerId:"%s"]', + request.method, peer.id); + + this._handleSocketRequest(peer, request, cb) + .catch((error) => + { + logger.error('"request" failed [error:"%o"]', error); + + cb(error); + }); + }); + + // Peer left before we were done joining + if (peer.closed) + this._handlePeerClose(peer); + } + + _handlePeerClose(peer) + { + logger.debug('_handlePeerClose() [peer:"%s"]', peer.id); + + if (this._closed) + return; + + // If the Peer was joined, notify all Peers. + if (peer.joined) + this._notification(peer.socket, 'peerClosed', { peerId: peer.id }, true); + + // Remove from lastN + this._lastN = this._lastN.filter((id) => id !== peer.id); + + // Need this to know if this peer was the last with PROMOTE_PEER + const hasPromotePeer = peer.roles.some((role) => + roomPermissions[PROMOTE_PEER].includes(role) + ); + + delete this._peers[peer.id]; + + // No peers left with PROMOTE_PEER, might need to give + // lobbyPeers to peers that are left. + if ( + hasPromotePeer && + !this._lobby.checkEmpty() && + roomAllowWhenRoleMissing.includes(PROMOTE_PEER) && + this._getPeersWithPermission(PROMOTE_PEER).length === 0 + ) + { + const lobbyPeers = this._lobby.peerList(); + + for (const allowedPeer of this._getAllowedPeers(PROMOTE_PEER)) + { + this._notification(allowedPeer.socket, 'parkedPeers', { lobbyPeers }); + } + } + + // If this is the last Peer in the room and + // lobby is empty, close the room after a while. + if (this.checkEmpty() && this._lobby.checkEmpty()) + this.selfDestructCountdown(); } async _handleSocketRequest(peer, request, cb) @@ -656,13 +706,9 @@ class Room extends EventEmitter // Tell the new Peer about already joined Peers. // And also create Consumers for existing Producers. - const joinedPeers = - [ - ...this._getJoinedPeers() - ]; + const joinedPeers = this._getJoinedPeers(peer); const peerInfos = joinedPeers - .filter((joinedPeer) => joinedPeer.id !== peer.id) .map((joinedPeer) => (joinedPeer.peerInfo)); let lobbyPeers = []; @@ -678,7 +724,7 @@ class Room extends EventEmitter authenticated : peer.authenticated, roomPermissions : roomPermissions, userRoles : userRoles, - allowWhenRoleMissing : config.allowWhenRoleMissing, + allowWhenRoleMissing : roomAllowWhenRoleMissing, chatHistory : this._chatHistory, fileHistory : this._fileHistory, lastNHistory : this._lastN, @@ -1622,8 +1668,7 @@ class Room extends EventEmitter // Allow if config is set, and no one is present if ( - 'allowWhenRoleMissing' in config && - config.allowWhenRoleMissing.includes(permission) && + roomAllowWhenRoleMissing.includes(permission) && this._getPeersWithPermission(permission).length === 0 ) return true; @@ -1645,6 +1690,20 @@ class Room extends EventEmitter .filter((peer) => peer.joined && peer !== excludePeer); } + _getAllowedPeers(permission = null, excludePeer = undefined, joined = true) + { + const peers = this._getPeersWithPermission(permission, excludePeer, joined); + + if (peers.length > 0) + return peers; + + // Allow if config is set, and no one is present + if (roomAllowWhenRoleMissing.includes(permission)) + return Object.values(this._peers); + + return peers; + } + _getPeersWithPermission(permission = null, excludePeer = undefined, joined = true) { return Object.values(this._peers) From c5143de64786c5489d309605c3f281fde2b6cd93 Mon Sep 17 00:00:00 2001 From: Roman Drozd <37835902+roman-drozd-it@users.noreply.github.com> Date: Fri, 8 May 2020 22:26:45 +0200 Subject: [PATCH 19/28] Fix ptt break layout (#298) --- app/src/components/Containers/Me.js | 31 +++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/app/src/components/Containers/Me.js b/app/src/components/Containers/Me.js index aa3a9a3..743c022 100644 --- a/app/src/components/Containers/Me.js +++ b/app/src/components/Containers/Me.js @@ -132,12 +132,12 @@ const styles = (theme) => { position : 'absolute', float : 'left', - top : '10%', + top : '25%', left : '50%', transform : 'translate(-50%, 0%)', color : 'rgba(255, 255, 255, 0.7)', - fontSize : '2vs', - backgroundColor : 'rgba(255, 0, 0, 0.5)', + fontSize : '1.3em', + backgroundColor : 'rgba(255, 0, 0, 0.9)', margin : '4px', padding : theme.spacing(2), zIndex : 31, @@ -322,19 +322,20 @@ const Me = (props) => }} style={spacingStyle} > + + { me.browser.platform !== 'mobile' && +
+ +
+ }
- { !smallContainer && -
- -
- }

Date: Sat, 9 May 2020 00:03:56 +0200 Subject: [PATCH 20/28] Option to make side drawer push videos to the side and be permanent, fixes #320 --- app/public/config/config.example.js | 3 + app/src/actions/settingsActions.js | 5 ++ app/src/components/Controls/TopBar.js | 49 ++++++++++- app/src/components/MeetingViews/Democratic.js | 8 +- app/src/components/MeetingViews/Filmstrip.js | 4 + app/src/components/Room.js | 83 +++++++++++++++---- .../components/Settings/AppearenceSettings.js | 17 ++++ app/src/reducers/settings.js | 8 ++ app/src/translations/cn.json | 1 + app/src/translations/cs.json | 1 + app/src/translations/de.json | 1 + app/src/translations/dk.json | 1 + app/src/translations/el.json | 1 + app/src/translations/en.json | 1 + app/src/translations/es.json | 1 + app/src/translations/fr.json | 1 + app/src/translations/hr.json | 1 + app/src/translations/hu.json | 1 + app/src/translations/it.json | 1 + app/src/translations/lv.json | 1 + app/src/translations/nb.json | 1 + app/src/translations/pl.json | 1 + app/src/translations/pt.json | 1 + app/src/translations/ro.json | 1 + app/src/translations/tr.json | 1 + app/src/translations/uk.json | 1 + 26 files changed, 174 insertions(+), 21 deletions(-) diff --git a/app/public/config/config.example.js b/app/public/config/config.example.js index 4bbc13f..7909284 100644 --- a/app/public/config/config.example.js +++ b/app/public/config/config.example.js @@ -57,6 +57,9 @@ var config = // If true, will show media control buttons in separate // control bar, not in the ME container. buttonControlBar : false, + // If false, will push videos away to make room for side + // drawer. If true, will overlay side drawer over videos + drawerOverlayed : true, // Timeout for autohiding topbar and button control bar hideTimeout : 3000, lastN : 4, diff --git a/app/src/actions/settingsActions.js b/app/src/actions/settingsActions.js index 4b7dc6d..21ff2fd 100644 --- a/app/src/actions/settingsActions.js +++ b/app/src/actions/settingsActions.js @@ -43,6 +43,11 @@ export const toggleButtonControlBar = () => type : 'TOGGLE_BUTTON_CONTROL_BAR' }); +export const toggleDrawerOverlayed = () => + ({ + type : 'TOGGLE_DRAWER_OVERLAYED' + }); + export const toggleShowNotifications = () => ({ type : 'TOGGLE_SHOW_NOTIFICATIONS' diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index b880c36..52b06cd 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -14,6 +14,7 @@ import { withStyles } from '@material-ui/core/styles'; import * as roomActions from '../../actions/roomActions'; import * as toolareaActions from '../../actions/toolareaActions'; import { useIntl, FormattedMessage } from 'react-intl'; +import classnames from 'classnames'; import AppBar from '@material-ui/core/AppBar'; import Toolbar from '@material-ui/core/Toolbar'; import MenuItem from '@material-ui/core/MenuItem'; @@ -43,6 +44,31 @@ import InfoIcon from '@material-ui/icons/Info'; const styles = (theme) => ({ + persistentDrawerOpen : + { + width : 'calc(100% - 30vw)', + marginLeft : '30vw', + [theme.breakpoints.down('lg')] : + { + width : 'calc(100% - 40vw)', + marginLeft : '40vw' + }, + [theme.breakpoints.down('md')] : + { + width : 'calc(100% - 50vw)', + marginLeft : '50vw' + }, + [theme.breakpoints.down('sm')] : + { + width : 'calc(100% - 70vw)', + marginLeft : '70vw' + }, + [theme.breakpoints.down('xs')] : + { + width : 'calc(100% - 90vw)', + marginLeft : '90vw' + } + }, menuButton : { margin : 0, @@ -188,6 +214,9 @@ const TopBar = (props) => peersLength, lobbyPeers, permanentTopBar, + drawerOverlayed, + toolAreaOpen, + isMobile, myPicture, loggedIn, loginEnabled, @@ -248,7 +277,12 @@ const TopBar = (props) => const mapStateToProps = (state) => ({ room : state.room, + isMobile : state.me.browser.platform === 'mobile', peersLength : peersLengthSelector(state), lobbyPeers : lobbyPeersKeySelector(state), permanentTopBar : state.settings.permanentTopBar, + drawerOverlayed : state.settings.drawerOverlayed, + toolAreaOpen : state.toolarea.toolAreaOpen, loggedIn : state.me.loggedIn, loginEnabled : state.me.loginEnabled, myPicture : state.me.picture, @@ -832,12 +872,15 @@ export default withRoomContext(connect( prev.peers === next.peers && prev.lobbyPeers === next.lobbyPeers && prev.settings.permanentTopBar === next.settings.permanentTopBar && + prev.settings.drawerOverlayed === next.settings.drawerOverlayed && prev.me.loggedIn === next.me.loggedIn && + prev.me.browser === next.me.browser && prev.me.loginEnabled === next.me.loginEnabled && prev.me.picture === next.me.picture && prev.me.roles === next.me.roles && prev.toolarea.unreadMessages === next.toolarea.unreadMessages && - prev.toolarea.unreadFiles === next.toolarea.unreadFiles + prev.toolarea.unreadFiles === next.toolarea.unreadFiles && + prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen ); } } diff --git a/app/src/components/MeetingViews/Democratic.js b/app/src/components/MeetingViews/Democratic.js index f2fff1a..e2e774c 100644 --- a/app/src/components/MeetingViews/Democratic.js +++ b/app/src/components/MeetingViews/Democratic.js @@ -22,6 +22,7 @@ const styles = (theme) => display : 'flex', flexDirection : 'row', flexWrap : 'wrap', + overflow : 'hidden', justifyContent : 'center', alignItems : 'center', alignContent : 'center' @@ -189,6 +190,7 @@ Democratic.propTypes = toolbarsVisible : PropTypes.bool.isRequired, permanentTopBar : PropTypes.bool.isRequired, buttonControlBar : PropTypes.bool.isRequired, + toolAreaOpen : PropTypes.bool.isRequired, classes : PropTypes.object.isRequired }; @@ -199,7 +201,8 @@ const mapStateToProps = (state) => spotlightsPeers : spotlightPeersSelector(state), toolbarsVisible : state.room.toolbarsVisible, permanentTopBar : state.settings.permanentTopBar, - buttonControlBar : state.settings.buttonControlBar + buttonControlBar : state.settings.buttonControlBar, + toolAreaOpen : state.toolarea.toolAreaOpen }; }; @@ -217,7 +220,8 @@ export default connect( prev.room.spotlights === next.room.spotlights && prev.room.toolbarsVisible === next.room.toolbarsVisible && prev.settings.permanentTopBar === next.settings.permanentTopBar && - prev.settings.buttonControlBar === next.settings.buttonControlBar + prev.settings.buttonControlBar === next.settings.buttonControlBar && + prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen ); } } diff --git a/app/src/components/MeetingViews/Filmstrip.js b/app/src/components/MeetingViews/Filmstrip.js index d1cfba6..120e985 100644 --- a/app/src/components/MeetingViews/Filmstrip.js +++ b/app/src/components/MeetingViews/Filmstrip.js @@ -25,6 +25,7 @@ const styles = () => height : '100%', width : '100%', display : 'grid', + overflow : 'hidden', gridTemplateColumns : '1fr', gridTemplateRows : '1fr 0.25fr' }, @@ -334,6 +335,7 @@ Filmstrip.propTypes = { spotlights : PropTypes.array.isRequired, boxes : PropTypes.number, toolbarsVisible : PropTypes.bool.isRequired, + toolAreaOpen : PropTypes.bool.isRequired, permanentTopBar : PropTypes.bool, classes : PropTypes.object.isRequired }; @@ -349,6 +351,7 @@ const mapStateToProps = (state) => spotlights : state.room.spotlights, boxes : videoBoxesSelector(state), toolbarsVisible : state.room.toolbarsVisible, + toolAreaOpen : state.toolarea.toolAreaOpen, permanentTopBar : state.settings.permanentTopBar }; }; @@ -364,6 +367,7 @@ export default withRoomContext(connect( prev.room.activeSpeakerId === next.room.activeSpeakerId && prev.room.selectedPeerId === next.room.selectedPeerId && prev.room.toolbarsVisible === next.room.toolbarsVisible && + prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen && prev.settings.permanentTopBar === next.settings.permanentTopBar && prev.peers === next.peers && prev.consumers === next.consumers && diff --git a/app/src/components/Room.js b/app/src/components/Room.js index d021132..4dee360 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -12,6 +12,7 @@ import { FormattedMessage } from 'react-intl'; import CookieConsent from 'react-cookie-consent'; import CssBaseline from '@material-ui/core/CssBaseline'; import SwipeableDrawer from '@material-ui/core/SwipeableDrawer'; +import Drawer from '@material-ui/core/Drawer'; import Hidden from '@material-ui/core/Hidden'; import Notifications from './Notifications/Notifications'; import MeetingDrawer from './MeetingDrawer/MeetingDrawer'; @@ -45,6 +46,27 @@ const styles = (theme) => backgroundSize : 'cover', backgroundRepeat : 'no-repeat' }, + drawer : + { + width : '30vw', + flexShrink : 0, + [theme.breakpoints.down('lg')] : + { + width : '40vw' + }, + [theme.breakpoints.down('md')] : + { + width : '50vw' + }, + [theme.breakpoints.down('sm')] : + { + width : '70vw' + }, + [theme.breakpoints.down('xs')] : + { + width : '90vw' + } + }, drawerPaper : { width : '30vw', @@ -147,6 +169,7 @@ class Room extends React.PureComponent advancedMode, showNotifications, buttonControlBar, + drawerOverlayed, toolAreaOpen, toggleToolArea, classes, @@ -159,6 +182,8 @@ class Room extends React.PureComponent democratic : Democratic }[room.mode]; + const container = window !== undefined ? window.document.body : undefined; + return (

{ !isElectron() && @@ -195,22 +220,45 @@ class Room extends React.PureComponent onFullscreen={this.handleToggleFullscreen} /> - + { (browser.platform === 'mobile' || drawerOverlayed) ? + + : + + } { browser.platform === 'mobile' && browser.os !== 'ios' && @@ -252,6 +300,7 @@ Room.propTypes = advancedMode : PropTypes.bool.isRequired, showNotifications : PropTypes.bool.isRequired, buttonControlBar : PropTypes.bool.isRequired, + drawerOverlayed : PropTypes.bool.isRequired, toolAreaOpen : PropTypes.bool.isRequired, setToolbarsVisible : PropTypes.func.isRequired, toggleToolArea : PropTypes.func.isRequired, @@ -266,6 +315,7 @@ const mapStateToProps = (state) => advancedMode : state.settings.advancedMode, showNotifications : state.settings.showNotifications, buttonControlBar : state.settings.buttonControlBar, + drawerOverlayed : state.settings.drawerOverlayed, toolAreaOpen : state.toolarea.toolAreaOpen }); @@ -294,6 +344,7 @@ export default connect( prev.settings.advancedMode === next.settings.advancedMode && prev.settings.showNotifications === next.settings.showNotifications && prev.settings.buttonControlBar === next.settings.buttonControlBar && + prev.settings.drawerOverlayed === next.settings.drawerOverlayed && prev.toolarea.toolAreaOpen === next.toolarea.toolAreaOpen ); } diff --git a/app/src/components/Settings/AppearenceSettings.js b/app/src/components/Settings/AppearenceSettings.js index a859ab4..46cc898 100644 --- a/app/src/components/Settings/AppearenceSettings.js +++ b/app/src/components/Settings/AppearenceSettings.js @@ -26,12 +26,14 @@ const styles = (theme) => }); const AppearenceSettings = ({ + isMobile, room, settings, onTogglePermanentTopBar, onToggleHiddenControls, onToggleButtonControlBar, onToggleShowNotifications, + onToggleDrawerOverlayed, handleChangeMode, classes }) => @@ -111,6 +113,16 @@ const AppearenceSettings = ({ defaultMessage : 'Separate media controls' })} /> + { !isMobile && + } + label={intl.formatMessage({ + id : 'settings.drawerOverlayed', + defaultMessage : 'Side drawer over content' + })} + /> + } } @@ -125,18 +137,21 @@ const AppearenceSettings = ({ AppearenceSettings.propTypes = { + isMobile : PropTypes.bool.isRequired, room : appPropTypes.Room.isRequired, settings : PropTypes.object.isRequired, onTogglePermanentTopBar : PropTypes.func.isRequired, onToggleHiddenControls : PropTypes.func.isRequired, onToggleButtonControlBar : PropTypes.func.isRequired, onToggleShowNotifications : PropTypes.func.isRequired, + onToggleDrawerOverlayed : PropTypes.func.isRequired, handleChangeMode : PropTypes.func.isRequired, classes : PropTypes.object.isRequired }; const mapStateToProps = (state) => ({ + isMobile : state.me.browser.platform === 'mobile', room : state.room, settings : state.settings }); @@ -146,6 +161,7 @@ const mapDispatchToProps = { onToggleHiddenControls : settingsActions.toggleHiddenControls, onToggleShowNotifications : settingsActions.toggleShowNotifications, onToggleButtonControlBar : settingsActions.toggleButtonControlBar, + onToggleDrawerOverlayed : settingsActions.toggleDrawerOverlayed, handleChangeMode : roomActions.setDisplayMode }; @@ -157,6 +173,7 @@ export default connect( areStatesEqual : (next, prev) => { return ( + prev.me.browser === next.me.browser && prev.room === next.room && prev.settings === next.settings ); diff --git a/app/src/reducers/settings.js b/app/src/reducers/settings.js index f306726..1c375bf 100644 --- a/app/src/reducers/settings.js +++ b/app/src/reducers/settings.js @@ -19,6 +19,7 @@ const initialState = showNotifications : true, notificationSounds : true, buttonControlBar : window.config.buttonControlBar || false, + drawerOverlayed : window.config.drawerOverlayed || true, ...window.config.defaultAudio }; @@ -153,6 +154,13 @@ const settings = (state = initialState, action) => return { ...state, buttonControlBar }; } + case 'TOGGLE_DRAWER_OVERLAYED': + { + const drawerOverlayed = !state.drawerOverlayed; + + return { ...state, drawerOverlayed }; + } + case 'TOGGLE_HIDDEN_CONTROLS': { const hiddenControls = !state.hiddenControls; diff --git a/app/src/translations/cn.json b/app/src/translations/cn.json index df97372..0ce5f5b 100644 --- a/app/src/translations/cn.json +++ b/app/src/translations/cn.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "无法保存文件", "filesharing.startingFileShare": "正在尝试共享文件", diff --git a/app/src/translations/cs.json b/app/src/translations/cs.json index f73144e..3eac16c 100644 --- a/app/src/translations/cs.json +++ b/app/src/translations/cs.json @@ -137,6 +137,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Není možné uložit soubor", "filesharing.startingFileShare": "Pokouším se sdílet soubor", diff --git a/app/src/translations/de.json b/app/src/translations/de.json index d141ae9..402c4b3 100644 --- a/app/src/translations/de.json +++ b/app/src/translations/de.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Fehler beim Speichern der Datei", "filesharing.startingFileShare": "Starte Teilen der Datei", diff --git a/app/src/translations/dk.json b/app/src/translations/dk.json index a0bc95f..c5c3a2d 100644 --- a/app/src/translations/dk.json +++ b/app/src/translations/dk.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Kan ikke gemme fil", "filesharing.startingFileShare": "Forsøger at dele filen", diff --git a/app/src/translations/el.json b/app/src/translations/el.json index c21ef1d..d71764f 100644 --- a/app/src/translations/el.json +++ b/app/src/translations/el.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Αδυναμία αποθήκευσης του αρχείου", "filesharing.startingFileShare": "Προσπάθεια διαμοιρασμού αρχείου", diff --git a/app/src/translations/en.json b/app/src/translations/en.json index 4b33dbd..344d660 100644 --- a/app/src/translations/en.json +++ b/app/src/translations/en.json @@ -138,6 +138,7 @@ "settings.echoCancellation": "Echo cancellation", "settings.autoGainControl": "Auto gain control", "settings.noiseSuppression": "Noise suppression", + "settings.drawerOverlayed": "Side drawer over content", "filesharing.saveFileError": "Unable to save file", "filesharing.startingFileShare": "Attempting to share file", diff --git a/app/src/translations/es.json b/app/src/translations/es.json index 9bf9744..758b0c8 100644 --- a/app/src/translations/es.json +++ b/app/src/translations/es.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "No ha sido posible guardar el fichero", "filesharing.startingFileShare": "Intentando compartir el fichero", diff --git a/app/src/translations/fr.json b/app/src/translations/fr.json index 2b74da9..2eb7edf 100644 --- a/app/src/translations/fr.json +++ b/app/src/translations/fr.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Impossible d'enregistrer le fichier", "filesharing.startingFileShare": "Début du transfert de fichier", diff --git a/app/src/translations/hr.json b/app/src/translations/hr.json index 9d55c44..c0f8879 100644 --- a/app/src/translations/hr.json +++ b/app/src/translations/hr.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Nije moguće spremiti datoteku", "filesharing.startingFileShare": "Pokušaj dijeljenja datoteke", diff --git a/app/src/translations/hu.json b/app/src/translations/hu.json index be0342d..cf65f1d 100644 --- a/app/src/translations/hu.json +++ b/app/src/translations/hu.json @@ -138,6 +138,7 @@ "settings.echoCancellation": "Visszhangelnyomás", "settings.autoGainControl": "Automatikus hangerő", "settings.noiseSuppression": "Zajelnyomás", + "settings.drawerOverlayed": null, "filesharing.saveFileError": "A file-t nem sikerült elmenteni", "filesharing.startingFileShare": "Fájl megosztása", diff --git a/app/src/translations/it.json b/app/src/translations/it.json index 8ce1764..5ce26b6 100644 --- a/app/src/translations/it.json +++ b/app/src/translations/it.json @@ -137,6 +137,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Impossibile salvare file", "filesharing.startingFileShare": "Tentativo di condivisione file", diff --git a/app/src/translations/lv.json b/app/src/translations/lv.json index 5a39315..7bf0b24 100644 --- a/app/src/translations/lv.json +++ b/app/src/translations/lv.json @@ -132,6 +132,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Nav iespējams saglabāt failu", "filesharing.startingFileShare": "Tiek mēģināts kopīgot failu", diff --git a/app/src/translations/nb.json b/app/src/translations/nb.json index 139341a..251858b 100644 --- a/app/src/translations/nb.json +++ b/app/src/translations/nb.json @@ -138,6 +138,7 @@ "settings.echoCancellation": "Echokansellering", "settings.autoGainControl": "Auto gain kontroll", "settings.noiseSuppression": "Støy reduksjon", + "settings.drawerOverlayed": "Sidemeny over innhold", "filesharing.saveFileError": "Klarte ikke å lagre fil", "filesharing.startingFileShare": "Starter fildeling", diff --git a/app/src/translations/pl.json b/app/src/translations/pl.json index 4c8f28a..705a6d7 100644 --- a/app/src/translations/pl.json +++ b/app/src/translations/pl.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Nie można zapisać pliku", "filesharing.startingFileShare": "Próba udostępnienia pliku", diff --git a/app/src/translations/pt.json b/app/src/translations/pt.json index df2bf73..8250231 100644 --- a/app/src/translations/pt.json +++ b/app/src/translations/pt.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Impossível de gravar o ficheiro", "filesharing.startingFileShare": "Tentando partilha de ficheiro", diff --git a/app/src/translations/ro.json b/app/src/translations/ro.json index fe7fb50..1ba455c 100644 --- a/app/src/translations/ro.json +++ b/app/src/translations/ro.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Încercarea de a salva fișierul a eșuat", "filesharing.startingFileShare": "Partajarea fișierului", diff --git a/app/src/translations/tr.json b/app/src/translations/tr.json index 4a105d4..524c557 100644 --- a/app/src/translations/tr.json +++ b/app/src/translations/tr.json @@ -135,6 +135,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Dosya kaydedilemiyor", "filesharing.startingFileShare": "Paylaşılan dosyaya erişiliyor", diff --git a/app/src/translations/uk.json b/app/src/translations/uk.json index 71ead8e..a8cc077 100644 --- a/app/src/translations/uk.json +++ b/app/src/translations/uk.json @@ -138,6 +138,7 @@ "settings.echoCancellation": null, "settings.autoGainControl": null, "settings.noiseSuppression": null, + "settings.drawerOverlayed": null, "filesharing.saveFileError": "Неможливо зберегти файл", "filesharing.startingFileShare": "Спроба поділитися файлом", From 13e611e17747481fb0ca9b207052255d32db34b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Sat, 9 May 2020 00:07:12 +0200 Subject: [PATCH 21/28] Missing tooltip, fixes #324 --- app/src/components/Controls/TopBar.js | 29 +++++++++++++++++---------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/app/src/components/Controls/TopBar.js b/app/src/components/Controls/TopBar.js index 52b06cd..bbf408d 100644 --- a/app/src/components/Controls/TopBar.js +++ b/app/src/components/Controls/TopBar.js @@ -312,18 +312,25 @@ const TopBar = (props) =>
- handleMenuOpen(event, 'moreActions')} - color='inherit' + - - + handleMenuOpen(event, 'moreActions')} + color='inherit' + > + + + { fullscreenEnabled && Date: Sat, 9 May 2020 00:09:52 +0200 Subject: [PATCH 22/28] Add h shortcut for help dialog --- app/src/RoomClient.js | 7 +++++++ app/src/components/Controls/Help.js | 1 + 2 files changed, 8 insertions(+) diff --git a/app/src/RoomClient.js b/app/src/RoomClient.js index bce2f03..2217d65 100644 --- a/app/src/RoomClient.js +++ b/app/src/RoomClient.js @@ -408,6 +408,13 @@ export default class RoomClient break; } + case 'H': // Open help dialog + { + store.dispatch(roomActions.setHelpOpen(true)); + + break; + } + default: { break; diff --git a/app/src/components/Controls/Help.js b/app/src/components/Controls/Help.js index 3091ec4..98415b5 100644 --- a/app/src/components/Controls/Help.js +++ b/app/src/components/Controls/Help.js @@ -17,6 +17,7 @@ import Tabs from '@material-ui/core/Tabs'; import Tab from '@material-ui/core/Tab'; const shortcuts=[ + { key: 'h', label: 'room.help', defaultMessage: 'Help' }, { key: 'm', label: 'device.muteAudio', defaultMessage: 'Mute Audio' }, { key: 'v', label: 'device.stopVideo', defaultMessage: 'Mute Video' }, { key: '1', label: 'label.democratic', defaultMessage: 'Democratic View' }, From 1bccc9a98568303e159fc66f9e833bdc16e4e8aa Mon Sep 17 00:00:00 2001 From: Roman Drozd Date: Sat, 9 May 2020 00:29:20 +0200 Subject: [PATCH 23/28] Update pl translation --- app/src/translations/pl.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/translations/pl.json b/app/src/translations/pl.json index 705a6d7..8d3ecf5 100644 --- a/app/src/translations/pl.json +++ b/app/src/translations/pl.json @@ -60,7 +60,7 @@ "room.loweredHand": null, "room.extraVideo": null, "room.overRoomLimit": null, - "room.help": null, + "room.help": "Pomoc", "room.about": null, "room.shortcutKeys": null, From aa5f75b79478662ccb11ecb5ce5f45e21cfe2251 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Sat, 9 May 2020 00:37:47 +0200 Subject: [PATCH 24/28] Do things in correct order. --- server/lib/Lobby.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/lib/Lobby.js b/server/lib/Lobby.js index 43fc30d..45cdc06 100644 --- a/server/lib/Lobby.js +++ b/server/lib/Lobby.js @@ -154,8 +154,6 @@ class Lobby extends EventEmitter this.emit('lobbyEmpty'); }; - this._notification(peer.socket, 'enteredLobby'); - this._peers[peer.id] = peer; peer.on('gotRole', peer.gotRoleHandler); @@ -165,6 +163,8 @@ class Lobby extends EventEmitter peer.socket.on('request', peer.socketRequestHandler); peer.on('close', peer.closeHandler); + + this._notification(peer.socket, 'enteredLobby'); } async _handleSocketRequest(peer, request, cb) @@ -189,7 +189,8 @@ class Lobby extends EventEmitter cb(); break; - } + } + case 'changePicture': { const { picture } = request.data; From f468f95cdc58a2ab8ca2b1a408ffb3f02e8afbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=C3=A5var=20Aamb=C3=B8=20Fosstveit?= Date: Sat, 9 May 2020 00:43:05 +0200 Subject: [PATCH 25/28] Translated media buttons, fixes #309 --- .../components/Controls/ButtonControlBar.js | 75 +++++++++++++++---- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/app/src/components/Controls/ButtonControlBar.js b/app/src/components/Controls/ButtonControlBar.js index 2ee5b7e..f6e4fd6 100644 --- a/app/src/components/Controls/ButtonControlBar.js +++ b/app/src/components/Controls/ButtonControlBar.js @@ -7,6 +7,7 @@ import useMediaQuery from '@material-ui/core/useMediaQuery'; import classnames from 'classnames'; import * as appPropTypes from '../appPropTypes'; import { withRoomContext } from '../../RoomContext'; +import { useIntl } from 'react-intl'; import Fab from '@material-ui/core/Fab'; import Tooltip from '@material-ui/core/Tooltip'; import MicIcon from '@material-ui/icons/Mic'; @@ -57,6 +58,8 @@ const styles = (theme) => const ButtonControlBar = (props) => { + const intl = useIntl(); + const { roomClient, toolbarsVisible, @@ -73,20 +76,37 @@ const ButtonControlBar = (props) => let micTip; - if (!me.canSendMic || !micProducer) + if (!me.canSendMic) { micState = 'unsupported'; - micTip = 'Audio unsupported'; + micTip = intl.formatMessage({ + id : 'device.audioUnsupported', + defaultMessage : 'Audio unsupported' + }); + } + else if (!micProducer) + { + micState = 'off'; + micTip = intl.formatMessage({ + id : 'device.activateAudio', + defaultMessage : 'Activate audio' + }); } else if (!micProducer.locallyPaused && !micProducer.remotelyPaused) { micState = 'on'; - micTip = 'Mute audio'; + micTip = intl.formatMessage({ + id : 'device.muteAudio', + defaultMessage : 'Mute audio' + }); } else { - micState = 'off'; - micTip = 'Unmute audio'; + micState = 'muted'; + micTip = intl.formatMessage({ + id : 'device.unMuteAudio', + defaultMessage : 'Unmute audio' + }); } let webcamState; @@ -96,17 +116,26 @@ const ButtonControlBar = (props) => if (!me.canSendWebcam) { webcamState = 'unsupported'; - webcamTip = 'Video unsupported'; + webcamTip = intl.formatMessage({ + id : 'device.videoUnsupported', + defaultMessage : 'Video unsupported' + }); } else if (webcamProducer) { webcamState = 'on'; - webcamTip = 'Stop video'; + webcamTip = intl.formatMessage({ + id : 'device.stopVideo', + defaultMessage : 'Stop video' + }); } else { webcamState = 'off'; - webcamTip = 'Start video'; + webcamTip = intl.formatMessage({ + id : 'device.startVideo', + defaultMessage : 'Start video' + }); } let screenState; @@ -116,17 +145,26 @@ const ButtonControlBar = (props) => if (!me.canShareScreen) { screenState = 'unsupported'; - screenTip = 'Screen sharing not supported'; + screenTip = intl.formatMessage({ + id : 'device.screenSharingUnsupported', + defaultMessage : 'Screen sharing not supported' + }); } else if (screenProducer) { screenState = 'on'; - screenTip = 'Stop screen sharing'; + screenTip = intl.formatMessage({ + id : 'device.stopScreenSharing', + defaultMessage : 'Stop screen sharing' + }); } else { screenState = 'off'; - screenTip = 'Start screen sharing'; + screenTip = intl.formatMessage({ + id : 'device.startScreenSharing', + defaultMessage : 'Start screen sharing' + }); } const smallScreen = useMediaQuery(theme.breakpoints.down('sm')); @@ -143,7 +181,10 @@ const ButtonControlBar = (props) => > Date: Sat, 9 May 2020 00:43:20 +0200 Subject: [PATCH 26/28] Remove unused function --- app/src/components/Room.js | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/components/Room.js b/app/src/components/Room.js index 4dee360..e862be6 100644 --- a/app/src/components/Room.js +++ b/app/src/components/Room.js @@ -249,7 +249,6 @@ class Room extends React.PureComponent anchor={theme.direction === 'rtl' ? 'right' : 'left'} open={toolAreaOpen} onClose={() => toggleToolArea()} - onOpen={() => toggleToolArea()} classes={{ paper : classes.drawerPaper }} From 2dcecae8889472fe74b18aec0783fe8064afade6 Mon Sep 17 00:00:00 2001 From: Roman Drozd Date: Sat, 9 May 2020 01:26:50 +0200 Subject: [PATCH 27/28] Update pl translation --- app/src/translations/pl.json | 80 ++++++++++++++++++------------------ 1 file changed, 40 insertions(+), 40 deletions(-) diff --git a/app/src/translations/pl.json b/app/src/translations/pl.json index 8d3ecf5..174684c 100644 --- a/app/src/translations/pl.json +++ b/app/src/translations/pl.json @@ -49,25 +49,25 @@ "room.spotlights": "Aktywni uczestnicy", "room.passive": "Pasywni uczestnicy", "room.videoPaused": "To wideo jest wstrzymane.", - "room.muteAll": null, - "room.stopAllVideo": null, - "room.closeMeeting": null, - "room.clearChat": null, - "room.clearFileSharing": null, - "room.speechUnsupported": null, - "room.moderatoractions": null, - "room.raisedHand": null, - "room.loweredHand": null, - "room.extraVideo": null, - "room.overRoomLimit": null, + "room.muteAll": "Wycisz wszystkich", + "room.stopAllVideo": "Zatrzymaj wszystkie Video", + "room.closeMeeting": "Zamknij spotkanie", + "room.clearChat": "Wyczyść Chat", + "room.clearFileSharing": "Wyczyść pliki", + "room.speechUnsupported": "Twoja przeglądarka nie rozpoznaje mowy", + "room.moderatoractions": "Akcje moderatora", + "room.raisedHand": "{displayName} podniósł rękę", + "room.loweredHand": "{displayName} opuścił rękę", + "room.extraVideo": "Dodatkowe Video", + "room.overRoomLimit": "Pokój jest pełny, spróbuj za jakiś czas.", "room.help": "Pomoc", - "room.about": null, - "room.shortcutKeys": null, + "room.about": "O pogramie", + "room.shortcutKeys": "Skróty klawiaturowe", - "me.mutedPTT": null, + "me.mutedPTT": "Masz wyciszony mikrofon, przytrzymaj spację aby mówić", - "roles.gotRole": null, - "roles.lostRole": null, + "roles.gotRole": "Masz rolę {role}", + "roles.lostRole": "Nie masz już roli {role}", "tooltip.login": "Zaloguj", "tooltip.logout": "Wyloguj", @@ -79,11 +79,11 @@ "tooltip.lobby": "Pokaż poczekalnię", "tooltip.settings": "Pokaż ustawienia", "tooltip.participants": "Pokaż uczestników", - "tooltip.kickParticipant": null, - "tooltip.muteParticipant": null, - "tooltip.muteParticipantVideo": null, - "tooltip.raisedHand": null, - "tooltip.muteScreenSharing": null, + "tooltip.kickParticipant": "Wyrzuć użytkownika", + "tooltip.muteParticipant": "Wycisz użytkownika", + "tooltip.muteParticipantVideo": "Wyłącz wideo użytkownika", + "tooltip.raisedHand": "Podnieś rękę", + "tooltip.muteScreenSharing": "Anuluj udostępniania pulpitu przez użytkownika", "label.roomName": "Nazwa konferencji", "label.chooseRoomButton": "Kontynuuj", @@ -97,7 +97,7 @@ "label.filesharing": "Udostępnianie plików", "label.participants": "Uczestnicy", "label.shareFile": "Udostępnij plik", - "label.shareGalleryFile": null, + "label.shareGalleryFile": "Udostępnij obraz", "label.fileSharingUnsupported": "Udostępnianie plików nie jest obsługiwane", "label.unknown": "Nieznane", "label.democratic": "Układ demokratyczny", @@ -108,12 +108,12 @@ "label.veryHigh": "Bardzo wysoka (FHD)", "label.ultra": "Ultra (UHD)", "label.close": "Zamknij", - "label.media": null, - "label.appearence": null, - "label.advanced": null, - "label.addVideo": null, - "label.promoteAllPeers": null, - "label.moreActions": null, + "label.media": "Media", + "label.appearence": "Wygląd", + "label.advanced": "Zaawansowane", + "label.addVideo": "Dodaj wideo", + "label.promoteAllPeers": "Wpuść wszystkich", + "label.moreActions": "Więcej akcji", "settings.settings": "Ustawienia", "settings.camera": "Kamera", @@ -131,14 +131,14 @@ "settings.advancedMode": "Tryb zaawansowany", "settings.permanentTopBar": "Stały górny pasek", "settings.lastn": "Liczba widocznych uczestników (zdalnych)", - "settings.hiddenControls": null, - "settings.notificationSounds": null, - "settings.showNotifications": null, - "settings.buttonControlBar": null, - "settings.echoCancellation": null, - "settings.autoGainControl": null, - "settings.noiseSuppression": null, - "settings.drawerOverlayed": null, + "settings.hiddenControls": "Ukryte kontrolki mediów", + "settings.notificationSounds": "Powiadomienia dźwiękiem", + "settings.showNotifications": "Pokaż powiadomienia", + "settings.buttonControlBar": "Rozdziel kontrolki mediów", + "settings.echoCancellation": "Usuwanie echa", + "settings.autoGainControl": "Auto korekta wzmocnienia", + "settings.noiseSuppression": "Wyciszenie szumów", + "settings.drawerOverlayed": "Szuflada nad zawartością", "filesharing.saveFileError": "Nie można zapisać pliku", "filesharing.startingFileShare": "Próba udostępnienia pliku", @@ -180,8 +180,8 @@ "devices.cameraDisconnected": "Kamera odłączona", "devices.cameraError": "Wystąpił błąd podczas uzyskiwania dostępu do kamery", - "moderator.clearChat": null, - "moderator.clearFiles": null, - "moderator.muteAudio": null, - "moderator.muteVideo": null + "moderator.clearChat": "Moderator wyczyścił chat", + "moderator.clearFiles": "Moderator wyczyścił pliki", + "moderator.muteAudio": "Moderator wyciszył audio", + "moderator.muteVideo": "Moderator wyciszył twoje video" } \ No newline at end of file From 36b40fd795e346372784f5bd631e2703498698b1 Mon Sep 17 00:00:00 2001 From: Stefan Otto Date: Sat, 9 May 2020 12:15:05 +0200 Subject: [PATCH 28/28] Added german translations --- app/src/translations/de.json | 72 ++++++++++++++++++------------------ 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/app/src/translations/de.json b/app/src/translations/de.json index 402c4b3..c4b67fe 100644 --- a/app/src/translations/de.json +++ b/app/src/translations/de.json @@ -52,22 +52,22 @@ "room.muteAll": "Alle stummschalten", "room.stopAllVideo": "Alle Videos stoppen", "room.closeMeeting": "Meeting schließen", - "room.clearChat": null, - "room.clearFileSharing": null, + "room.clearChat": "Liste löschen", + "room.clearFileSharing": "Liste löschen", "room.speechUnsupported": "Dein Browser unterstützt keine Spracherkennung", - "room.moderatoractions": null, - "room.raisedHand": null, - "room.loweredHand": null, - "room.extraVideo": null, - "room.overRoomLimit": null, - "room.help": null, - "room.about": null, - "room.shortcutKeys": null, + "room.moderatoractions": "Moderator Aktionen", + "room.raisedHand": "{displayName} hebt die Hand", + "room.loweredHand": "{displayName} senkt die Hand", + "room.extraVideo": "Video hinzufügen", + "room.overRoomLimit": "Der Raum ist voll, probiere es später nochmal", + "room.help": "Hilfe", + "room.about": "Impressum", + "room.shortcutKeys": "Tastaturkürzel", "me.mutedPTT": "Du bist stummgeschalted, Halte die SPACE-Taste um zu sprechen", - "roles.gotRole": null, - "roles.lostRole": null, + "roles.gotRole": "Rolle erhalten: {role}", + "roles.lostRole": "Rolle entzogen: {role}", "tooltip.login": "Anmelden", "tooltip.logout": "Abmelden", @@ -79,11 +79,11 @@ "tooltip.lobby": "Warteraum", "tooltip.settings": "Einstellungen", "tooltip.participants": "Teilnehmer", - "tooltip.kickParticipant": "Teilnehmer rauswerfen", - "tooltip.muteParticipant": null, - "tooltip.muteParticipantVideo": null, - "tooltip.raisedHand": null, - "tooltip.muteScreenSharing": null, + "tooltip.kickParticipant": "Rauswerfen", + "tooltip.muteParticipant": "Stummschalten", + "tooltip.muteParticipantVideo": "Video stoppen", + "tooltip.raisedHand": "Hand heben", + "tooltip.muteScreenSharing": "Stoppe Bildschirmfreigabe", "label.roomName": "Name des Raums", "label.chooseRoomButton": "Weiter", @@ -97,7 +97,7 @@ "label.filesharing": "Dateien", "label.participants": "Teilnehmer", "label.shareFile": "Datei hochladen", - "label.shareGalleryFile": null, + "label.shareGalleryFile": "Bild teilen", "label.fileSharingUnsupported": "Dateifreigabe nicht unterstützt", "label.unknown": "Unbekannt", "label.democratic": "Demokratisch", @@ -108,12 +108,12 @@ "label.veryHigh": "Sehr hoch (FHD)", "label.ultra": "Ultra (UHD)", "label.close": "Schließen", - "label.media": null, - "label.appearence": null, - "label.advanced": null, - "label.addVideo": null, - "label.promoteAllPeers": null, - "label.moreActions": null, + "label.media": "Audio / Video", + "label.appearence": "Erscheinung", + "label.advanced": "Erweiter", + "label.addVideo": "Video hinzufügen", + "label.promoteAllPeers": "Alle Teinehmer einlassen", + "label.moreActions": "Weitere Aktionen", "settings.settings": "Einstellungen", "settings.camera": "Kamera", @@ -131,14 +131,14 @@ "settings.advancedMode": "Erweiterter Modus", "settings.permanentTopBar": "Permanente obere Leiste", "settings.lastn": "Anzahl der sichtbaren Videos", - "settings.hiddenControls": null, - "settings.notificationSounds": null, - "settings.showNotifications": null, - "settings.buttonControlBar": null, - "settings.echoCancellation": null, - "settings.autoGainControl": null, - "settings.noiseSuppression": null, - "settings.drawerOverlayed": null, + "settings.hiddenControls": "Medienwerkzeugleiste automatisch ausblenden", + "settings.notificationSounds": "Audiosignal bei Benachrichtigungen", + "settings.showNotifications": "Zeige Benachrichtigungen", + "settings.buttonControlBar": "Seperate seitliche Medienwerkzeugleiste", + "settings.echoCancellation": "Echounterdrückung", + "settings.autoGainControl": "Automatische Pegelregelung (Audioeingang)", + "settings.noiseSuppression": "Rauschunterdrückung", + "settings.drawerOverlayed": "Seitenpanel verdeckt Hauptinhalt", "filesharing.saveFileError": "Fehler beim Speichern der Datei", "filesharing.startingFileShare": "Starte Teilen der Datei", @@ -180,8 +180,8 @@ "devices.cameraDisconnected": "Kamera getrennt", "devices.cameraError": "Fehler mit deiner Kamera", - "moderator.clearChat": null, - "moderator.clearFiles": null, - "moderator.muteAudio": null, - "moderator.muteVideo": null + "moderator.clearChat": "Moderator hat Chat gelöscht", + "moderator.clearFiles": "Moderator hat geteilte Dateiliste gelöscht", + "moderator.muteAudio": "Moderator hat dich stummgeschaltet", + "moderator.muteVideo": "Moderator hat dein Video gestoppt" }